# Compatibility luish generally treats [dash](http://gondor.apana.org.au/~herbert/dash/) as its reference: scripts should behave as they do under dash, including where POSIX leaves the behaviour open. This page lists where luish deliberately differs, and what isn't implemented yet. Where luish goes beyond dash, zsh is the preferred model, since luish is meant to replace it as an interactive shell; some older differences follow bash instead. ## Differences from dash ### Following zsh | Behaviour | dash | luish | |---|---|---| | `source` | Not a built-in (`not found`, status 127) | As in zsh: `.`, but a name without `/` is looked for in the current directory before `PATH`, and further arguments are the positional parameters while the file runs. Special, as in zsh's sh emulation. A file that can't be read is an error with status 2, as for `.` (zsh uses 1) | | `builtin` | Not a built-in (`not found`, status 127) | As in zsh and bash: runs a built-in, bypassing functions, and a missing one is status 1. A special built-in stays special for its errors, as in zsh, but its assignments (`x=1 builtin export`) are temporary, as in bash (zsh keeps them). `builtin -- name` skips the `--`, as in bash (zsh looks for a built-in named `--`) | | `function` | An ordinary word: `function f { ...; }` is a syntax error, and a command named `function` is looked for | A reserved word (as POSIX allows), as in zsh and bash: `function NAME [()] BODY` defines a function whose body is a compound command, on the same line or a later one. The names needn't be variable names (`function git-up`), aren't alias-expanded, and there can be several (`function f g { ...; }`), as in zsh. As in bash, the body can also start right after the name with `(`, `if`, `while`, `until`, `for` or `case` (`function f (cmd)`), so those can't be a second name. Unlike zsh: a name can't be quoted or expanded, contain `/`, or be a special built-in (`Bad function name`, as for `f()`), and there are no anonymous functions (`function { ...; } args`) | | `[[ ... ]]` | `[[` is looked for as a command (`not found`, status 127) | A reserved word (as POSIX allows), as in zsh and bash. The words are expanded without field splitting or globbing, and only as far as the expression is evaluated. `test`'s operators (`-a` and `-o` are unary: a file exists, an option is on), `-v NAME` (a variable or positional parameter is set), `-N`, `!`, `&&`, `\|\|` and parentheses, with operators recognized only if unquoted. The right side of `=`, `==` and `!=` is a pattern, as in `case`. `<` and `>` compare bytes, and the operands of `-eq` and the like are arithmetic expressions. `=~` matches an extended regular expression (the C library's), after quote removal as in zsh (in bash, quoted parts match literally), and sets `MATCH`, `MBEGIN` and `MEND`, with offsets from 0 as in zsh's sh emulation (from 1 in native zsh), and for a regular expression with groups, the arrays `match`, `mbegin` and `mend` (zsh; empty and -1 for a group that took no part). It also sets `BASH_REMATCH` (the matched text, then each group's), as bash does; a failed match changes none of them, as in zsh (bash empties `BASH_REMATCH`, and ignores `local` and `readonly` for it). A bad regular expression is reported, and is false. After `=~`, `(` and `\|` are part of the word, as in bash (`=~ ^(a\|b)$`; zsh needs quotes). With `-nt` and `-ot`, a file that exists is newer than one that doesn't, as in bash and `test` (in zsh, both must exist). `-n = x` compares strings, as in zsh (bash rejects it), and `[[ -n ]]` is a syntax error, as in bash (in zsh, an error when run). `set -x` shows it on one line, as zsh does. No zsh extended patterns such as `(a\|b)` | | `test a == b`, `[ a == b ]` | `==` is an error (`unexpected operator`, status 2) | As in zsh and bash: the same as `=`, also in how expressions are parsed | | `**`, `++`, `--` and `,` in arithmetic | `**` and `,` are syntax errors, as are `x++` and `x--`; `++x` and `--x` are two unary operators, so they are `x` | As in zsh and bash: `**` is a power (binding tighter than `*`, less tightly than unary minus, and right-associative; a negative exponent is an error, as in bash), with `**=`; `++` and `--` increment and decrement a variable or an array element, before or after its value is taken; `,` evaluates both sides and gives the value of the right one. Before or between numbers, `++` and `--` are still two operators, as in dash and bash (`--5` is 5, `5--1` is 6; zsh rejects both), but `x--1` is an error, as in zsh and bash (dash: `x - -1`) | | `disown` | Not a built-in (`not found`, status 127) | As in zsh (see `help disown`): removes jobs from the table, so they aren't reported, waited for by `wait`, or warned about by `exit`, with bash's options `-a`, `-r` and `-h` (which does nothing: luish, like dash, never sends SIGHUP to its jobs) and process ids as well as jobs. An unknown job is status 1, as in bash (zsh: 127). Without job control, jobs have no text (as in dash), so `%name` can't name them | | `a \|& b` | Syntax error | As in zsh and bash: `a 2>&1 \| b`, with the redirection after `a`'s own | | `;&`, `;\|` and `;;&` in `case` | Syntax errors | As in zsh and bash: after an arm's body, `;&` runs the next arm's body too (without trying its patterns), and `;\|` (zsh) or `;;&` (bash) goes on trying the patterns of the arms that follow | | `cmd &\|`, `cmd &!` | Syntax errors (`a &! b` is `a & ! b`) | As in zsh: `cmd` runs in the background and is disowned (see `help disown`). `a &! b` stays `a & ! b`, as in dash (zsh disowns `a` there too): `&!` disowns only where no command can follow it, such as at the end of a line, before `;;`, `}` or `fi`, or before a comment | | `let` | Not a built-in (`not found`, status 127) | As in zsh: evaluates each argument as `$((...))`, with status 0 if the last value is non-zero and 1 if it is zero or on an error, which doesn't exit the shell. Arithmetic is on integers only, without zsh's floating point or comma operator | | `pushd`, `popd`, `dirs` | Not built-ins (`not found`, status 127) | As in zsh with its default options: `+n` and `-n` name entries of the stack (zsh's sh emulation sets `POSIX_CD`, which makes them directory names). `popd` with an argument other than `+n` or `-n` is an error with status 1 (zsh usually does nothing, with status 0) | | `setopt`, `unsetopt` | Not built-ins (`not found`, status 127) | As in zsh: set options by name (case and `_` don't matter, a `no` prefix inverts), dash's and luish's own (such as `prompt.percent`, whose zsh name `prompt_percent` also works). Without arguments they list the options that are on or off; zsh lists those that differ from their defaults | | `%` sequences in prompts | Not supported | With `setopt prompt.percent`, as in zsh (after parameter expansion, as with zsh's `PROMPT_SUBST`), with a subset of its sequences, and also long names (`%[hostname]`, `%[fg:red]`, `%([status].yes.no)`), which zsh doesn't have, where an unknown name is an error. The escape sequences for attributes and colours are ANSI SGR codes rather than the terminal's own (so turning bold off is `\e[22m`, where zsh writes `\e[0m` and restores the rest) | | `RPROMPT`, `RPS1`, `RPROMPT2`, `RPS2` | Ordinary variables | In an interactive shell, a prompt at the right edge of the terminal, as in zsh (see [Usage](usage.md#the-right-prompt)). They are separate variables, where zsh ties `RPROMPT` to `RPS1` (and `RPROMPT2` to `RPS2`): `RPROMPT` is used if set and not empty, else `RPS1` | | `<(list)`, `>(list)` | A syntax error | Process substitution, as in zsh and bash: a word that is the path of a pipe, `/dev/fd/N` (N is 64 or more, so as not to clash with the descriptors that scripts use), to the standard output of `list` or from its standard input. It isn't split or globbed, and can be inside a word (`--input=<(list)`). Unlike in bash and zsh, the shell waits for a `>(list)` process when the command is over (they don't, so what it prints comes at any time); it doesn't wait for `<(list)` (which ends when the command stops reading, on SIGPIPE), and `exec 3< <(list)` waits for neither. A command with a substitution isn't `exec`'d in place when it is the last of a subshell or a pipeline, so that the pipes can be closed. There is no `=(list)`, and the processes aren't in the command's process group or job | | `<<< word` | A syntax error (`redirection unexpected`) | A here-string, as in zsh and bash: the word, expanded without field splitting or globbing (`"$@"` is joined with spaces), and a newline, on standard input (or on the fd before it, `3<<< word`) | | `**/` | The same as `*/` | With `setopt glob.star` (off by default), matches any number of directories, as in zsh (where it is always on) | | Brace expansion, `a{b,c}`, `{1..10}` | Text | With `setopt expand.braces` (bash's `braceexpand`; off by default), as in bash and zsh, before the other expansions (as in bash) of command words, the words of `for` and the elements of arrays, not of assignments (also those of `local` and `export`), `case`, `[[ ... ]]` or here-documents. Where the two differ, as in bash: the command's name is expanded, the sign of a step is ignored (zsh: a negative step reverses the order), characters can have a step (`{a..e..2}`), `{1..a}` isn't a sequence (zsh: the characters between), `{+1..3}` and `{1..3..0}` are sequences, an empty word (`{a,}`) is removed as other unquoted empty words are (zsh keeps it), tilde expansion comes after (zsh's sh emulation does it first), a stray `}` is text (zsh: a parse error) and a redirection to more than one word is an error (zsh writes to each). As in zsh: the ends and the step can be expansions (`{1..$n}`), expanded once (bash leaves them as they are), and a sequence can be of any characters, also beyond ASCII (`{α..γ}`), whatever the locale. There is no zsh `brace_ccl` (`{abc}`) | | Glob qualifiers, `*(/)` | A syntax error | With `setopt glob.bare_qualifiers` (zsh's `bareglobqual`, off by default), zsh's glob qualifiers. Subscripts count from 0, as in zsh's `sh` emulation (which sets `KSH_ARRAYS`; native zsh counts from 1) | | `setopt glob.null`, `glob.dots`, `glob.fail`, `glob.fail_all` | No `setopt` | zsh's `NULL_GLOB`, `GLOB_DOTS`, `NOMATCH` and `CSH_NULL_GLOB` (and bash's `nullglob`, `dotglob` and `failglob`, which `shopt` sets), all off by default: a pattern that matches nothing is removed, or is an error (status 1), and patterns match hidden files (see [Extended globbing](globbing.md#patterns-that-match-nothing-and-hidden-files)). As in zsh, `glob.null` wins over the others and `glob.fail_all` over `glob.fail` (in bash, `failglob` wins over `nullglob`). Unlike zsh and bash, the error exits a non-interactive shell also in `eval` (where they return 1 from `eval`), as other expansion errors do in dash. `.*` still matches `.` and `..` with `glob.dots`, as without it (as in dash) | | A directory as a command | `not found` (status 127) | With `setopt cd.auto` (zsh's `autocd`, off by default), changes to it when read from standard input, as in zsh | | `bindkey` | Not a built-in (`not found`, status 127) | In interactive shells (and their subshells), as zsh's for its emacs keymap and the widgets luish has; `__luish_internal bindkey` anywhere. Unlike zsh: Up and Down are bound to `history-beginning-search-backward`/`-forward` by default (zsh: `up-line-or-history`), `-r` gives a key back to the line editor's own binding, vi mode can't be changed, and keys can be named (`Up`, `Ctrl-Right`, `'Ctrl-X Ctrl-E'`), so an argument such as `Up` is that key, not U and p (written `\Up` or `\125p`, as listings show it) | | `clipcopy` | Not a built-in (`not found`, status 127, or a program of that name) | In interactive shells (and their subshells): copies its input or a file to the clipboard through the terminal (OSC 52), as oh-my-zsh's `clipcopy` function does through `xclip` and the like; `__luish_internal clipcopy` anywhere | | `vared` | Not a built-in (`not found`, status 127, or a program of that name) | In interactive shells (and their subshells), as zsh's: edits a variable's value with the line editor (see `help vared`); `__luish_internal vared` anywhere. Unlike zsh: `-i` and `-f` (luish has no widgets of the user's own) and `-t` are not supported, `-M` takes only `emacs`, `viins` and `main`, and `-m` only `vicmd`; `-p` and `-r` expand `%` sequences whether or not `prompt.percent` is on; a read-only variable is refused before editing (zsh reports it after); Ctrl-D on an empty line without `-e` does nothing (zsh lists completions); Tab completes as in a command's argument (zsh's `compinit` completes according to the variable); subscripts count from 0 and aren't expanded (`vared 'a[i]'` and `vared "a[$i]"` work, `vared 'a[$i]'` doesn't), and keys come in the order they were added; a value that isn't valid UTF-8 can't be edited | | `print` | Not a built-in (`not found`, status 127, or a program of that name) | In interactive shells (and their subshells), as zsh's, with its options but `-p` (luish has no coprocesses, so it always fails) and `-S`; `__luish_internal print` anywhere. `-P` expands `%` sequences whether or not `prompt.percent` is on (in zsh, it follows `PROMPT_PERCENT`, which is on unless zsh emulates sh). A bad or readonly name for `-v` is an error with status 1, where zsh exits a non-interactive shell. `-f` reports bad numbers as luish's `printf` does | | History file | None (Debian's dash has no line editor; upstream dash with libedit keeps the history in memory only) | In zsh's format, so the two shells can share a file (see [History](usage.md#history)). Unlike zsh: without `HISTFILE` the file is `$XDG_STATE_HOME/luish/history` (zsh saves nothing), `SAVEHIST` defaults to `HISTSIZE` (zsh: 0), `HISTFILE` and `HISTSIZE` set in the startup files take effect, a command equal to the previous one is never added (zsh's `hist_ignore_dups`, off there by default), and the elapsed time is always written as 0 | | History expansion, `!!` | None: `!` is an ordinary character | With `setopt history.expand` (off by default), in interactive shells, as in zsh and bash (see [History expansion](usage.md#history-expansion)). Where the two differ: as in bash, `!` before `"` and in `[!...]` isn't expanded (zsh's `!"` turns history expansion off for the rest of the line, so `echo "hi!"` has an unclosed quote there, and zsh expands `[!a]`); each line of a multi-line command is echoed as it is expanded (zsh echoes the whole command once complete); `!` isn't expanded in comments (zsh's is); `:h` without a `/`, and `:r` and `:e` without an extension, leave the text as it is (zsh's fail). As in zsh, a `!` alone before a designator (`!$`) is the event of the reference before it in the line (bash: the previous command); `!{...}` works (bash has no braces), and an empty `!{}` is an error (`event not found`; zsh leaves `!}`). Unlike both, an expansion of more than 1 MiB is an error (`expansion too long`), as `!#` doubles the line each time; a range keeps the text between its words (bash joins them with spaces); `!` before an operator is left as it is (`echo hi!;`, an error in bash). Unlike both, `!` isn't expanded in `$((...))`. Not supported: `histchars`, zsh's `:c`, `:l`, `:u` and `:G` modifiers, and bash's `histreedit` | | `alias`, `unalias` options | No options: `alias -g x=y` defines an alias named `-g`, and lists a missing alias `-g`. `unalias` has only `-a` | zsh's options: `alias -g` and `-s` define global and suffix aliases (below), `-r`, `-g`, `-s` select which to list, `-m` takes patterns, `-L` prints `alias` commands and `+` prints names only; `unalias -s` and `-m`. So an argument to `alias` that starts with `-` or `+` is an option, and a name that starts with one needs `--` before it. Plain `alias` lists regular and global aliases, not suffix ones. Unlike zsh: values are always quoted (as in dash), `alias -L` quotes names that need it, errors are dash's (status 2 for a bad option, a message for a missing alias), `unalias -a` ignores further arguments (as in dash), and `command -v` shows a suffix alias as `alias -s SUFFIX=VALUE` (zsh shows its value) | | Global aliases (`alias -g`) | None | Expanded in any position, not only as a command name, as in zsh: also arguments, `for` and `case` words, redirection targets, and inside `$(...)`, but not when quoted, and never as a reserved word (as with zsh's `posix_aliases`, set by its sh emulation). Unlike zsh, a here-document delimiter isn't expanded | | Suffix aliases (`alias -s`) | None | As in zsh: a command name `TEXT.SUFFIX` (`TEXT` not empty) becomes the alias's value followed by the name, when it isn't a regular alias. A blank at the end of the value makes the next word eligible, as zsh does (its manual says otherwise) | | `RANDOM`, `SECONDS`, `UID`, `EUID`, `GID`, `EGID`, `HISTCMD`, `EPOCHSECONDS`, `EPOCHREALTIME`, `pipestatus`, `PIPESTATUS`, `path`, `dirstack`, `LUISH_VERSION`, `LUISH_PATCHLEVEL`, `MACHTYPE`, `HOSTTYPE`, `OSTYPE` | Ordinary variables | zsh's special parameters, computed when read (see [Special variables](usage.md#special-variables)), with `LUISH_VERSION` and `LUISH_PATCHLEVEL` for zsh's `ZSH_VERSION` and `ZSH_PATCHLEVEL`, and bash's `HOSTTYPE` (the same as `MACHTYPE`, the processor, where bash's `MACHTYPE` is `x86_64-pc-linux-gnu`); their values in the environment are ignored, as in zsh (bash takes `MACHTYPE`, `HOSTTYPE` and `OSTYPE` from it). Unset, they read as unset until assigned again, as in zsh. Unlike zsh: assigning to `UID`, `EUID`, `GID`, `EGID`, `EPOCHSECONDS`, `EPOCHREALTIME`, `pipestatus`, `PIPESTATUS`, `LUISH_VERSION` or `LUISH_PATCHLEVEL` makes it an ordinary variable (zsh calls `setuid`, refuses, or sets `pipestatus` until the next pipeline), so scripts that use these names still work; `EPOCHREALTIME` has microseconds, as in bash, and `EPOCH*` need no `zmodload zsh/datetime`; a subshell seeds `RANDOM` anew unless it was assigned (zsh's subshells repeat the parent's numbers); `set` doesn't list them, and `local` doesn't give a function its own `SECONDS`. `pipestatus` follows zsh, where bash's `PIPESTATUS` differs: after an `if`, a loop or `{ ...; }` it has that command's status (bash's has the last pipeline's inside), and an assignment or `[[` leaves it (bash sets it); it is empty in a new shell (bash's is `0`). `path` is the array of the directories in `PATH`, as in zsh (whose sh emulation doesn't have it), so `$path` is the first directory where dash has it unset; an array assignment (`path=(...)`, `path+=(...)`, `path[i]=x`, all errors in dash) sets `PATH`, but unlike zsh `path=x` makes it an ordinary variable (zsh sets `PATH` to `x`), as do `unset path` (zsh unsets `PATH` too) and `local path` (zsh makes `PATH` local too); a `path` in the environment is an ordinary variable; a read-only `PATH` can't be set through it (zsh ignores `readonly PATH` there). `dirstack` is the directory stack, tied in the same way: indices from 0 (as elsewhere), and `dirstack=x`, `unset dirstack` and `local dirstack` make it an ordinary variable (zsh's `dirstack=x` sets the stack to `x`) | | Arrays, `a=(x y)`, `${a[i]}`, `"${a[@]}"` | A syntax error (`a=(`), or a bad substitution when expanded (`${a[i]}`); `a[i]=x` and `a+=x` are commands | As in zsh's sh emulation (see [Arrays](usage.md#arrays)): indices from 0, `$a` is `${a[0]}`, no holes (`unset 'a[i]'` empties the element), `a[i]=x` past the end fills with empty elements (so does `a=([i]=x)`), `"${a[*]/x/y}"` replaces in the joined string, arrays aren't exported, and `local`, `export` and `readonly` take arrays (`local a=(x y)`). As in bash: a string is an array of one element (zsh indexes its characters: `s=abc; echo ${s[1]}` prints `b`), an index before the start is an error (`a[-9]=x`; zsh prepends), and so is one of 64 Mi (67108864) or more, `unset 'a[@]'` unsets the array, assignments to an element before a command are temporary, as the others (`a[0]=x cmd`; zsh keeps them). `local a` keeps the global value, as `local x` does in dash (zsh makes a new, empty one). Errors have status 2, as other expansion errors in dash (zsh uses 1). `set` and `readonly -p` show arrays as `a=('x' 'y')` | | Associative arrays, `typeset -A h`, `h[key]=v`, `${h[key]}` | `typeset` is looked for as a command | As in zsh's sh emulation and bash (see [Arrays](usage.md#arrays)): keys are strings (also in arithmetic, `$((h[x]))`), `h=([k]=v)` and `h=(k v)` assign, `+=` adds, `"${h[@]}"` gives the values, `unset 'h[k]'` removes a key, and `read -A` reads pairs (zsh). The values come in the order the keys were added (zsh and bash use their hash order). As in zsh: an odd number of elements, or a mix of `[k]=v` and pairs, is an error (bash takes an empty value, or ignores the elements without a key). As in bash: `$h` is `${h[0]}` and `h=x` assigns to it (zsh reads some value, and refuses the assignment); `typeset -A x` makes a string the value at key `0` (zsh empties it), and an indexed array can't become an associative one or the other way round (zsh empties it); `unset 'h[k]'` of a read-only one is an error (zsh removes the key). `typeset -p` shows them as `typeset -A h=(['k']='v')` | | Parameter flags, `${(o)a[@]}`, `${(j:,:)a[@]}`, `${(k)h[@]}` | Bad substitution when expanded | zsh's flags `@`, `k`, `v`, `j:sep:`, `s:sep:`, `f`, `F`, `L`, `U`, `C`, `u`, `o`, `O`, `i`, `n` and `a` (see [Arrays](usage.md#arrays)), applied in zsh's order whatever order they are written in, with zsh's rules for quoting: `"${(o)a[*]}"` joins before sorting, `s` splits also in double quotes (where only the first and last words can be empty, unless `@` is given), its words aren't split again, and where the result is a single word (`x=${(o)a[@]}`, `case`, here-documents) the elements are joined first and `s` doesn't split. As in zsh's sh emulation, flags apply to what the parameter gives, so `${(k)h}` is `${h[0]}`: the keys are `${(k)h[@]}`. Other flags (such as `P`, `q`, `e`, `l:n:`), `${(flags)#name}` and nested `${(o)${...}}` are bad substitutions (zsh: an error in flags, status 1), as is `${(U)}` (zsh: empty). `o` sorts by bytes (zsh uses the locale's collation, which is the same in the `C` locale), and `L`, `U` and `C` change only ASCII letters, as `typeset -l` does | | `typeset`, `declare` | Looked for as commands (`not found`, status 127) | Built-ins, as in zsh and bash, with `-a`, `-A`, `-f`, `-g`, `-i`, `-l`, `-p`, `-r`, `-u`, `-U` (zsh) and `-x` (not zsh's other options, nor zsh's base for `-i`, nor `-F`, which is floating point in zsh and the names of functions in bash; `typeset +f` lists the names, as in zsh). In a function they make local variables that start unset, as in zsh, and hide a read-only one (bash refuses). As in bash: `typeset -a x` makes a string an array of its value (zsh empties it, and refuses `typeset -a x=value`). `typeset -i x` evaluates the value `x` already has (as in zsh; bash keeps it), and `typeset -i` applies to the elements of arrays and associative arrays (as in bash; zsh has no integer arrays). The value of an integer variable is printed in decimal (zsh uses the base of the first number assigned, `8#10` for `010`). `-l` and `-u` convert values when they are assigned, as in bash (zsh converts them when they are read, so `typeset +l` gives back the original), and apply to the elements of arrays, as in bash (zsh leaves arrays alone). `-U` on `PATH` also applies to array assignments to `path` (zsh keeps them apart), and `-U` on a string other than `PATH` does nothing (zsh also dedupes its other colon-separated variables, such as `FPATH`). `typeset -p` quotes as `export -p` does (`typeset x='1'`, `typeset -a a=('x' 'y')`; zsh quotes only where needed and writes exported strings as `export x=...`). `typeset -f` prints definitions in luish's own layout (four-space indents, `f() {`; zsh indents with tabs and keeps the text of words as written, bash has its own layout), with backquotes as `$(...)` and here-document delimiters renamed, and refuses variable attributes and `name=value` with `-f` (as bash; zsh ignores most of them). `local` takes the same options (but `-f` and `-g`) too, but still keeps the value, as in dash | | `read -A`, `read -a NAME` | Illegal option | Read the fields into an array: `-A` as in zsh (one name), `-a NAME` as in bash (other names are ignored). Split as field splitting and bash do: zsh adds an empty element after a trailing delimiter (also after trailing whitespace), and makes an empty line one empty element | | `${x:offset:length}`, `${x/pattern/replacement}` | Bad substitution when expanded | As in zsh's sh emulation and bash (see [Parameter expansion](usage.md#parameter-expansion)), counting bytes. As in zsh: a negative offset before the start is the start (bash gives nothing), `${@:1:-1}` works, `&` in the replacement is literal (bash 5.2 replaces it with the match), an unset or empty variable matches `*`, and `"${*/a/b}"` replaces in the joined string. A letter after the `:` starts a modifier (below), where bash evaluates `${x:i}` as arithmetic, and `${x:1:}` is a bad substitution | | Modifiers, `${x:h}`, `${x:t}`, `${x:A}` | Bad substitution when expanded | zsh's `h`, `t` (also with a count, `:h2`), `r`, `e`, `a`, `A`, `u` and `l`, one after another, as in zsh (see [Parameter expansion](usage.md#parameter-expansion)), also in glob qualifiers. As in zsh, `:a` is relative to the current directory as the system has it, not `PWD`. Unlike zsh, `..` at the root is the root (zsh's `:a` gives `//a` for `/../a`); an unknown modifier is a bad substitution when expanded, with status 2 (zsh fails with status 1 when parsing); `${#x:h}` is a bad substitution (zsh gives the length of `${x:h}`). zsh's `q`, `Q`, `P`, `s`, `g`, `&`, `c` and `x` are bad substitutions | | `SHLVL` | Not maintained | Incremented at startup, as in zsh, but set to 1 when it isn't in the environment only in an interactive shell (zsh always sets it), so scripts see dash's environment | | Last command of `sh -c` | Debian's dash forks it (a Debian patch; upstream dash execs it) | Replaces the shell with it unless a trap is set, as zsh, bash and upstream dash do | ### Following POSIX where dash doesn't | Behaviour | dash | luish | |---|---|---| | Script read from a pipe | Reads ahead in blocks, so commands in the script that read stdin miss data | Never reads past the current command (POSIX requirement), as zsh | | `$LINENO` | Not supported (empty) | Current line number, as in POSIX and bash | | fd numbers in redirections | Only a single digit is an fd number: `exec 20>f` runs a command named `20`, and `echo hi 99>&1` prints `hi 99` | Any number of digits, as POSIX allows (and bash does; zsh is like dash) | | `exec -- cmd` | No `--` handling: tries to run a command named `--` (status 127) | `--` ends the options, as POSIX requires (and bash and zsh do) | | `cd -e` | Not supported (`Illegal option -e`, status 2) | The POSIX 2024 option: with `-P`, status 1 if the directory is changed but its name can't be found (as bash does) | | `set -o pipefail` | Not supported (`Illegal option -o pipefail`, status 2) | The POSIX 2024 option, as in bash: a pipeline's status is that of its last command that failed. The setting when a pipeline starts applies, also to `wait` for a background one (zsh ignores it there) | | Options listed by `set -o` / `set +o` | The last one is `debug` (no option letter) | The last ones are `pipefail` and `hashall` (`-h`), which POSIX has and dash lacks; luish has no `debug` option | ### Following bash | Behaviour | dash | luish | |---|---|---| | `set -x` output | Arguments printed unquoted | Arguments quoted so the trace can be re-read as input (as bash does) | | `kill %n` for a job started without job control | Signals the process group `-pid`, which doesn't exist, and fails with "No such process" | Signals each process of the job (as bash does) | | `fc` | Debian's dash has none (`fc: not found`, status 127). Upstream dash (with libedit) lists as `%5d cmd`, counts its own entry, and doesn't echo edited commands | The POSIX list format (`N\tcmd`, continuation lines indented by a tab). As in bash, its own entry is left out, and commands it re-runs replace that entry and are echoed to stderr. An event number outside the history is moved to the nearest end (dash moves a `first` that is too large to the oldest entry). Fails with status 2 in a non-interactive shell | | `$((` that is not arithmetic | Syntax error (dash always reads `$((` as arithmetic) | Read as `$( (...) )`, a command substitution of a subshell, as bash does (POSIX leaves it unspecified) | | `${ list; }`, `${\| list; }` | Bad substitution when expanded | As in bash 5.3 and mksh (see [Usage](usage.md#shell-language-extensions)): the list runs in the shell itself, as a function body does but with the caller's positional parameters, and the substitution is its output (as `$(list)` gives it) or the value of `REPLY` (local to it) after it. The `}` must come where a reserved word can, as in bash. The list is parsed with the rest of the script, so one that doesn't parse is a syntax error even if it is never run. zsh's version (not yet released) keeps all but one trailing newline and doesn't split the output | | `${!a[@]}`, `${!a[*]}` | Bad substitution when expanded | The indices of an array, or the keys of an associative one (in the order they were added), as bash's, which zsh's sh emulation doesn't have (native zsh has `${(k)a}`). As in bash, an unset array has none, even with `set -u` | | `${!x}`, `${!x[i]}`, `${!prefix@}`, `${!prefix*}` | Bad substitution when expanded | bash's indirection, which zsh's sh emulation doesn't have (native zsh has `${(P)x}`): the parameter named by the value of `x` (or of the element), a variable, `name[index]`, a positional or a special parameter, with any operator applied to it (`${!x:-d}`, `${!x#p}`). As in bash, an unset or invalid name is an error, even without `set -u`. Unlike bash, the index in the value is only evaluated, not expanded, as in `unset 'a[i]'` (so `$(...)` in it is an error), and `${!x:=v}` can assign an element. `${!prefix@}` lists the set variables whose names start with the prefix, sorted; unlike bash, specials such as `RANDOM` are listed only once assigned, as in `set` | | `FUNCNAME`, `BASH_LINENO`, `caller` | Ordinary variables; `caller` is looked for as a command | The call stack, as in bash (see [Special variables](usage.md#special-variables) and `help caller`), with the frames of `BASH_SOURCE`. As there, assigning makes them ordinary variables (bash ignores the assignment). zsh's sh emulation has none of them (native zsh has `funcstack` and `funcfiletrace`) | | `BASH_SOURCE` | An ordinary variable | The files being run, innermost first: the script, the files read with `.` and the startup files, and in a function the file it was defined in (see [Special variables](usage.md#special-variables)). It is unset in `-c` and on standard input, as in bash, but a function defined there has an empty file (bash: `main` or `environment`). Assigning to it makes it an ordinary variable, as for luish's other special variables (bash ignores the assignment). zsh has no `BASH_SOURCE` (its `funcfiletrace` and `%x` have the same information) | | `emacs` option in interactive shells | Off (Debian's dash has no line editor) | On unless `vi` is set, since it is the line editor's mode, so `$-` has `E` (as in bash) | ### luish's own | Behaviour | dash | luish | |---|---|---| | `${a[i..j]}` | Bad substitution when expanded | A slice of an array, as in Python, which no other shell has (see [Arrays](usage.md#arrays)): from `i` up to (not including) `j`, either end left out, negative ends from the end, and clamped to the array. It expands as `${a[@]}` does, so `"${a[i..j]}"` gives separate words (zsh's `"${a[i,j]}"`, with both ends included, joins them). A string is an array of one element, as for `${s[0]}`. For an associative array the subscript is a key, as before (`${h[a..b]}`). `$a[i..j]` is still `$a` followed by a pattern, as in dash. No steps, and no assignment to a slice | | Command cache (`hash`) in an interactive shell | Kept until `PATH` is assigned or `hash -r`, so a command installed earlier in `PATH` than a cached one is ignored | Also cleared when a `PATH` directory changes, checked after each line is read (see [Improvements](improvements.md)) | | Running out of stack | Recursion through `eval`, `.` or a trap, and very deeply nested commands, crash the shell (SIGSEGV). A function calling itself stops at a depth of 1000, as a Debian patch (upstream dash crashes), but in an interactive shell the depth isn't reset by the error, so every function call fails after that | The same limit for functions (`Maximum function recursion depth (1000) reached`, status 2), reset by the error. Other nesting that would run out of stack is an error too, `nested too deeply`, with status 2, which exits a non-interactive shell and returns an interactive one to its prompt | | Command-line options | `-o` takes only the names that `set -o` lists, and any `--name` other than `--` is an error (`Illegal option --`, status 2) | `-o` and `+o` also take any option named as for `setopt` (such as `-o ERR_EXIT` or `-o prompt_percent`), and there are long options such as `--login` and `--no-rcs` (see [Usage](usage.md)) | | `__luish_internal` | Not a built-in (`not found`, status 127) | luish's own built-in, with subcommands such as `savestate`, which prints commands that restore the shell's state, and `print-git-rev` | | Startup files | Login shells read `/etc/profile` and `~/.profile`; interactive shells read `$ENV` | Interactive shells first apply the settings, aliases and variables in `config.toml`, if it exists, and load the plugins it enables (login shells that aren't interactive apply only its `env` and `path` tables, and the option `startup.auto_cache`); if `rc.d` exists, they then run its `*.lsh` files (then the plugins' `post-rc.lsh`); if `login.d` exists, login shells then run its files instead of `/etc/profile` and `~/.profile`. Their `__luish_cache` blocks are cached, and with `startup.auto_cache` all their effects (see [Cached startup files](usage.md#cached-startup-files)). Without them, as dash | | Grouped option names, `setopt NAME=VALUE`, `setopt -p GROUP` | No `setopt` | luish's own options are named in groups (`history.share`, `glob.star`), with zsh's names as aliases, and a `no` prefix goes on the last part (`history.no_share`). `setopt NAME=VALUE` sets an option or a setting with a value, and such arguments are expanded as assignments, as for `export`. `setopt -p GROUP` puts the names that follow in `GROUP`. zsh has none of these (`setopt a=b` is an error there, and `setopt -p` sets `privileged`) | | `help` | Not a built-in (`not found`, status 127) | In interactive shells (and their subshells), a built-in that shows help for the built-ins. Not a built-in in scripts or `-c`, as in dash; there, `__luish_internal help` does the same | | `jobs -i` | An illegal option (status 2) | On a terminal, a menu of the jobs, to bring one to the foreground, continue, stop or end it (see [`jobs`](builtins.md#jobs)). Without a terminal, an error with status 2, as in dash | | `plugin` | Not a built-in (`not found`, status 127) | In interactive shells (and their subshells), a built-in that loads [plugins](plugins.md). Not a built-in in scripts or `-c`, as in dash; there, `__luish_internal plugin` does the same | | `where` | Not a built-in (`not found`, status 127) | In interactive shells (and their subshells), and in any shell while `vars.trace` or `vars.trace_history` is on, a built-in that shows where variables were set (see [Tracing variables](usage.md#tracing-variables)). Otherwise not a built-in, as in dash; `__luish_internal where` does the same. zsh's `where` lists the commands a name finds instead, which luish doesn't have | | Startup caches with `vars.trace` | No startup caches | While variables are traced, the startup files run instead of being restored from the caches, so that where they set variables is recorded | | Error messages | `$0: LINE: message` | `FILE: LINE: message`, where `FILE` is the file of the code that failed (a file read with `.`, or the file a function was defined in), followed by the text of the failing line and the call stack: a line for each function call and file read with `.` that led there, with the text of the line it was called from (see [Error messages](usage.md#error-messages)) | | `shopt` | Not a built-in (`not found`, status 127) | bash's built-in: `shopt -s NAME` and `shopt -u NAME` set the luish option that does the same (`globstar` is `glob.star`, `autocd` is `cd.auto`, `histverify` is `history.verify`). bash's options that describe what luish always does (`histappend`, `expand_aliases`, `inherit_errexit`, `xpg_echo` ...) are always on, `login_shell` tells whether the shell is a login shell (and can't be changed; bash ignores the change), and the others are always off; setting or unsetting one the other way is an error (status 1) | ## Known limitations Scripts: - `trap` with no arguments, run inside a subshell or `$(...)`, doesn't show the parent's traps. - `read` is not interrupted by trapped signals. - `${@#pat}` and `${*%pat}` operate on the joined string rather than on each parameter. - `set -v` output is approximate. - Glob results are sorted in byte order; locale collation is not implemented. - `command local x=1` keeps the variable in the function; in dash it is local to the `command` invocation and disappears. - Commands, `$(...)`, `${...}` and `$((...))` can't be nested as deeply as in dash: with the usual 8 MB of stack, about 4000 levels of `( ... )` in a release build (dash: more than 10,000). Deeper nesting is an error, `nested too deeply`, as is recursion through `eval` or `.` that runs out of stack (see above). Interactive use: - History entries are stored as UTF-8, so invalid bytes in a command are replaced when it is recorded. They have no elapsed time (written as 0, as zsh does with `share_history`), nor other metadata such as the directory or exit status, and `fc` can't show their times (zsh's `fc -d`, `-i`). Entries read from other shells are not marked as such (zsh's `set-local-history`). zsh's `hist_ignore_dups`, `append_history`, `extended_history` and `hist_fcntl_lock` are always on, and aren't options. - `fc -e` records the edited text as one history entry, rather than one entry per command. With `history.inc_append` or `history.share`, the `fc` command has been written to the file before it replaces its own entry (as in zsh). - `set -b` (immediate job notification) is accepted but does nothing: jobs are reported only before a prompt. Job notifications are given only for input read a line at a time (interactive or stdin), not in scripts run with `set -m`. - Syntax highlighting doesn't expand aliases. - The state that `__luish_internal savestate` prints can be changed by aliases the shell reading it has already: a global alias there may be any word of it (the startup cache reads its own without aliases). Function bodies are safe, since their words that are aliases are quoted, apart from a `for` variable named like a global alias. - Completion skips filenames that are not valid UTF-8. Matching has no fuzzy matching and no ranking, and isn't configurable. The menu has no groups (such as zsh's headings for commands, files and so on), no colours by file type (`LS_COLORS`), no narrowing by typing, and no mouse. After Ctrl-C it stays on the screen above the next prompt. Completion doesn't know about `**/` or glob qualifiers. - Key bindings: only the emacs keymap can be changed (vi mode keeps the line editor's keys); there are no user-defined widgets (`zle -N`), numeric arguments aren't given to luish's own widgets, and some word kills don't go to the kill ring. After Ctrl-O the next line is filled with the entry, but the history position is the end of the history (zsh's is the entry). - Prompt expansion lacks zsh's `%_`, `%e`, `%I`, `%N`, `%x`, `%v`, the `l`, `S`, `_`, `e` and `v` conditions, and widths relative to the terminal's (negative truncation lengths). - Glob qualifiers lack zsh's `e`, `+`, `f`, `F`, `Y` and `P`, `(#q...)`, the modifiers other than `h`, `t`, `r`, `e`, `a`, `A`, `u` and `l`, and `EXTENDED_GLOB` patterns. - There is no `funcstack`, `funcfiletrace`, `functrace` or `funcsourcetrace` (zsh's, which its sh emulation lacks too); bash's `FUNCNAME`, `BASH_LINENO` and `BASH_SOURCE` have the same information. - In a function restored from the startup cache or a saved state, `LINENO` (and so `BASH_LINENO` and `caller` for calls from it) counts lines of the restored text, not of the file the function was defined in. - The startup cache trusts its keys: a file's entry (with `startup.auto_cache`) depends on `PATH`, `HOME`, the files before it and the files it reads with `.`, and a `__luish_cache` block's only on what it lists. What else they read (other variables, files that commands read) isn't seen, other than by `check-cache`; see [Cached startup files](usage.md#cached-startup-files). - Shell functions named `chpwd`, `precmd`, `preexec`, `periodic` or `zshexit` are not hooks, nor are the `*_functions` arrays: extensions register hooks instead (see [](extensions.md)). - Extensions' hooks have no time limit (only completers do), so a slow `precmd` or prompt hook, or `prompt-vars.lsh`, delays the prompt. Completers can't be interrupted with Ctrl-C, only by their time limit. The static musl build (see [](installation.md)) differs from the usual build, which uses glibc as dash does: - musl reserves signals 32 to 34, so its real-time signals start at 35 rather than 34: `RTMIN+n` is signal `35+n`, one more than in dash, `kill -l 34` prints `34`, and signals 32 to 34 can't be trapped. - Some error messages take musl's wording, such as `Symbolic link loop` for glibc's `Too many levels of symbolic links`, `Filename too long` for `File name too long`, and `Arithmetic exception` for a process killed by `SIGFPE`. - `~user` only finds users in `/etc/passwd`, not those from other sources such as LDAP (through NSS). - Scripts that do much work inside the shell take up to twice as long, since musl's `malloc` is slower.