The purpose of this library is to improve type safety when using the System.CommandLine
API in F# by utilizing computation expression syntax.
- Mismatches between
inputs
andsetHandler
are caught at compile time
- The
Input.Option
helper method avoids the need use theSystem.CommandLine.Option
type directly (which conflicts with the F#Option
type) - The
FSharp.SystemCommandLine.Aliases
module containsOpt
andArg
aliases and can be opened if direct access is needed to the core API.
Input.OptionMaybe
andInput.ArgumentMaybe
allow you to use F#option
types in 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}"
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
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:
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
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
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