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.