Skip to content

Examples

The scripts in examples/ run from a clone of the repository without installing anything:

git clone https://github.com/JBlaschke/luash.git && cd luash
examples/run_all.sh                # every example, with `lua`
LUA_PATH='./?/init.lua;./?.lua;;' lua examples/01_basics.lua

examples/run_all.sh luajit picks another interpreter, and LUASH_BACKEND=popen examples/run_all.sh runs everything on the popen backend. With the library installed, drop the LUA_PATH. The output below comes from a macOS machine with luaposix and luv installed; user names, directories and backend names differ on yours.

1. Programs as functions

-- Run programs as functions and read their output.
local sh = require("sh")

-- Every program on PATH is a function of the module. The arguments
-- become the program's arguments, and the call returns a Result.
print(sh.uname("-s"))              -- Darwin, Linux, ...
print(sh.echo("hello", "world"))   -- hello world

-- A Result prints as its trimmed stdout and concatenates with `..`.
print("I am " .. sh.whoami())

-- The raw fields are there too.
local r = sh.printf("%s\n", "line one", "line two")
io.write(r.__stdout)               -- line one / line two, with newlines
print(r.__exitcode)                -- 0
print(sh.stdout(r) == r.__stdout)  -- true

-- Walk the lines of an output.
for line in sh.seq(1, 3).__stdout:gmatch("[^\n]+") do
    print("got", line)
end

-- sh.install() makes undefined globals commands: ls("-l") is sh.ls("-l").
sh.install()
print(wc(seq(1, 100), "-l"))       -- 100
Darwin
hello world
I am <your user name>
line one
line two
0
true
got 1
got 2
got 3
100

2. Pipelines

-- Chain commands: the stdout of one becomes the stdin of the next.
local sh = require("sh")

-- A Result passed as an argument feeds the command's stdin.
print(sh.sort(sh.printf("b\na\nc\n"), "-r"))   -- c, b, a

-- Chained calls read like a shell pipeline. Each call after the first is
-- looked up in the sh module, so only the first one needs the prefix.
print(sh.seq(1, 10):grep("5"):wc("-l"))        -- 1

-- Lua lets you drop the parentheses around a single string argument.
sh.seq(1, 5) : tail("-n", 2) : print()         -- 4, 5
sh.ls "/" : head "-3" : print()                -- the first three entries

-- Several Results feed one command: their outputs are concatenated, and
-- the pipeline renders them as a group.
local both = sh.wc(sh.seq(1, 10), sh.seq(20, 25), "-l")
print(both)                                    -- 16
print(both.__pipeline)                         -- { seq 1 10; seq 20 25; } | wc -l

-- Literal input: a table with an __input field is stdin.
print(sh.tr({ __input = "hello" }, "a-z", "A-Z"))   -- HELLO

-- The stages run one after another: a stage runs to completion, and its
-- whole output is then the input of the next stage.
c
b
a
1
4
5
Applications
Library
System
16
{ seq 1 10; seq 20 25; } | wc -l
HELLO

3. Flags, partial commands and tricky names

-- Flags from tables, partial commands, and programs with tricky names.
local sh = require("sh")

-- String keys of a table are flags: a one-letter key gives -k, a longer
-- key gives --key; true gives the bare flag, false gives nothing, and a
-- string or number gives key=value. They come after the positional
-- entries of their table, sorted by key, so the command line is the same
-- on every run.
print(sh.sort({ __input = "10\n9\n100\n" }, { n = true, r = false }))

-- Build the arguments as data and look at the command line:
local c = sh.Command("tar", "-c", { file = "site.tar", verbose = true }, "docs")
print(c)                              -- tar -c --file=site.tar --verbose docs

-- When the order of flags matters, use positional strings or :flag().
print(sh.Command("ls"):flag("l"):flag("color", "never"))   -- ls -l --color=never

-- sh.command (lower case) returns a function with arguments preset:
local gitlog = sh.command("git", "log", "--oneline")
print(gitlog("-n", 1))

-- It also names programs that are not valid Lua identifiers, or are
-- keywords. sh(...) is the same as sh.command(...).
local truecmd = sh.command("true")
local pkgconf = sh("pkg-config")
print(truecmd().__exitcode, type(pkgconf))   -- 0  function

-- Shell builtins are not programs on PATH: run them through a shell. A
-- table groups several arguments; {"-c", "type type"} stays two words.
print(sh.sh("-c", "echo $0"))
print(sh("bash"){ "-c", "type type" })
9
10
100
tar -c --file=site.tar --verbose docs
ls -l --color=never
bd6986a Merge pull request #14 from JBlaschke/claude/backend-registry
0   function
sh
type is a shell builtin

4. Commands as data

-- Build commands and pipelines first, look at them, run them later.
local sh = require("sh")

local c = sh.Command("ls", "-l")      -- nothing runs yet
c:arg("/tmp"):flag("a")
print(c)                              -- ls -l /tmp -a
print(c:argv()[1], #c:argv())         -- ls  4

-- A Pipeline comes from Command:pipe() or sh.Pipeline(); on Lua 5.3 and
-- newer, `a | b` builds one too.
local p = sh.Command("seq", 1, 10):pipe(sh.Command("wc", "-l"))
print(p)                              -- seq 1 10 | wc -l
print(p:run())                        -- 10

-- Literal input belongs to a Command and is never rendered.
local first = sh.Command("sort"):input("c\nb\na\n")
local q     = sh.Pipeline(first, sh.Command("head", "-n", 1))
print(q)                              -- sort | head -n 1
print(q:run())                        -- a

-- Arguments that need quoting are quoted in the rendering only; the
-- program receives them verbatim.
print(sh.Command("echo", "it's", "two words"))   -- echo 'it'\''s' 'two words'

-- Every Result remembers what produced it.
local r = sh.seq(1, 10):grep("5"):wc("-l")
print(r.__pipeline)                   -- seq 1 10 | grep 5 | wc -l

-- A Command is mutable; copy() takes a snapshot.
local base = sh.Command("git", "log")
local snap = base:copy()
base:flag("oneline")
print(snap, "/", base)                -- git log / git log --oneline
ls -l /tmp -a
ls  4
seq 1 10 | wc -l
10
sort | head -n 1
a
echo 'it'\''s' 'two words'
seq 1 10 | grep 5 | wc -l
git log /   git log --oneline

5. Errors

-- A failing command raises by default. Catch it, or turn raising off.
local sh = require("sh")

-- When the program wrote to stderr, that text is the error message.
local ok, err = pcall(sh.ls, "/no/such/directory")
print(ok, err)

-- Otherwise the message names the pipeline and how it ended.
ok, err = pcall(function() return sh.sh("-c", "exit 3") end)
print(ok, err)                 -- false  sh -c 'exit 3': exit status 3

-- With raise_errors off, the Result comes back and you check it.
sh.config.raise_errors = false
local r = sh.grep("needle", { __input = "hay\n" })
if r.__exitcode ~= 0 then
    print("grep found nothing: status " .. r.__exitcode)
end

-- A program that cannot be started is reported the way a shell reports
-- it: exit status 127 and a message naming the program (its wording
-- depends on the backend).
r = sh.no_such_program_xyz()
print(r.__exitcode)            -- 127
print(sh.stderr(r))
sh.config.raise_errors = true

-- The builtins never raise; they report through the Result.
local c = sh.cd("/no/such/directory")
print(c.__exitcode, sh.stderr(c))
false   ls: /no/such/directory: No such file or directory
false   sh -c 'exit 3': exit status 3
grep found nothing: status 1
127
sh: no_such_program_xyz: No such file or directory

1   cd: The directory '/no/such/directory' does not exist

The second message has no file position because pcall called the function directly; raised from a script line, the message starts with that line. The wording of the 127 message depends on the backend.

6. Trace, verbose output and the sink

-- Watch commands run: trace prints each pipeline, verbose streams output.
local sh = require("sh")

-- "+ <pipeline>" goes to stderr before each stage runs, like `set -x`.
sh.config.trace = true
sh.seq(1, 3):wc("-l")
-- + seq 1 3
-- + seq 1 3 | wc -l

-- Verbose copies the output to stdout and stderr while the program runs,
-- byte for byte: progress bars that redraw a line with \r animate.
sh.config.verbose = true
sh.sh("-c", "echo out; echo err >&2")

-- A sink takes over from the io writes. It gets the stream name, which
-- is "stdout", "stderr" or "trace", and the chunk.
local log = {}
sh.config.sink = function(stream, chunk)
    log[#log + 1] = stream .. ": " .. chunk
end
sh.echo("a", "b")
sh.config.verbose = false
sh.config.trace   = false
sh.config.sink    = nil
io.write(table.concat(log))
-- trace: + echo a b
-- stdout: a b

stderr and stdout interleaved as a terminal shows them:

+ seq 1 3
+ seq 1 3 | wc -l
+ sh -c 'echo out; echo err >&2'
out
err
trace: + echo a b
stdout: a b

7. Directories

-- cd, pushd and popd are builtins: they change where the commands run.
local sh = require("sh")

print(sh.pwd())
sh.cd("tests")
print(sh.pwd())                        -- .../tests
sh.cd("..")

-- pushd and popd bracket a directory change; they chain like commands.
sh.pushd("sh") : ls("backend") : print()
sh.popd()
print(sh.pwd())                        -- back where we started

-- A directory that cannot be entered is reported, never raised, and the
-- input of the stage before passes through a builtin unchanged.
local r = sh.echo("passed through"):cd("/no/such/directory")
print(r.__exitcode, sh.stderr(r))
print(r:cat())                         -- passed through
<the clone>
<the clone>/tests
init.lua
luv.lua
popen.lua
posix.lua
<the clone>
1   cd: The directory '/no/such/directory' does not exist
passed through

8. Backends

-- See which backend runs your commands, and pick another one.
local sh = require("sh")

print("in use:    " .. sh.backend_name())
print("available: " .. table.concat(sh.available_backends(), " "))

-- What the backend in use can do.
local caps = sh.capabilities()
local keys = {}
for key in pairs(caps) do keys[#keys + 1] = key end
table.sort(keys)
for _, key in ipairs(keys) do print(key, caps[key]) end

-- Select a backend now; it raises when that one is unavailable here, and
-- the backend in use then stays as it was.
local ok, err = pcall(sh.set_backend, "popen", { strategy = "standard" })
if ok then
    print("now using " .. sh.backend_name() .. "/" .. sh.capabilities().strategy)
else
    print(err)
end
print(sh.echo("still works"))

-- A popen strategy that merges stderr into stdout.
sh.set_backend("popen", { strategy = "basic" })
print(sh.sh("-c", "echo out; echo err >&2"))

-- Back to the platform's default. sh.config records every selection.
sh.set_backend("auto", { strategy = "auto" })
print(sh.backend_name(), sh.config.backend, sh.config.popen.strategy)
in use:    posix
available: posix luv popen
exitcode    true
name    posix
separate_stderr true
signals true
stdin   true
streaming   true
now using popen/standard
still works
out
err
posix   auto    auto

9. A backend of your own

-- A backend is a table with a name and six functions (docs/DESIGN.md,
-- section 4). This one wraps the backend in use and logs every run.
local sh      = require("sh")
local backend = require("sh.backend")   -- the registry

local real    = backend.current()
local logging = { name = "logging" }

-- The directory and the options stay with the real backend.
for _, f in ipairs({ "available", "configure", "chdir", "getcwd" }) do
    logging[f] = real[f]
end

function logging.capabilities()
    local caps = real.capabilities()
    caps.name  = logging.name
    return caps
end

function logging.run(argv, input, opts)
    io.stderr:write(
        "[", logging.name, "] ", table.concat(argv, " "),
        " (", #input, " bytes of input)\n"
    )
    return real.run(argv, input, opts)
end

sh.set_backend(logging)
print(sh.backend_name())                -- logging
print(sh.seq(1, 3):wc("-l"))            -- 3, with two log lines on stderr
logging
[logging] seq 1 3 (0 bytes of input)
[logging] wc -l (6 bytes of input)
3

Testing and your own backend explains the contract this backend implements and how to test one.