LSH — Lilush Shell Scripts

Overview

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.

Running scripts

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

Script arguments

Arguments are exposed as environment variables, accessible with the standard ${VAR} expansion:

VariableMeaning
${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"

Line processing

Each line of an .lsh script is parsed and executed independently.

There are no multi-line constructs — every command must fit on a single line.

Supported syntax

Pipes

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.

Operators

mkdir -p /tmp/test && echo "created"
ls /nonexistent || echo "not found"
echo one ; echo two ; echo three

Operators and pipes can be combined:

cat file.txt | grep pattern && echo "found" || echo "not found"

Redirects

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
OperatorStreamModeNotes
< filestdinreadFirst command in pipeline only
> filestdouttruncateAny command in pipeline
>> filestdoutappendAny command in pipeline
err> filestderrtruncateAny command in pipeline
err>> filestderrappendAny command in pipeline
all> filestdout + stderrtruncateAny command in pipeline
all>> filestdout + stderrappendAny 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.

Environment variables

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.

Home directory expansion

~ at the start of a word expands to $HOME:

ls ~/documents
cat ~/.config/lilush/init.lsh

Command substitution

echo "Current directory: $(pwd)"
echo "Today is $(date +%Y-%m-%d)"

$(command) executes a command and substitutes its output (trailing newlines stripped).

Secret variable shorthand

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.

Quoting

echo 'literal ${VAR} not expanded'
echo "expanded: ${HOME}"
echo "file has spaces.txt"

Builtins available in scripts

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.

What LSH is NOT

If you're coming from bash, here's what to leave at the door:

No glob / pathname expansion

# 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.

No control flow

There is no if/then/else, for, while, case, or select. Each line is an independent command. Use && and || for simple conditional chaining.

No functions

You cannot define shell functions. Factor reusable logic into separate .lsh scripts and call them with run_script, or write it in Lua.

No arithmetic

# DOES NOT WORK:
echo $((2 + 2))
i=$((i + 1))

There is no arithmetic expansion. Use external tools or Lua for math.

No parameter expansion modifiers

# NONE OF THESE WORK:
${VAR:-default}
${VAR:=value}
${VAR#prefix}
${VAR%suffix}
${#VAR}

Only plain ${NAME} is supported.

No here-documents

# DOES NOT WORK:
cat <<EOF
hello
EOF

No background execution operator

# DOES NOT WORK:
long_command &

The & operator is not supported. Use the job builtin for background tasks in interactive mode.

No mid-line comments

# 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.

When to use LSH vs Lua

LSH scripts are good for:

For anything requiring logic, loops, data structures, or error handling, write a Lua script instead and run it with lilush script.lua.