✨ New: Compose inputs with the input { } computation expression
The new input { } CE composes any number of inputs into a single composite input using applicative let!/and! bindings. Each value is named right at its binding site, and your action receives whatever shape you return — typically an anonymous record. No more 8‑input tuple limit, and no more keeping the inputs tuple and setAction parameters in positional sync.
Declare inputs inline
[<EntryPoint>]
let main argv =
rootCommand argv {
description "Appends words together"
input {
let! words = option<string[]> "--word" |> alias "-w" |> desc "A list of words to be appended"
and! separator = optionMaybe "--separator" |> alias "-s" |> desc "A character that will separate the joined words."
return {| Words = words; Separator = separator |}
}
setAction (fun io ->
let separator = defaultArg io.Separator ", "
io.Words |> String.concat separator |> printfn "%s"
)
}Or declare separately and pass to inputs
The composite input is a normal ActionInput<'T>, so everything that accepts an input accepts it — including the existing inputs operation, alone or mixed into a tuple alongside plain inputs:
let io = input {
let! words = option<string[]> "--word" |> alias "-w"
and! separator = option "--separator" |> alias "-s" |> def ", "
return {| Words = words; Separator = separator |}
}
rootCommand argv {
description "Appends words together"
inputs (io, option<bool> "--verbose")
setAction (fun (io, verbose) ->
if verbose then printfn $"Joining {io.Words.Length} words"
io.Words |> String.concat io.Separator |> printfn "%s"
)
}Share common options between commands
Composites nest inside other composites, which makes shared option groups trivial — define the group once, bind it from any command that needs it. Shared options are registered only once per command:
let common = input {
let! verbose = option<bool> "--verbose"
and! config = option "--configuration" |> def "Release"
return {| Verbose = verbose; Config = config |}
}
let buildCmd = command "build" {
description "Builds the solution"
input {
let! common = common
and! quick = option<bool> "--quick"
return {| Common = common; Quick = quick |}
}
setAction (fun io -> build io.Common.Config (io.Quick || not io.Common.Verbose))
}
let testCmd = command "test" {
description "Runs the tests"
input {
let! common = common
and! filter = optionMaybe<string> "--filter"
return {| Common = common; Filter = filter |}
}
setAction (fun io -> test io.Common.Config io.Filter)
}Works with your existing action functions
Prefer calling an action function declared above the command? The CE can return any shape — return a tuple and your existing function wires up unchanged:
let unzip (zipFile: FileInfo, outputDirMaybe: DirectoryInfo option) =
let outputDir = defaultArg outputDirMaybe zipFile.Directory
printfn $"Unzipping {zipFile.Name} to {outputDir.FullName}..."
[<EntryPoint>]
let main argv =
rootCommand argv {
description "Unzips a .zip file"
input {
let! zipFile = argument "zipfile" |> desc "The file to unzip" |> validateFileExists
and! outputDir = optionMaybe "--output" |> alias "-o" |> desc "The output directory"
return (zipFile, outputDir)
}
setAction unzip
}Or give the command a named contract with a record type — likely the go-to pattern for larger commands, since the action's signature becomes self-documenting:
type UnzipArgs = {
ZipFile: FileInfo
OutputDir: DirectoryInfo option
}
let unzip (args: UnzipArgs) =
let outputDir = defaultArg args.OutputDir args.ZipFile.Directory
printfn $"Unzipping {args.ZipFile.Name} to {outputDir.FullName}..."
// ...same command as above, with: return { ZipFile = zipFile; OutputDir = outputDir }Rule of thumb: inline action → anonymous record; standalone function → tuple, or a named record type once the inputs grow.
Everything is purely additive — the existing inputs tuple API is unchanged. See the README section for the full set of examples and rules (bind all sources in one let!/and! group; one input { } block per command, before setAction).
🛠️ Fixes
ActionInput.GetValuenow has aCancellationTokenoverload, and itsContextcase constructs a properActionContext(previously it would fail if invoked directly).
❤️ Credits
This feature was proposed and first prototyped by @shayanhabibi (#34), who designed the applicative inputs CE for Partas.Build — his fusion of FSharp.SystemCommandLine and Fun.Build. The design here builds directly on his idea. Thank you! 🥇