lmscript tutorial
In about twenty minutes you write a handful of small scripts and learn the parts of lmscript you will use every day. You need Node.js 24.15 or later and a terminal. Make an empty directory for the files and run every command from it. The outputs below come from real runs of lmscript 0.2.348; where a line shows a file path, yours will show your own directory.
Step 1: Install and check the version
npm install -g @lmctl-ai/lmctl added 1 package in 366ms
lmscript --version lmscript 0.2.348
Step 2: Run your first script
Save this as hello.lms. Every statement ends with ;; # starts a comment.
# hello.lms: the smallest useful script
name = "world";
println("Hello, " + name + "!");
lmscript hello.lms Hello, world!
Step 3: Lists, maps and formatted strings
Lists use [ ], maps use { }. An f"…" string evaluates the expressions in braces. Negative list indexes count from the end. Save as values.lms:
langs = ["lmscript", "C++", "Rust"];
person = {name: "Ada", year: 1815};
println(f"{person.name} was born in {person.year}.");
println(f"{len(langs)} languages, first: {langs[0]}, last: {langs[-1]}");
println(upper("quiet") + " " + str(3 * 4) + " " + json_stringify(person));
lmscript values.lms
Ada was born in 1815.
3 languages, first: lmscript, last: Rust
QUIET 12 {"name":"Ada","year":1815}
Step 4: Loops and conditions
Every if, for and while body needs braces. range(1, 6) is 1 to 5. A map's get(key, default) reads a key that may be missing. Save as loops.lms:
total = 0;
for (n in range(1, 6)) {
if (n % 2 == 0) {
continue;
}
total = total + n;
}
println(f"sum of odd numbers 1..5 = {total}");
counts = {};
for (word in split("to be or not to be", " ")) {
counts[word] = counts.get(word, 0) + 1;
}
for (word in keys(counts)) {
println(f"{word}: {counts[word]}");
}
lmscript loops.lms sum of odd numbers 1..5 = 9 to: 2 be: 2 or: 1 not: 1
Step 5: Functions
Define functions with fn. They can call themselves and can be used before their definition. Save as functions.lms:
fn greet(name, punctuation) {
return f"Hello, {name}{punctuation}";
}
fn factorial(n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
println(greet("Ada", "!"));
println(f"10! = {factorial(10)}");
lmscript functions.lms Hello, Ada! 10! = 3628800
Step 6: Command-line arguments
Words after the script name arrive in the list ARGV. Save as args.lms:
who = ARGV.get(0, "nobody");
println(f"{len(ARGV)} argument(s); first: {who}");
lmscript args.lms Ada Grace 2 argument(s); first: Ada
lmscript args.lms 0 argument(s); first: nobody
Step 7: Files
The standard library reads and writes text, lines, JSON and CSV. Save as files.lms:
mkdir("out");
write_lines("out/notes.txt", ["first", "second", "third"]);
lines = read_lines("out/notes.txt");
println(f"{len(lines)} lines, last: {last(lines)}");
write_json("out/summary.json", {count: len(lines), items: lines});
data = read_json("out/summary.json");
println(f"summary.json count = {data.count}");
lmscript files.lms 3 lines, last: third summary.json count = 3
Step 8: Data written inside the script
A data block puts CSV, JSON, TOML or properties text straight into the script. The statement ends with ; on the marker line and the block closes with a line end _csv_ (or end _json_, …). CSV numbers become numbers. Save as data.lms:
scores = _csv_;
name,score
Ada,95
Grace,88
Linus,79
end _csv_
best = scores[0];
for (row in scores) {
if (row.score > best.score) {
best = row;
}
}
println(f"{len(scores)} rows; best: {best.name} ({best.score})");
config = _json_;
{"retries": 3, "hosts": ["a.example", "b.example"]}
end _json_
println(f"retries={config.retries} hosts={join(config.hosts, ",")}");
lmscript data.lms 3 rows; best: Ada (95) retries=3 hosts=a.example,b.example
Step 9: Run other programs
system runs a program directly (no shell) and returns its output and exit code as data; cmd only builds the argument list. ${…} inserts a value as one argument. Save as run.lms:
r = system node --version;
println(f"exit code {r.code}; node says {trim(r.stdout)}");
argv = cmd git log --oneline -n ${3};
println(json_stringify(argv));
lmscript run.lms exit code 0; node says v24.22.0 ["git","log","--oneline","-n","3"]
Step 10: Let the checker find mistakes
--check parses and checks a script without running it. This file forgets a ;:
total = 1 + 2
println(total);
lmscript --check broken.lms error:/home/me/lmscript-tutorial/broken.lms:1:14: Expected ';' after the assignment; every statement ends with ';'
The exit code is 2 for check errors. A clean file prints nothing:
lmscript --check hello.lms
Step 11: Missing keys are errors
Reading a key that a map does not have stops the script with a message that names the fix. Save as strict.lms:
user = {name: "Ada"};
println(user.email);
lmscript strict.lms
error:/home/me/lmscript-tutorial/strict.lms:2:14: map user has no key "email"; test with user.has("email") or read user.get("email", default)
Step 12: Preview effects with a dry run
--dry-run executes the script in a simulation and prints the plan of effects instead of performing them. Save as cleanup.lms:
write_file("out/report.txt", "done\n");
delete_file("out/notes.txt");
println("cleaned up");
lmscript --dry-run cleanup.lms PLAN complete 1. /home/me/lmscript-tutorial/cleanup.lms:1:1 dummy write_file ["out/report.txt","done\n"] 2. /home/me/lmscript-tutorial/cleanup.lms:2:1 dummy delete_file ["out/notes.txt"] 3. /home/me/lmscript-tutorial/cleanup.lms:3:1 handled println ["cleaned up"]
ls out notes.txt summary.json
Nothing was written or deleted. A normal run does this dry pass silently first and only then runs for real.
Step 13: Put functions in a module
A file that starts with package NAME; is a module; import includes it and its functions are called as NAME.fn(). Save as lib/textutil.lms:
package textutil;
fn shout(s) {
return upper(s) + "!";
}
fn initials(full_name) {
parts = split(full_name, " ");
result = "";
for (p in parts) {
result = result + char_at(p, 0);
}
return result;
}
and use_module.lms next to the lib directory:
import "lib/textutil.lms";
println(textutil.shout("hello"));
println(textutil.initials("Ada King Lovelace"));
lmscript use_module.lms HELLO! AKL
Step 14: Exit codes and quick help
exit(n) ends the script with that exit code. Save as errors.lms:
path = ARGV.get(0, "missing.txt");
if (!file_exists(path)) {
println(f"no such file: {path}");
exit(3);
}
println(read_file(path));
lmscript errors.lms no such file: missing.txt
echo $? 3
Every built-in function documents itself, and -e runs a one-line program:
lmscript help trim
Usage: lmscript [--check|--dry-run|--lint] [--no-preflight] [--trace FILE]
[--dry-loop-limit N] [--dryrun-timeout MS] [--plan-format text|json]
[--resumepoint PATH] [--pref FILE] [--module SPEC=FILE]... [--check-files]
[--base-dir dir] [--source-name label] <file>|-| -e <code> [args...]
Function: trim(s: string): string
Built-in: core
Category: Strings
Description: Trims whitespace from both ends of a string.
Example: clean = trim(" hello ");
lmscript -e 'println(join(reverse(split("a,b,c", ",")), "-"));'
c-b-a
What you learned
- Statements end with ; and every block needs braces.
- Lists, maps, f"…" strings, for (x in …) loops and fn functions.
- Arguments arrive in ARGV; exit(n) sets the exit code.
- Files and data: read_lines, write_json, and inline _csv_ / _json_ blocks.
- system runs programs without a shell and returns their result as data.
- --check finds mistakes, --dry-run shows effects before they happen, and missing keys are errors.
- Modules with package and import; lmscript help NAME for any function.
Next: the user guide for the full reference, or the skill page if an agent will write your scripts.