Control Flow
What’s control flow?
Section titled “What’s control flow?”Control flow is how a program decides what to do next. So far the scripts in earlier tutorials have been mostly straight-line: read a file, transform it, write it back. Control flow lets you branch (“only do this if the file is too big”), repeat (“do it for every image in the folder”), and dispatch (“based on the extension, pick the right tool”).
lash has four building blocks:
if/else if/else— choose one of several blocks based on a condition.for— iterate over a list, range, or command output.while— repeat as long as a condition holds.match— pick one branch based on which pattern fits a value.
You’ll use all four in the script we’re about to build.
Build a script that finds every image in a folder wider than a set limit and resizes them in place. We’ll start with the “is the tool installed” check, then loop, then add a dry-run flag, then dispatch on file extension. The pure helpers get unittests; the I/O parts get a manual smoke check.
lash --test resize.lash # the unittests onlylash resize.lash --help # auto-generated helpThe --test flag runs only the unittest { } blocks; the fn main
body is skipped — so the tests don’t need an actual convert binary
or any image files.
Step 1: Conditionals — guard against a missing tool
Section titled “Step 1: Conditionals — guard against a missing tool”Before doing any work, verify ImageMagick is installed. This is a
classic if:
let check = `which convert`.captureif check.isFailure { exit "ImageMagick 'convert' is required but not installed"}if blocks use braces — no then/fi. The full syntax:
if count > 10 { echo "many"} else if count > 0 { echo "some"} else { echo "none"}Conditions can use &&, ||, !, and the comparison operators
(==, !=, <, <=, >, >=):
if age >= 18 && country == "NL" { echo "allowed"}This is straight wiring around an external check — there’s nothing pure to test here yet.
Step 2: Decide whether a file needs resizing
Section titled “Step 2: Decide whether a file needs resizing”The decision “should this file be resized” is a pure function of width and the limit. Pin it down first:
/// needs_resize is true iff the actual width exceeds the targetunittest { needs_resize(2400, 1920).must.equal(true) // wider — resize needs_resize(1920, 1920).must.equal(false) // exactly at limit — skip needs_resize(800, 1920).must.equal(false) // narrower — skip}The middle row matters: if the image is exactly at the limit, we
don’t want to bounce it through convert and lose quality for nothing.
The implementation:
fn needs_resize(width, limit) { return width > limit}A two-line function with three test rows isn’t overkill — it’s the
record of what we decided. If a future contributor changes the
condition to >=, that test stops them.
Step 3: For loop over the image files
Section titled “Step 3: For loop over the image files”Now the loop. for iterates over a list, a range, or the output of a
command:
for file in `find $dir -type f \( -name "*.jpg" -o -name "*.png" \)` { echo $file}Other shapes:
# list literalfor name in ["alice", "bob", "carol"] { echo "hello $name"}
# rangefor i in 1..10 { echo "item $i"}For our resizer, we read each file’s width and skip the small ones with
continue:
mut resized = 0mut skipped = 0
for file in `find $dir -type f \( -name "*.jpg" -o -name "*.png" \)` { let width = `identify -format "%w" $file`.trim().toNumber()
if !needs_resize(width, max_width) { skipped = skipped + 1 continue # jump to the next file }
# ... resize logic next ...}continue moves to the next iteration. break exits the loop entirely:
for line in `cat big.log` { if line.contains("FATAL") { echo "Fatal error found: $line" break }}Step 4: Match on the extension
Section titled “Step 4: Match on the extension”The script supports both .jpg and .png — and we want a pretty label
in the output. That’s a multi-way dispatch — exactly what match is for.
Test it first:
/// label_for picks a friendly label per file extensionunittest { label_for("jpg").must.equal("JPEG") label_for("jpeg").must.equal("JPEG") label_for("png").must.equal("PNG") label_for("gif").must.equal("GIF") label_for("xyz").must.equal("unknown")}Five rows because there are five behaviours we care about: each named
extension gives its own label, the jpg/jpeg aliases collapse, and
unknown extensions get a sentinel rather than crashing.
fn label_for(ext) { return match ext { "jpg" | "jpeg" => "JPEG" "png" => "PNG" "gif" => "GIF" _ => "unknown" }}match checks patterns from top to bottom and stops at the first
hit. | combines patterns. _ is the wildcard that catches anything
not matched above. Every arm must produce a value (the last expression
in the arm body), because match is an expression — it returns the
value of the matching arm.
Step 5: Wire it into a main
Section titled “Step 5: Wire it into a main”The runtime glue takes a directory, a width limit, and a dry-run flag.
We don’t unittest main — it touches the filesystem and shells out to
convert. The pure helpers (needs_resize, label_for) already have
tests.
fn main(dir: string, max_width: int = 1920, dry_run: bool = false) { let check = `which convert`.capture if check.isFailure { exit "ImageMagick 'convert' is required but not installed" }
mut resized = 0 mut skipped = 0
for file in `find $dir -type f \( -name "*.jpg" -o -name "*.png" \)` { let width = `identify -format "%w" $file`.trim().toNumber()
if !needs_resize(width, max_width) { skipped = skipped + 1 continue }
let ext = file.split(".").last let kind = label_for(ext)
if dry_run { echo " would resize ($kind): $file ($width px -> $max_width px)" } else { convert $file -resize $max_width $file echo " resized ($kind): $file" } resized = resized + 1 }
echo "" echo "$resized resized, $skipped already within limit"}mut declares a mutable binding — let would refuse the later
reassignment. We need it here because the counters change across loop
iterations.
Complete script
Section titled “Complete script”#!/usr/bin/env lash
/// Resize images larger than a given width.fn main(dir: string, max_width: int = 1920, dry_run: bool = false) { let check = `which convert`.capture if check.isFailure { exit "ImageMagick 'convert' is required but not installed" }
mut resized = 0 mut skipped = 0
for file in `find $dir -type f \( -name "*.jpg" -o -name "*.png" \)` { let width = `identify -format "%w" $file`.trim().toNumber()
if !needs_resize(width, max_width) { skipped = skipped + 1 continue }
let ext = file.split(".").last let kind = label_for(ext)
if dry_run { echo " would resize ($kind): $file ($width px -> $max_width px)" } else { convert $file -resize $max_width $file echo " resized ($kind): $file" } resized = resized + 1 }
echo "" echo "$resized resized, $skipped already within limit"}
fn needs_resize(width, limit) { return width > limit}
fn label_for(ext) { return match ext { "jpg" | "jpeg" => "JPEG" "png" => "PNG" "gif" => "GIF" _ => "unknown" }}
/// needs_resize is true iff the actual width exceeds the targetunittest { needs_resize(2400, 1920).must.equal(true) needs_resize(1920, 1920).must.equal(false) needs_resize(800, 1920).must.equal(false)}
/// label_for picks a friendly label per file extensionunittest { label_for("jpg").must.equal("JPEG") label_for("jpeg").must.equal("JPEG") label_for("png").must.equal("PNG") label_for("gif").must.equal("GIF") label_for("xyz").must.equal("unknown")}Run it:
lash --test resize.lash # the unittestslash resize.lash --help # auto-generated helplash resize.lash ~/Photos # use the default 1920 limitlash resize.lash ~/Photos 2560 true # 2560 px, dry runThe doc-comment plus typed parameters become the script’s --help:
lash resize.lash --helpresize.lash — Resize images larger than a given width.
Arguments: dir (string) max_width (int) default: 1920 dry_run (bool) default: falseFurther reading
Section titled “Further reading”While loops: repeat as long as a condition holds:
mut n = 1while n <= 5 { echo $n n = n + 1}A while loop usually pairs with a mut counter so the loop has a
chance to terminate.
Chains with filters: for many “loop, skip, do” patterns, a chain
is more concise than a for + continue:
`find $dir -name "*.jpg"` .filter(x => needs_resize(`identify -format "%w" $x`.trim().toNumber(), max_width)) .each(x => { convert $x -resize $max_width $x })filter replaces the if … continue; each replaces the loop body.
Job control: background a long-running job with &:
make -j8 &| Command | Description |
|---|---|
jobs | List all running and stopped background jobs |
fg %n | Bring job n to the foreground |
bg %n | Resume a stopped job in the background |
Ctrl+Z | Suspend the foreground process (SIGTSTP) |
Ctrl+C | Interrupt the foreground process (SIGINT) |