skip to content

Your team's shared .bashrc is being ported to .zshrc after everyone's login shell changed to zsh. Beyond word splitting and array indexing, which bash-specific constructs in that file will simply not work under zsh, and what replaces each?

level: seniorimportance: nice to knowfreq 28%

answer

  1. the loud failures are the easy half
  2. one shell ignores the other's variables
  3. a hook function, not a variable string
  4. history that never reaches the file
  5. split portable from shell-specific

basics

~10 s

shopt, HISTCONTROL, PROMPT_COMMAND, PS1 backslash escapes, complete/compgen completion scripts and $BASH_SOURCE are all bash-only. zsh replaces them with setopt, HIST_* options plus SAVEHIST, the precmd function, percent-style prompt escapes, bashcompinit and $0.

solid answer

~40 s

The parts that carry over untouched are aliases, exported variables, plain functions, and POSIX conditionals. What breaks is everything bash-specific: `shopt` does not exist — zsh uses `setopt`/`unsetopt` with different option names; `HISTCONTROL` and `HISTFILESIZE` do nothing — zsh needs `SAVEHIST` set plus options like `HIST_IGNORE_DUPS` and `HIST_IGNORE_SPACE`; `PROMPT_COMMAND` is not honoured — you define a `precmd` function or append to `precmd_functions`; `PS1` backslash escapes are not zsh's prompt escapes; and bash completion scripts that call `complete`/`compgen` need `autoload -U +X bashcompinit && bashcompinit` first. `${BASH_SOURCE[0]}` has no zsh equivalent under that name; `$0` gives the sourced file's path. My practical approach is to split the file: keep the portable env, alias and function parts in a shared file both shells source, and put shell-specific lines in each shell's own rc.

code

bash · 8 lines
bash
# bash                                  # zsh equivalent
# shopt -s histappend                   setopt APPEND_HISTORY INC_APPEND_HISTORY
# HISTCONTROL=ignoreboth                setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE
# HISTFILESIZE=10000                    SAVEHIST=10000
# PROMPT_COMMAND='_my_hook'             precmd_functions+=(_my_hook)
# dir=$(dirname "${BASH_SOURCE[0]}")    dir=${0:A:h}
# read -p "Name: " name                 read "name?Name: "
# dedupe PATH by hand                   typeset -U path

go deeper

for a junior

Know that a .bashrc is not a drop-in .zshrc: aliases, exports and simple functions carry over, but anything named for bash — shopt, PROMPT_COMMAND, HISTCONTROL, BASH_SOURCE — needs a zsh replacement.

for a middle

Name the specific replacements: setopt for shopt, SAVEHIST plus HIST_* options for the history variables, precmd_functions for PROMPT_COMMAND, ${0:A:h} for BASH_SOURCE, and bashcompinit for bash completion files.

for a senior

Emphasise that the dangerous half of the port is silent: ignored variables leave the file sourcing cleanly while history, PATH order and prompt hooks quietly misbehave, so verify behaviour rather than absence of errors.

for a principal

Decide the долгосрочная structure for a mixed-shell team — a portable shared file plus thin shell-specific rc files, with feature detection over shell-name branching — and treat dotfiles as code that deserves review and a verification step like anything else you ship.

## Start by separating what is portable Most of a typical `.bashrc` is not bash-specific at all. Exported variables, `alias` definitions, POSIX-shaped functions, `[[ ... ]]` conditionals, `$RANDOM`, `$SECONDS`, and `command -v` checks all behave the same in zsh. Those move across untouched. The work is in the bash-only remainder, and the honest way to do the migration is a line-by-line audit rather than a hopeful copy. ## The construct-by-construct list **`shopt` → `setopt` / `unsetopt`.** zsh has no `shopt` builtin at all; the line fails outright with a command-not-found. The option *names* also differ, so it is not a mechanical rename. `shopt -s histappend` has no single equivalent — zsh's history-appending behaviour is governed by `APPEND_HISTORY` (on by default) and `INC_APPEND_HISTORY`. `shopt -s checkwinsize` is unnecessary; zsh tracks terminal size itself. **History variables.** `HISTCONTROL=ignoreboth` and `HISTFILESIZE` are bash variables that zsh ignores silently — the worst kind of failure, because the file sources cleanly and your history behaves differently. In zsh, `HISTSIZE` is the in-memory size and **`SAVEHIST`** is how many lines are written to `HISTFILE`; if `SAVEHIST` is unset or zero, nothing is written at all, which is the classic "my zsh history disappears" report. The `ignoreboth` behaviour splits into two options: `HIST_IGNORE_DUPS` and `HIST_IGNORE_SPACE`. **`PROMPT_COMMAND` → `precmd`.** bash runs the contents of `PROMPT_COMMAND` before each prompt. zsh calls a function named `precmd`, and also every function whose name is in the `precmd_functions` array — the array is what you should append to, so you do not clobber another plugin's hook. The sibling hook `preexec` runs after you accept a line and before the command executes, which bash gained an equivalent for only much later. **Prompt strings.** zsh does not understand bash's backslash escapes such as `\u` and `\w`; it has its own percent-escape vocabulary, and command or parameter substitution inside a prompt requires enabling `PROMPT_SUBST`. Expect to rewrite the prompt rather than translate it. **Completion scripts.** A file that calls `complete -F _foo foo` or `compgen -W` is using bash builtins zsh does not natively provide. zsh can load a shim: ```zsh autoload -U +X bashcompinit && bashcompinit source /path/to/foo-completion.bash ``` That is a compatibility layer, not a translation — it works for many vendor-supplied completion files and can be flaky for elaborate ones. **`${BASH_SOURCE[0]}`.** Widely used in dotfiles to find the directory a sourced file lives in. It does not exist in zsh. In zsh, `$0` inside a sourced file is the file's path, so the idiom becomes `${0:A:h}` (zsh's `:A` modifier resolves to an absolute path, `:h` takes the directory). **`read -p prompt var`.** zsh's `read` does not take `-p` as a prompt flag; the prompt is attached to the variable name as `read "var?Prompt: "`. Similarly, bash's `read -a arr` is `read -A arr` in zsh. **PATH management.** zsh ties the scalar `PATH` to an array named `path` (and `FPATH` to `fpath`, `CDPATH` to `cdpath`). `typeset -U path` makes it deduplicate automatically, which replaces the hand-rolled "is this already in PATH" loop most `.bashrc` files carry. ## The strategy, not just the list For a team file, the durable answer is to stop maintaining one file per shell. Put everything portable — exported variables, aliases, POSIX functions — in a `~/.shell_common` sourced from both `.bashrc` and `.zshrc`, and keep only shell-specific lines in each rc. Two rules make that work: quote every expansion (so the file means the same thing under both splitting regimes), and use `"${arr[@]}"` for arrays. Guard genuinely shell-specific blocks by feature detection rather than by shell name where you can — `(( $+commands[foo] ))` in zsh versus `command -v foo` in both — and where you cannot, branch on `${ZSH_VERSION:-}` / `${BASH_VERSION:-}`, which are set only by their own shell. Finally, port incrementally and verify: source the new file in a fresh interactive shell and check the things that fail *silently* — history actually persisting to disk, PATH order, and the prompt hooks firing — because the loud failures like `shopt: command not found` will find you on their own.

  • A colleague reports that after the port their zsh history is empty in every new terminal. What do you check first?
    `SAVEHIST`. zsh writes history to `HISTFILE` only if `SAVEHIST` is set to a non-zero value; `HISTSIZE` alone governs the in-memory list, and bash's `HISTFILESIZE` is ignored entirely. It is the classic silent-failure of a ported dotfile, because nothing errors — the file simply never gets written.
  • How would you structure dotfiles for a team where some people stay on bash?
    One `~/.shell_common` holding exported variables, aliases and POSIX functions, sourced from both `.bashrc` and `.zshrc`, with only shell-specific lines left in each rc. Keep the common file quoted throughout and use `"${arr[@]}"` for arrays, and branch on `${ZSH_VERSION:-}` or `${BASH_VERSION:-}` when a block genuinely cannot be shared.
  • Can you just source your existing bash completion files from zsh?
    Often, via `autoload -U +X bashcompinit && bashcompinit`, which provides `complete` and `compgen` shims so vendor-supplied bash completion files load. It is a compatibility layer rather than a translation, so elaborate completions can behave imperfectly, and anything relying on other bash internals will still fail.
  • Which bash constructs are safe to keep verbatim in a shared file?
    Exported variables, `alias` definitions, POSIX-shaped functions, `[[ ... ]]` conditionals, `command -v` checks, and `$RANDOM`/`$SECONDS` all behave the same. The rule of thumb is that anything named after bash — `BASH_SOURCE`, `HISTCONTROL`, `PROMPT_COMMAND`, `shopt` — is a porting item, and anything POSIX is not.

saying these in an interview costs you the question

  • Assuming a bashrc can be renamed to zshrc unchanged
  • Expecting zsh to honour HISTCONTROL or PROMPT_COMMAND
  • Translating PS1 backslash escapes literally into zsh
  • Treating a silently ignored variable as a working line
  • Believing shopt is a POSIX builtin available everywhere

context