Examples
Thirteen complete programs, from a one-line hello to a status page lippy serves itself. They're real scripts you can read top to bottom and run — each one shows off a different part of the language. The full source of every one is below the table.
| # | Program | What it shows |
|---|---|---|
| 01 | hello | say |
| 02 | fizzbuzz | for n in 1 to 100, if/else if/else, mod, equals |
| 03 | greet | fun, paren-free calls, ask, truthiness, interpolation |
| 04 | rename-photos | first of args, files of, starts with / ends with, move … to |
| 05 | findin | args at n, lines of read …, contains, exit |
| 06 | backup | run (stops on failure) and capture (returns output) |
| 07 | disk-alert | reading a command's output, at, skip when, greater than |
| 08 | todo | subcommand arguments, appending to a list, write … to, not equals |
| 09 | guess | loop / stop when, less than, random 1 to 100 |
| 10 | service-check | maps, for key, value in map, attempt, ok of result |
| 11 | status-page | serve with a route table of functions, :name path parameters, attempt fetch |
| 12 | tally | options for the command line, for line in stdin (a filter), warn to stderr |
| 13 | notes | connect / query / execute — a SQLite-backed CLI, and the ? placeholder |
The scripts
01-hello.lip
#!/usr/bin/env lippy
# The smallest lippy program.
say "hello, world!"
02-fizzbuzz.lip
#!/usr/bin/env lippy
# The classic. Shows: the counting loop, if/else if/else, `mod`, word comparisons.
for n in 1 to 100
if n mod 15 equals 0 then
say "fizzbuzz"
else if n mod 3 equals 0 then
say "fizz"
else if n mod 5 equals 0 then
say "buzz"
else
say n
end
end
03-greet.lip
#!/usr/bin/env lippy
# Functions, `ask` for input, string interpolation, truthiness of empty string.
# Calls are Ruby-style: no parens needed — `greet name`, not `greet(name)`.
fun greet(who)
if not who then
who is "stranger"
end
say "hello, {who}!"
end
name is ask "what's your name? "
greet name
04-rename-photos.lip
#!/usr/bin/env lippy
# Rename IMG_1234.JPG style files to holiday-1234.jpg.
# Shows: `first of args`, string verbs, `starts with`/`ends with`, moving files.
folder is first of args
if not folder then
say "usage: rename-photos <folder>"
exit 1
end
count is 0
for name in files of folder
if name starts with "IMG_" and lower(name) ends with ".jpg" then
digits is trim(name, "IMG_", ".JPG", ".jpg")
move "{folder}/{name}" to "{folder}/holiday-{digits}.jpg"
count is count + 1
end
end
say "renamed {count} photos"
05-findin.lip
#!/usr/bin/env lippy
# A tiny grep: findin <word> <file>
# Shows: `args at n` indexing, reading a file line by line, `contains`.
word is args at 1
path is args at 2
if not word or not path then
say "usage: findin <word> <file>"
exit 1
end
lineno is 0
for line in lines of read path
lineno is lineno + 1
if line contains word then
say "{path}:{lineno}: {line}"
end
end
06-backup.lip
#!/usr/bin/env lippy
# Back up Projects to the NAS. Shows: `run` (dies loudly on failure),
# `capture` (returns trimmed stdout), interpolation into commands.
source is "/home/lee/Projects"
dest is "crate.local:/volume1/backups"
today is capture "date +%F"
say "backing up {source} -> {dest} ({today})"
run "rsync -a --delete {source}/ {dest}/projects-{today}/"
size is capture "du -sh {source}"
say "done. backed up {size}"
07-disk-alert.lip
#!/usr/bin/env lippy
# Warn when any disk is over 90% full.
# Shows: parsing command output, `at` indexing, `skip when`, word comparisons.
for line in lines of capture "df --output=pcent,target"
parts is split trim(line)
skip when parts at 1 equals "Use%"
used is number trim(parts at 1, "%")
mount is parts at 2
if used greater than 90 then
say "WARNING: {mount} is {used}% full"
end
end
08-todo.lip
#!/usr/bin/env lippy
# A tiny todo list kept in a text file: todo add "buy milk" / todo list / todo done 2
# Shows: subcommand-style args, read/write files, lists, `not equals`.
path is "{home}/.todo.txt"
command is first of args
if command equals "add" then
append "{args at 2}\n" to path
say "added"
else if command equals "list" then
n is 0
for item in lines of read path
n is n + 1
say "{n}. {item}"
end
else if command equals "done" then
keep is []
n is 0
for item in lines of read path
n is n + 1
if n not equals number(args at 2) then
push item to keep
end
end
write join(keep, "\n") to path
say "done"
else
say "usage: todo add <text> | todo list | todo done <n>"
exit 1
end
09-guess.lip
#!/usr/bin/env lippy
# Number guessing game.
# Shows: the condition-less loop with `stop when`, random numbers, word
# comparisons, and `is_number` — the gentle way to check before converting.
secret is random 1 to 100
tries is 0
say "I'm thinking of a number between 1 and 100"
loop
answer is ask "your guess? "
# number() stops the script on rubbish, which is right for a script
# reading a config file and wrong for a game. is_number asks instead.
if not is_number(answer) then
say "that's not a number — have another go"
skip
end
guess is number(answer)
tries is tries + 1
stop when guess equals secret
if guess less than secret then
say "higher!"
else
say "lower!"
end
end
say "got it in {tries} tries!"
10-service-check.lip
#!/usr/bin/env lippy
# Check homelab services and report. Shows: maps, `attempt` (a run that's
# allowed to fail), and `of` for reading properties — no dot notation.
services is {
"gitea": "the git server",
"jellyfin": "movies and tv",
"navidrome": "music",
}
problems is 0
for name, what in services
result is attempt capture("systemctl is-active --quiet {name}")
if ok of result then
say " ok {name} ({what})"
else
say " DOWN {name} ({what}) — exit code {code of result}"
problems is problems + 1
end
end
if problems greater than 0 then
say "{problems} services need attention"
exit 1
end
say "all good"
11-status-page.lip
#!/usr/bin/env lippy
# A status page for the homelab, served by lippy itself. Shows: functions
# as values (a route table is just a map of them), `:name` path parameters,
# and answers whose type picks the content type — a string goes back as
# text, a map as JSON.
#
# Run it, then visit http://localhost:8080
boxes is {
"crate": "http://crate.local",
"create": "http://create.local",
}
# Ask every box how it's doing. `attempt` never stops the script, so one box
# being down doesn't take the status page with it.
fun check_all()
results is {}
for name, address in boxes
answer is attempt fetch("{address}/health")
results at name is ok of answer
end
return results
end
fun home(request)
lines is ["homelab status", ""]
for name, healthy in check_all()
mark is "DOWN"
if healthy then
mark is "up"
end
push "{name}: {mark}" to lines
end
push "" to lines
push "checked at {now()}" to lines
return join(lines, "\n")
end
# A map comes back as JSON, so this is an API with no extra work.
fun health(request)
return {"boxes": check_all(), "at": now()}
end
# :name is a path parameter — /box/crate arrives as params of request.
# `reply` is for when the code matters; a plain return is a 200.
fun box(request)
name is params of request at "name"
if not (boxes contains name) then
return reply "there's no box called {name}", 404
end
answer is attempt fetch("{boxes at name}/health")
return {"box": name, "ok": ok of answer, "why": why of answer}
end
serve {
"/": home,
"/health": health,
"/box/:name": box,
}, 8080
12-tally.lip
#!/usr/bin/env lippy
# A filter: reads lines from a pipe, writes lines to a pipe.
# Shows: `options` for the command line, `for line in stdin` (streamed,
# never slurped), and `warn` for diagnostics — once stdout is data, notes
# have to go somewhere else.
#
# journalctl -u gitea --no-pager | ./12-tally.lip ERROR --limit=5
#
# Counts matching lines and prints them; the tally goes to stderr, so it
# doesn't pollute the output for whatever comes next in the pipeline.
# Each option's default says what it is: false is a flag, nothing takes a
# value, a number takes a number. Everything that isn't an option lands in
# `rest of opts`.
opts is options {
"limit": 0, # stop after this many matches (0 means no limit)
"quiet": false, # skip the tally
}
wanted is first of (rest of opts)
if not wanted then
warn "usage: … | tally.lip <text-to-match> [--limit=n] [--quiet]"
exit 1
end
seen is 0
matched is 0
for line in stdin
seen is seen + 1
skip when not (line contains wanted)
matched is matched + 1
say line
stop when limit of opts greater than 0 and matched at least limit of opts
end
if not quiet of opts then
warn "{matched} of {seen} lines contained {wanted}"
end
13-notes.lip
#!/usr/bin/env lippy
# A note keeper backed by SQLite — lippy with its own database, no server.
#
# notes.lip add "check the backups ran"
# notes.lip list
# notes.lip done 3
#
# Shows: connect / execute / query, and the point of the whole thing — the
# `?` placeholder, where a value is a bound parameter and can never become
# SQL. `notes.lip add "'; drop table notes; --"` stores that text and does
# no harm.
db is connect("sqlite", "{home}/.notes.db")
execute(db, "create table if not exists notes (id integer primary key, text text, done number default 0)")
command is first of (rest of options {})
rest is rest of options {}
if command equals "add" then
text is rest at 2
if not text then
warn "usage: notes.lip add \"the note\""
exit 1
end
execute(db, "insert into notes (text) values (?)", text)
say "added"
else if command equals "list" then
rows is query(db, "select id, text, done from notes order by id")
if not rows then
say "no notes"
end
for note in rows
mark is "[ ]"
if done of note then
mark is "[x]"
end
say "{mark} {id of note}. {text of note}"
end
else if command equals "done" then
id is number(rest at 2)
r is execute(db, "update notes set done = 1 where id = ?", id)
if changed of r equals 0 then
warn "there's no note {id}"
exit 1
end
say "marked {id} done"
else
warn "usage: notes.lip add|list|done"
exit 1
end
close(db)