Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 6 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Lua][lua-shield]][lua-url]
[![LuaRocks][luarocks-shield]][luarocks-url]

Tiny library for shell scripting with Lua (inspired by zserge/luash).
Tiny library for shell scripting with Lua (inspired by [zserge/luash](https://github.com/zserge/luash)).

`luash` is interesting, but it modifies `_G` in an extreme way.

Expand All @@ -20,15 +20,6 @@ It works with any "posix-enough" shell by default such as `bash`, `zsh`, and `da

But it will not work by default with `fish`, `nushell`, `cmd` or `powershell` unless you define a [representation](./REPR.md) for that shell.

It also exports a [small nix helper](#in-addition-to-the-library) that allows you
to use `shelua` to write `nix` derivations in `lua` instead of `bash`.

It is `pkgs.runCommand` except it is `pkgs.runLuaCommand` because the command is in `lua`.

It is useful when you have a short build or wrapper script that needs to deal with a lot of structured data.

Especially when you have a lot of `json` and would rather use `cjson` and deal with tables than use `jq` and bash arrays

## Install

via luarocks: `luarocks install shelua`
Expand All @@ -39,7 +30,7 @@ Or just clone this repo and copy `lua/sh.lua` into your project.

## Simple usage

Every command that can be called via os.execute can be called via the sh table.
Every command that can be called via `os.execute` can be called via the sh table.
All the arguments passed into the function become command arguments.

``` lua
Expand All @@ -56,7 +47,7 @@ end
## Command input and pipelines

If command argument is a table which has a `__input` field - it will be used as
a command input (stdin). Multiple arguments with input are allowed, they will
a command input (`stdin`). Multiple arguments with input are allowed, they will
be concatenated.

The each command function returns a structure that contains the `__input`
Expand Down Expand Up @@ -84,8 +75,8 @@ sh.ls '/bin' : grep "$filter" : wc '-l'
```

Note that the commands are not running in parallel (because Lua can only handle
one I/O loop at a time). So the inner-most command is executed, its output is
read, the the outer command is execute with the output redirected etc.
one `I/O` loop at a time). So the inner-most command is executed, its output is
read, the outer command is execute with the output redirected etc.

However, `shelua` also offers a `proper_pipes` [setting](#settings).

Expand Down Expand Up @@ -116,7 +107,7 @@ arguments without manually changing the arguments list.
## Partial commands and commands with tricky names or characters

You can call `sh` with a string as the first argument to construct a command function, optionally
pre-setting the arguments:
presetting the arguments:

``` lua
local sh = require('sh')
Expand Down
85 changes: 41 additions & 44 deletions REPR.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,14 @@ Its first return value will then be passed through any defined `transforms`.

the result, in addition to its optional second return value will then be passed to one of the 2 following run functions based on current lua version.

`post_5_2_run` and `pre_5_2_run` are what call the actual final shell command when needed.
`run_cmd` is what calls the actual final shell command when needed.

Their job is to run the command and report the result, exit and signal codes.
Its job is to run the command and report the result, exit and signal codes.

Prior to 5.2 the io.popen command does not return exit code or signal. You can decide to support older than 5.2 or not.
Prior to 5.2 the `io.popen` command does not return exit code or signal. You can decide to support older than 5.2 or not.

```lua
---returns cmd and an optional item such as path to a tempfile to be passed to post_5_2_run or pre_5_2_run
---returns cmd and an optional item such as path to a tempfile to be passed to run_cmd
---called only when proper_pipes is false
---cmd is the result of add_args
---codes is the list of codes that correspond with each input such as `__exitcode`, empty if none
Expand All @@ -105,53 +105,50 @@ Prior to 5.2 the io.popen command does not return exit code or signal. You can d
end,
---runs the command and returns the result and exit code and signal
-- cmd is the result of single_stdin or concat_cmd, after being passed through any defined transforms
---@field post_5_2_run fun(opts: Shelua.Opts, cmd: string|any, msg: any?): { __input: string, __exitcode: number, __signal: number }
post_5_2_run = function(opts, cmd, tmp)
local p = io.popen(cmd, 'r')
local output, exit, status
if p then
output = p:read('*a')
_, exit, status = p:close()
end
pcall(os.remove, tmp)
---@field run_cmd fun(opts: Shelua.Opts, cmd: string|any, msg: any?): { __input: string, __exitcode: number, __signal: number }
run_cmd = function(opts, cmd, tmp)
if is_5_2_plus then
local p = io.popen(cmd, 'r')
local output, _, exit, status
if p then
output = p:read('*a')
_, exit, status = p:close()
end
pcall(os.remove, tmp)

return {
__input = output,
__exitcode = exit == 'exit' and status or 127,
__signal = exit == 'signal' and status or 0,
}
end,
---runs the command and returns the result and exit code and signal
---Should return the flags using the same format as io.popen does in 5.2+
-- cmd is the result of single_stdin or concat_cmd, after being passed through any defined transforms
---@field pre_5_2_run fun(opts: Shelua.Opts, cmd: string|any, msg: any?): { __input: string, __exitcode: number, __signal: number }
pre_5_2_run = function(opts, cmd, tmp)
local p = io.popen(cmd .. "\necho __EXITCODE__$?", 'r')
local output
if p then
output = p:read('*a')
p:close()
return {
__input = output,
__exitcode = exit == 'exit' and status or 127,
__signal = exit == 'signal' and status or 0,
}
else
local p = io.popen(cmd .. "\necho __EXITCODE__$?", 'r')
local output
if p then
output = p:read('*a')
p:close()
end
pcall(os.remove, tmp)
local exit
output = (output or ""):gsub("__EXITCODE__(%d*)\r?\n?$", function(code)
exit = tonumber(code)
return ""
end)
return {
__input = output,
__exitcode = exit or 127,
__signal = (exit and exit > 128) and (exit - 128) or 0
}
end
pcall(os.remove, tmp)
local exit
output = (output or ""):gsub("__EXITCODE__(%d*)\r?\n?$", function(code)
exit = tonumber(code)
return ""
end)
return {
__input = output,
__exitcode = exit or 127,
__signal = (exit and exit > 128) and (exit - 128) or 0
}
end,
---if your pre_5_2_run or post_5_2_run returns a table with extra keys, e.g. `__stderr`
---if your run_cmd returns a table with extra keys, e.g. `__stderr`
---proper_pipes will need to know that accessing them should be a trigger to resolve the pipe.
---each string in this table must begin with '__' or it will be ignored
---@field extra_cmd_results string[]|fun(opts: Shelua.Opts): string[]
extra_cmd_results = {},
---a list of functions to run in order on the command before running it.
---each one recieves the previous value and returns a new one.
---they are ran after concat_cmd or single_stdin and before the post_5_2_run and pre_5_2_run functions
---they are ran after concat_cmd or single_stdin and before the run_cmd functions
---@field transforms? (fun(cmd: string|any): string|any)[]
transforms = {},
```
Expand All @@ -172,7 +169,7 @@ which is the optional second return value of the call to `concat_cmd`
Your goal in this function is to construct a string from the prior inputs,
that pipes them into the command, and then return that string, if there are any prior inputs to pipe.

Its result will be provided to the same run function as `single_stdin` would have, either `pre_5_2_run` or `post_5_2_run`,
Its result will be provided to the same run function as `single_stdin` would have, `run_cmd`,
after adding the newly resolved values to the command result being resolved.

```lua
Expand All @@ -192,7 +189,7 @@ after adding the newly resolved values to the command result being resolved.

---strategy to combine piped inputs, 0, 1, or many, return resolved command to run
---called only when proper_pipes is true
---may return an optional second value to be placed in another PipeInput, or returned to post_5_2_run or pre_5_2_run
---may return an optional second value to be placed in another PipeInput, or returned to run_cmd
-- cmd is the same type as the result of add_args
---@field concat_cmd fun(opts: Shelua.Opts, cmd: string|any, input: Shelua.PipeInput[]): (string|any, any?)
concat_cmd = function(opts, cmd, input)
Expand Down
133 changes: 72 additions & 61 deletions lua/sh.lua
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
---Will contain either `s`, a plain string,
---or `c`, an input command string
---@class Shelua.PipeInput
---@class Shelua.PipeInputStdin
---string stdin to combine
---@field s? string|any
---if string input came from a command,
---`e` will contain a table of all other command result fields
---such as `__exitcode`
---@field e? table

---@class Shelua.PipeInputClass
---cmd to combine
---@field c? string|any
---optional 2nd return of concat_cmd
---@field m? any

---@alias Shelua.PipeInput Shelua.PipeInputStdin | Shelua.PipeInputClass

---@class Shelua.Repr
---escapes a string for the shell
---@field escape fun(arg: any, opts: Shelua.Opts?): string
Expand All @@ -20,7 +24,7 @@
---@field arg_tbl fun(opts: Shelua.Opts, k: string, a: any): string|string[]?
---adds args to the command
---@field add_args fun(opts: Shelua.Opts, cmd: string, args: string[]): string|any
---returns cmd and an optional item such as path to a tempfile to be passed to post_5_2_run or pre_5_2_run
---returns cmd and an optional item such as path to a tempfile to be passed to run_cmd
---called when proper_pipes is false
---cmd is the result of add_args
---codes is the list of codes that correspond with each input such as `__exitcode`, empty if none
Expand All @@ -30,13 +34,11 @@
---@field concat_cmd fun(opts: Shelua.Opts, cmd: string|any, input: Shelua.PipeInput[]): (string|any, any?)
---a list of functions to run in order on the command before running it.
---each one recieves the previous value and returns a new one.
---they are ran after concat_cmd or single_stdin and before the post_5_2_run and pre_5_2_run functions
---they are ran after concat_cmd or single_stdin and before the run_cmd functions
---@field transforms? (fun(cmd: string|any): string|any)[]
---runs the command and returns the result and exit code and signal
---@field post_5_2_run fun(opts: Shelua.Opts, cmd: string|any, msg: any?): { __input: string, __exitcode: number, __signal: number }
---runs the command and returns the result and exit code and signal
---@field pre_5_2_run fun(opts: Shelua.Opts, cmd: string|any, msg: any?): { __input: string, __exitcode: number, __signal: number }
---if your pre_5_2_run or post_5_2_run returns a table with extra keys, e.g. `__stderr`
---@field run_cmd fun(opts: Shelua.Opts, cmd: string|any, msg: any?): { __input: string, __exitcode: number, __signal: number }
---if your run_cmd returns a table with extra keys, e.g. `__stderr`
---proper_pipes will need to know that accessing them should be a trigger to resolve the pipe.
---each string in this table must begin with '__' or it will be ignored
---@field extra_cmd_results string[]|fun(opts: Shelua.Opts): string[]
Expand Down Expand Up @@ -134,10 +136,34 @@ local function tbl_get(t, default, ...)
return t or default
end

local warned_run_cmd_shim = false

---@param opts Shelua.Opts
---@param attr string
---@return function
local get_repr_fn = function(opts, attr)
if attr == "run_cmd" then
local shell = opts.shell or "posix"
local shell_repr = tbl_get(opts, nil, "repr", shell)
if shell_repr and not shell_repr.run_cmd then
local old_post = shell_repr.post_5_2_run
local old_pre = shell_repr.pre_5_2_run
if old_post or old_pre then
if not warned_run_cmd_shim then
warned_run_cmd_shim = true
io.stderr:write("shelua: post_5_2_run/pre_5_2_run are deprecated. ")
io.stderr:write("Use run_cmd(opts, cmd, msg) instead.\n")
end
shell_repr.run_cmd = function(o, cmd, msg)
if is_5_2_plus and old_post then
return old_post(o, cmd, msg)
else
return (old_pre or old_post)(o, cmd, msg)
end
end
end
end
end
return tbl_get(opts, tbl_get(opts, function()
error("Shelua Repr Error: " ..
tostring(attr) .. " function required for shell: " .. tostring(opts.shell or "posix"))
Expand Down Expand Up @@ -222,39 +248,40 @@ local posix = {
return cmd
end
end,
post_5_2_run = function(opts, cmd, tmp)
local p = io.popen(cmd, 'r')
local output, _, exit, status
if p then
output = p:read('*a')
_, exit, status = p:close()
end
pcall(os.remove, tmp)
run_cmd = function(opts, cmd, tmp)
if is_5_2_plus then
local p = io.popen(cmd, 'r')
local output, _, exit, status
if p then
output = p:read('*a')
_, exit, status = p:close()
end
pcall(os.remove, tmp)

return {
__input = output,
__exitcode = exit == 'exit' and status or 127,
__signal = exit == 'signal' and status or 0,
}
end,
pre_5_2_run = function(opts, cmd, tmp)
local p = io.popen(cmd .. "\necho __EXITCODE__$?", 'r')
local output
if p then
output = p:read('*a')
p:close()
return {
__input = output,
__exitcode = exit == 'exit' and status or 127,
__signal = exit == 'signal' and status or 0,
}
else
local p = io.popen(cmd .. "\necho __EXITCODE__$?", 'r')
local output
if p then
output = p:read('*a')
p:close()
end
pcall(os.remove, tmp)
local exit
output = (output or ""):gsub("__EXITCODE__(%d*)\r?\n?$", function(code)
exit = tonumber(code)
return ""
end)
return {
__input = output,
__exitcode = exit or 127,
__signal = (exit and exit > 128) and (exit - 128) or 0
}
end
pcall(os.remove, tmp)
local exit
output = (output or ""):gsub("__EXITCODE__(%d*)\r?\n?$", function(code)
exit = tonumber(code)
return ""
end)
return {
__input = output,
__exitcode = exit or 127,
__signal = (exit and exit > 128) and (exit - 128) or 0
}
end,
extra_cmd_results = {},
transforms = {},
Expand Down Expand Up @@ -362,21 +389,14 @@ local cmd_mt = {
end
if check_if_cmd_result(opts, c) then
local apply = function(com)
local transforms = opts.transforms
if transforms then print("Shelua Deprecation: transforms option moved to be a repr-specific option") end
transforms = tbl_get(opts, transforms or {}, "repr", opts.shell or "posix", "transforms")
local transforms = tbl_get(opts, {}, "repr", opts.shell or "posix", "transforms")
for _, f in ipairs(transforms) do
com = f(com)
end
return com
end
local cmd, msg = resolve(self, opts)
local res
if is_5_2_plus then
res = get_repr_fn(opts, "post_5_2_run")(opts, apply(cmd), msg)
else
res = get_repr_fn(opts, "pre_5_2_run")(opts, apply(cmd), msg)
end
local res = get_repr_fn(opts, "run_cmd")(opts, apply(cmd), msg)
for k, v in pairs(res or {}) do
rawset(self, k, v)
end
Expand Down Expand Up @@ -494,25 +514,16 @@ command = function(self, cmdstr, ...)
unresolved[t] = { cmd = cmd, unres = unres, input = input, codes = codes }
else
local apply = function(com)
local transforms = shmt.transforms
if transforms then print("Shelua Deprecation: transforms option moved to be a repr-specific option") end
transforms = tbl_get(shmt, transforms or {}, "repr", shmt.shell or "posix", "transforms")
local transforms = tbl_get(shmt, {}, "repr", shmt.shell or "posix", "transforms")
for _, f in ipairs(transforms) do
com = f(com)
end
return com
end
if is_5_2_plus then
local msg
cmd, msg = get_repr_fn(shmt, "single_stdin")(shmt, cmd, #input > 0 and input or nil,
#codes > 0 and codes or nil)
t = get_repr_fn(shmt, "post_5_2_run")(shmt, apply(cmd), msg)
else
local msg
cmd, msg = get_repr_fn(shmt, "single_stdin")(shmt, cmd, #input > 0 and input or nil,
#codes > 0 and codes or nil)
t = get_repr_fn(shmt, "pre_5_2_run")(shmt, apply(cmd), msg)
end
local msg
cmd, msg = get_repr_fn(shmt, "single_stdin")(shmt, cmd, #input > 0 and input or nil,
#codes > 0 and codes or nil)
t = get_repr_fn(shmt, "run_cmd")(shmt, apply(cmd), msg)
if shmt.assert_zero and t.__exitcode ~= 0 then
error("Command " .. tostring(cmd) .. " exited with non-zero status: " .. tostring(t.__exitcode))
end
Expand Down