CaptnCodr / Fli

Execute CLI commands from your F# code in F# style!
MIT License
155 stars 5 forks source link
cli command-line computation-expressions fsharp process shell

Fli

build Nuget

Execute CLI commands from your F# code in F# style!

Fli is part of the F# Advent Calendar 2022: A little story about Fli

Features

Install

Get it from Nuget: dotnet add package Fli

Usage

open Fli and start

For example:

cli {
    Shell CMD
    Command "echo Hello World!"
}
|> Command.execute

that 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.execute

Executing programs with arguments:

cli {
    Exec "path/to/executable"
    Arguments "--info"
}
|> Command.execute

an example with git:

cli {
    Exec "git"
    Arguments ["commit"; "-m"; "Fixing issue #1337."]
}
|> Command.execute

Add a verb to your executing program:

cli {
    Exec "adobe.exe"
    Arguments (Path.Combine ((Environment.GetFolderPath Environment.SpecialFolder.UserProfile), "test.pdf"))
    Verb "open"
}
|> Command.execute

or 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.execute

Write 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.execute

Add environment variables for the executing program:

cli {
    Exec "git"
    EnvironmentVariables [("GIT_AUTHOR_NAME", "Jon Doe"); ("GIT_AUTHOR_EMAIL", "jon.doe@domain.com")]
    Output ""
}
|> Command.execute

Hint: 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.execute

Hint: 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.execute

For 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.execute

Command.execute

Command.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 -> string

example:

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!"

Output functions

throwIfErrored: Output -> Output
throw: (Output -> bool) -> Output -> Output

Output.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.toError

or, 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.toError

Printing Output fields

There are printing methods in Output too:

printId: Output -> unit
printText: Output -> unit
printExitCode: Output -> unit
printError: Output -> unit

Instead of writing:

cli { ... }
|> Command.execute
|> Output.toText
|> printfn "%s"

For a little shorter code you can use:

cli { ... }
|> Command.execute
|> Output.printText

Command.toString

Command.toString concatenates only the 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!"

Builder operations:

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:

Provided Fli.Outputs:

Provided Fli.WindowStyle:

Do you miss something?

Open an issue or start a discussion.

Inspiration

Use CE's for CLI commands came in mind while using FsHttp.