Skip to content

Command Execution

10.1 Statement Context (Output to Terminal)

When a command appears as a statement (not in an expression), its output goes directly to the terminal.

# Commands print to stdout/stderr
ls -la
echo "Hello, World"
find . -name "*.txt"

# Side effects execute
rm -f temp.txt
mkdir -p new_directory
git commit -m "Update"

# Pipeline statements
ps aux | grep nginx
cat file.txt | sort | uniq

# In blocks
if needsUpdate then {
    echo "Updating..."
    git pull
    make build
}

10.2 Expression Context (Capture Output)

When a command is part of an expression (assignment, function argument, etc.), its stdout is captured.

# Assignment captures stdout
let files = ls -la
let count = wc -l < README.md
let user = whoami
let today = date +%Y-%m-%d

# Trailing newline is trimmed
let name = echo "test"            # "test" not "test\n"

# Capture in expressions
let greeting = $"Hello, {whoami}!"
let info = $"Files: {ls | wc -l}"

# Captured output as list
let fileList = ls | lines
let nonEmpty = cat file.txt | lines |> filter (fun l -> l != "")

# In conditionals
if $(grep -q pattern file.txt) then
    echo "Pattern found"

# As function arguments
process (cat config.txt)
analyze (git diff HEAD~1)

10.3 String Interpolation

# Variable interpolation
let name = "World"
echo "Hello, $name"               # Hello, World
echo "Path: ${HOME}/docs"         # Path: /home/user/docs

# Expression interpolation
echo "Sum: $((1 + 2 * 3))"        # Sum: 7
echo "Files: $(ls | wc -l)"       # Files: 42
echo "Upper: ${name |> toUpper}"  # Upper: WORLD

# Nested interpolation
let user = "alice"
echo "Home: ${getenv "HOME_$user"}"

# Escape to prevent interpolation
echo "Literal \$name"             # Literal $name
echo "Price: \$99.99"             # Price: $99.99

# Single quotes: no interpolation
echo 'No $interpolation here'     # No $interpolation here
echo 'Path: $HOME'                # Path: $HOME

10.4 Redirections

# Output redirection
echo "log entry" > logfile.txt    # Overwrite
echo "more" >> logfile.txt        # Append

# Input redirection
sort < unsorted.txt
wc -l < README.md

# Stderr redirection
command 2> errors.txt             # Stderr to file
command 2>> errors.txt            # Append stderr
command 2>&1                      # Stderr to stdout

# Combined redirects
command > output.txt 2>&1         # Both to file
command &> all.txt                # Shorthand for above
command 2>&1 | tee log.txt        # Both to pipe

# Discard output
command > /dev/null               # Discard stdout
command 2> /dev/null              # Discard stderr
command &> /dev/null              # Discard both

# Here documents
cat <<EOF
This is a multi-line
here document with $name interpolation
and $(command) substitution
EOF

# Here document without interpolation
cat <<'EOF'
No interpolation here
$VAR stays as literal $VAR
$(cmd) stays as literal
EOF

# Here strings
cat <<< "Single line input"
grep pattern <<< $variable
wc -w <<< "count these words"

10.5 Process Substitution

Treat command output as a file.

# Compare output of two commands
diff <(ls dir1) <(ls dir2)
diff <(sort file1) <(sort file2)

# Use command output as input file
while read line do
    process $line
end < <(find . -name "*.txt")

# Multiple process substitutions
paste <(cut -f1 file1) <(cut -f2 file2)

# Output process substitution
tee >(gzip > backup.gz) < input.txt

# Complex example
comm -12 <(sort users_today | uniq) <(sort users_yesterday | uniq)

Windows

Process substitution is not yet available on Windows. On Linux and macOS, it is implemented using /dev/fd paths. A future release may add Windows support via named pipes. See Platform Differences for details.

10.6 Command Substitution

# $() syntax (preferred)
let user = $(whoami)
let files = $(ls *.txt)
echo "Today is $(date +%A)"

# Backtick syntax (legacy, supported)
let user = `whoami`
echo "Today is `date +%A`"

# Nested substitution (only works with $())
let result = $(cat $(find . -name "config.txt" | head -1))

# Arithmetic substitution
let sum = $((1 + 2 * 3))
let next = $((counter + 1))
echo "Result: $((a * b + c))"

$(...) in F# Expressions

$(...) also works as a self-delimiting F# expression that captures stdout as a string. Unlike & command which is greedy (consumes up to a statement boundary), $(...) composes naturally inline:

# Self-delimiting — composes with operators
let greeting = $(whoami) + "@" + $(hostname)

# In if-conditions
if $(git status --porcelain) == "" then print "clean" else print "dirty"

# As pipeline source
$(echo 42) |> string_length |> print   # prints 2

# Compare with & command (needs parentheses for inline use)
let greeting = (& whoami) + "@" + (& hostname)

10.7 Dynamic Command Execution (exec)

The exec keyword executes a dynamically-resolved program path with F# expression arguments. Unlike shell commands where the program name is a compile-time literal, exec takes runtime string values — enabling conditional command dispatch via which and pattern matching.

# Single command with literal path
exec "/usr/bin/fortune"

# With arguments
exec "/usr/bin/fortune" "-s"

# Pipeline via | — true OS-level streaming pipes
exec "/usr/bin/fortune" | exec "/usr/bin/lolcat"

# Three-stage pipeline
exec "/bin/echo" "hello" | exec "/usr/bin/tr" "a-z" "A-Z" | exec "/bin/cat"

# Variable program paths (the main use case)
let f = "/usr/bin/fortune"
let l = "/usr/bin/lolcat"
exec f | exec l

# F# expression arguments
let flags = "-s"
exec f flags

# The motivating use case: which + match + exec
match which "fortune", which "lolcat" with
| Some f, Some l -> (exec f | exec l)
| Some f, None   -> exec f
| _              -> println "Commands not found"

Key differences from shell | pipes: - Program paths and arguments are F# expressions (variables, function calls, string literals) - True OS-level pipe semantics (stdout→stdin streaming) - Inside match arms, parentheses are needed: (exec f | exec l) to disambiguate | from arm separators - Returns the exit code of the last command in the pipeline


See also: Operators & Pipelines | Error Handling | Interoperability