Lazy evaluation for the shell.
Delays exec
of its argument until it sees the first byte of input.
The example below demonstrates that lazy
allows us to avoid either primary or secondary effects modifying the state of the system until after an earlier long-running command completes.
With lazy
:
(sleep 10; date +%s) | tee ts | (lazy touch file; cat >/dev/null)
printf "The file was modified %s seconds ago\n" $(( $(<ts) - $(date -r file +%s) ))
# => The file was modified 0 seconds ago
Without lazy
:
(sleep 10; date +%s) | tee ts | (touch file; cat >/dev/null)
printf "The file was modified %s seconds ago\n" $(( $(<ts) - $(date -r file +%s) ))
# => The file was modified 10 seconds ago
Note: the default behavior if the input pipe is closed without seeing any input is to exit with an error:
</dev/null lazy true ; echo $?
# => 3
- Linux
- TODO macOS
- TODO other 'nixes?
"just" build from source and copy the binary wherever you want it to be. The dream of the '90s!
A word of warning: lazy
can only defer evaluation of its own arguments, not the work the shell's already done before it is invoked. What that means is that neither of these work to prevent file
from being clobbered:
# caution: eagerly evaluates the redirect!
maybe_output | lazy cat > file # always clobbers `file`
# caution: eagerly evaluates the pipeline!
maybe_output | lazy cat | tee file # always clobbers `file`
Instead, make sure the program that'll be opening the files is the one whose execution is being delayed:
# works!
maybe_output | lazy tee file
# also works!
maybe_output | lazy sh -c 'cat >file'
lazy
is not particularly useful with streaming- or incrementally-oriented tools: it shines when output is all-or-nothing, not when the output can degrade in quality partway through.lazy
makes no attempt to handle the perennial mismatch between a computer's notion of byte streams and a human's notion of information that they care to look at: a single newline or a tab or a non-printable unicode code point are all at least one byte, though they will almost certainly not alone produce a meaningful output downstream.lazy
seeks to replace itself (viaexec
) with its argument as rapidly as possible. This simplifies the implementation, which means we don't have to do things like signal forwarding, but it also meanslazy
can take no actions once it observes any input, as it very quickly ceases to exist.- Like all UNIX tools,
lazy
suffers from a few common limitations:- "everything is a file" until it isn't; it's impossible to determine whether an arbitrary file handle will produce at least one byte of input without consuming that input, so special handling abounds.
- byte streams and single integers are not quite structured enough; there's an awful lot of shotgun parsers out there, and technically
lazy
adds to that pile. - argv-as-hci has some rough edges, not least of which is predicting the outcome through the many layers of variable expansion and shell interpolation and quoting.
Some goals of this project that are often in tension with each other:
- Small: both conceptually and in size on disk.
- Portable: available everywhere.
- Reliable: prefer predictability.
- Helpful
- The software ought to explain itself, and
- The project ought to act as a repository of knowledge for solving similar problems, especially where
lazy
is helpful or is definitely the wrong tool.
Some non-goals include:
- Comprehensive: discussion about how to solve a particular problem is welcomed, implementing functionality that could exist elsewhere less so.
- Novelty: shells, pipes, processes, and files are all quite a lot already! They're all flawed tools that we aim to augment and explain (where necessary) rather than replace.
- Strictness: On balance, we'd prefer to practice repairing our tools and relationships over preventing a breach, especially a hypothetical one.
The focus on keeping the absolute size of the program small hopefully also serves the goal of avoiding introducing too many new attack vectors by way of lazy
. It uses relatively few syscalls on its own, and once it exec
s then any additional surface area it exposed disappears.
That said, it does use exec
, which means if it can be tricked then it'll happily execute arbitrary code. Potentially evenon the attacker's schedule if they can also control the input pipe. Further, because pipes don't offer a peek
operation, the goal of getting to exec
and overwriting itself means that it'll need to use more exotic syscalls, potentially even in unintended ways. That naturally exposes some additional risk.
If you do discover a security issue that affects lazy
, please report it to @sethp (email address in profile), and I'll work with you to get it addressed as effectively as possible.