LSH is the scripting format for the Lilush shell. While .lsh scripts may
look similar to bash at a glance, they are not bash scripts — the syntax
is simpler and more limited.
Three ways to run an .lsh script:
# Via the lsh symlink (preferred)
lsh /path/to/script.lsh arg1 arg2
# Via shebang (make script executable first)
chmod +x script.lsh
./script.lsh arg1 arg2
# From within the interactive shell
run_script /path/to/script.lsh
For the shebang approach, the first line should be:
#!/usr/bin/lsh
Arguments are exposed as environment variables, accessible with the
standard ${VAR} expansion:
| Variable | Meaning |
|---|---|
${0} | Absolute path to the script |
${1}, ${2}, ... | Positional arguments |
${#} | Argument count (excludes script path) |
Example:
#!/usr/bin/lsh
echo "Running: ${0}"
echo "First arg: ${1}"
echo "Got ${#} arguments"
Each line of an .lsh script is parsed and executed independently.
Empty lines are skipped
Lines starting with # are skipped (comments, shebangs)
All other lines are parsed as shell commands
Execution stops on the first line that returns a non-zero exit code
There are no multi-line constructs — every command must fit on a single line.
cat /etc/hosts | grep localhost | wc -l
Commands can be chained with |. Standard output of each command feeds
into the standard input of the next.
mkdir -p /tmp/test && echo "created"
ls /nonexistent || echo "not found"
echo one ; echo two ; echo three
&& — run next command only if previous succeeded (exit code 0)
|| — run next command only if previous failed (exit code != 0)
; — run next command regardless of previous exit code
Operators and pipes can be combined:
cat file.txt | grep pattern && echo "found" || echo "not found"
sort < unsorted.txt
echo hello > output.txt
echo hello >> log.txt
cmd err> errors.log
cmd all> combined.log
cmd > out.txt err> err.txt
sort < input.txt > sorted.txt
| Operator | Stream | Mode | Notes |
|---|---|---|---|
< file | stdin | read | First command in pipeline only |
> file | stdout | truncate | Any command in pipeline |
>> file | stdout | append | Any command in pipeline |
err> file | stderr | truncate | Any command in pipeline |
err>> file | stderr | append | Any command in pipeline |
all> file | stdout + stderr | truncate | Any command in pipeline |
all>> file | stdout + stderr | append | Any command in pipeline |
err> and all> require a word boundary — stderr> is parsed as the
argument stderr followed by >, not as err>.
Multiple redirects can be combined on a single command:
cmd < infile >> outfile err> errfile
Conflicting redirects (e.g. > file all> file, or duplicate err>) are
rejected with an error.
echo "Home is ${HOME}"
echo "Path is ${PATH}"
setenv MY_VAR=hello
echo "${MY_VAR}"
Variables use the ${NAME} syntax. The bare $NAME form is not supported.
~ at the start of a word expands to $HOME:
ls ~/documents
cat ~/.config/lilush/init.lsh
echo "Current directory: $(pwd)"
echo "Today is $(date +%Y-%m-%d)"
$(command) executes a command and substitutes its output (trailing
newlines stripped).
In LSH scripts, a bare line with a variable name followed by a mneme://secrets/ URI
is treated as a shorthand for setenv:
# These two lines are equivalent:
DB_PASSWORD mneme://secrets/db_password
setenv DB_PASSWORD=mneme://secrets/db_password
This only triggers when the first token is a valid environment variable name
([A-Za-z_][A-Za-z0-9_]*) and the second token starts with mneme://secrets/.
Regular commands that happen to receive a mneme:// argument are not affected.
This shorthand is commonly used in ~/.config/lilush/init.lsh to declare
which environment variables are backed by local encrypted secrets. See the
Local Secrets section in the Shell docs
for details on storing, fetching, and managing these secrets.
echo 'literal ${VAR} not expanded'
echo "expanded: ${HOME}"
echo "file has spaces.txt"
Single quotes: ${VAR} not expanded, spaces preserved. Note: $(cmd)
substitution occurs before quote processing and is always expanded.
Double quotes: ${VAR} and $(cmd) are expanded, spaces preserved
Scripts have access to a subset of shell builtins. The following builtins are handled at the script level and work in all contexts:
rehash, alias, unalias, run_script, pyvenv, prompt, theme, secrets, calm
All other builtins (ls, cd, mkdir, rm, kat, setenv, unsetenv,
envlist, ps, netstat, dig, history, files_matching, exec,
job, notify, and others) are dispatched through the standard builtin
table and are also available.
External commands found in ${PATH} work as expected.
If you're coming from bash, here's what to leave at the door:
# DOES NOT WORK:
ls *.txt
rm /tmp/test-[0-9]*
# Instead, use `files_matching` or explicit paths:
files_matching txt ls
*, ?, [...] are not expanded. Use the files_matching builtin
or explicit paths.
There is no if/then/else, for, while, case, or select.
Each line is an independent command. Use && and || for simple
conditional chaining.
You cannot define shell functions. Factor reusable logic into separate
.lsh scripts and call them with run_script, or write it in Lua.
# DOES NOT WORK:
echo $((2 + 2))
i=$((i + 1))
There is no arithmetic expansion. Use external tools or Lua for math.
# NONE OF THESE WORK:
${VAR:-default}
${VAR:=value}
${VAR#prefix}
${VAR%suffix}
${#VAR}
Only plain ${NAME} is supported.
# DOES NOT WORK:
cat <<EOF
hello
EOF
# DOES NOT WORK:
long_command &
The & operator is not supported. Use the job builtin for background
tasks in interactive mode.
# This is a comment (works — start of line)
echo hello # this is NOT a comment (the # is passed to echo)
Comments only work at the start of a line.
LSH scripts are good for:
Sequences of shell commands with simple conditional chaining
Automation tasks that are mostly about running external programs
Configuration scripts (like ~/.config/lilush/init.lsh)
For anything requiring logic, loops, data structures, or error handling,
write a Lua script instead and run it with lilush script.lua.