Execute CLI commands from your F# code in F# style!
Fli is part of the F# Advent Calendar 2022: A little story about Fli
- Starting processes easily
- Execute CLI commands in your favourite shell
- F# computation expression syntax
- Wrap authenticated CLI tools
- No external dependencies
Get it from Nuget: dotnet add package Fli
open Fli and start
For example:
cli {
Shell CMD
Command "echo Hello World!"
}
|> Command.executethat starts CMD.exe as Shell and echo Hello World! is the command to execute.
Run a file with PowerShell from a specific directory:
cli {
Shell PWSH
Command "test.bat"
WorkingDirectory (Environment.GetFolderPath Environment.SpecialFolder.UserProfile)
}
|> Command.executeExecuting programs with arguments:
cli {
Exec "path/to/executable"
Arguments "--info"
}
|> Command.executean example with git:
cli {
Exec "git"
Arguments ["commit"; "-m"; "\"Fixing issue #1337.\""]
}
|> Command.executeAdd a verb to your executing program:
cli {
Exec "adobe.exe"
Arguments (Path.Combine ((Environment.GetFolderPath Environment.SpecialFolder.UserProfile), "test.pdf"))
Verb "open"
}
|> Command.executeor open a file in the default/assigned program:
cli {
Exec "test.pdf"
}
|> Command.execute(Hint: if file extension is not assigned to any installed program, it will throw a System.NullReferenceException)
Write output to a specific file:
cli {
Exec "dotnet"
Arguments "--list-sdks"
Output @"absolute\path\to\dotnet-sdks.txt"
}
|> Command.executeWrite output to a function (logging, printing, etc.):
let log (output: string) = Debug.Log($"CLI log: {output}")
cli {
Exec "dotnet"
Arguments "--list-sdks"
Output log
}
|> Command.executeWrite output to a stream:
// using a console stream
cli {
Exec "dotnet"
Arguments "--list-sdks"
Output (new StreamWriter(Console.OpenStandardOutput()))
}
|> Command.execute
// using a file stream
cli {
Exec "dotnet"
Arguments "--list-sdks"
Output (new StreamWriter(new FileStream("test.txt", FileMode.OpenOrCreate)))
}
|> Command.executeHint: Using Output (new StreamWriter(...)) will redirect the output text to your desired target and not save it into Output.Text nor Output.Error but in order to fix that you can use Output.from:
let sb = StringBuilder()
cli {
Exec "dotnet"
Arguments "--list-sdks"
Output (new StringWriter(sb))
}
|> Command.execute
|> Output.from (sb.ToString())Add environment variables for the executing program:
cli {
Exec "git"
EnvironmentVariables [("GIT_AUTHOR_NAME", "Jon Doe"); ("GIT_AUTHOR_EMAIL", "[email protected]")]
Output ""
}
|> Command.executeHint: Output "" will be ignored. This is for conditional cases, e.g.: Output (if true then logFilePath else "").
Add credentials to program:
cli {
Exec "program"
Credentials ("domain", "bobk", "password123")
}
|> Command.executeHint: Running a process as a different user is supported on all platforms. Other options (Domain, Password) are only available on Windows. As an alternative for not Windows based systems there is:
cli {
Exec "path/to/program"
Username "admin"
}
|> Command.executeFor Windows applications it's possible to set their visibility. There are four possible values: Hidden, Maximized, Minimized and Normal. The default is Hidden.
cli {
Exec @"C:\Windows\regedit.exe"
WindowStyle Normal
}
|> Command.executeCommand.execute returns record: type Output = { Id: int; Text: string option; ExitCode: int; Error: string option }
which has getter methods to get only one value:
toId: Output -> int
toText: Output -> string
toExitCode: Output -> int
toError: Output -> stringexample:
cli {
Shell CMD
Command "echo Hello World!"
}
|> Command.execute // { Id = 123; Text = Some "Hello World!"; ExitCode = 0; Error = None }
|> Output.toText // "Hello World!"
// same with Output.toId:
cli { ... }
|> Command.execute // { Id = 123; Text = Some "Hello World!"; ExitCode = 0; Error = None }
|> Output.toId // 123
// same with Output.toExitCode:
cli { ... }
|> Command.execute // { Id = 123; Text = Some "Hello World!"; ExitCode = 0; Error = None }
|> Output.toExitCode // 0
// in case of an error:
cli { ... }
|> Command.execute // { Id = 123; Text = None; ExitCode = 1; Error = Some "This is an error!" }
|> Output.toError // "This is an error!"throwIfErrored: Output -> Output
throw: (Output -> bool) -> Output -> OutputOutput.throw and Output.throwIfErrored are assertion functions that if something's not right it will throw an exception.
That is useful for build scripts to stop the execution immediately, here is an example:
cli {
Exec "dotnet"
Arguments [| "build"; "-c"; "Release" |]
WorkingDirectory "src/"
}
|> Command.execute // returns { Id = 123; Text = None; ExitCode = 1; Error = Some "This is an error!" }
|> Output.throwIfErrored // <- Exception thrown!
|> Output.toErroror, you can define when to "fail":
cli { ... }
|> Command.execute // returns { Id = 123; Text = "An error occured: ..."; ExitCode = 1; Error = Some "Error detail." }
|> Output.throw (fun output -> output.Text.Contains("error")) // <- Exception thrown!
|> Output.toErrorThere are printing methods in Output too:
printId: Output -> unit
printText: Output -> unit
printExitCode: Output -> unit
printError: Output -> unitInstead of writing:
cli { ... }
|> Command.execute
|> Output.toText
|> printfn "%s"For a little shorter code you can use:
cli { ... }
|> Command.execute
|> Output.printTextCommand.toString concatenates only the executing shell/program + the given commands/arguments:
cli {
Shell PS
Command "Write-Host Hello World!"
}
|> Command.toString // "powershell.exe -Command Write-Host Hello World!"and:
cli {
Exec "cmd.exe"
Arguments [ "/C"; "echo"; "Hello World!" ]
}
|> Command.toString // "cmd.exe /C echo Hello World!"ShellContext operations (cli { Shell ... }):
| Operation | Type |
|---|---|
Shell |
Fli.Shells |
Command |
string |
Input |
string |
Output |
Fli.Outputs |
WorkingDirectory |
string |
WindowStyle |
Fli.WindowStyle |
EnvironmentVariable |
string * string |
EnvironmentVariables |
(string * string) list |
Encoding |
System.Text.Encoding |
CancelAfter |
int |
ExecContext operations (cli { Exec ... }):
| Operation | Type |
|---|---|
Exec |
string |
Arguments |
string / string seq / string list / string array |
Input |
string |
Output |
Fli.Outputs |
Verb |
string |
Username |
string |
Credentials |
string * string * string |
WorkingDirectory |
string |
WindowStyle |
Fli.WindowStyle |
EnvironmentVariable |
string * string |
EnvironmentVariables |
(string * string) list |
Encoding |
System.Text.Encoding |
CancelAfter |
int |
Currently provided Fli.Shells:
CMDruns eithercmd.exe /c ...orcmd.exe /k ...(ifInputis provided)PSrunspowershell.exe -Command ...PWSHrunspwsh.exe -Command ...WSLrunswsl.exe -- ...SHrunssh -c ...BASHrunsbash -c ...ZSHrunszsh -c ...CUSTOM (shell: string * flag: string)runs the specifiedshellwith the specified starting argument (flag)
Provided Fli.Outputs:
File of stringa string with an absolute path of the output file.StringBuilder of StringBuildera StringBuilder which will be filled with the output text.Custom of Func<string, unit>a custom function (string -> unit) that will be called with the output string (logging, printing etc.).Stream of TextWritera stream that will redirect the output text to the designated target (file, console etc.).
Provided Fli.WindowStyle:
Hidden(default)MaximizedMinimizedNormal
Open an issue or start a discussion.
After cloning this repository, there are some steps to start:
dotnet tool restoredotnet paket restoredotnet restoredotnet paket installdotnet build
After that, you can start coding, build and test.
Every contribution is welcome. :)
Use CE's for CLI commands came in mind while using FsHttp.