A Lua library to help execute shell commands more easily and safely.
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci Improving/fixing some development and release steps. Dec 11, 2018
lib Fix Lua 5.2 mode detection. Add more tests across Lua versions. Dec 11, 2018
spec Fixing OpenResty, non-lua 5.2 compat tests. Dec 11, 2018
.busted Initial abstraction of some shell helpers into separate library. Dec 2, 2018
.gitignore Improving/fixing some development and release steps. Dec 11, 2018
.luacheckrc Initial abstraction of some shell helpers into separate library. Dec 2, 2018
CHANGELOG.md Preparing release. Dec 11, 2018
Dockerfile Fix capture handling when running under OpenResty. Dec 3, 2018
LICENSE.txt Some initial documentation. Dec 3, 2018
Makefile Fix Lua 5.2 mode detection. Add more tests across Lua versions. Dec 11, 2018
README.md Fix Lua 5.2 mode detection. Add more tests across Lua versions. Dec 11, 2018
dist.ini
docker-compose.yml Improving/fixing some development and release steps. Dec 11, 2018
shell-games-1.0.0-1.rockspec Preparing release. Dec 11, 2018
shell-games-git-1.rockspec Some initial documentation. Dec 3, 2018

README.md

shell-games

Circle CI

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.