Giter Club home page Giter Club logo

fli's Introduction

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

  • Starting processes easily
  • Execute CLI commands in your favourite shell
  • F# computation expression syntax
  • Wrap authenticated CLI tools
  • No external dependencies

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", "[email protected]")]
    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:

  • CMD runs cmd.exe /c ... or cmd.exe /k ... (depends if Input is provided or not)
  • PS runs powershell.exe -Command ...
  • PWSH runs pwsh.exe -Command ...
  • WSL runs wsl.exe -- ...
  • SH runs sh -c ...
  • BASH runs bash -c ...
  • ZSH runs zsh -c ...
  • CUSTOM (shell: string * flag: string) runs the specified shell with the specified starting argument (flag)

Provided Fli.Outputs:

  • File of string a string with an absolute path of the output file.
  • StringBuilder of StringBuilder a 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.).

Provided Fli.WindowStyle:

  • Hidden (default)
  • Maximized
  • Minimized
  • Normal

Do you miss something?

Open an issue or start a discussion.

Inspiration

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

fli's People

Contributors

captncodr avatar mrboring avatar xgqt avatar yanikceulemans avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

fli's Issues

`Command.executeAsync` does not await output

First of all, I would like to say thanks for creating this library. It has been super useful for me.

When using the Command.executeAsync function, it seems like the output of the command is not awaited.

Running the example program:

open Fli

let mainAsync = async {
    let! output =
        cli {
            Shell PWSH
            Command "echo test"
        }
        |> Command.executeAsync
    output |> Output.toExitCode |> printfn "exit code: %d"
    output |> Output.toError |> printfn "stderr: %s"
    output |> Output.toText |> printfn "stdout: %s"
}

[<EntryPoint>]
let main argv =
    async {
        do! Async.SwitchToThreadPool ()
        return! mainAsync
    }
    |> Async.RunSynchronously

    0

results in the following output:

exit code: 0
stderr:     
stdout: 

Running the same program with Command.execute instead, gives the expected output:

exit code: 0
stderr: 
stdout: test

I have tested using both .NET 7 and .NET 6 with the same results.

Furthermore, it seems like the tests for the executeAsync function don't actually test anything because they do not await the result.
For example, the following test in src/Fli.Tests/ShellContext/ShellCommandExecuteTests.fs:

[<Test>]
let ``Hello World with BASH async`` () =
    if OperatingSystem.IsWindows() |> not then
        async {
            let! output =
                cli {
                    Shell BASH
                    Command "echo Hello World!"
                }
                |> Command.executeAsync

            // output |> Output.toText |> should equal "Hello World!"
            should equal 2 1
        }
        |> Async.Start
    else
        Assert.Pass()

The use of Async.Start just starts the async computation but does not await its results, leading to the test immediately passing

Running this updated test using the command: dotnet test --filter 'BASH&async' will report a passing test like so:

Microsoft (R) Test Execution Command Line Tool Version 17.7.0-preview-23364-03+bc17bb9693cfc4778ded51aa0ab7f1065433f989 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: 64 ms - Fli.Tests.dll (net7.0)

Some additional perhaps useful diagnostic information.
OS: Windows 10
dotnet version: 7.0.400
Fli version: 1.10.0

Please let me know if you need any additional information.

Standard error support

Most commands will provide useful information in stderr in case they return non-zero exit code. Some commands may also break if the parent process isn't reading the stderr.

Thus, I believe it would be useful to provide access to stderr in Fli.

How would I open an HTML page in the default browser?

Hi I'm trying to open an HTML file in the default browser.

cli {
    Exec @"<full path to HTML file>"
}
|> Command.execute

The above gives me the following exception:

System.ComponentModel.Win32Exception (193): An error occurred trying to start process 'C:\Data\FSharp\Projects\AudioUtilities\DO NOT COMMIT\Script Files\Find Audio Issues\Untitled-1.html' with working directory 'C:\Data\FSharp\Projects\AudioUtilities'. The specified executable is not a valid application for this OS platform.
   at System.Diagnostics.Process.StartWithCreateProcess(ProcessStartInfo startInfo)
   at System.Diagnostics.Process.Start(ProcessStartInfo startInfo)
   at Fli.Command.startProcess(FSharpFunc`2 inputFunc, FSharpFunc`2 outputFunc, ProcessStartInfo psi) in C:\Users\const\github\repos\Fli\src\Fli\Command.fs:line 87
   at Fli.Command.Command.execute(ExecContext context) in C:\Users\const\github\repos\Fli\src\Fli\Command.fs:line 223
   at <StartupCode$FSI_0024>.$FSI_0024.main@()
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)        
Stopped due to error

I searched for The specified executable is not a valid application for this OS platform and found the following (old ) Stack Overflow page:

.Net Core 2.0 Process.Start throws "The specified executable is not a valid application for this OS platform"

It suggests setting UseShellExecute = true. How would I do this with Fli, or is there a better way?

Thanks.

add output to stream of lines (seq string) ?

would be useful to process the output as stream, like less or more commands, so as a stream of lines, and to have control to interrupt or modify the behaviour of code based on content

e.g.

dotnet test |> break if containing 'Exception' so it will stop at the first error in tests instead of running the full suite

Cannot pass data on stdin to a program with Exec

Hi,

I testing your library for scripting purposes, and I found a small bug. In short, you can't pass data on stdin to a command (at least on Linux and Mac OS X where I tried it).

I have a PR to fix the bug and increase test coverage for Linux and Mac OS X which I'll raise in relation to this issue.

Reproduction

Running the following through dotnet fsi results in the exception below. I would've expected it to be equivalent to echo "foo" | cat.

#r "nuget: Fli"

open Fli

cli {
    Exec "cat"
    Input "foo"
}
|> Command.execute
System.InvalidOperationException: StandardIn has not been redirected.
   at System.Diagnostics.Process.get_StandardInput()
   at Fli.Command.writeInput$cont@167(FSharpOption`1 input, FSharpOption`1 encoding, Process p, Unit unitVar) in D:\github\repos\Fli\src\Fli\Command.fs:line 168
   at Fli.Command.writeInput(FSharpOption`1 input, FSharpOption`1 encoding, Process p) in D:\github\repos\Fli\src\Fli\Command.fs:line 165
   at [email protected](Process p)
   at Fli.Command.startProcess(FSharpFunc`2 inputFunc, FSharpFunc`2 outputFunc, ProcessStartInfo psi) in D:\github\repos\Fli\src\Fli\Command.fs:line 110
   at Fli.Command.Command.execute(ExecContext context) in D:\github\repos\Fli\src\Fli\Command.fs:line 282
   at <StartupCode$FSI_0002>.$FSI_0002.main@() in /tmp/tmp.QVPGcNRmGK/bug.fsx:line 6
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)

Can't use custom func as output

Trying to use a custom function in Output I am getting the following exception:

 System.Exception: Cannot convert output type.
         at Fli.CE.ICommandContext`1.outputTypeMapping[a,a](ICommandContext`1 _, a output)
         at Fli.CE.ICommandContext`1.Output[a,a](ICommandContext`1 this, ICommandContext`1 context, a output)

My command:

    let! output =
                    cli {
                        Shell BASH
                        Command (opts.Value.Path)
                        WorkingDirectory(opts.Value.WorkingDir)
                        Output (Custom(Func<string,unit>(fun s -> ctx.GetLogger().LogInformation("log: {Message}", s) )
                        ))
                    }
                    |> Command.executeAsync
                    |> Async.StartAsTask

BASH pipe

BASH does not accept pipes so:

seq { "test.txt" }
|> Seq.iter (fun file ->
    cli {
        Shell BASH
        Command $"{file} | echo"
        WorkingDirectory __SOURCE_DIRECTORY__
    }
    |> Command.execute
    |> Output.printText)

Does nothing.

Implement Zero value for computation expression.

Would it be possible to implement the Zero value for the cli computation expression? This is needed for letting other values be added conditionally.

In my current process, I'm trying to add the output logging if the user specifies a log file. and it would allow the following to happen

cli {
    Command "command"
    Arguments ["arg1"; "arg2"]
    
    if logPath.isSome then
        Output logPath.Value
}

Apps are not displayed when run as admin

Hi

I'm trying to launch Image for Windows. This is a Windows application. It launches and I can see it in Task Manager, but the window is not visible.

I'm running VS Code elevated as the application requires admin rights.

I'm using the following code inside an fsx file:

#r "nuget: Fli, 1.101.0"

open System
open System.IO
open Fli

cli {
        Exec @"C:\Program Files (x86)\TeraByte Drive Image Backup and Restore Suite\imagew64.exe"
        WorkingDirectory @"C:\Program Files (x86)\TeraByte Drive Image Backup and Restore Suite"
}
|> Command.execute

I tried the above with and without the WorkingDirectory line. I also tried running the fsx file from the command line:

dotnet fsi '.\Image For Windows Runner.fsx'

If I run the follwing from an elevated PowerShell prompt, it works fine:

& "C:\Program Files (x86)\TeraByte Drive Image Backup and Restore Suite\imagew64.exe"

I also tried Disk2Vhd which also requires elevation. Same results, I can see it in task manager, but the window is not visible. Running it from PowerShell works:

& "C:\Users\<removed>\AppData\Local\Microsoft\WindowsApps\disk2vhd.exe"

FSharp.Core, Version=7.0.0.0

I currently have a deployment with the following .NET SDKs available:

dotnet --list-sdks
3.1.404
5.0.403
6.0.400

When running a Fli test with version 1.0.0 and 0.11.0, I get the following error:

Unhandled exception. System.IO.FileLoadException: Could not load file or assembly 'FSharp.Core, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'.
The located assembly's manifest definition does not match the assembly reference. (0x80131040)
File name: 'FSharp.Core, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

Can the FSharp.Core version be set to greater than or equal to 6 in the paket.dependencies file so .NET 6 can be used?

nuget FSharp.Core >= 6

Thanks

Standard input support

In some cases, a command requires the caller to provide something in the stdin (consider an example of a program feeding a stream of shell commands to the shell line-by-line).

For such cases, something like this could be useful:

cli {
    Shell CMD
    input "echo 123\necho 345"
}

BASH issue

I'm converting a bash script to an F# script to fix a problem with regex in the bash script. But after the regex runs, I need to run more code.

I'm trying to run code like shown below, but it's not doing anything, and it doesn't output anything. I have confirmed that pwd returns the expected current directory. When I run the same command in a bash script, it works as expected.

// 3. Fetch all the new/modified files into the delivery folder.
list_of_files
|> Seq.iter (fun filePath -> 
    cli {
        Shell BASH
        Command $"git show HEAD:{filePath} | install -D /dev/stdin {delivery_folder}/{repo_name}/{branch_name}/{filePath}"
    }
    |> Command.execute
    |> ignore

    Console.WriteLine $"Downloaded {filePath}"
)

The original bash script looked like this:

IFS="
"
for filename in $list_of_files; do
    trimmed_filename=$(echo ${filename} | xargs)
    git show HEAD:${trimmed_filename} | install -D /dev/stdin ${delivery_folder}/${repo_name}/${branch_name}/${trimmed_filename}
    echo \'${trimmed_filename}\'
done

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.