Releases: JordanMarr/FSharp.SystemCommandLine
v2.0.0-beta7
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 (becauseSystem.CommandLine.CommandLineConfiguration
has been removed). - A new
ManualInvocation.rootCommand
has been added. Unlike the defaultrootCommand
which is automatically executes, this simply returns aSystem.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
(inrootCommand
) has been deprecated.configureParser
has been added torootCommand
and the newManualInvocation.rootCommand
.configureInvocation
has been added torootCommand
.
v2.0.0-beta5
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 ofsetAction
to maintain parity with System.CommandLine v2.0.0.beta5 naming changes.Input
methods likeInput.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
Changes
- Adds support for "global options".
- Added an overload to
Input
GetValue
method to get value from anInvocationContext
or aParseResult
. - Bumped version from
v0.18.0-beta4
tov1.0.0-beta4
so as not to scare people away from using the library.
See Global Options example in readme.
v0.18.0-beta4
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
Changes in v0.17.0-beta4
- New
addAlias
andaddAliases
operations to add command aliases. (thanks to @AngelMunoz! 🚀) - Deprecated
add
overloads and renamed toaddInput
andaddInputs
for clarity and consistency
Support for netstandard, `add` overload
Input helpers
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 aname
/alias
and aconfigure
function that allows you to manually set properties on the S.CLOption<'T>
. - New
Input.OptionMaybe
overloads that take aname
/alias
and aconfigure
function that allows you to manually set properties on the S.CLOption<'T option>
. - New
Input.Argument
overloads that take aname
/alias
and aconfigure
function that allows you to manually set properties on the S.CLArgument<'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 forHandlerInput.OfOption
Input.OfArgument
- An alias forHandlerInput.OfArgument
Handling of greater than 8 inputs
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
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 likeInvocationContext
,CancellationToken
,IConsole
, etc. -
⭐ Added
Input.Context()
for passingInvocationContext
to handler function.
This replacesInput.InjectedDependency<'T>()
becauseInvocationContext
can be used to get aCancellationToken
,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 theInvocationContext
to your handler function using the newInput.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
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()