Skip to content

Tutorial: Real-World Scripts

Each tutorial in this section walks you through building one complete, runnable script from a clean slate. They’re test-first — you write a unittest before each piece of logic — and they all share the same small vocabulary: fn main for arguments and help, unittest { } for inline tests, .capture for command results, chains for transforming output.

If you read them in order, every tutorial only relies on concepts introduced earlier. If you skip ahead, you can always come back when something looks unfamiliar.

These three teach the core syntax through small, focused projects.

Read a VERSION file, increment the patch number, write it back. You learn let / mut, the six built-in types, list and string handling, and string interpolation. The pure helpers (version_parts, bump_patch) get unittests; the file I/O happens in fn main.

Find the largest files in a directory and pretty-print them. You learn how POSIX pipes (|) compose with lash’s functional chains (.map, .filter, .take, .sortNumeric), and when to reach for each. The line-parsing function gets a unittest with three rounding cases.

Find images wider than a limit and resize them in place. You learn if / else if, for and while loops, continue / break, and match for multi-way dispatch. The pure decisions (needs_resize, label_for) get unittests; the convert shell-out happens in fn main.

These build on the foundations to teach the patterns that show up in real automation work.

Parse access logs to count error codes. You learn closures, lambda expressions, higher-order functions, and .reduce. Three pure functions, three unittest blocks.

Take the script from “Pipes and Chains” and add typed arguments, auto-generated --help (from the fn main signature), --test for inline unit tests, and a JSON-output branch. The introduction explains what fn main is for before showing the code.

Back up a directory to a remote server with automatic retry on failure. You learn .capture, .isFailure / .isSuccess, exit "message", and how lash’s own error messages are structured. The retry-back-off function is the pure piece that gets a unittest.

Standalone scripts that combine everything above.

A category map (folder_for) wrapped in a match expression, an extension extractor that handles edge cases like Makefile and archive.tar.gz, and a --dry_run flag for previewing. Both pure helpers get tested; fn main does the I/O.

A four-section summary of a Common Log Format access log: top paths, status codes, top IPs, error count. Built around .wordcount() and small named helpers like is_error and format_entry.

A staging/production deploy with prerequisite checks, build, optional tests, and a confirmation pause for production. Pure server_for mapping plus a tested is_production predicate; require is the single I/O helper.

Generate systemd unit files (or any other configuration format) from a typed lash value. Demonstrates the type Foo { template = "..." } binding, the strict typing of {{#if}} / {{#each}} blocks, and the shape-validated Foo { ... } literal. Closes with the release.lash case as a real-world reference.


By the end of any tutorial above you’ll have settled into the same loop:

  1. Identify the pure decisions hiding inside the work — anything that maps inputs to outputs without touching the filesystem or the network.
  2. Write a unittest { } block that pins down what each decision should do, including the edge cases your intuition might miss.
  3. Run lash --test script.lash — see it fail, write the function, see it pass.
  4. Wire the tested helpers into a fn main that does the I/O. Don’t unittest main; trust the helpers.
  5. Use lash script.lash --help (auto-derived from the fn main doc-comment and signature) to remind yourself how to invoke it.

The same loop scales from a 20-line one-off to the release.lash that ships every release of lash itself.