Skip to content

Commands ​

Docker. $, $1, $2 and $*, explicit $[env], interpolation, shell_quote, run, and the command ... on ... declarations that dispatch a bare $.

bash
lask sync
lask eval probe --path /etc/os-release
lask eval inspect            # a command that exits 3, without raising
lask eval report

The thing to take away: a command string swallows the rest of its line. $ uname -s |> trim hands |> trim to the shell and returns the empty string; the pipe belongs on the next line.

To try the commands above, paste the source below into main.lask in an empty directory and run lask sync first, or clone the repository:

bash
git clone https://github.com/lask-task-runner/lask.git
cd lask/example/02-language/05-commands

Source ​

This is example/02-language/05-commands in the lask repository.

lask
// Commands
//
// Every way of running a program, and the rules that decide where it
// runs. Needs Docker for the container parts:
//
//   lask envs list       # every image this module can reach for
//   lask sync            # materialize them; run and eval never pull
//   lask eval greeting
//   lask run  probe --path /etc/os-release

alpine = #alpine:3.22.2

// A `$` with no environment gets one from the command words in the
// string, matched against these declarations. Matching is lexical and
// exact — `/bin/cat` matches nothing, a basename is never taken.
command { "cat", "wc", "grep", "sort" } on alpine
command { "uname", "echo" } on #local

// The kernel of the host, from `uname`.
//
// `$` is the value of stdout, with a trailing newline like any program
// leaves. A non-zero exit code raises, carrying that code.
//
// @return The kernel name of this machine.
host_kernel(): String = $ uname -s
  |> trim

// Run a failing command and look at everything it left.
//
// The four forms differ only in what they hand back.
//
//   $  / $1   stdout, fails when the exit code is not 0
//   $2        stderr, same failure rule
//   $*        CommandResult {code, stdout, stderr}, never fails on the code
//
// @return The exit code, stdout and stderr of a command that fails.
inspect(): CommandResult = $*[alpine] sh -c 'echo out; echo err 1>&2; exit 3'

// Report whether a file is readable in the image.
//
// Because `$*` does not raise, it is how an expected failure is read.
//
// @example lask eval probe --path /etc/os-release
probe(--path: String = "/etc/os-release"): String = do {
  r = $* cat #{path}
  r.code == 0
    ? "found #{path}: #{size(lines(r.stdout))} lines"
    : "missing #{path} (exit #{r.code})"
}

// The first line of the image's /etc/os-release.
//
// An environment may also be named at the command, which overrides
// dispatch. Any expression of type Environment will do.
os_release(): String = $[alpine] cat /etc/os-release
  |> lines
  |> first

// The last line of a file, sorted.
//
// A command string runs to the end of its line, and *everything* still
// on that line is command text: `$ uname -s |> trim` hands `|>` and
// `trim` to the shell and quietly returns the empty string. The pipe
// therefore goes on the next line, where a leading binary operator
// continues the expression (6.5) instead.
//
// A line ending in `\` continues the command itself, which is how a
// long shell pipeline is broken across lines — the command ends at the
// first line that does not end in `\`.
longest_line(--file: String = "/etc/passwd"): String =
  $[alpine] cat #{file} | \
    sort | \
    tail -n 1
  |> trim

// Greet someone from inside the container.
//
// Interpolation takes any stringifiable value. A `String | Null` is not
// one — resolve it with `case` before it reaches a command.
greeting(--name: String = "Lask"): String = do {
  greeting = $[alpine] echo "hello, #{name}"
  trim(greeting)
}

// Pass text through the shell as a single word.
//
// An interpolated value is a value, not shell source. When it has to
// survive the shell as a single word, quote it with `shell_quote`.
echo_raw(--text: String = "a b; rm -rf /"): String =
  $[alpine] echo #{shell_quote(text)}
  |> trim

// Count lines with a command assembled at run time.
//
// `run` is the same execution as a function call: the command
// line comes from a String, so it can be assembled at runtime. It
// returns a CommandResult and never raises on the exit code.
//
// @param args  Passed to `wc`, e.g. `-l`.
count(--args: String = "-l", --file: String = "/etc/passwd"): Number = do {
  r = run(alpine, "wc #{args} #{file}")
  // No command string here, so this whole line is ordinary expression.
  r.stdout |> trim |> split_first |> to_number
}

internal split_first(s: String): String = first(split(s, " "))

// Two commands in one string, one environment.
//
// Commands chain with the shell's own operators, and dispatch then has
// to agree on one environment for the whole string: `cat x && wc -l x`
// is fine because both words are declared on `alpine`, while mixing in
// a `#local` program would be E-TYPE-COMMAND-CONFLICT — and a word
// declared nowhere is E-TYPE-COMMAND-NOENV, never a fall back to the host.
words_in(--file: String = "/etc/hostname"): String =
  $ cat #{file} && wc -c #{file}

// Everything above, in one value.
//
// Commands compose like anything else: this task runs three of them,
// two of which are in a container and one on the host.
//
// @return A short report about this machine and the image.
report(): Record<host: String, image: String, passwd_lines: Number> = {
  host: host_kernel(),
  image: os_release(),
  passwd_lines: count()
}

Released under the MIT License.