Capture

One of the most important features of a shell is to be able to manipulate the standard I/O streams of programs. There are three main ways of doing so: pipes, redirection, and capturing. The first two are commonly used when we just want to forward the output or input of a program. But if we want to preprocess the output, or use it as an argument to another program, we'll usually reach for capturing the output stream as a string in the shell, so that we can handle it ourselves.

In Hush, we can capture the output of a whole command block by prefixing it with a dollar:

let result = ${ echo Hello world! }

Instead of resulting in nil or error, as does the standard command block, a capture block will result in either a dict containing the stdout and stderr string fields, or an error if the block fails.

function get_first_word()
	let output = ${ echo Hello world! }?.stdout
	let first_word = std.split(std.trim(output), " ")[0]
	if std.is_empty(first_word) then
		std.error("output was empty", output)
	else
		first_word
	end
end

std.print(get_first_word()) # Hello

The separation of the stdout and stderr fields in the resulting dict enables the programmer to properly handle the standard error stream separately from the standard output stream, something that is more complicated than it should in traditional shells.

If the command block fails, the resulting error will contain the output that was captured before the block failed:

let result = ${
	echo Hello world!;
	echo The next command wil fail 1>2;
	this-command-does-not-exists;
}

std.assert(std.type(result) == "error")
std.assert(result.context.stdout == "Hello world!\n")
std.assert(result.context.stderr == "The next command wil fail\n")