Skip to content

Input, output and secrets ​

Docker for the two secret tasks only. This topic covers stdin, the stdout contract, get_env / find_env / has_env / get_env_or, !! secret bindings, mark_secret, secret references, and log.

bash
echo '{"name":"api","port":8080}' | lask eval service-line
printf 'api\nweb-frontend\n' | lask eval longest-name
APP_ENV=prod lask eval where-am-i
lask sync && lask run show-token     # watch the log mask it
# the same task, with the token held in Vault (spec 9.8)
LASK_SECRETS=vault VAULT_ADDR=https://vault.example.com VAULT_TOKEN=... \
  APP_TOKEN='{vault://secret/app#token}' lask secrets check show-token

The thing to take away: run writes nothing to stdout and eval writes the return value there, which is what makes lask eval a | lask run b a pipeline.

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/12-io-and-secrets

Source ​

This is example/02-language/12-io-and-secrets in the lask repository.

lask
// Input, output and secrets
//
// `run` writes nothing to stdout, `eval` writes the return value there,
// and logs and diagnostics always go to stderr. That is what makes
// `lask eval a | lask run b` a pipeline rather than a mess.
//
//   echo '{"name":"api","port":8080}' | lask eval service_line
//   APP_ENV=prod lask eval where_am_i
//   lask run show_token

alpine = #alpine:3.22.2

// Read the whole of stdin as text.
//
// `stdin` is a reserved identifier, bound once per run and always a
// `String` — empty when nothing was piped in. Structure it in the
// language rather than in a shell: `lines`, `from_json`, then `cast`.
//
// @return One line per input line, numbered.
numbered(): Array<String> =
  for (e : enumerate(lines(stdin))) { "#{e.index + 1}: #{e.value}" }

type Service = Record<name: String, port: Number>

// Take a JSON object on stdin and return one line of text.
//
// @example echo '{"name":"api","port":8080}' | lask eval service-line
service_line(): String = do {
  s: Service = cast(from_json(stdin))
  "#{s.name} listens on #{s.port}"
}

// The longest name on stdin.
//
// Stdin is text until the program says otherwise, so a plain list of
// words needs no JSON at all.
//
// @example printf 'api\nweb\n' | lask eval longest-name
longest_name(): String = do {
  names = filter(lines(stdin), \(l: String) -> l != "")
  is_empty(names) ? "(nothing on stdin)" : last(sort_by(names, length))
}

// Report what this run sees of its environment.
//
// The four ways of reading a process environment variable differ only
// in what an unset variable means: a failure, a `Null`, a `Bool`, or a
// fallback.
//
// @return What each form says about APP_ENV and HOME.
where_am_i(): Record<stage: String, present: Bool, region: String> = do {
  // `find_env` returns `String | Null`, resolved like any union.
  found = find_env("APP_ENV")
  stage = case (found) {
    Null -> "(unset)"
    else -> found
  }
  {
    stage: stage,
    present: has_env("APP_ENV"),
    // The form to prefer where a default is sensible: it says at the
    // call site what the default is.
    region: get_env_or("AWS_REGION", "eu-west-1")
  }
}

// The AWS profile this task insists on.
//
// A variable the task cannot run without is read with `get_env`, which
// fails, naming the variable, at the point it is missing.
//
// @example AWS_PROFILE=dev lask eval required-profile
required_profile(): String = get_env("AWS_PROFILE")

// Use a credential, and watch the log mask it.
//
// `!!` marks a binding secret: the value is masked wherever it appears
// in the command execution log afterwards.
//
// Run this and watch the log — the token is replaced, while the
// harmless argument beside it is not. The marker is allowed on
// `String` only, means nothing to the type system, and does not mask
// `eval`'s own output: it protects the log, not the result.
//
// APP_TOKEN may also hold a secret reference, such as
// `{vault://secret/app#token}`. With `LASK_SECRETS=vault` and Vault's
// own variables set, the value is read from Vault and masked the same
// way: the task stays as it is, and only the environment changes.
//
// @param token  Stands in for a credential read from the environment.
// @example lask run show-token
show_token(--token!!: String = get_env_or("APP_TOKEN", "s3cr3t-value")): Void = do {
  log("about to use #{token}")
  $[alpine] echo "token=#{token} user=lask"
  log("done")
}

// Mask a value that was not declared secret.
//
// `mark_secret` is the same thing as a function, for a value that was
// not declared with `!!` — the return value of a command, say.
issue_token(): String = do {
  generated = mark_secret(random_string(12))
  $[alpine] echo "issued #{generated}"
  "issued"
}

// Report progress while computing.
//
// `log` is how a task reports progress: it needs no environment, where
// `$ echo` would materialize a whole container to print one line.
//
// Under `run`, where nothing is printed to stdout, it is also the only
// way to watch an intermediate value.
progress(): Number = do {
  log("step 1/2")
  n = 21
  log("step 2/2")
  n * 2
}

Released under the MIT License.