Compatibility¶
luish generally treats 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 |
|---|---|---|
|
Not a built-in ( |
As in zsh: |
|
Not a built-in ( |
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 ( |
|
An ordinary word: |
A reserved word (as POSIX allows), as in zsh and bash: |
|
|
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. |
|
|
As in zsh and bash: the same as |
|
|
As in zsh and bash: |
|
Not a built-in ( |
As in zsh (see |
|
Syntax error |
As in zsh and bash: |
|
Syntax errors |
As in zsh and bash: after an arm’s body, |
|
Syntax errors ( |
As in zsh: |
|
Not a built-in ( |
As in zsh: evaluates each argument as |
|
Not built-ins ( |
As in zsh with its default options: |
|
Not built-ins ( |
As in zsh: set options by name (case and |
|
Not supported |
With |
|
Ordinary variables |
In an interactive shell, a prompt at the right edge of the terminal, as in zsh (see Usage). They are separate variables, where zsh ties |
|
A syntax error |
Process substitution, as in zsh and bash: a word that is the path of a pipe, |
|
A syntax error ( |
A here-string, as in zsh and bash: the word, expanded without field splitting or globbing ( |
|
The same as |
With |
Brace expansion, |
Text |
With |
Glob qualifiers, |
A syntax error |
With |
|
No |
zsh’s |
A directory as a command |
|
With |
|
Not a built-in ( |
In interactive shells (and their subshells), as zsh’s for its emacs keymap and the widgets luish has; |
|
Not a built-in ( |
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 |
|
Not a built-in ( |
In interactive shells (and their subshells), as zsh’s: edits a variable’s value with the line editor (see |
|
Not a built-in ( |
In interactive shells (and their subshells), as zsh’s, with its options but |
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). Unlike zsh: without |
History expansion, |
None: |
With |
|
No options: |
zsh’s options: |
Global aliases ( |
None |
Expanded in any position, not only as a command name, as in zsh: also arguments, |
Suffix aliases ( |
None |
As in zsh: a command name |
|
Ordinary variables |
zsh’s special parameters, computed when read (see Special variables), with |
Arrays, |
A syntax error ( |
As in zsh’s sh emulation (see Arrays): indices from 0, |
Associative arrays, |
|
As in zsh’s sh emulation and bash (see Arrays): keys are strings (also in arithmetic, |
Parameter flags, |
Bad substitution when expanded |
zsh’s flags |
|
Looked for as commands ( |
Built-ins, as in zsh and bash, with |
|
Illegal option |
Read the fields into an array: |
|
Bad substitution when expanded |
As in zsh’s sh emulation and bash (see Parameter expansion), counting bytes. As in zsh: a negative offset before the start is the start (bash gives nothing), |
Modifiers, |
Bad substitution when expanded |
zsh’s |
|
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 |
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 |
|
Not supported (empty) |
Current line number, as in POSIX and bash |
fd numbers in redirections |
Only a single digit is an fd number: |
Any number of digits, as POSIX allows (and bash does; zsh is like dash) |
|
No |
|
|
Not supported ( |
The POSIX 2024 option: with |
|
Not supported ( |
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 |
Options listed by |
The last one is |
The last ones are |
Following bash¶
Behaviour |
dash |
luish |
|---|---|---|
|
Arguments printed unquoted |
Arguments quoted so the trace can be re-read as input (as bash does) |
|
Signals the process group |
Signals each process of the job (as bash does) |
|
Debian’s dash has none ( |
The POSIX list format ( |
|
Syntax error (dash always reads |
Read as |
|
Bad substitution when expanded |
As in bash 5.3 and mksh (see Usage): 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 |
|
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 |
|
Bad substitution when expanded |
bash’s indirection, which zsh’s sh emulation doesn’t have (native zsh has |
|
Ordinary variables; |
The call stack, as in bash (see Special variables and |
|
An ordinary variable |
The files being run, innermost first: the script, the files read with |
|
Off (Debian’s dash has no line editor) |
On unless |
luish’s own¶
Behaviour |
dash |
luish |
|---|---|---|
|
Bad substitution when expanded |
A slice of an array, as in Python, which no other shell has (see Arrays): from |
Command cache ( |
Kept until |
Also cleared when a |
Running out of stack |
Recursion through |
The same limit for functions ( |
Command-line options |
|
|
|
Not a built-in ( |
luish’s own built-in, with subcommands such as |
Startup files |
Login shells read |
Interactive shells first apply the settings, aliases and variables in |
Grouped option names, |
No |
luish’s own options are named in groups ( |
|
Not a built-in ( |
In interactive shells (and their subshells), a built-in that shows help for the built-ins. Not a built-in in scripts or |
|
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 |
|
Not a built-in ( |
In interactive shells (and their subshells), a built-in that loads plugins. Not a built-in in scripts or |
|
Not a built-in ( |
In interactive shells (and their subshells), and in any shell while |
Startup caches with |
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 |
|
|
|
Not a built-in ( |
bash’s built-in: |
Known limitations¶
Scripts:
trapwith no arguments, run inside a subshell or$(...), doesn’t show the parent’s traps.readis not interrupted by trapped signals.${@#pat}and${*%pat}operate on the joined string rather than on each parameter.set -voutput is approximate.Glob results are sorted in byte order; locale collation is not implemented.
command local x=1keeps the variable in the function; in dash it is local to thecommandinvocation 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 throughevalor.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, andfccan’t show their times (zsh’sfc -d,-i). Entries read from other shells are not marked as such (zsh’sset-local-history). zsh’shist_ignore_dups,append_history,extended_historyandhist_fcntl_lockare always on, and aren’t options.fc -erecords the edited text as one history entry, rather than one entry per command. Withhistory.inc_appendorhistory.share, thefccommand 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 withset -m.Syntax highlighting doesn’t expand aliases.
The state that
__luish_internal savestateprints 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 aforvariable 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, thel,S,_,eandvconditions, and widths relative to the terminal’s (negative truncation lengths).Glob qualifiers lack zsh’s
e,+,f,F,YandP,(#q...), the modifiers other thanh,t,r,e,a,A,uandl, andEXTENDED_GLOBpatterns.There is no
funcstack,funcfiletrace,functraceorfuncsourcetrace(zsh’s, which its sh emulation lacks too); bash’sFUNCNAME,BASH_LINENOandBASH_SOURCEhave the same information.In a function restored from the startup cache or a saved state,
LINENO(and soBASH_LINENOandcallerfor 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 onPATH,HOME, the files before it and the files it reads with., and a__luish_cacheblock’s only on what it lists. What else they read (other variables, files that commands read) isn’t seen, other than bycheck-cache; see Cached startup files.Shell functions named
chpwd,precmd,preexec,periodicorzshexitare not hooks, nor are the*_functionsarrays: extensions register hooks instead (see Extensions).Extensions’ hooks have no time limit (only completers do), so a slow
precmdor prompt hook, orprompt-vars.lsh, delays the prompt. Completers can’t be interrupted with Ctrl-C, only by their time limit.
The static musl build (see Installation) 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+nis signal35+n, one more than in dash,kill -l 34prints34, and signals 32 to 34 can’t be trapped.Some error messages take musl’s wording, such as
Symbolic link loopfor glibc’sToo many levels of symbolic links,Filename too longforFile name too long, andArithmetic exceptionfor a process killed bySIGFPE.~useronly 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
mallocis slower.