optparse-applicative

repository·master·Indexed 21 days ago

https://github.com/pcapriotti/optparse-applicative

A Haskell library for parsing command-line options using an applicative interface. It provides automated argument validation, error reporting, help text generation, and shell completions for bash, zsh, and fish. The library supports regular options, flags, positional arguments, and complex subcommands via a combinator-based syntax using Parser, ParserInfo, and execParser.

Tokens
6.3K
Snippets
17
Records
24
Agent score
26%

What's inside optparse-applicative

  1. Introduction to optparse-applicative

    master

    optparse-applicative is a Haskell library for parsing command-line options using an applicative interface. It handles argument validation, error reporting, usage line generation, help screens, and shell completions (bash, zsh, fish).

    The core type is Parser a, which represents a specification for a set of options that yields a value of type a upon successful parsing. It implements the following typeclass instances:

    • Functor Parser
    • Applicative Parser
    • Alternative Parser
  2. How Parsers work: Regular options and Flags

    master

    Parsers are built using modifiers composed with the semigroup operator (<>).

    • Regular Options: Options that require an argument (e.g., --hello TARGET). Use strOption for strings. You can use metavar to specify the placeholder name in help text and help for the description.
    • Flags: Options that do not take arguments and return a predetermined value. The most common is a switch, which returns True if the flag is present and False otherwise. You can use short to provide a single-letter alias (e.g., -q for --quiet).
    target :: Parser String
    target = strOption
      (  long "hello"
      <> metavar "TARGET"
      <> help "Target for the greeting" )
    
    quiet :: Parser Bool
    quiet = switch ( long "quiet" <> short 'q' <> help "Whether to be quiet" )
  3. Use the Arrow interface for complex parsers

    master

    While the Applicative interface is the standard, you can use the Arrow syntax (from Options.Applicative.Arrows) to build parsers. This is particularly helpful when the data structure is deeply nested or when the order of parsing needs to differ from the order of field construction.

    Use asA to convert a parser to an arrow and runA to convert the composed arrow back into a Parser.

    import Options.Applicative.Arrows
    
    data Options = Options
      { optArgs :: [String]
      , optVerbose :: Bool }
    
    opts :: Parser Options
    opts = runA $ proc () -> do
      verbosity  <- asA (option auto (short 'v' <> value 0)) -< ()
      let verbose = verbosity > 0
      args       <- asA (many (argument str idm)) -< ()
      returnA -< Options args verbose
  4. Why optparse-applicative is not Monadic

    master

    The Parser type does not implement the Monad typeclass. This is an intentional design choice to ensure that the parser structure is fully known before any input is processed.

    By remaining strictly Applicative, the library can:

    1. Traverse the parser structure to automatically generate usage/help strings.
    2. Allow command-line options to be provided in any order.

    If you attempt to use monadic syntax without the ApplicativeDo extension, you will encounter compilation errors stating that no Monad instance was found.

  5. How optparse-applicative works internally

    master

    An applicative Parser is a heterogeneous tree of Options constructed using existential types.

    Key characteristics of this model:

    1. Static Structure: All options are known before parsing begins. This allows the library to traverse the tree to generate help text and shell completions.
    2. Parsing Process: The library examines user input to determine if tokens are options/flags or positional arguments. It searches the Parser tree for matches; when a match is found, that leaf is replaced with the provided value.
    3. Validation: Once all input is processed, the library checks if the complete value can be generated from the tree. If not, it issues an error.
  6. Handling multiple configurations with Alternative

    master

    The Parser type is an instance of Alternative, allowing you to provide multiple ways to configure a program using the choice operator (<|>). This is useful for modeling sum types (e.g., choosing between a file input or standard input).

    • Choice (<|>): If the first parser fails, the second is tried. If both fail, the parser fails. Note that if a command line contains options from both sides of the <|> operator, it will be rejected.
    • Optional (optional): You can use the optional combinator to make a parser return Nothing instead of failing if the user does not provide the option.
    data Input
      = FileInput FilePath
      | StdInput
    
    fileInput :: Parser Input
    fileInput = FileInput <$> strOption
      (  long "file"
      <> short 'f'
      <> metavar "FILENAME"
      <> help "Input file" )
    
    stdInput :: Parser Input
    stdInput = flag' StdInput
      (  long "stdin"
      <> help "Read from stdin" )
    
    -- Combines them: either --file or --stdin
    input :: Parser Input
    input = fileInput <|> stdInput
    
    -- Makes an option optional
    optionalOutput :: Parser (Maybe String)
    optionalOutput = optional $ strOption
      ( long "output" <> metavar "DIRECTORY" )
  7. Composing Parsers with Applicative

    master

    Because Parser is an instance of Applicative, you can combine multiple parsers into a single parser that returns a composite data structure using the <$> (fmap) and <*> (apply) operators.

    This creates a permutation parser: the order in which options are defined in the code does not restrict the order in which they must appear on the command line. For example, --target world -q is equivalent to -q --target world.

    data Options = Options
      { optTarget :: String
      , optQuiet :: Bool }
    
    opts :: Parser Options
    opts = Options <$> target <*> quiet
  8. Enable automatic option disambiguation

    master

    By default, optparse-applicative does not allow partial matches for long options. You can enable automatic disambiguation (e.g., allowing --fil to match --filename) by using the disambiguate PrefsMod modifier with customExecParser.

    Note: If one option name is a prefix of another, the prefix option will never be matched when disambiguation is enabled.

    import Options.Applicative
    
    sample :: Parser ()
    sample = () <$ 
      switch (long "filename") <* 
      switch (long "filler")
    
    main :: IO ()
    main = customExecParser p opts
      where
        opts = info (helper <*> sample) idm
        p = prefs disambiguate
  9. How Builders and Modifiers work

    master

    Builders allow you to define parsers using a convenient combinator-based syntax. They work by building an option from scratch and then lifting it to a single-option parser that can be combined with others using Applicative and Alternative combinators.

    Builders always take a modifier argument. Modifiers are instances of the Semigroup and Monoid typeclasses, meaning they are combined using the <> operator. Modifiers are type-safe: they use a type parameter to ensure that certain modifiers can only be used with specific builders (e.g., a CommandFields modifier can only be used with commands).

  10. Handling overlapping flags and options

    master

    The library does not support overlapping flags/options or options with optional arguments because they create ambiguity. For example, if an option --foo has an optional value, the parser cannot distinguish if the next token --bar is the value for --foo or a separate flag.

    Workaround: Use the Alternative instance of Parser to define distinct paths for a flag, an option, and a default value, using different names for the flag and the option to avoid collision.

  11. Using ApplicativeDo for cleaner parser syntax

    master

    While Parser is not a Monad, you can use a do-style syntax to define parsers if you enable the ApplicativeDo GHC extension. This allows you to write parser specifications that look like monadic code, which can be cleaner when constructing complex data types.

    Note on GHC versions: In some older versions of GHC (like 8.0.1), there were desugaring bugs where function application with ($) might fail or pure values needed to be wrapped in parentheses. Ensure you are using a modern GHC version for the best experience.

    {-# LANGUAGE RecordWildCards            #-}
    {-# LANGUAGE ApplicativeDo              #-}
    
    data Options = Options
      { optArgs :: [String]
      , optVerbose :: Bool }
    
    opts :: Parser Options
    opts = do
      optVerbose    <- switch (short 'v')
      optArgs       <- many (argument str idm)
      pure Options {..}
  12. Customize the help screen and usage text

    master

    You can control the appearance and behavior of the help screen using several methods:

    Text Content

    • progDesc: Brief description of the program.
    • header: Tagline or header text.
    • footer: Detailed information at the end of the help text.

    Display Behavior

    Use PrefsMods with customExecParser to control when help is shown:

    • showHelpOnError: Display help text if parsing fails.
    • showHelpOnEmpty: Display help text if the command is incomplete at the start of parsing.

    Grouping Options

    • parserOptionGroup: Groups options under a common heading in the --help output. Groups are listed in creation order and duplicate groups are consolidated. Nested groups are automatically indented.