4

I'm writing a program that has both a command-line interface and an interactive mode. In CLI mode it executes one command, prints results and exits. In interactive mode it repeatedly reads commands using GNU readline, executes them and prints results (in spirit of a REPL).

The syntax for commands and their parameters is almost the same regardless of whether they come from command-line or frmo stdin. I would like to maximize code-reuse by using a single framework for parsing both command-line and interactive mode inputs.

My proposed syntax is (square brackets denote optional parts, braces repetition) as follows:

  • From shell:

    program-name {[GLOBAL OPTION] ...} <command> [{<command arg>|<GLOBAL OPTION>|<LOCAL OPTION> ...}]  
    
  • In interactive mode:

    <command> [{<command arg>|<GLOBAL OPTION>|<LOCAL OPTION> ...}]
    

Local options are only valid for one particular command (different commands may assign a different meaning to one option).

My problem is that there are some differences between the CL and interactive interfaces: Some global options are only valid from command line (like --help, --version or --config-file). There is obviously also the 'quit'-command which is very important in interactive mode, but using it from CL makes no sense.

To solve this I've searched web and hackage for command-line parsing libraries. The most interesting ones I've found are cmdlib and optparse-applicative. However, I'm quite new to Haskell and even though I can create a working program by copying and modifying example code from library docs, I haven't quite understood the mechanics of these libraries and therefore have not been able to solve my problem.

I have these questions in mind:
How to make a base parser for commands and options that are common to CL and REPL interfaces and then be able to extend the base parser with new commands and options?
How to prevent these libraries from exiting my program upon incorrect input or when '--help' is used?
I plan to add complete i18n support to my program. Therefore I would like to prevent my chosen library from printing any messages, because all messages need to be translated. How to achieve this?

So I wish you could give me some hints on where to go from here. Does cmdlib or optparse-applicative (or some other library) support what I'm looking for? Or should I revert to a hand-crafted parser?

2
  • There is a quite straightforward command line parsing tool in base: haskell.org/ghc/docs/latest/html/libraries/base/…. If your options are given to as a list, you can have 3 lists: one for common commands, one for REPL-only and one for CL only. GetOpts doesn't crash, it returns error message, as I'd assume any good library should do. It also doesn't do any printing, because printing has nothing to do with command line parsing. Commented May 14, 2014 at 14:05
  • There is a review of existing libraries for parsing command-line options. Check out here: haskell.org/haskellwiki/Command_line_option_parsers Commented May 14, 2014 at 15:42

1 Answer 1

2

I think you could use my library http://hackage.haskell.org/package/options to do this. The subcommands feature exactly matches the command flag parsing behavior you're looking for.

It'd be a little tricky to share subcommands between two disjoint sets of options, but a helper typeclass should be able to do it. Rough sample code:

-- A type for options shared between CLI and interactive modes.
data CommonOptions = CommonOptions
  {  optSomeOption :: Bool
  }
instance Options CommonOptions where ...

-- A type for options only available in CLI mode (such as --version or --config-file)
data CliOptions = CliOptions
  { common :: CommonOptions
  , version :: Bool
  , configFile :: String
  }
instance Options CliOptions where ...

-- if a command takes only global options, it can use this subcommand option type.
data NoOptions = NoOptions

instance Options NoOptions where
  defineOptions = pure NoOptions

-- typeclass to let commands available in both modes access common options
class HasCommonOptions a where
  getCommonOptions :: a -> CommonOptions
instance HasCommonOptions CommonOptions where
  getCommonOptions = id
instance HasCommonOptions CliOptions where
  getCommonOptions = common

commonCommands :: HasCommonOptions a => [Subcommand a (IO ())]
commonCommands = [... {- your commands here -} ...]

cliCommands :: HasCommonOptions a => [Subcommand a (IO ())]
cliCommands = commonCommands ++ [cmdRepl]

interactiveCommands :: HasCommonOptions a => [Subcommand a (IO ())]
interactiveCommands = commonCommands ++ [cmdQuit]

cmdRepl :: HasCommonOptions a => Subcommand a (IO ())
cmdRepl = subcommand "repl" $ \opts NoOptions -> do
  {- run your interactive REPL here -}

cmdQuit :: Subcommand a (IO ())
cmdQuit = subcommand "quit" (\_ NoOptions -> exitSuccess)

I suspect the helper functions like runSubcommand wouldn't be specialized enough, so you'll want to invoke the parser with parseSubcommand once you've split up the input string from the REPL prompt. The docs have examples of how to inspect the parsed options, including checking whether the user requested help.

The options parser itself won't print any output, but it may be difficult to internationalize error messages generated by the default type parsers. Please let me know if there's any changes to the library that would help.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.