Cli

Weave together a CLI parser using the : <- builder notation!

This module is the entry point for creating CLIs using Weaver. To get started, call the weave method and pass a record builder to it. You can pass Opts, Params, or Subcommands as fields, and Weaver will automatically register them in its config as well as build a parser with the inferred types of the fields you set.

Cli.weave {
    alpha: <- Opt.u64 { short: "a", help: "Set the alpha level" },
    verbosity: <- Opt.count { short: "v", long: "verbose", help: "How loud we should be." },
    files: <- Param.strList { name: "files", help: "The files to process." },
}
|> Cli.finish {
    name: "example",
    version: "v1.0.0",
    authors: ["Some One <some.one@mail.com>"],
    description: "Do some work with some files."
}
|> Cli.assertValid

You can also add create subcommands in the same way:

fooSubcommand =
    Cli.weave {
        alpha: <- Opt.u64 {
            short: "a",
            help: "Set the alpha level",
        },
    }
    |> Subcommand.finish {
        name: "foo",
        description: "Foo some stuff."
        mapper: Foo,
    }

barSubcommand =
    Cli.weave {
        # We allow two subcommands of the same parent to have overlapping
        # fields since only one can ever be parsed at a time.
        alpha: <- Opt.u64 {
            short: "a",
            help: "Set the alpha level",
        },
    }
    |> Subcommand.finish {
        name: "bar",
        description: "Bar some stuff."
        mapper: Bar,
    }

Cli.weave {
    sc: <- Subcommand.optional [fooSubcommand, barSubcommand],
}

And those subcommands can have their own subcommands! But anyway...

Once you have a command with all of its fields configured, you can turn it into a parser using the finish function, followed by the assertValid function that asserts that the CLI is well configured.

From there, you can take in command line arguments and use your data if it parses correctly:

cliParser =
    Cli.weave {
        alpha: <- Opt.u64 { short: "a", help: "Set the alpha level" },
        verbosity: <- Opt.count { short: "v", long: "verbose", help: "How loud we should be." },
        files: <- Param.strList { name: "files", help: "The files to process." },
    }
    |> Cli.finish {
        name: "example",
        version: "v1.0.0",
        authors: ["Some One <some.one@mail.com>"],
        description: "Do some work with some files."
    }
    |> Cli.assertValid

expect
    cliParser
    |> Cli.parseOrDisplayMessage ["example", "-a", "123", "-vvv", "file.txt", "file-2.txt"]
    == Ok { alpha: 123, verbosity: 3, files: ["file.txt", "file-2.txt"] }

You're ready to start parsing arguments using Weaver!

If you want to see more examples, check the examples folder in the repository.

note: Opts must be set before an optional Subcommand field is given, and the Subcommand field needs to be set before Params are set. Param lists also cannot be followed by anything else including themselves. These requirements ensure we parse arguments in the right order. Luckily, all of this is ensured at the type level.

CliParser state

A parser that interprets command line arguments and returns well-formed data.

weave : base -> CliBuilder base GetOptionsAction

Begin weaving together a CLI builder using the : <- builder notation.

Check the module-level documentation for general usage instructions.

expect
    { parser } =
        Cli.weave {
            verbosity: <- Opt.count { short: "v", long: "verbose" },
        }
        |> Cli.finish { name: "example" }
        |> Cli.assertValid

   parser ["example", "-vvv"]
   == SuccessfullyParsed { verbosity: 3 }

finish : CliBuilder state action, CliConfigParams -> Result (CliParser state) CliValidationErr

Bundle a CLI builder into a parser, ensuring that its configuration is valid.

Though the majority of the validation we'd need to do for type safety is rendered unnecessary by the design of this library, there are some things that the type system isn't able to prevent. Here are the checks we currently perform after building your CLI parser:

If you would like to avoid these validations, you can use finishWithoutValidating instead, but you may receive some suprising results when parsing because our parsing logic assumes the above validations have been made.

expect
    Cli.weave {
        verbosity: <- Opt.count { short: "v", long: "verbose" },
    }
    |> Cli.finish { name: "example" }
    |> Result.isOk

expect
    Cli.weave {
        verbosity: <- Opt.count { short: "" },
    }
    |> Cli.finish { name: "example" }
    |> Result.isErr

finishWithoutValidating : CliBuilder state action, CliConfigParams -> CliParser state

Bundle a CLI builder into a parser without validating its configuration.

We recommend using the finish function to validate your parser as our library's logic assumes said validation has taken place. However, this method could be useful if you know better than our validations about the correctness of your CLI.

expect
    { parser } =
        Cli.weave {
            verbosity: <- Opt.count { short: "v", long: "verbose" },
        }
        |> Cli.finishWithoutValidating { name: "example" }

    parser ["example", "-v", "-v"]
    == SuccessfullyParsed { verbosity: 2 }

assertValid : Result (CliParser state) CliValidationErr -> CliParser state

Assert that a CLI is properly configured, crashing your program if not.

Given that there are some aspects of a CLI that we cannot ensure are correct at compile time, the easiest way to ensure that your CLI is properly configured is to validate it and crash immediately on failure, following the Fail Fast principle.

You can avoid making this assertion by handling the error yourself or by finish your CLI with the finishWithoutValidating function, but the validations we perform (detailed in finish's docs) are important for correct parsing.

Cli.weave {
    a: <- Opt.num { short: "a" }
}
|> Cli.finish { name: "example" }
|> Cli.assertValid

parseOrDisplayMessage : CliParser state, List Str -> Result state Str

Parse arguments using a CLI parser or show a useful message on failure.

We have the following priorities in returning messages to the user:

  1. If the -h/--help flag is passed, the help page for the command/subcommand called will be displayed no matter if your arguments were correctly parsed.
  2. If the -V/--version flag is passed, the version for the app will be displayed no matter if your arguments were correctly parsed.
  3. If the provided arguments were parsed and neither of the above two built-in flags were passed, we return to you your data.
  4. If the provided arguments were not correct, we return a short message with which argument was not provided correctly, followed by the usage section of the relevant command/subcommand's help text.
exampleCli =
    Cli.weave {
        verbosity: <- Opt.count { short: "v", help: "How verbose our logs should be." },
    }
    |> Cli.finish {
        name: "example",
        version: "v0.1.0",
        description: "An example CLI.",
    }
    |> Cli.assertValid

expect
    exampleCli
    |> Cli.parseOrDisplayMessage ["example", "-h"]
    == Err
        """
        example v0.1.0

        An example CLI.

        Usage:
          example [OPTIONS]

        Options:
          -v             How verbose our logs should be.
          -h, --help     Show this help page.
          -V, --version  Show the version.
        """

expect
    exampleCli
    |> Cli.parseOrDisplayMessage ["example", "-V"]
    == Err "v0.1.0"

expect
    exampleCli
    |> Cli.parseOrDisplayMessage ["example", "-v"]
    == Ok { verbosity: 1 }

expect
    exampleCli
    |> Cli.parseOrDisplayMessage ["example", "-x"]
    == Err
        """
        Error: The argument -x was not recognized.

        Usage:
          example [OPTIONS]
        """