skip to content

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`?

level: seniorimportance: should knowfreq 32%

answer

  1. your code runs in someone else's shell
  2. declare the dialect you wrote in
  3. one flag scopes it to the function
  4. there is no emulate bash

basics

~20 s

emulate -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
bash
# 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context