Skip to main content

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 as cmd 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)
helper = OA.option helpReader $ mconcat
    [ OA.short 'h'
    , OA.long "help"
    , OA.value id
    , OA.metavar ""
    , OABI.noGlobal
    , OA.noArgError (OA.ShowHelpText Nothing)
    , OA.help "show this help text"
    , OA.hidden
    ]
  where
    helpReader = do
      potentialCommand <- OAT.readerAsk
      OA.readerAbort $ OA.ShowHelpText (Just potentialCommand)
#else
helper = OA.abortOption OA.ShowHelpText $ mconcat
    [ OA.short 'h'
    , OA.long "help"
    , OA.help "show help and exit"
    , 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.

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.

Author

Travis Cardwell

Published

Tags