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")¶
- The module table has no field
ls. Its metatable'sindex_modulereturnscommand("ls"), a function that builds a fresh Command at each call. - 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. executerecords the provenance, the Pipeline the Result will show, and callspipe.runwith the vector, the stdin data and that Pipeline.pipe.runadds the verbose callbacks and the trace line and callsbackend.run, which goes to the backend selected whenshloaded.- The backend starts the program, feeds its stdin, collects stdout and
stderr, and returns
{ exitcode, signal, stdout, stderr, cause }as section 4 specifies. executewraps that in a Result; withraise_errorson, 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 |