You ship a zsh plugin whose functions misbehave for users who have set options such as SH_WORD_SPLIT or KSH_ARRAYS in their own configuration. What does adding `emulate -L zsh` at the top of each function do, and how is it different from `emulate sh`?
answer
- your code runs in someone else's shell
- declare the dialect you wrote in
- one flag scopes it to the function
- there is no emulate bash
basics
~20 semulate -L zsh resets options to zsh's native defaults for the duration of the enclosing function, so a user's unusual setopts cannot change how your code behaves. emulate sh instead switches the shell into sh-compatible mode, which is what you want for running Bourne-style code.
solid answer
~40 s`emulate zsh` sets the option set to zsh's own defaults; `emulate sh` and `emulate ksh` set it to those shells' compatible behaviour, turning on things like `SH_WORD_SPLIT`. The `-L` flag is the important part for a plugin author: it makes the change local to the enclosing function by setting `LOCAL_OPTIONS` and `LOCAL_TRAPS`, so everything is restored when the function returns and the user's interactive shell is untouched. Adding `-R` resets *all* options to that emulation's defaults rather than only the ones that affect compatibility, which is stricter and occasionally surprising. So `emulate -L zsh` is the standard defensive prologue for a function you distribute, and `emulate -L sh` is what you use when a block genuinely is Bourne-style code you do not want to rewrite.
code
bash · 15 lines# defensive prologue for a distributed zsh function
my_widget() {
emulate -L zsh # zsh defaults, restored on return
setopt extended_glob # local because -L set LOCAL_OPTIONS
local -a logs
logs=(*.log(N)) # N = NULL_GLOB for this pattern only
print -r -- "found ${#logs} log files"
}
# run a Bourne-style block under sh rules instead
port_me() {
emulate -L sh
files="a.txt b.txt"
touch $files # splits, because sh emulation sets SH_WORD_SPLIT
}go deeper
Know that zsh options change how code behaves, and that a function you share can start with emulate -L zsh so it runs under predictable defaults.
Explain what the emulation targets are, what -L and -R each add, and name concrete options — SH_WORD_SPLIT, KSH_ARRAYS, EXTENDED_GLOB — whose presence would otherwise change your function's behaviour.
Treat distributed shell functions like library code: state the defensive prologue, its limits around aliases, IFS and PATH, and describe how invocation name and sticky emulation apply emulation without an explicit call.
Argue the general principle — global mutable configuration makes shared code unreliable — and decide how far a team should go: a mandated prologue and lint rule, or shipping executables with an explicit interpreter instead of sourced functions.
## The problem being solved zsh has well over a hundred options, and users set them freely. A function you ship runs in *their* shell with *their* options. Any of these will change what your code does without touching a line of it: - `SH_WORD_SPLIT` — your unquoted expansions suddenly split on whitespace. - `KSH_ARRAYS` — your subscripts are off by one and bare array names collapse to element zero. - `EXTENDED_GLOB` — `^`, `#`, and `~` become pattern characters inside your patterns. - `NULL_GLOB` / `NOMATCH` changes — your globs silently vanish or hard-fail. - `NO_UNSET` — reading an unset variable becomes an error. This is the shell equivalent of a library that behaves differently depending on the caller's global settings. The fix is to declare the dialect you were written in. ## What `emulate` actually does ```zsh emulate [-lLR] [zsh|sh|ksh|csh] ``` It sets the shell options to those appropriate for the named shell. `emulate zsh` restores zsh's own defaults; `emulate sh` and `emulate ksh` switch to those shells' compatible behaviour, which among other things turns `SH_WORD_SPLIT` on. `-l` lists the resulting settings instead of applying them, which is a useful way to see exactly what an emulation changes. `-R` means *reset*: set all options to their defaults for that emulation, not only the ones that affect compatibility. Without `-R`, options that are purely a matter of taste — interactive conveniences, for instance — are left as the user had them. With `-R` you get a clean slate, which is more predictable and also more likely to surprise you by turning off something you were relying on. ## What `-L` adds, and why it is the whole point `-L` implies `LOCAL_OPTIONS` and `LOCAL_TRAPS`. Inside a function, those mean every option change and trap installed during the function is undone when the function returns. Without `-L`, `emulate zsh` in a function permanently rewrites the user's option set — you would fix your own function by breaking their shell. ```zsh my_plugin_widget() { emulate -L zsh setopt extended_glob # scoped: reverted on return local -a matches matches=(*.log) # behaves the way you wrote it, whatever the user set print -r -- ${#matches} } ``` That prologue is the convention in well-written zsh plugins and in zsh's own shipped functions. Many authors write `emulate -L zsh -o no_unset` or similar to add the specific options they *do* want on top of the reset baseline. ## `emulate sh` is a different tool for a different job `emulate -L zsh` says "my code is zsh". `emulate -L sh` says "the block that follows is Bourne-style code, please apply Bourne rules". Use it when you are sourcing or inlining something written for `/bin/sh` and you would rather not port it: word splitting comes back, and other sh-compatible behaviours are enabled for the duration. zsh also applies emulation automatically based on the name it was invoked under. If the basename of `argv[0]` is `sh` or `ksh`, zsh emulates that shell from startup, including following sh's startup-file conventions rather than reading the zsh ones. That is how a system that makes `/bin/sh` a link to zsh keeps Bourne scripts working. There is one more form worth knowing, *sticky emulation*: running an `autoload` inside `emulate <shell> -c '...'` attaches that emulation to the loaded function, so it runs under those rules every time it is called, without needing a prologue of its own. ```zsh emulate sh -c 'autoload -Uz legacy_helper' # legacy_helper always runs sh-style ``` ## Limits worth stating out loud Emulation covers *options*. It does not sandbox everything: variables such as `IFS`, aliases defined by the user, functions that shadow builtins, and `PATH` are not reset. Robust plugin code still declares `local` variables, uses `command`/`builtin` prefixes where an alias or override would be catastrophic, and avoids depending on ambient state. `emulate -L zsh` is the cheap 90% of the defence, not the whole of it. And it is not a compatibility layer for *bash*: there is no `emulate bash`. `emulate sh` gets you Bourne semantics, but bashisms such as `${var^^}`, `local -n`, or `BASH_SOURCE` are simply absent, so genuinely bash-specific code must be ported rather than emulated.
- What happens if you write `emulate zsh` without the -L inside a function?The option reset applies to the whole shell and persists after the function returns, silently discarding whatever options the user had set. `-L` implies LOCAL_OPTIONS and LOCAL_TRAPS so the state is restored on return — omitting it turns a defensive measure into a bug that damages the caller's session.
- What does zsh do when it is invoked under the name sh?It emulates sh from startup, applying sh-compatible options and following sh's startup-file conventions rather than reading the zsh startup files. That is what lets a system where `/bin/sh` points at zsh continue to run Bourne scripts, and it is why the shell you get depends on argv[0], not just on the binary.
- Does `emulate -L zsh` make a function fully immune to the caller's environment?No. It normalises options only. Aliases, IFS, PATH, exported variables, and user-defined functions shadowing builtins are all untouched. Hardened functions additionally declare variables `local`, prefix critical calls with `command` or `builtin`, and avoid depending on ambient state.
saying these in an interview costs you the question
- Thinking emulate sh is how you make your zsh code portable to bash
- Omitting -L and permanently changing the user's options
- Believing emulate resets variables and aliases as well as options
- Assuming an emulate bash mode exists
- Setting KSH_ARRAYS globally instead of scoping it to a function