Skip to content

Architecture

LuaSH is the module directory sh/. Each file has one job, fixed by the design document in section 2; the public API is section 3.

Module Job
sh/init.lua the public API: Results, the builtins, sh.config, sh.install() and backend control
sh/command.lua Command and Pipeline, argument flattening, quoting and rendering; starts no process
sh/pipe.lua the run path: one command through the backend in use, with verbose and trace
sh/backend/init.lua the registry: selects one backend and routes run, chdir and getcwd to it
posix.lua, luv.lua, popen.lua the backends, each a complete implementation of the contract
sh/sys.lua platform detection and load_pcall, the guard around library loads
sh/stack.lua the stack behind pushd and popd

Dependencies point one way: init requires command, pipe, backend and sys; pipe requires backend; backend requires sys; the backends and command require nothing else of sh. A backend never sees a Command or a Result, which is what lets tests/test_backend.lua check each one alone.

What happens at sh.ls("-l")

  1. The module table has no field ls. Its metatable's index_module returns command("ls"), a function that builds a fresh Command at each call.
  2. That function calls Command("ls", "-l"). The arguments are flattened (section 7, commands as data) into an argument vector, the literal stdin, and the Results that were passed in.
  3. execute records the provenance, the Pipeline the Result will show, and calls pipe.run with the vector, the stdin data and that Pipeline.
  4. pipe.run adds the verbose callbacks and the trace line and calls backend.run, which goes to the backend selected when sh loaded.
  5. The backend starts the program, feeds its stdin, collects stdout and stderr, and returns { exitcode, signal, stdout, stderr, cause } as section 4 specifies.
  6. execute wraps that in a Result; with raise_errors on, a failure becomes the error message of section 3.6.

A chained call such as r:grep("x") takes the same path. Indexing a Result falls back to the module (result_index), so grep is the command function, and the Result arrives as its first argument, where flattening takes __input as the stdin and __pipeline as the source.

Two more entry points

sh.install() gives _G an __index so that an undefined global becomes command(name). First it loads the platform's backends with preload: libraries such as luaposix detect features by reading optional globals, which must still be nil while they load. For a backend loaded later, sys.load_pcall keeps them nil during the load.

sh.Command and sh.Pipeline build the same objects without running them. run() on either calls the runner that init installs with set_runner, which is execute again. That is what keeps command.lua free of process code.

The other pages follow this path. Pipes is step 5 in each backend, and streaming follows one chunk of output from the backend's pump to the verbose callbacks of step 4; backend resolution is how step 4 found its backend, results is step 6, and configuration covers sh.config and the bundled single file.

How to

Call a program

Index the module with the program's name, or call sh.install() once and use the name as a global. Either call runs the program at once and returns its Result:

local sh = require("sh")
print(sh.uname("-s"))           -- through the module

sh.install()
print(uname("-s"))              -- as a global, after install()

A library should keep to the first form: sh.install() changes _G for the whole program, and a misspelled global then runs a program of that name, so the error is sh: lenght: No such file or directory rather than attempt to call a nil value.

Name a program that is not an identifier

sh("pkg-config") and sh.command("true") return the command function for a name with a - in it or a Lua keyword. Further arguments are preset: sh.command("git", "log", "--oneline") is a git log --oneline that takes more arguments at each call (example 3).

Keep a global out of install()

After sh.install() every undefined global is a command function, which a library that reads an optional global to detect a feature would take for the feature; the backends load before the hook goes in for that reason. A global of your own that must stay nil goes on sh.__index_ignore_exact, or its prefix on sh.__index_ignore_prefix, which the hook consults at every lookup:

sh.install()
table.insert(sh.__index_ignore_exact, "optional_hook")
print(type(optional_hook))      -- nil, not a function

Use a strict _G

A library that rejects assignments to undeclared globals, such as safer.lua's safer.globals() or the classic strict.lua, gives _G a metatable with a __newindex. Load it before sh.install(): the hook keeps the metatable it finds and replaces only __index, so assignments stay checked while an undefined name still runs a program. Loaded after sh.install(), such a library replaces the whole metatable, and an undefined name raises instead of running. LuaSH writes no global of its own, which tests/test.lua checks by running a command under a _G whose __newindex records every write.

require("safer").globals()      -- or dofile("strict.lua")
local sh = require("sh")
sh.install()
print(uname("-s"))              -- runs; assignments stay checked

Build a command without running it

sh.Command and sh.Pipeline do step 2 and stop; run() continues at step 3. Commands as data has the recipes.

Find the code behind a behaviour

You see Look in
a wrong word or flag on the rendered command line sh/command.lua: walk_table, format_flag, quote; commands
a setting that has no effect sh/init.lua: apply_config, set_backend; configuration
output in the wrong stream, merged, or arriving late the backend in use, sh.backend_name(), under sh/backend/; pipes, streaming
a cd that the programs follow but io.open does not sh/backend/popen.lua; working directory
an unexpected error message or exit status sh/init.lua: failure, execute; results