Skip to content

JordanMarr/FSharp.SystemCommandLine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

45efba7 · Apr 17, 2022

History

99 Commits
Apr 17, 2022
Mar 13, 2022
Mar 13, 2022
Apr 8, 2022

Repository files navigation

FSharp.SystemCommandLine

NuGet version (FSharp.SystemCommandLine)

The purpose of this library is to improve type safety when using the System.CommandLine API in F# by utilizing computation expression syntax.

Features

Improved type safety

  • Mismatches between inputs and setHandler are caught at compile time

Helper methods for creating options and arguments

  • The Input.Option helper method avoids the need use the System.CommandLine.Option type directly (which conflicts with the F# Option type)
  • The FSharp.SystemCommandLine.Aliases module contains Opt and Arg aliases and can be opened if direct access is needed to the core API.

Support for F# option type

  • Input.OptionMaybe and Input.ArgumentMaybe allow you to use F# option types in your handler function.

Examples

Simple App

open FSharp.SystemCommandLine
open System.IO

let unzip (zipFile: FileInfo, outputDirMaybe: DirectoryInfo option) = 
    // Default to the zip file dir if None
    let outputDir = outputDirMaybe |> Option.defaultValue zipFile.Directory

    if zipFile.Exists
    then printfn $"Unzipping {zipFile.Name} to {outputDir.FullName}"
    else printfn $"File does not exist: {zipFile.FullName}"
    
[<EntryPoint>]
let main argv = 
    let zipFile = Input.Argument("The file to unzip")    
    let outputDirMaybe = Input.OptionMaybe(["--output"; "-o"], "The output directory")

    rootCommand argv {
        description "Unzips a .zip file"
        inputs (zipFile, outputDirMaybe)
        setHandler unzip
    }
> unzip.exe "c:\test\stuff.zip"
    Result: Unzipping stuff.zip to c:\test
    
> unzip.exe "c:\test\stuff.zip" -o "c:\test\output"
    Result: Unzipping stuff.zip to c:\test\output

Simple App that Returns a Status Code

You may optionally return a status code from your handler function.

open FSharp.SystemCommandLine
open System.IO

let unzip (zipFile: FileInfo, outputDirMaybe: DirectoryInfo option) = 
    // Default to the zip file dir if None
    let outputDir = outputDirMaybe |> Option.defaultValue zipFile.Directory

    if zipFile.Exists then
        printfn $"Unzipping {zipFile.Name} to {outputDir.FullName}"
        0 // Program successfully completed.
    else 
        printfn $"File does not exist: {zipFile.FullName}"
        2 // The system cannot find the file specified.
    
[<EntryPoint>]
let main argv = 
    let zipFile = Input.Argument("The file to unzip")    
    let outputDirMaybe = Input.OptionMaybe(["--output"; "-o"], "The output directory")

    rootCommand argv {
        description "Unzips a .zip file"
        inputs (zipFile, outputDirMaybe)
        setHandler unzip
    }

Notice that mismatches between the setHandler and the inputs are caught as a compile time error: cli safety

App with SubCommands

open System.IO
open FSharp.SystemCommandLine

// Ex: fsm.exe list "c:\temp"
let listCmd = 
    let handler (dir: DirectoryInfo) = 
        if dir.Exists 
        then dir.EnumerateFiles() |> Seq.iter (fun f -> printfn "%s" f.Name)
        else printfn $"{dir.FullName} does not exist."
        
    let dir = Input.Argument("dir", "The directory to list")

    command "list" {
        description "lists contents of a directory"
        inputs dir
        setHandler handler
    }

// Ex: fsm.exe delete "c:\temp" --recursive
let deleteCmd = 
    let handler (dir: DirectoryInfo, recursive: bool) = 
        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("dir", "The directory to delete")
    let recursive = Input.Option("--recursive", false)

    command "delete" {
        description "deletes a directory"
        inputs (dir, recursive)
        setHandler handler
    }
        

[<EntryPoint>]
let main argv = 
    rootCommand argv {
        description "File System Manager"
        setHandler id
        setCommand listCmd
        setCommand deleteCmd
    }
> fsm.exe list "c:\_github\FSharp.SystemCommandLine\src\FSharp.SystemCommandLine"
    CommandBuilders.fs
    FSharp.SystemCommandLine.fsproj
    pack.cmd
    Types.fs

> fsm.exe delete "c:\_github\FSharp.SystemCommandLine\src\FSharp.SystemCommandLine"
    Deleting c:\_github\FSharp.SystemCommandLine\src\FSharp.SystemCommandLine

> fsm.exe delete "c:\_github\FSharp.SystemCommandLine\src\FSharp.SystemCommandLine" --recursive
    Recursively deleting c:\_github\FSharp.SystemCommandLine\src\FSharp.SystemCommandLine

Async App with an Injected CancellationToken

System.CommandLine has a built-in dependency injection system and provides a handlful of built-in types that can be injected into your handler function by default:

  • CancellationToken
  • InvocationContext
  • ParseResult
  • IConsole
  • HelpBuilder
  • BindingContext

You can declare injected dependencies via the Input.InjectedDependency method.

module Program

open FSharp.SystemCommandLine
open System.Threading
open System.Threading.Tasks

let app (cancel: CancellationToken, words: string array, separator: string) =
    task {
        for i in [1..20] do
            if cancel.IsCancellationRequested then 
                printfn "Cancellation Requested"
                raise (new System.OperationCanceledException())
            else 
                printfn $"{i}"
                do! Task.Delay(1000)

        System.String.Join(separator, words)
        |> printfn "Result: %s"
    }
    
[<EntryPoint>]
let main argv = 
    let cancel = Input.InjectedDependency()
    let words = Input.Option(["--word"; "-w"], [||], "A list of words to be appended")
    let separator = Input.Option(["--separator"; "-s"], ", ", "A character that will separate the joined words.")

    rootCommand argv {
        description "Appends words together"
        inputs (cancel, words, separator)
        setHandler app
    }
    |> Async.AwaitTask
    |> Async.RunSynchronously

Async App with a Partially Applied Dependency

open FSharp.SystemCommandLine
open System.CommandLine.Builder
open System.Threading.Tasks

type WordService() = 
    member _.Join(separator: string, words: string array) = 
        task {
            do! Task.Delay(1000)
            return System.String.Join(separator, words)
        }

let app (svc: WordService) (words: string array, separator: string) =
    task {
        let! result = svc.Join(separator, words)
        result |> printfn "Result: %s"
    }
    
[<EntryPoint>]
let main argv = 
    let words = Input.Option(["--word"; "-w"], Array.empty, "A list of words to be appended")
    let separator = Input.Option(["--separator"; "-s"], ", ", "A character that will separate the joined words.")

    // Initialize app dependencies
    let svc = WordService()

    rootCommand argv {
        description "Appends words together"
        inputs (words, separator)
        usePipeline (fun builder -> 
            CommandLineBuilder()            // Pipeline is initialized with .UseDefaults() by default,
                .UseTypoCorrections(3)      // but you can override it here if needed.
        )
        setHandler (app svc)                // Partially apply app dependencies
    }
    |> Async.AwaitTask
    |> Async.RunSynchronously