Working directory¶
The builtins¶
cd, pushd, popd and print are not programs: sh/init.lua
implements them, and index_module returns them for those
names (the list is __index_ignore_function; sh.install()
consults the same list for globals). Each parses its arguments with
command.parse and returns a passthrough Result:
the stdout, stderr and status of the Results it was given, with the last
one's Pipeline. That is why a builtin is a neutral stage: in
sh.echo("x"):cd("tests"):cat(), cat still reads x.
cd(dir) asks the backend to change directory and, when that fails,
returns the Result with cd: The directory '...' does not exist in
__stderr and 1 in __exitcode; the builtins never raise.
pushd remembers the current directory on
sh.PUSHD_STACK before changing, and popd returns to
the top entry.
The backend owns the directory¶
The design makes the working directory the backend's
(section 4): the contract has
chdir(dir) and getcwd(), the registry forwards to them
(backend.chdir checks that dir is a non-empty
string), and run takes an optional cwd for one run only, which the
public API never sets.
- posix and luv call
posix.chdiranduv.chdir: the directory of the whole process changes, as in a shell, and Lua's ownio.openfollows. - popen has no
chdir.chdirrunscd -P <old> && cd -P <dir> && pwd -Pthrough the shell (RESOLVE_DIR), which checks that the directory can be entered and resolves..and symlinks, then remembers the result. Every laterrunprefixes its command withcd -P <dir> || exit 127, andgetcwdreturns the remembered path. The emulation covers the programs LuaSH runs only: aftersh.cd("tests")under popen,io.open("file")still opens it in the process's directory.
That difference is the one place where the backends show in ordinary use.
The design accepts it because a
real chdir needs a C module, which popen exists to avoid.
Within one run¶
The cwd option of run, when a caller of the backend sets it, is
entered in the child only: posix calls chdir after the fork
(exec_child), luv passes the cwd spawn option, and popen
adds the cd prefix. A directory that cannot be entered is a start
failure, exit status 127 with a message, like a program that is not found.
The contract test exercises this (testing).
How to¶
Run commands in another directory¶
cd moves for the rest of the script; pushd and popd bracket a
stretch and chain like commands:
sh.pushd("build")
sh.make()
sh.popd()
sh.pushd("build"):make():popd() -- the same, in one line
A make that raises leaves the script in build. A helper that pops
under an error as well keeps the directory change local:
local function within(dir, f, ...)
local entered = sh.pushd(dir)
if entered.__exitcode ~= 0 then error(sh.stderr(entered), 2) end
local ok, result = pcall(f, ...)
sh.popd()
if not ok then error(result, 0) end
return result
end
within("build", sh.make, "-j4")
Many programs also take the directory as an option, with no change at
all: git -C, make -C, tar -C.
Check that a cd worked¶
The builtins never raise; read the Result:
local r = sh.cd(dir)
if r.__exitcode ~= 0 then error(sh.stderr(r)) end
The message is cd: The directory '...' does not exist for any
directory that cannot be entered.
Know where you are¶
sh.pwd() runs the program, so it answers for the backend's directory
on every backend, popen included. sh.PUSHD_STACK:size() is the depth
of the pushd stack.
Open files from Lua after a cd¶
Under posix and luv the process moved, so io.open("file") follows the
cd; under popen only the programs moved. Code for every backend builds
paths from the backend's directory, or keeps them absolute:
local dir = tostring(sh.pwd())
local f = assert(io.open(dir .. "/file"))