Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/gui/lua-shell-games

A Lua library to help execute shell commands more easily and safely.
https://github.com/gui/lua-shell-games

lua shell

Last synced: 16 days ago
JSON representation

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

Awesome Lists containing this project

README

        

# shell-games

[![Circle CI](https://circleci.com/gh/GUI/lua-shell-games.svg?style=svg)](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](https://github.com/juce/lua-resty-shell) or [lua-resty-exec](https://github.com/jprjr/lua-resty-exec) if blocking is a concern.

## Installation

Via [LuaRocks](https://luarocks.org):

```sh
luarocks install shell-games
```

Or via [OPM](https://opm.openresty.org):

```sh
opm get GUI/lua-shell-games
```

## Usage

```lua
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`](#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`](#capture_combined) and [`capture`](#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`](#capture_combined) and [`capture`](#capture) for convenience wrappers for more easily running commands while capturing output.

```lua
-- 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`](#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`](#run).

```lua
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`](#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`](#run).

```lua
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`](#run), [`capture`](#capture), or [`capture_combined`](#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`](#run).

```lua
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.

```lua
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`](#quote)) and joined together by spaces. Suitable for turning a list of command arguments into a single command string.

```lua
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:

```sh
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`.