interface Arg exposes [ Parser, NamedParser, parse, toHelp, parseFormatted, succeed, boolOption, strOption, i64Option, str, subCommand, choice, withParser, program, list, ] imports [Effect, InternalTask, Task.{ Task }] ## Gives a list of the program's command-line arguments. list : Task (List Str) * list = Effect.args |> Effect.map Ok |> InternalTask.fromEffect ## A parser for a command-line application. ## A [NamedParser] is usually built from a [Parser] using [program]. NamedParser a := { name : Str, help : Str, parser : Parser a, } ## Describes how to parse a slice of command-line arguments. ## [Parser]s can be composed in various ways, including via [withParser] and ## [subCommand]. ## Once you have a [Parser] that describes your application's entire parsing ## needs, consider transforming it into a [NamedParser]. Parser a := [ Succeed a, Option OptionConfig (MarkedArgs -> Result { newlyTaken : Taken, val : a } (ParseError [])), Positional PositionalConfig (MarkedArgs -> Result { newlyTaken : Taken, val : a } (ParseError [])), # TODO: hiding the record behind an alias currently causes a panic SubCommand (List { name : Str, parser : Parser a, }), # Constructed during transformations of the above variants WithConfig (Parser a) Config, Lazy ({} -> a), ] ## Indices in an arguments list that have already been parsed. Taken : Set Nat ## A representation of parsed and unparsed arguments in a constant list of ## command-line arguments. ## Used only internally, for efficient representation of parsed and unparsed ## arguments. MarkedArgs : { args : List Str, taken : Taken } ## Enumerates errors that can occur during parsing a list of command line arguments. ParseError a : [ ## The program name was not found as the first argument to be parsed. ProgramNameNotProvided Str, ## A positional argument (inherently required) was not found. MissingPositional Str, ## An option argument is required, but it was not found. MissingRequiredOption Str, ## An argument was found, but it didn't have the expected [OptionType]. WrongOptionType { arg : Str, expected : OptionType, }, ## A subcommand is required, but it was not found. SubCommandNotFound { choices : List Str, }, ## A subcommand was found, but it was not the expected one. IncorrectSubCommand { found : Str, choices : List Str, }, ]a ## Expected type of an option, in an argument list being parsed. ## Describes how a string option should be interpreted as a certain type. OptionType : [ Str, Bool, I64, ] ## Help metadata extracted from a [Parser]. Help : [ SubCommands (List { name : Str, help : Help }), Config (List Config), ] OptionConfig : { long : Str, short : Str, help : Str, type : OptionType, } PositionalConfig : { name : Str, help : Str, } Config : [Option OptionConfig, Positional PositionalConfig] ## Generates help metadata from a [Parser]. ## ## This is useful if you would like to use this metadata to generate your own ## human-readable help or hint menus. ## ## A default help menu can be generated with [formatHelp]. toHelp : Parser * -> Help toHelp = \parser -> toHelpHelper parser [] ## A parser that immediately succeeds with its given input. succeed : a -> Parser a succeed = \val -> @Parser (Succeed val) toHelpHelper : Parser *, List Config -> Help toHelpHelper = \@Parser parser, configs -> when parser is Succeed _ -> Config configs Lazy _ -> Config configs WithConfig innerParser config -> toHelpHelper innerParser (List.append configs config) Option config _ -> List.append configs (Option config) |> Config SubCommand commands -> List.map commands (\{ name, parser: innerParser } -> { name, help: toHelpHelper innerParser [] }) |> SubCommands Positional config _ -> List.append configs (Positional config) |> Config findOneArg : Str, Str, MarkedArgs -> Result { val : Str, newlyTaken : Taken } [NotFound] findOneArg = \long, short, { args, taken } -> argMatches = \{ index, found: _ }, arg -> if Set.contains taken index || Set.contains taken (index + 1) then Continue { index: index + 1, found: Bool.false } else if arg == "--\(long)" then Break { index, found: Bool.true } else if Bool.not (Str.isEmpty short) && arg == "-\(short)" then Break { index, found: Bool.true } else Continue { index: index + 1, found: Bool.false } # TODO allow = as well, etc. { index: argIndex, found } = List.walkUntil args { index: 0, found: Bool.false } argMatches if !found then Err NotFound else # Return the next argument after the given one List.get args (argIndex + 1) |> Result.mapErr (\_ -> NotFound) |> Result.map (\val -> newUsed = Set.fromList [argIndex, argIndex + 1] { val, newlyTaken: newUsed }) updateTaken : MarkedArgs, Taken -> MarkedArgs updateTaken = \{ args, taken }, taken2 -> { args, taken: Set.union taken taken2 } # andMap : Parser a, Parser (a -> b) -> Parser b andMap = \@Parser parser, @Parser mapper -> unwrapped = when mapper is Succeed fn -> when parser is Succeed a -> Lazy \{} -> fn a Lazy thunk -> Lazy \{} -> fn (thunk {}) WithConfig parser2 config -> parser2 |> andMap (@Parser mapper) |> WithConfig config Option config run -> Option config \args -> run args |> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken }) Positional config run -> Positional config \args -> run args |> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken }) SubCommand cmds -> mapSubParser = \{ name, parser: parser2 } -> { name, parser: andMap parser2 (@Parser mapper) } List.map cmds mapSubParser |> SubCommand Option config run -> when parser is Succeed a -> Option config \args -> when run args is Ok { val: fn, newlyTaken } -> Ok { val: fn a, newlyTaken } Err err -> Err err Lazy thunk -> Option config \args -> when run args is Ok { val: fn, newlyTaken } -> Ok { val: fn (thunk {}), newlyTaken } Err err -> Err err WithConfig parser2 config2 -> parser2 |> andMap (@Parser mapper) |> WithConfig config2 Option config2 run2 -> # Parse first the one and then the other. combinedParser = Option config2 \args -> when run args is Ok { val: fn, newlyTaken } -> run2 (updateTaken args newlyTaken) |> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 }) Err err -> Err err # Store the extra config. @Parser combinedParser |> WithConfig (Option config) Positional config2 run2 -> combinedParser = Positional config2 \args -> when run args is Ok { val: fn, newlyTaken } -> run2 (updateTaken args newlyTaken) |> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 }) Err err -> Err err # Store the extra config. @Parser combinedParser |> WithConfig (Option config) SubCommand cmds -> # For each subcommand, first run the subcommand, then # push the result through the arg parser. mapSubParser = \{ name, parser: parser2 } -> { name, parser: andMap parser2 (@Parser mapper) } List.map cmds mapSubParser |> SubCommand Positional config run -> when parser is Succeed a -> Positional config \args -> when run args is Ok { val: fn, newlyTaken } -> Ok { val: fn a, newlyTaken } Err err -> Err err Lazy thunk -> Positional config \args -> when run args is Ok { val: fn, newlyTaken } -> Ok { val: fn (thunk {}), newlyTaken } Err err -> Err err WithConfig parser2 config2 -> parser2 |> andMap (@Parser mapper) |> WithConfig config2 Option config2 run2 -> # Parse first the one and then the other. combinedParser = Option config2 \args -> when run args is Ok { val: fn, newlyTaken } -> run2 (updateTaken args newlyTaken) |> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 }) Err err -> Err err # Store the extra config. @Parser combinedParser |> WithConfig (Positional config) Positional config2 run2 -> combinedParser = Positional config2 \args -> when run args is Ok { val: fn, newlyTaken } -> run2 (updateTaken args newlyTaken) |> Result.map (\{ val, newlyTaken: newlyTaken2 } -> { val: fn val, newlyTaken: Set.union newlyTaken newlyTaken2 }) Err err -> Err err # Store the extra config. @Parser combinedParser |> WithConfig (Positional config) SubCommand cmds -> # For each subcommand, first run the subcommand, then # push the result through the arg parser. mapSubParser = \{ name, parser: parser2 } -> { name, parser: andMap parser2 (@Parser mapper) } List.map cmds mapSubParser |> SubCommand Lazy thunk -> fn = thunk {} when parser is Succeed a -> Lazy \{} -> fn a Lazy innerThunk -> Lazy \{} -> fn (innerThunk {}) WithConfig parser2 config -> parser2 |> andMap (@Parser mapper) |> WithConfig config Option config run -> Option config \args -> run args |> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken }) Positional config run -> Positional config \args -> run args |> Result.map (\{ val, newlyTaken } -> { val: fn val, newlyTaken }) SubCommand cmds -> mapSubParser = \{ name, parser: parser2 } -> { name, parser: andMap parser2 (@Parser mapper) } List.map cmds mapSubParser |> SubCommand WithConfig mapper2 config -> @Parser parser |> andMap mapper2 |> WithConfig config SubCommand cmds -> mapSubParser = \{ name, parser: mapper2 } -> { name, parser: andMap (@Parser parser) mapper2 } List.map cmds mapSubParser |> SubCommand @Parser unwrapped ## Marks a [Parser] as the entry point for parsing a command-line application, ## taking the program name and optionally a high-level help message for the ## application. ## ## The produced [NamedParser] can be used to parse arguments via [parse] or ## [parseFormatted]. program = \parser, { name, help ? "" } -> @NamedParser { name, help, parser } ## Parses a list of command-line arguments with the given parser. The list of ## arguments is expected to contain the name of the program in the first ## position. ## ## If the arguments do not conform with what is expected by the parser, the ## first error seen will be returned. # TODO panics in alias analysis when this annotation is included # parse : NamedParser a, List Str -> Result a (ParseError*) parse = \@NamedParser parser, args -> # By convention the first string in the argument list is the program name. if List.isEmpty args then Err (ProgramNameNotProvided parser.name) else markedArgs = { args, taken: Set.single 0 } parseHelp parser.parser markedArgs parseHelp : Parser a, MarkedArgs -> Result a (ParseError []) parseHelp = \@Parser parser, args -> when parser is Succeed val -> Ok val Option _ run -> run args |> Result.map .val Positional _ run -> run args |> Result.map .val SubCommand cmds -> when nextUnmarked args is Ok { index, val: cmd } -> argsRest = { args & taken: Set.insert args.taken index } state = List.walkUntil cmds (Err {}) \st, { name, parser: subParser } -> if cmd == name then Break (Ok (parseHelp subParser argsRest)) else Continue st when state is Ok result -> result Err {} -> Err (IncorrectSubCommand { found: cmd, choices: List.map cmds .name }) Err OutOfBounds -> Err (SubCommandNotFound { choices: List.map cmds .name }) Lazy thunk -> Ok (thunk {}) WithConfig parser2 _config -> parseHelp parser2 args nextUnmarked : MarkedArgs -> Result { index : Nat, val : Str } [OutOfBounds] nextUnmarked = \marked -> help = \index -> if Set.contains marked.taken index then help (index + 1) else List.get marked.args index |> Result.map \val -> { index, val } help 0 ## Creates a parser for a boolean option argument. ## Options of value "true" and "false" will be parsed as [Bool.true] and [Bool.false], respectively. ## All other values will result in a `WrongOptionType` error. boolOption : _ -> Parser Bool # TODO: panics if parameter annotation given boolOption = \{ long, short ? "", help ? "" } -> fn = \args -> when findOneArg long short args is Err NotFound -> Err (MissingRequiredOption long) Ok { val, newlyTaken } -> when val is "true" -> Ok { val: Bool.true, newlyTaken } "false" -> Ok { val: Bool.false, newlyTaken } _ -> Err (WrongOptionType { arg: long, expected: Bool }) @Parser (Option { long, short, help, type: Bool } fn) ## Creates a parser for a string option argument. strOption : _ -> Parser Str # TODO: panics if parameter annotation given strOption = \{ long, short ? "", help ? "" } -> fn = \args -> when findOneArg long short args is Err NotFound -> Err (MissingRequiredOption long) Ok { val, newlyTaken } -> Ok { val, newlyTaken } @Parser (Option { long, short, help, type: Str } fn) ## Creates a parser for a 64-bit signed integer ([I64]) option argument. i64Option : _ -> Parser I64 # TODO: panics if parameter annotation given i64Option = \{ long, short ? "", help ? "" } -> fn = \args -> when findOneArg long short args is Err NotFound -> Err (MissingRequiredOption long) Ok { val, newlyTaken } -> Str.toI64 val |> Result.mapErr (\_ -> WrongOptionType { arg: long, expected: I64 }) |> Result.map (\v -> { val: v, newlyTaken }) @Parser (Option { long, short, help, type: I64 } fn) ## Parses a single positional argument as a string. str : _ -> Parser Str str = \{ name, help ? "" } -> fn = \args -> nextUnmarked args |> Result.mapErr (\OutOfBounds -> MissingPositional name) |> Result.map (\{ val, index } -> { val, newlyTaken: Set.insert args.taken index }) @Parser (Positional { name, help } fn) ## Wraps a given parser as a subcommand parser. ## ## When parsing arguments, the subcommand name will be expected to be parsed ## first, and then the wrapped parser will be applied to the rest of the ## arguments. ## ## To support multiple subcommands, use [choice]. subCommand : Parser a, Str -> { name : Str, parser : Parser a } subCommand = \parser, name -> { name, parser } ## Creates a parser that matches over a list of subcommands. ## ## The given list of subcommands is expected to be non-empty, and unique in the ## subcommand name. These invariants are not enforced today, but may be in the ## future. ## ## During argument parsing, the list of subcommands will be tried in-order. Due ## to the described invariant, at most one given subcommand will match any ## argument list. choice : List { name : Str, parser : Parser a } -> Parser a choice = \subCommands -> @Parser (SubCommand subCommands) ## Like [parse], runs a parser to completion on a list of arguments. ## ## If the parser fails, a formatted error and help message is returned. # TODO: mono panics in the args example if the type annotation is included # parseFormatted : NamedParser a, List Str -> Result a Str parseFormatted = \@NamedParser parser, args -> Result.mapErr (parse (@NamedParser parser) args) \e -> Str.concat (Str.concat (formatHelp (@NamedParser parser)) "\n\n") (formatError e) indent : Nat -> Str indent = \n -> Str.repeat " " n indentLevel : Nat indentLevel = 4 mapNonEmptyStr = \s, f -> if Str.isEmpty s then s else f s filterMap : List a, (a -> [Some b, None]) -> List b filterMap = \lst, transform -> List.walk lst [] \all, elem -> when transform elem is Some v -> List.append all v None -> all # formatHelp : NamedParser a -> Str formatHelp = \@NamedParser { name, help, parser } -> fmtHelp = mapNonEmptyStr help \helpStr -> "\n\(helpStr)" cmdHelp = toHelp parser fmtCmdHelp = formatHelpHelp 0 cmdHelp """ \(name)\(fmtHelp) \(fmtCmdHelp) """ # formatHelpHelp : Nat, Help -> Str formatHelpHelp = \n, cmdHelp -> indented = indent n when cmdHelp is SubCommands cmds -> fmtCmdHelp = Str.joinWith (List.map cmds \subCmd -> formatSubCommand (n + indentLevel) subCmd) "\n\n" """ \(indented)COMMANDS: \(fmtCmdHelp) """ Config configs -> optionConfigs = filterMap configs (\config -> when config is Option c -> Some c _ -> None) positionalConfigs = filterMap configs (\config -> when config is Positional c -> Some c _ -> None) fmtOptionsHelp = if List.isEmpty optionConfigs then "" else helpStr = optionConfigs |> List.map (\c -> formatOptionConfig (n + indentLevel) c) |> Str.joinWith "\n" """ \(indented)OPTIONS: \(helpStr) """ fmtPositionalsHelp = if List.isEmpty positionalConfigs then "" else helpStr = positionalConfigs |> List.map (\c -> formatPositionalConfig (n + indentLevel) c) |> Str.joinWith "\n" """ \(indented)ARGS: \(helpStr) """ Str.concat fmtPositionalsHelp fmtOptionsHelp formatSubCommand = \n, { name, help } -> indented = indent n fmtHelp = formatHelpHelp (n + indentLevel) help "\(indented)\(name)\(fmtHelp)" formatOptionConfig : Nat, OptionConfig -> Str formatOptionConfig = \n, { long, short, help, type } -> indented = indent n formattedShort = mapNonEmptyStr short \s -> ", -\(s)" formattedType = formatOptionType type formattedHelp = mapNonEmptyStr help \h -> " \(h)" "\(indented)--\(long)\(formattedShort)\(formattedHelp) (\(formattedType))" formatPositionalConfig : Nat, PositionalConfig -> Str formatPositionalConfig = \n, { name, help } -> indented = indent n formattedHelp = mapNonEmptyStr help \h -> " \(h)" "\(indented)\(name)\(formattedHelp)" formatOptionType : OptionType -> Str formatOptionType = \type -> when type is Bool -> "boolean" Str -> "string" I64 -> "integer, 64-bit signed" quote = \s -> "\"\(s)\"" formatError : ParseError [] -> Str formatError = \err -> when err is ProgramNameNotProvided programName -> "The program name \"\(programName)\" was not provided as a first argument!" MissingPositional arg -> "The argument `\(arg)` is required but was not provided!" MissingRequiredOption arg -> "The option `--\(arg)` is required but was not provided!" WrongOptionType { arg, expected } -> formattedType = formatOptionType expected "The option `--\(arg)` expects a value of type \(formattedType)!" SubCommandNotFound { choices } -> fmtChoices = List.map choices quote |> Str.joinWith ", " """ A subcommand was expected, but not found! The available subcommands are: \t\(fmtChoices) """ IncorrectSubCommand { found, choices } -> fmtFound = quote found fmtChoices = List.map choices quote |> Str.joinWith ", " """ The \(fmtFound) subcommand was found, but it's not expected in this context! The available subcommands are: \t\(fmtChoices) """ ## Applies one parser over another, mapping parser. ## ## `withParser mapper parser` produces a parser that will parse an argument list ## with `parser` first, then parse the remaining list with `mapper`, and feed ## the result of `parser` to `mapper`. ## ## This provides a way to chain the results of multiple parsers together. For ## example, to combine the results of two [strOption] arguments into a record, ## you could use ## ## ``` ## succeed (\host -> \port -> { host, port }) ## |> withParser (strOption { long: "host" }) ## |> withParser (strOption { long: "port" }) ## ``` withParser = \arg1, arg2 -> andMap arg2 arg1 mark = \args -> { args, taken: Set.empty {} } # boolean undashed long optional is missing expect parser = boolOption { long: "foo" } parseHelp parser (mark ["foo"]) == Err (MissingRequiredOption "foo") # boolean dashed long optional without value is missing expect parser = boolOption { long: "foo" } parseHelp parser (mark ["--foo"]) == Err (MissingRequiredOption "foo") # boolean dashed long optional with value is determined true expect parser = boolOption { long: "foo" } parseHelp parser (mark ["--foo", "true"]) == Ok Bool.true # boolean dashed long optional with value is determined false expect parser = boolOption { long: "foo" } parseHelp parser (mark ["--foo", "false"]) == Ok Bool.false # boolean dashed long optional with value is determined wrong type expect parser = boolOption { long: "foo" } parseHelp parser (mark ["--foo", "not-a-boolean"]) == Err (WrongOptionType { arg: "foo", expected: Bool }) # boolean dashed short optional with value is determined true expect parser = boolOption { long: "foo", short: "F" } parseHelp parser (mark ["-F", "true"]) == Ok Bool.true # boolean dashed short optional with value is determined false expect parser = boolOption { long: "foo", short: "F" } parseHelp parser (mark ["-F", "false"]) == Ok Bool.false # boolean dashed short optional with value is determined wrong type expect parser = boolOption { long: "foo", short: "F" } parseHelp parser (mark ["-F", "not-a-boolean"]) == Err (WrongOptionType { arg: "foo", expected: Bool }) # string dashed long option without value is missing expect parser = strOption { long: "foo" } parseHelp parser (mark ["--foo"]) == Err (MissingRequiredOption "foo") # string dashed long option with value is determined expect parser = strOption { long: "foo" } parseHelp parser (mark ["--foo", "itsme"]) == Ok "itsme" # string dashed short option without value is missing expect parser = strOption { long: "foo", short: "F" } parseHelp parser (mark ["-F"]) == Err (MissingRequiredOption "foo") # string dashed short option with value is determined expect parser = strOption { long: "foo", short: "F" } parseHelp parser (mark ["-F", "itsme"]) == Ok "itsme" # i64 dashed long option without value is missing expect parser = i64Option { long: "foo" } parseHelp parser (mark ["--foo"]) == Err (MissingRequiredOption "foo") # i64 dashed long option with value is determined positive expect parser = i64Option { long: "foo" } parseHelp parser (mark ["--foo", "1234"]) == Ok 1234 # i64 dashed long option with value is determined negative expect parser = i64Option { long: "foo" } parseHelp parser (mark ["--foo", "-1234"]) == Ok -1234 # i64 dashed short option without value is missing expect parser = i64Option { long: "foo", short: "F" } parseHelp parser (mark ["-F"]) == Err (MissingRequiredOption "foo") # i64 dashed short option with value is determined expect parser = i64Option { long: "foo", short: "F" } parseHelp parser (mark ["-F", "1234"]) == Ok 1234 # two string parsers complete cases expect parser = succeed (\foo -> \bar -> "foo: \(foo) bar: \(bar)") |> withParser (strOption { long: "foo" }) |> withParser (strOption { long: "bar" }) cases = [ ["--foo", "true", "--bar", "baz"], ["--bar", "baz", "--foo", "true"], ["--foo", "true", "--bar", "baz", "--other", "something"], ] List.all cases \args -> parseHelp parser (mark args) == Ok "foo: true bar: baz" # one argument is missing out of multiple expect parser = succeed (\foo -> \bar -> "foo: \(foo) bar: \(bar)") |> withParser (strOption { long: "foo" }) |> withParser (strOption { long: "bar" }) List.all [ parseHelp parser (mark ["--foo", "zaz"]) == Err (MissingRequiredOption "bar"), parseHelp parser (mark ["--bar", "zaz"]) == Err (MissingRequiredOption "foo"), ] (\b -> b) # string and boolean parsers build help expect parser = succeed (\foo -> \bar -> \_bool -> "foo: \(foo) bar: \(bar)") |> withParser (strOption { long: "foo", help: "the foo option" }) |> withParser (strOption { long: "bar", short: "B" }) |> withParser (boolOption { long: "boolean" }) toHelp parser == Config [ Option { long: "foo", short: "", help: "the foo option", type: Str }, Option { long: "bar", short: "B", help: "", type: Str }, Option { long: "boolean", short: "", help: "", type: Bool }, ] # format option is missing expect parser = boolOption { long: "foo" } when parseHelp parser (mark ["foo"]) is Ok _ -> Bool.false Err e -> err = formatError e err == "The option `--foo` is required but was not provided!" # format option has wrong type expect parser = boolOption { long: "foo" } when parseHelp parser (mark ["--foo", "12"]) is Ok _ -> Bool.false Err e -> err = formatError e err == "The option `--foo` expects a value of type boolean!" # format help menu with only options expect parser = succeed (\_foo -> \_bar -> \_baz -> \_bool -> "") |> withParser (strOption { long: "foo", help: "the foo option" }) |> withParser (strOption { long: "bar", short: "B" }) |> withParser (strOption { long: "baz", short: "z", help: "the baz option" }) |> withParser (boolOption { long: "boolean" }) |> program { name: "test" } formatHelp parser == """ test OPTIONS: --foo the foo option (string) --bar, -B (string) --baz, -z the baz option (string) --boolean (boolean) """ # format help menu with subcommands expect parser = choice [ succeed (\user -> \pw -> "\(user)\(pw)") |> withParser (strOption { long: "user" }) |> withParser (strOption { long: "pw" }) |> subCommand "login", succeed (\file -> \url -> "\(file)\(url)") |> withParser (strOption { long: "file" }) |> withParser (strOption { long: "url" }) |> subCommand "publish", ] |> program { name: "test" } formatHelp parser == """ test COMMANDS: login OPTIONS: --user (string) --pw (string) publish OPTIONS: --file (string) --url (string) """ # format help menu with program help message expect parser = choice [subCommand (succeed "") "login"] |> program { name: "test", help: "a test cli app" } formatHelp parser == """ test a test cli app COMMANDS: login """ # subcommand parser expect parser = choice [ succeed (\user -> \pw -> "logging in \(user) with \(pw)") |> withParser (strOption { long: "user" }) |> withParser (strOption { long: "pw" }) |> subCommand "login", succeed (\file -> \url -> "\(file)\(url)") |> withParser (strOption { long: "file" }) |> withParser (strOption { long: "url" }) |> subCommand "publish", ] |> program { name: "test" } when parse parser ["test", "login", "--pw", "123", "--user", "abc"] is Ok result -> result == "logging in abc with 123" Err _ -> Bool.false # subcommand of subcommand parser expect parser = choice [ choice [ succeed (\user -> \pw -> "logging in \(user) with \(pw)") |> withParser (strOption { long: "user" }) |> withParser (strOption { long: "pw" }) |> subCommand "login", ] |> subCommand "auth", ] |> program { name: "test" } when parse parser ["test", "auth", "login", "--pw", "123", "--user", "abc"] is Ok result -> result == "logging in abc with 123" Err _ -> Bool.false # subcommand not provided expect parser = choice [subCommand (succeed "") "auth", subCommand (succeed "") "publish"] when parseHelp parser (mark []) is Ok _ -> Bool.true Err e -> err = formatError e err == """ A subcommand was expected, but not found! The available subcommands are: \t"auth", "publish" """ # subcommand doesn't match choices expect parser = choice [subCommand (succeed "") "auth", subCommand (succeed "") "publish"] when parseHelp parser (mark ["logs"]) is Ok _ -> Bool.true Err e -> err = formatError e err == """ The "logs" subcommand was found, but it's not expected in this context! The available subcommands are: \t"auth", "publish" """ # parse positional expect parser = str { name: "foo" } parseHelp parser (mark ["myArg"]) == Ok "myArg" # parse positional with option expect parser = succeed (\foo -> \bar -> "foo: \(foo), bar: \(bar)") |> withParser (strOption { long: "foo" }) |> withParser (str { name: "bar" }) cases = [ ["--foo", "true", "baz"], ["baz", "--foo", "true"], ] List.all cases \args -> parseHelp parser (mark args) == Ok "foo: true, bar: baz" # parse positional with subcommand expect parser = choice [ str { name: "bar" } |> subCommand "hello", ] parseHelp parser (mark ["hello", "foo"]) == Ok "foo" # missing positional expect parser = str { name: "bar" } parseHelp parser (mark []) == Err (MissingPositional "bar")