Skip to content

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.chdir and uv.chdir: the directory of the whole process changes, as in a shell, and Lua's own io.open follows.
  • popen has no chdir. chdir runs cd -P <old> && cd -P <dir> && pwd -P through the shell (RESOLVE_DIR), which checks that the directory can be entered and resolves .. and symlinks, then remembers the result. Every later run prefixes its command with cd -P <dir> || exit 127, and getcwd returns the remembered path. The emulation covers the programs LuaSH runs only: after sh.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"))