Skip to content

Releases: JordanMarr/FSharp.SystemCommandLine

v2.0.0-beta7

17 Aug 15:12
5050816
Compare
Choose a tag to compare

This release brings FSharp.SystemCommandLine up to date with the new v2.0.0-beta7 of System.CommandLine.

Changes

  • BREAKING: The commandLineConfiguration computation expression has been removed (because System.CommandLine.CommandLineConfiguration has been removed).
  • A new ManualInvocation.rootCommand has been added. Unlike the default rootCommand which is automatically executes, this simply returns a System.CommandLine.RootCommand for you to manually invoke:
module Global = 
    let enableLogging = option "--enable-logging" |> recursive

let zipCmd = 
  command "zip" {
    description "Zips a directory."
    inputs (argument "directory" |> validateDirectoryExists)
    setAction (fun dir -> async { ... })
  } 

let unzipCmd = 
  command "unzip" {
    description "Unzips a directory."
    inputs (argument "directory" |> validateDirectoryExists)
    setAction (fun dir -> async { ... })
  } 

let main (argv: string[]) =
    let cmd = 
        ManualInvocation.rootCommand {
            description "Zip or Unzip a Directory"
            noActionAsync
            addInputs Global.options
            addCommands [ zipCmd; unzipCmd ]
        }

    let parseResult = cmd.Parse(argv)

    let loggingEnabled = Global.enableLogging.GetValue parseResult
    printfn $"Logging enabled: {loggingEnabled}"

    parseResult.InvokeAsync()
    |> Async.AwaitTask
    |> Async.RunSynchronously
  • configure (in rootCommand) has been deprecated.
  • configureParser has been added to rootCommand and the new ManualInvocation.rootCommand.
  • configureInvocation has been added to rootCommand.

v2.0.0-beta5

26 Jun 16:29
Compare
Choose a tag to compare

This release provides compatibility with the latest System.CommandLine v2.0.0-beta5 rework!

New Input API

The new Input API has been redesigned to be easier to use and more extensible.
View the Input API in the README for more details and extensibility examples.

Old:

let unzipFile = Input.Argument<FileInfo>("The file to unzip")
let outputDir = Input.OptionMaybe<DirectoryInfo>(["--output"; "-o"], "The output directory")

New:

let unzipFile = argument<FileInfo> "unzipFile" |> desc "The file to unzip"
let outputDir = optionMaybe<DirectoryInfo> "--output" |> alias "-o" |> desc "The output directory"

Improved Validation

The new Input.validate function allows you to return a validation Result against the parsed value.
(There is also an addValidator function that aligns more closely to the built-in System.CommandLine approach.)

open System.IO
open FSharp.SystemCommandLine
open Input

let unzip (zipFile: FileInfo, outputDirMaybe: DirectoryInfo option) = 
    // Default to the zip file dir if None
    let outputDir = defaultArg outputDirMaybe zipFile.Directory
    printfn $"Unzipping {zipFile.Name} to {outputDir.FullName}..."
    
[<EntryPoint>]
let main argv = 
    rootCommand argv {
        description "Unzips a .zip file"
        inputs (
            argument "zipfile"
            |> description "The file to unzip"
            |> validateFileExists
            |> validate (fun zipFile ->
                if zipFile.Length > 500000
                then Error $"File cannot be bigger than 500 KB"
                else Ok ()
            ),

            optionMaybe "--output"
            |> alias "-o"
            |> description "The output directory"
            |> validateDirectoryExists
        )
        setAction unzip
    }

Showing Help by Default in Root Command

For apps that utilize sub-commands, a common pattern is to display help output when no sub-command is entered or do nothing.

To show help, you can replace setAction with one of two new custom operations: helpAction or helpActionAsync.
NOTE that you must include inputs Input.context.

Non-async:

[<EntryPoint>]
let main argv =
    rootCommand argv {
        description "File System Manager"
        inputs Input.context
        helpAction
        addCommands [ listCmd; deleteCmd ]
    }

Async:

[<EntryPoint>]
let main argv =
    rootCommand argv {
        description "File System Manager"
        inputs Input.context
        helpActionAsync
        addCommands [ listCmd; deleteCmd ]
    }
    |> Async.AwaitTask
    |> Async.RunSynchronously

No Action in Root Command

Another common use case for apps that use only sub-commands is to not provide any action (handler) function for the rootCommand.
Previously, you would do this by setting the action as setAction id or setAction Task.FromResult for async apps.

Now you can express these as: noAction or noActionAsync for async apps.

Non-async:

[<EntryPoint>]
let main argv =
    rootCommand argv {
        description "File System Manager"
        noAction
        addCommands [ listCmd; deleteCmd ]
    }

Async app:

[<EntryPoint>]
let main argv =
    rootCommand argv {
        description "File System Manager"
        noActionAsync
        addCommands [ listCmd; deleteCmd ]
    }
    |> Async.AwaitTask
    |> Async.RunSynchronously

Breaking Changes

  • setHandler is deprecated in favor of setAction to maintain parity with System.CommandLine v2.0.0.beta5 naming changes.
  • Input methods like Input.Option, Input.Argument, Input.OptionMaybe, Input.ArgumentMaybe have been replaced with pipelined functions to avoid "overload soup" and to make it easier and more extensible for users to create their own custom functions against the underlying System.CommandLine API.

v1.0.0-beta4

14 Mar 17:40
47781fa
Compare
Choose a tag to compare

Changes

  • Adds support for "global options".
  • Added an overload to Input GetValue method to get value from an InvocationContext or a ParseResult.
  • Bumped version from v0.18.0-beta4 to v1.0.0-beta4 so as not to scare people away from using the library.

See Global Options example in readme.

v0.18.0-beta4

12 Mar 20:03
Compare
Choose a tag to compare

New Features

  • Adds support for "global options".
module ProgramNestedSubCommands

open System.IO
open FSharp.SystemCommandLine
open System.CommandLine.Invocation

module Global = 
    let enableLogging = Input.Option<bool>("--enable-logging", false)
    let logFile = Input.Option<FileInfo>("--log-file", FileInfo @"c:\temp\default.log")

    type Options = { EnableLogging: bool; LogFile: FileInfo }

    let options: HandlerInput seq = [ enableLogging; logFile ] 

    let bind (ctx: InvocationContext) = 
        { EnableLogging = enableLogging.GetValue ctx
          LogFile = logFile.GetValue ctx }

let listCmd =
    let handler (ctx: InvocationContext, dir: DirectoryInfo) =
        let options = Global.bind ctx
        if options.EnableLogging then 
            printfn $"Logging enabled to {options.LogFile.FullName}"

        if dir.Exists then
            dir.EnumerateFiles()
            |> Seq.iter (fun f -> printfn "%s" f.FullName)
        else
            printfn $"{dir.FullName} does not exist."

    let dir = Input.Argument("directory", DirectoryInfo(@"c:\default"))

    command "list" {
        description "lists contents of a directory"
        inputs (Input.Context(), dir)
        setHandler handler
        addAlias "ls"
    }

let deleteCmd =
    let handler (ctx: InvocationContext, dir: DirectoryInfo, recursive: bool) =
        let options = Global.bind ctx
        if options.EnableLogging then 
            printfn $"Logging enabled to {options.LogFile.FullName}"
        
        if dir.Exists then
            if recursive then
                printfn $"Recursively deleting {dir.FullName}"
            else
                printfn $"Deleting {dir.FullName}"
        else
            printfn $"{dir.FullName} does not exist."

    let dir = Input.Argument("directory", DirectoryInfo(@"c:\default"))
    let recursive = Input.Option("--recursive", false)

    command "delete" {
        description "deletes a directory"
        inputs (Input.Context(), dir, recursive)
        setHandler handler
        addAlias "del"
    }

let ioCmd = 
    command "io" {
        description "Contains IO related subcommands."
        setHandler id
        addGlobalOptions Global.options
        addCommands [ deleteCmd; listCmd ]
    }

[<EntryPoint>]
let main argv =
    rootCommand argv {
        description "Sample app for System.CommandLine"
        setHandler id  
        addCommand ioCmd
    }

Support for command aliases

08 Apr 15:19
Compare
Choose a tag to compare

Changes in v0.17.0-beta4

  • New addAlias and addAliases operations to add command aliases. (thanks to @AngelMunoz! 🚀)
  • Deprecated add overloads and renamed to addInput and addInputs for clarity and consistency

Support for netstandard, `add` overload

01 Apr 19:08
Compare
Choose a tag to compare

New in v0.16.0-beta4

  • Support added for netstandard2.0
  • New add overload that accepts multiple inputs (for when you have a lot!)

Thanks to @jbtule for contributing this in PR #14!

Input helpers

25 Feb 20:25
Compare
Choose a tag to compare

This release adds a few new Input methods to make it more convenient to modify the underlying System.CommandLine properties for Option and Argument inputs.

  • New Input.Option overloads that take a name/alias and a configure function that allows you to manually set properties on the S.CL Option<'T>.
  • New Input.OptionMaybe overloads that take a name/alias and a configure function that allows you to manually set properties on the S.CL Option<'T option>.
  • New Input.Argument overloads that take a name/alias and a configure function that allows you to manually set properties on the S.CL Argument<'T>.

Please see updated docs here:
https://github.com/JordanMarr/FSharp.SystemCommandLine#setting-input-properties-manually

Also, two new alias methods (for easier discoverability) for converting existing SCL inputs:

  • Input.OfOption - An alias for HandlerInput.OfOption
  • Input.OfArgument - An alias for HandlerInput.OfArgument

Handling of greater than 8 inputs

14 Jan 22:12
Compare
Choose a tag to compare

This release provides a convenient way to handle commands that require more than 8 inputs via the new add custom operation on the rootCommand and command builders.

Additional info

Currently, a command handler function is limited to accept a tuple with no more than eight inputs.
If you need more than eight inputs, you can now pass in the InvocationContext to your handler, which will allow you manually get as many input values as you like (assuming they have been registered via the rootCommand or command builder's add operation:

module Program

open FSharp.SystemCommandLine

module Parameters = 
    let words = Input.Option<string[]>(["--word"; "-w"], Array.empty, "A list of words to be appended")
    let separator = Input.OptionMaybe<string>(["--separator"; "-s"], "A character that will separate the joined words.")

let app (ctx: System.CommandLine.Invocation.InvocationContext) =
    // Manually parse as many parameters as you need
    let words = Parameters.words.GetValue ctx
    let separator = Parameters.separator.GetValue ctx

    // Do work
    let separator = separator |> Option.defaultValue ", "
    System.String.Join(separator, words) |> printfn "Result: %s"
    0
    
[<EntryPoint>]
let main argv = 
    let ctx = Input.Context()

    rootCommand argv {
        description "Appends words together"
        inputs ctx
        setHandler app
        add Parameters.words
        add Parameters.separator
    }

Caveats

This manual binding mechanism is less type safe because the compiler has no way to determine if you have manually added all input parameters to the command; so if you forget to add one that is used, it will result in a runtime error.

(For this reason, you should try to limit your commands to 8 parameters when possible, by factoring your app into separate commands.)

beta4 changes

07 Jun 19:49
Compare
Choose a tag to compare

v0.13.0-beta4 adapts to the System.CommandLine beta4 changes.
The API stays mostly the same with the following changes:

  • 💥 Removed Input.InjectedDependency<'T>() (this allowed you to pass in S.CL injected system dependencies like InvocationContext, CancellationToken, IConsole, etc.

  • ⭐ Added Input.Context() for passing InvocationContext to handler function.
    This replaces Input.InjectedDependency<'T>() because InvocationContext can be used to get a CancellationToken, IConsole, etc.

  • Handler functions now support 8 input bindings instead of 16 (following changes to S.CL).
    (If you need more than 8 input bindings, you can pass the InvocationContext to your handler function using the new Input.Context() method which will allow you to manually get as many parsed values as you want.)

The readme has been updated with an example of passing the InvocationContext:
https://github.com/JordanMarr/FSharp.SystemCommandLine#passing-the-invocationcontext

NEW: rootCommandParser CE

29 Apr 13:31
Compare
Choose a tag to compare

Added a rootCommandParser to provide the opportunity to manually parse and invoke a root command (since the rootCommand is auto-executing).

open FSharp.SystemCommandLine
open System.CommandLine.Parsing

let app (words: string array, separator: string option) =
    let separator = separator |> Option.defaultValue ", "
    System.String.Join(separator, words) |> printfn "Result: %s"
    0
    
[<EntryPoint>]
let main argv = 
    let words = Input.Option(["--word"; "-w"], Array.empty, "A list of words to be appended")
    let separator = Input.OptionMaybe(["--separator"; "-s"], "A character that will separate the joined words.")

    let parser = 
        rootCommandParser {
            description "Appends words together"
            inputs (words, separator)
            setHandler app
        }

    parser.Parse(argv).Invoke()