Fsharp: call shell command

By Xah Lee. Date: .

ai answer

xtodo
open System
open System.Diagnostics
open System.Text

type ProcessResult = {
    ExitCode: int
    StdOut: string
    StdErr: string
}

let executeProcess (processName: string) (processArgs: string) =
    let psi = ProcessStartInfo(processName, processArgs)
    psi.UseShellExecute <- false
    psi.RedirectStandardOutput <- true
    psi.RedirectStandardError <- true
    psi.CreateNoWindow <- true

    use proc = Process.Start(psi)
    let output = StringBuilder()
    let error = StringBuilder()

    proc.OutputDataReceived.Add(fun args ->
        if not (isNull args.Data) then output.AppendLine(args.Data) |> ignore)
    proc.ErrorDataReceived.Add(fun args ->
        if not (isNull args.Data) then error.AppendLine(args.Data) |> ignore)

    proc.BeginOutputReadLine()
    proc.BeginErrorReadLine()
    proc.WaitForExit()

    {
        ExitCode = proc.ExitCode
        StdOut = output.ToString()
        StdErr = error.ToString()
    }

// s------------------------------

// let result = executeProcess "uptime" "-p" // Linux/macOS
// or
let result = executeProcess "cmd.exe" "/c dir" // Windows

printfn "Exit code: %d" result.ExitCode
printfn "Stdout:\n%s" result.StdOut
if result.StdErr <> "" then
    eprintfn "Stderr:\n%s" result.StdErr

(*
Exit code: 0
Stdout:
 Volume in drive C is Windows
 Volume Serial Number is 86F8-588E

 Directory of c:\Users\xah\.emacs.d\temp

07/29/2026  08:10 AM    <DIR>          .
07/27/2026  08:56 PM    <DIR>          ..
07/16/2026  05:52 PM               288 20260716_1752_ac24.js~
07/16/2026  05:55 PM               967 20260716_1755_19ce.js~
...
 *)

How to call shell commands in Fsharp?

How to call shell commands in Fsharp?

ai answer

In F#, you call shell commands (or any external process) using the .NET System.Diagnostics.Process class. This is the standard, built-in approach—no extra packages required.

Basic synchronous example (capture stdout, stderr, and exit code)

open System
open System.Diagnostics
open System.Text

type ProcessResult = {
    ExitCode: int
    StdOut: string
    StdErr: string
}

let executeProcess (processName: string) (processArgs: string) =
    let psi = ProcessStartInfo(processName, processArgs)
    psi.UseShellExecute <- false
    psi.RedirectStandardOutput <- true
    psi.RedirectStandardError <- true
    psi.CreateNoWindow <- true

    use proc = Process.Start(psi)
    let output = StringBuilder()
    let error = StringBuilder()

    proc.OutputDataReceived.Add(fun args ->
        if not (isNull args.Data) then output.AppendLine(args.Data) |> ignore)
    proc.ErrorDataReceived.Add(fun args ->
        if not (isNull args.Data) then error.AppendLine(args.Data) |> ignore)

    proc.BeginOutputReadLine()
    proc.BeginErrorReadLine()
    proc.WaitForExit()

    {
        ExitCode = proc.ExitCode
        StdOut = output.ToString()
        StdErr = error.ToString()
    }

Usage:

let result = executeProcess "uptime" "-p"          // Linux/macOS
// or
let result = executeProcess "cmd.exe" "/c dir"     // Windows

printfn "Exit code: %d" result.ExitCode
printfn "Stdout:\n%s" result.StdOut
if result.StdErr <> "" then
    eprintfn "Stderr:\n%s" result.StdErr

Simpler version (when you mainly care about exit code + full output)

open System.Diagnostics

let run (cmd: string) (args: string) =
    let startInfo = ProcessStartInfo(
        FileName = cmd,
        Arguments = args,
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false,
        CreateNoWindow = true
    )
    use p = Process.Start(startInfo)
    let stdout = p.StandardOutput.ReadToEnd()
    let stderr = p.StandardError.ReadToEnd()
    p.WaitForExit()
    p.ExitCode, stdout, stderr

Running a full shell command (e.g. with pipes, redirects, etc.)

On Linux/macOS you typically invoke a shell:

// Using bash
let result = executeProcess "/bin/bash" "-c \"ls -la | head -5\""

// Or more portably with env
let result = executeProcess "/usr/bin/env" "bash -c \"echo Hello from shell\""

On Windows:

let result = executeProcess "cmd.exe" "/c \"dir & echo Done\""
// or
let result = executeProcess "powershell.exe" "-Command \"Get-Process | Select-Object -First 5\""

Asynchronous version (recommended for modern code)

open System.Diagnostics
open System.Threading.Tasks

let executeCommandAsync executable (args: string list) = async {
    let startInfo = ProcessStartInfo()
    startInfo.FileName <- executable
    startInfo.RedirectStandardOutput <- true
    startInfo.RedirectStandardError <- true
    startInfo.UseShellExecute <- false
    startInfo.CreateNoWindow <- true
    args |> List.iter startInfo.ArgumentList.Add

    use p = new Process(StartInfo = startInfo)
    p.Start() |> ignore

    let! stdoutTask = p.StandardOutput.ReadToEndAsync() |> Async.AwaitTask
    let! stderrTask = p.StandardError.ReadToEndAsync() |> Async.AwaitTask
    do! p.WaitForExitAsync() |> Async.AwaitTask

    return p.ExitCode, stdoutTask, stderrTask
}

Important notes

Higher-level library (optional)

If you prefer a more F#-idiomatic API with computation expressions, consider the Fli library:

dotnet add package Fli
open Fli

cli {
    Shell BASH          // or CMD, PWSH, etc.
    Command "ls -la | head -3"
}
|> Command.execute
|> Output.toText
|> printfn "%s"

For most cases the built-in System.Diagnostics.Process approach is sufficient and has zero dependencies.

fsharp, file operations