Skip to content

Lexical Structure

lash keeps commands and variables in two separate namespaces. Which namespace a name belongs to depends on where it appears in your code:

  • Commands — the first word of a line is always looked up as a command. This includes built-in methods (like sort), your own functions, and programs installed on your system (like grep or git).

    echo "hello" # echo is a command
    sort data.txt # sort is a built-in method
    git status # git is a program from $PATH
  • Variables — everywhere else (right side of =, inside conditions, etc.) names are looked up as variables.

    let name = "world" # name is a variable
    echo "hello $name" # $name reads the variable
    if name == "world" { echo "hi" } # name in a condition is a variable

When lash looks up a command, it checks in this order:

  1. Built-in method (e.g., sort, filter, map)
  2. A function you defined
  3. A program from your $PATH

Because commands and variables live in separate namespaces, they can share the same name without conflicting:

let grep = "my pattern"
grep "foo" file.txt # runs /usr/bin/grep (command)
let result = grep # reads the variable
echo "pattern: $grep" # also reads the variable

To run a command where lash expects a variable, wrap it in backticks (see Functional Chains).

lash supports three quoting styles:

String with interpolation:

echo "hello $name"
echo "hello ${name}"
echo "result: ${count + 1}"
echo "$parts[0]"
echo "literal \$sign"

Literal string, no processing:

echo 'no $interpolation'
echo 'no \escapes'

Command execution and functional bridge (not POSIX command substitution):

let files = `ls -la`
`ls -la`.sort().take(5)

Available inside double-quoted strings only:

SequenceMeaning
\nNewline
\tTab
\rCarriage return
\\Literal backslash
\$Literal dollar sign
\"Literal double quote

In command position, unquoted words are literal arguments. In expression context, unquoted words resolve as variable names.

Backticks capture command output. To embed command output in a string, assign to a variable first:

let today = `date`.trim()
echo "today is $today"
let branch = `git branch --show-current`.trim()

Uppercase names before a command set environment variables for that command only:

DFLAGS="-O2" dub build --compiler=ldc2
CC=gcc CFLAGS="-Wall" make

Only names matching [A-Z][A-Z0-9_]* are recognized as env var prefixes. Lowercase names are treated as commands.

Unquoted brace patterns expand into multiple arguments at tokenization, before variable expansion runs.

FormExpansion
{a,b,c}a b c
pre{a,b}postprea preb postb (prefix + suffix)
{a,b}{x,y}ax ay bx by (Cartesian product)
{1..5}1 2 3 4 5
{5..1}5 4 3 2 1 (descending)
{0..10..2}0 2 4 6 8 10 (with step)
{a..e}a b c d e (single-letter range)
{a,{b,c}}a b c (nested)

A pattern with fewer than two comma-separated alternatives and no range is left literal (matching bash). Single-quoted arguments pass through untouched. Use \{ / \} to escape literal brace characters.

mkdir -p src/{lib,bin,test}
echo file{1..3}.txt
cp main.{c,h} backup/
PatternMatches
*any sequence in a single path component
?any single character
[abc]any one of the bracketed characters
**zero or more path components (recursive descent)

**/*.d matches every .d file at any depth below the working directory. Hidden entries (leading .) are excluded unless the pattern explicitly starts with ..

ls source/**/*.d # every .d file under source/
rm -f build/**/*.tmp # recursive cleanup

Inside double-quoted strings and command arguments, ${...} accepts these bash-compatible forms:

FormResult
${VAR:-default}VAR if set and non-empty, otherwise default
${VAR:+alt}alt if VAR is set and non-empty, otherwise empty
${VAR:?msg}VAR if set and non-empty; otherwise emits msg to stderr
${#VAR}character length of VAR (0 if unset)
${VAR/pat/rep}VAR with the first pat replaced by rep
${VAR//pat/rep}VAR with every pat replaced by rep
${VAR/pat/}delete the first match of pat

The default and alt branches are themselves expanded:

let host = "${SERVER:-localhost}"
echo "${file/.txt/.bak}"
echo "${PATH//:/\\n}"

${ ... + 1 } and other lash expressions remain available without the leading sigil — "${count + 1}" is a lash arithmetic expression, not a POSIX form.