optparse-applicative Breaking Change
This morning, I implemented the first version of a new project (to be
announced soon) and ran into a breaking change in the optparse-applicative
library. The change has to do with how --help
options are
processed. I use a custom helper
function and did not
notice that my implementation caused a runtime failure when using
--help
.
The change was made in version 0.16.0.0, released on August 14, 2020. The changelog describes the change:
Allow the
--help
option to take the name of a command. Usage without any arguments is the same, but now, when an argument is given, if it is the name of a currently reachable command, the help text for that command will be show.Fixes issues:
# 379
-cmd --help subcmd
is not the same ascmd subcmd --help
With the new implementation, the --help
option reads an
argument. When no argument is available, the top-level help is displayed
instead of an error. When a command is specified, the help for that
command is displayed. When I added support for 0.16.0.0,
I did not configure the option correctly, and I failed to notice the
issue. As an example of the issue, here is a test using hr:
$ hr --help
The option `--help` expects an argument.
Usage: hr [-w|--width CHARS] [-d|--default CHARS] [-a|--ascii] [-t|--time]
[-f|--format FORMAT] [-i|--input] [--timeout MS] [NOTE ...]
horizontal rule for the terminal
An option parsing error is displayed instead of the full help.
Stackage LTS 18.0,
released on June 16, is the first LTS to use a version of
optparse-applicative
with the change. Luckily, I happened
to implement a new program using that LTS and test the
--help
output! None of my software has been released with
the bug, and I can fix the bug before making releases with the recent
Nix configuration changes.
The fixed version is as follows:
-- https://hackage.haskell.org/package/optparse-applicative
import qualified Options.Applicative as OA
#if MIN_VERSION_optparse_applicative (0,16,0)
import qualified Options.Applicative.Builder.Internal as OABI
#endif
import qualified Options.Applicative.Types as OAT
-- | A hidden @-h@ / @--help@ option that always fails, showing the help
--
-- This is the same as 'OA.helper' except that it has a different help
-- message.
helper :: OA.Parser (a -> a)
#if MIN_VERSION_optparse_applicative (0,16,0)
= OA.option helpReader $ mconcat
helper 'h'
[ OA.short "help"
, OA.long id
, OA.value ""
, OA.metavar
, OABI.noGlobalOA.ShowHelpText Nothing)
, OA.noArgError ("show this help text"
, OA.help
, OA.hidden
]where
= do
helpReader <- OAT.readerAsk
potentialCommand $ OA.ShowHelpText (Just potentialCommand)
OA.readerAbort #else
= OA.abortOption OA.ShowHelpText $ mconcat
helper 'h'
[ OA.short "help"
, OA.long "show help and exit"
, OA.help
, OA.hidden
]#endif
Note that the noGlobal
builder is only exported from an
internal module, unfortunately.
I tested this code with the following Stackage snapshots. In software that does not need to support old versions of the library, the CPP usage and implementation for old versions can be removed.
- LTS 11.22 using GHC 8.2.2
- LTS 12.26 using GHC 8.4.4
- LTS 14.27 using GHC 8.6.5
- LTS 16.31 using GHC 8.8.4
- LTS 18.0 using GHC 8.10.4
- Nightly 2021-06-21 using GHC 9.0.1
Custom helper
?
Some may wonder why I implement a custom helper
function. I do so for consistency of case in the help messages. Neither
POSIX nor GNU specify if a help message should start with a lower case
or capital letter, but standard Linux utilities tend to use lower case.
I have been following that convention for many years. The custom
helper
function allows me to be consistent with other
software that I have written, as well as standard Linux utilities.