Skip to content

Latest commit

 

History

History
548 lines (416 loc) · 23.7 KB

README.md

File metadata and controls

548 lines (416 loc) · 23.7 KB

Console Application

NuGet Version and Downloads count Check

The Console application eases the creation of beautiful and testable command line interfaces in F#.

The Console application allows you to create command-line commands. Your console commands can be used for any recurring task, such as cronjobs, imports, or other batch jobs.

This library is inspired by Symfony/Style and Symfony/Console

Table of Contents

Installation

dotnet add package MF.ConsoleApplication

Creating a Console Application

open MF.ConsoleApplication

[<EntryPoint>]
let main argv =
    consoleApplication {
        name "Example"
        version "1.0.0"
        info ApplicationInfo.NameAndVersion

        command "my:first-command" {
            Description = "This is my first command."
            Help = Some "It even has some explicit help. 🎉"
            Arguments = [
                Argument.required "firstName" "First name of the user."
                Argument.optional "lastName" "Last name" None
            ]
            Options = [
                Option.noValue "formal" None "Whether to use a formal greetings."
                Option.optional "yell" (Some "y") "Whether to greet by yelling." None
            ]
            Initialize = None
            Interact = None
            Execute = Execute <| fun (input, output) ->
                let names =
                    let firstName = input |> Input.getArgumentValue "firstName"

                    match input with
                    | Input.Argument.OptionalValue "lastName" lastName -> sprintf "%s %s" firstName lastName
                    | _ -> firstName

                let greet name =
                    match input with
                    | Input.Option.IsSet "formal" _ -> sprintf "Good morning, %s" name
                    | _ -> sprintf "Hello, %s" name

                let (shouldYell, loudly) =
                    match input with
                    | Input.Option.Has "yell" value ->
                        match value |> OptionValue.stringValue with
                        | Some "loud" -> (true, true)
                        | _ -> (true, false)
                    | _ -> (false, false)

                names
                |> greet
                |> fun greetings ->
                    let greetings = if shouldYell then greetings.ToUpper() else greetings

                    if loudly then greetings + "!!!" else greetings + "."
                |> output.Message

                ExitCode.Success
        }
    }
    |> run argv

Outputs:

Command: dotnet example.dll my:first-command Mortal

Example <1.0.0>
===============

Hello, Mortal!

Command: dotnet example.dll my:first-command Mortal Flesh

Example <1.0.0>
===============

Hello, Mortal Flesh!

Command: dotnet example.dll my:first-command Mortal Flesh --formal

Example <1.0.0>
===============

Good morning, Mortal Flesh.

Command: dotnet example.dll my:first-command Mortal Flesh --yell

Example <1.0.0>
===============

HELLO, MORTAL FLESH.

Command: dotnet example.dll my:first-command Mortal Flesh --yell loud --formal

Example <1.0.0>
===============

GOOD MORNING, MORTAL FLESH!!!

Command: dotnet example.dll my:first-command --help

Example <1.0.0>
===============

Description:
    This is my first command.

Usage:
    my:first-command [options] [--] <firstName> [<lastName>]

Arguments:
    firstName  First name of the user.
    lastName   Last name

Options:
        --formal          Whether to use a formal greetings.
    -y, --yell[=YELL]     Whether to greet by yelling.
    -h, --help            Display this help message
    -q, --quiet           Do not output any message
    -V, --version         Display this application version
    -n, --no-interaction  Do not ask any interactive question
    -v|vv|vvv, --verbose  Increase the verbosity of messages

Help
    It even has some explicit help. 🎉

Builder

Function Arguments Description
name name: string It will set a name of the application. (This is part of ApplicationInfo.)
version version: string It will set a version of the application. (This is part of ApplicationInfo.)
title title: string It will set a main title of the application. (This is part of ApplicationInfo.)
description description: string It will set a description of the application. (It is visible in about command.)
meta string * string It will add an application meta information. (It is visible in about command.)
(string * string) list It will add multiple application meta information. (It is visible in about command.)
git repository: string option * branch: string option * commit: string option It will register git meta information, which is visible in about command.
gitRepository repository: string It will register git repository meta information, which is visible in about command.
gitBranch branch: string It will register git branch meta information, which is visible in about command.
gitCommit commit: string It will register git commit meta information, which is visible in about command.
info ApplicationInfo It will define, how application info will be shown in commands output. (Default is Hidden)
showOptions OptionDecorationLevel It will define, how options will be shown in the command help output. (Default is Minimal)
command commandName: string, CommandDefinition It will register a command to the application.
defaultCommand commandName: string It will set a name of default command. Default command is run when no command name is pass to the arguments. (Default is list.)
useOutput Output It will override Output in IO, which gets every command life-cycle function. (Default is implemented by ConsoleStyle)
useAsk question: string -> answer: string It will override an Ask function, which is used in Interact life-cycle stage. (Default is implemented by ConsoleStyle)
updateOutput Output -> Output Function which allows to change the output (set style, different outputInterface for a ConsoleStyle and more)
withStyle MF.ConsoleStyle.Style A style which will be set to the Output.
withCustomTags MF.ConsoleStyle.CustomTag list It will register custom tags to the Output Style.
Result<MF.ConsoleStyle.CustomTag, string> list It will handle results and register custom tags to the Output Style.

NOTES:

  • All parts of ApplicationInfo are shown in about command
  • All functions has the first argument for the state: Definition, but this is a current state of the application and it is passed implicitly in the background by computation expression.
  • All functions are optional to call. Those which sets a value will override the previous definition.

Life-cycle

Commands have three life-cycle functions that are invoked when running the command:

  • Initialize (optional)
    • This function is executed before the interact and the execute functions. Its main purpose is to initialize variables used in the rest of the command functions.
  • Interact (optional)
    • This function is executed after initialize and before execute. Its purpose is to check if some of the options/arguments are missing and interactively ask the user for those values. This is the last place where you can ask for missing options/arguments. After this function, missing options/arguments will result in an error.
    • This stage may be skipped by setting --no-interaction option.
  • Execute (required)
    • This method is executed after initialize and interact. It contains the logic you want the command to execute.
    • It has multiple variants:
      • Execute
      • ExecuteResult
      • ExecuteAsync
      • ExecuteAsyncResult

All life-cycle functions gets an IO (Input * Output).

Both Initialize and Interact may change the IO on the way. Execute will get final result of IO.

TIP: There is a Type for every Life-cycle function.

Interaction

As was mentioned before, its purpose is to check if some of the options/arguments are missing and interactively ask the user for those values.

Interact function gets an InteractiveInput, which is { Input: Input; Ask: Ask } record, where Ask is string -> string function.

Examples:

Add missing argument:

Interact = Some (fun ({ Input = input; Ask = ask }, output) ->
    let input =
        match input with
        | Input.Argument.Has "mandatoryArg" _ -> input   // already has a value
        | _ ->  // value is missing, as user for a value
            ask "Please, give a value for mandatory argument:"
            |> Input.Argument.set input "mandatoryArg"

    (input, output)
)

Add missing option:

Interact = Some (fun (input, output) ->
    let input =
        match input.Input with
        | Input.Option.Has "message" value ->
            output.Message <| sprintf "Message value is already given from arguments, it is: %s" (value |> OptionValue.value)
            input.Input
        | _ ->
            input.Ask "Message:"
            |> Input.Option.set input.Input "message"

    (input, output)
)

Console Input (Arguments & Options)

The most interesting part of the commands are the arguments and options that you can make available. These arguments and options allow you to pass dynamic information from the terminal to the command.

Command name

Command name is a special type of required argument, which has a reserved name (command) and will always be there (if it is not passed by user, it will be a default command).

Shortcut Syntax

You do not have to type out the full command names. You can just type the shortest unambiguous name to run a command. So if there are non-clashing commands, then you can run help like this:

dotnet example.dll h

If you have commands using : to namespace commands then you only need to type the shortest unambiguous text for each part. If you have created the my:first-command as shown above then you can run it with:

dotnet example.dll m:f Mortal Flesh

If you enter a short command that's ambiguous (i.e. there are more than one command that match), then no command will be run and some suggestions of the possible commands to choose from will be output.

Arguments

Arguments are the strings - separated by spaces - that come after the command name itself. They are ordered, and can be optional or required. It is also possible to let an argument take a list of values (only the last argument ca be a list).

Note: There is a more detailed documentation here.

There are four argument variants you can use:

  • Required
    • The argument is mandatory. The command doesn't run if the argument isn't provided
  • Optional
    • The argument is optional and therefore can be omitted.
  • RequiredArray
    • The argument can contain one or more values. For that reason, it must be used at the end of the argument list.
  • Array
    • The argument can contain any number of values. For that reason, it must be used at the end of the argument list.

There are many ways how to access Arguments:

  • Through pattern matching

    Active Pattern Description
    Input.Argument.IsDefined Matched when given string is defined as argument name.
    Input.Argument.Has Matched when given string has any value in current Input (default or from args).
    Input.Argument.IsSet Matched when input has argument AND that value is not empty.
    • Active patterns for accessing a value
    Active Pattern Description Value
    Input.Argument.Value Matched when input has argument. (Fail with exception when value is not set or it is a list.) string
    Input.Argument.OptionalValue Matched when input has argument AND it has a single value. string
    Input.Argument.ListValue Matched when input has argument. string list
  • Just get a value from Input

    Function Description
    Input.Argument.tryGet Returns an ArgumentValue option, when Input has argument.
    Input.Argument.get Returns an ArgumentValue, when Input has argument OR fail with exception.
    Input.Argument.value Returns a string value from ArgumentValue, when Input has argument OR fail with exception.
    Input.Argument.asString Returns a string option value from ArgumentValue, when Input has argument.
    Input.Argument.asInt Returns an int option value from ArgumentValue, when Input has argument. (It fails with an exception when string value is not int.)
    Input.Argument.asList Returns an string list value from ArgumentValue, when Input has argument. (It returns a list even for single values.)
    Input.Argument.tryGetAsInt Returns an int option value from ArgumentValue, when Input has argument. (It returns None when string value is not int.)
    Input.Argument.isValueSet Checks whether argument has a value AND that value is not empty.

    Note: All functions above will fail with an exception when given "argument" is not defined.

Options

Unlike arguments, options are not ordered (meaning you can specify them in any order) and are specified with two dashes (e.g. --yell). Options are always optional, and can be setup to accept a value (e.g. --dir=src) or simply as a boolean flag without a value (e.g. --yell).

You can also declare a one-letter shortcut that you can call with a single dash (e.g. -y).

Note that to comply with the docopt standard, long options can specify their values after a white space or an = sign (e.g. --iterations 5 or --iterations=5), but short options can only use white spaces or no separation at all (e.g. -i 5 or -i5).

Note: There is a more detailed documentation here.

There are five option variants you can use:

  • ValueNone
    • Do not accept input for this option (e.g. --yell).
  • ValueRequired
    • This value is required (e.g. --iterations=5 or -i5), the option itself is still optional;
  • ValueOptional
    • This option may or may not have a value (e.g. --yell or --yell=loud).
  • ValueIsArray
    • This option accepts multiple values (e.g. --dir=/foo --dir=/bar)
  • ValueRequiredArray
    • This option accepts multiple not empty values (e.g. --dir=/foo --dir=/bar)

Application Options

Are built-in options, which every command has. And they are parsed before other given arguments. They may even bypass other values.

  • Help (--help, -h)
    • If only --help option is passed, it will show overall help for console application.
    • You can get help information for any command, if you pass a command name and --help option (this would ignore any other options).
  • Version (--version, -V)
    • This will show current application name and version. (Default name is Console Application)
  • NoInteraction (--no-interaction, -n)
    • You can suppress any interactive questions from the command you are running with this option.
  • Quiet (--quiet, -q)
    • You can suppress output with this option.
  • Verbose (--verbose, -v|vv|vvv)
    • You can get more verbose message (if this is supported for a command).
    • Number of given v determines a level of verbosity.

There are many ways how to access Options:

  • Through pattern matching

    Active Pattern Description
    Input.Option.IsDefined Matched when given string is defined as option name.
    Input.Option.Has Matched when given string has any value in current Input (default or from args).
    Input.Option.IsSet Matched when input has option AND that value is not empty.
    • Active patterns for accessing a value
    Active Pattern Description Value
    Input.Option.Value Matched when input has option. (Fail with exception when value is not set or it is a list.) string
    Input.Option.OptionalValue Matched when input has option AND it has a single value. string
    Input.Option.ListValue Matched when input has option. string list
  • Just get a value from Input

    Function Description
    Input.Option_.tryGet Returns an OptionValue option, when Input has option.
    Input.Option_.get Returns an OptionValue, when Input has option OR fail with exception.
    Input.Option_.value Returns a string value from OptionValue, when Input has option OR fail with exception.
    Input.Option_.asString Returns a string option value from OptionValue, when Input has option.
    Input.Option_.asInt Returns an int option value from OptionValue, when Input has option. (It fails with an exception when string value is not int.)
    Input.Option_.asList Returns an string list value from OptionValue, when Input has option. (It returns a list even for single values.)
    Input.Option_.tryGetAsInt Returns an int option value from OptionValue, when Input has option. (It returns None when string value is not int.)
    Input.Option.isValueSet Checks whether option has a value AND that value is not empty.

    Note: All functions above will fail with an exception when given "option" is not defined.

Console Output

Output is handled by ConsoleStyle.

You can even alter an Output by using useOutput function to set your own implementation.


Default commands

There are two default commands:

  • List
    • Shows list of available commands.
    • It is a default command, when you do not specify your own (by defaultCommand console application function), it means, when user do not specify a command in arguments, the List will be used.
  • Help
    • Shows help for commands.
    • It can be triggered by --help (-h) option as well.

There are some different ways to display the same output:

  • help, help --help or help help will execute a Help command for itself
  • list or no-command will execute list command (when no other default command is set)
  • list --help, help list or --help will execute Help command for List command

Help

Displays help for a command

dotnet path/to/console.dll help
{application info}

Description:
    Displays help for a command

Usage:
    help [options] [--] [<command_name>]

Arguments:
    command_name  The command name [default: "help"]

Options:
    -h, --help            Display this help message
    -q, --quiet           Do not output any message
    -V, --version         Display this application version
    -n, --no-interaction  Do not ask any interactive question
        --no-progress     Whether to disable all progress bars
        --no-ansi         Whether to disable all markup with ansi formatting
    -v|vv|vvv, --verbose  Increase the verbosity of messages

Help
    The help command displays help for a given command:

        dotnet path/to/console.dll help list

    To display list of available commands, please use list command.

List

Show list of available commands.

dotnet path/to/console.dll list
{application info}

Description:
    Lists commands

Usage:
    list [options] [--] [<namespace>]

Arguments:
    namespace  The namespace name

Options:
    -h, --help            Display this help message
    -q, --quiet           Do not output any message
    -V, --version         Display this application version
    -n, --no-interaction  Do not ask any interactive question
        --no-progress     Whether to disable all progress bars
        --no-ansi         Whether to disable all markup with ansi formatting
    -v|vv|vvv, --verbose  Increase the verbosity of messages

Help
    The list command lists all commands:

        dotnet path/to/console.dll list

    You can also display the commands for a specific namespace:

        dotnet path/to/console.dll list test

About

Show list of available commands.

dotnet path/to/console.dll about
{application info}

Description:
    Displays information about the current project.

Usage:
    about [options]

Options:
    -h, --help            Display this help message
    -q, --quiet           Do not output any message
    -V, --version         Display this application version
    -n, --no-interaction  Do not ask any interactive question
        --no-progress     Whether to disable all progress bars
        --no-ansi         Whether to disable all markup with ansi formatting
    -v|vv|vvv, --verbose  Increase the verbosity of messages

Help:
    The about command displays information about the current project:

        dotnet bin/Debug/net6.0/example.dll about about

    There are multiple sections shown in the output:
      - current project details/meta information
      - environment
      - console application library

Create help message

There are some placeholders which may help you to create a better help message.

  • {{command.name}} - The name of the current command.
  • {{command.full_name}} - The name of the current command including a relative path.

TIP: You can use a Help.lines utility function to format your lines.

This is a help, used in list command:

Help =
    Help.lines [
        "The <c:green>{{command.name}}</c> command lists all commands:"
        "        <c:green>dotnet {{command.full_name}}</c>"
        "    You can also display the commands for a specific namespace:"
        "        <c:green>dotnet {{command.full_name}} test</c>"
    ]

Tips

Add bin/console file with following content to allow a simple entry point for your application

#!/usr/bin/env bash

APP="my-console"
NET="net6.0"

CONSOLE="bin/Debug/$NET/$APP.dll"
if [ ! -f "$CONSOLE" ]; then
    CONSOLE="bin/Release/$NET/$APP.dll"
fi

dotnet "$CONSOLE" "$@"

Then just go by bin/console list or other commands.