lua-shell-games

Execute shell commands while capturing their output and exit codes. Includes helpers for performing shell escaping/quoting.

shell-games

[!Circle CI](https://circleci.com/gh/GUI/lua-shell-games)

A Lua library to help execute shell commands more easily and safely.

  • Easily execute shell commands, while capturing the command's output and exit code. Includes compatibility across versions of Lua, LuaJIT, and OpenResty where io.popen may not return exit codes (pre Lua 5.2 behavior).

  • Utilities to quote and escape shell arguments for safer, less error-prone execution.

When executing shell commands, shell-games wraps either os.execute or io.popen (depending on whether the output is being captured).

OpenResty Note: If using shell-games with OpenResty, be aware that executing shell commands with this library is a blocking operation. This may or may not be okay depending on the nginx phase (blocking in init is okay) and your application's requirements. You may want to consider alternatives like lua-resty-shell or lua-resty-exec if blocking is a concern.

Installation

Via LuaRocks:

    luarocks install shell-games

Or via OPM:

    opm get GUI/lua-shell-games

Usage

    local shell = require "shell-games"
    
    -- Execute a command, with error handling.
    local result, err = shell.run({ "touch", "/tmp/hello.txt" })
    if err then
      print(err)
      exit(1)
    end
    
    -- Executue a command, capturing both its stdout and stderr output.
    local result, err = shell.capture_combined({ "ls", "-l", "/tmp" })
    print("Exit code: " .. result["status"])
    print("Command output: " .. result["output"])
    
    -- Quoting
    shell.quote("ls") -- "ls"
    shell.quote("It's happening.") -- [['It'"'"'s happening.']]
    shell.quote("$PATH") -- "'$PATH'"
    
    -- Quote and join
    shell.join({ "ls", "-l", "/tmp/foo bar" }) -- "ls -l '/tmp/foo bar'"

Compatibility

Tested against:

  • Lua 5.1

  • Lua 5.2

  • Lua 5.3

  • LuaJIT 2.0

  • LuaJIT 2.0 compiled with LUAJIT_ENABLE_LUA52COMPAT

  • LuaJIT 2.1-beta

  • LuaJIT 2.1-beta compiled with LUAJIT_ENABLE_LUA52COMPAT

  • OpenResty

API

run

syntax: result, err = shell.run(command[, options])

Execute a shell command.

The comamnd argument must be passed as a table of individual arguments that will be escaped. While this is the recommended way to execute commands, see `run_raw` if you need to execute an arbitrary shell command from a string.

The options table accepts the following fields:

  • capture (boolean): Whether or not to capture the command's output. (default: false)

    By default, command output will not be captured, so any stdout/stderr generated by the command will be displayed in the parent process.

    If true, then the command's stdout will be captured and returned in the result, but stderr will still be printed in the parent process. To capture the command's stderr too, it must be redirected. See `capture_combined` and `capture` for convenience wrappers for more easily running commands while capturing output.

  • chdir (string): Change the current working directory to this path before executing the command. (default: nil)

  • env (table): Set environment variables before executing the command. Accepts a table of environment variable names and values. (default: {})

  • stderr (string): Redirect the command's stderr output to a different path. (default: nil)

  • stdout (string): Redirect the command's stdout output to a different path. (default: nil)

  • umask (string): Change the process's umask before executing the command. (default: nil)

If executing the command fails (returning a non-0 exit code), then err will be a string describing the error. In the case of failure, the result table will still be returned (with the command's exit code and output reflected in the table).

The returned result table has the following fields:

  • command (string): A string that shows the full command that was executed, after taking into account escaping and the options.

  • status (integer): The exit code of the command.

  • output (string): This field is only present if the capture option was enabled. If capturing was enabled, then this reflects the output of the command. By default, this will only contains the stdout from the command, and not the stderr. See `capture_combined` and `capture` for convenience wrappers for more easily running commands while capturing output.

    -- Basic example
    local result, err = shell.run({ "ls", "-l", "/tmp" })
    
    -- Example with options
    local result, err = shell.run({ "ls", "-l", "/tmp" }, {
      capture = true,
      chdir = "/tmp",
      env = {
        LD_LIBRARY_PATH = "/usr/local/lib",
      },
      stderr = "/tmp/stderr.log",
      stdout = "/tmp/stdout.log",
      umask = "0022",
    })

capture

syntax: result, err = shell.capture(command[, options])

Execute a shell command, capturing stdout as a string.

This is a convenience wrapper around executing a command with `run` while capturing stdout. It is equivalent to calling run with the options set to { capture = true }.

The command and options arguments are given in the same format as in `run`.

    local result, err = shell.capture({ "ls", "-l", "/tmp" })

capture_combined

syntax: result, err = shell.capture_combined(command[, options])

Execute a shell command, capturing both stdout and stderr as a single string.

This is a convenience wrapper around executing a command with `run` while capturing both stdout and stderr (by redirecting stderr to stdout). It is equivalent to calling run with the options set to { capture = true, stderr = "&1" }.

The command and options arguments are given in the same format as in `run`.

    local result, err = shell.capture_combined({ "ls", "-l", "/tmp/non-existent" })

run_raw

syntax: result, err = shell.run_raw(command[, options])

Execute a shell command given as a raw string.

Usually, using `run`, `capture`, or `capture_combined` is recommended, which all accept the command to run as a table of individual arguments (since these handle shell escaping and quoting for you). However, in cases where you need to execute unescaped commands, this run_raw can be used to directly run a command string verbatim.

The comamnd argument must be passed as a string. Be sure to handle any escaping manually within this string.

The options argument is given in the same format as in `run`.

    local result, err = shell.run_raw("echo $PATH", {
      capture = true,
    })
    print(result["output"])

quote

syntax: quoted_string = shell.quote(str)

Return a shell-escaped version of the string. The escaped string can safely be used as one token in a shell command.

    shell.quote("ls") -- "ls"
    shell.quote("It's happening.") -- [['It'"'"'s happening.']]
    shell.quote("$PATH") -- "'$PATH'"

join

syntax: quoted_string = shell.join(table)

Accepts a table of individual command arguments, which will be escaped (using `quote`) and joined together by spaces. Suitable for turning a list of command arguments into a single command string.

    shell.join({ "ls", "-l", "/tmp/foo bar" }) -- "ls -l '/tmp/foo bar'"

Development

After checking out the repo, Docker can be used to run the test suite:

    docker-compose run --rm app make test

Release Process

To release a new version to LuaRocks and OPM:

  • Ensure CHANGELOG.md is up to date.

  • Update the _VERSION in lib/shell-games.lua.

  • Move the rockspec file to the new version number (git mv shell-games-X.X.X-1.rockspec shell-games-X.X.X-1.rockspec), and update the version and tag variables in the rockspec file.

  • Commit and tag the release (git tag -a vX.X.X -m "Tagging vX.X.X" && git push origin vX.X.X).

  • Run make release VERSION=X.X.X.

Authors

Nick Muerdter

License

mit

Versions

  • Execute shell commands while capturing their output and exit codes. Includes helpers for performing shell escaping/quoting. 2019-09-30 00:42:38
  • Execute shell commands while capturing their output and exit codes. Includes helpers for performing shell escaping/quoting. 2019-09-29 16:31:02
  • Execute shell commands while capturing their output and exit codes. Includes helpers for performing shell escaping/quoting. 2019-05-01 03:24:52
  • Execute shell commands while capturing their output and exit codes. Includes helpers for performing shell escaping/quoting. 2018-12-11 03:49:47