skip to content

You change your login shell to fish with `chsh`. Which of your existing shell scripts stop working as a result, and what genuinely does break because fish's syntax is deliberately not POSIX-compatible?

level: middleimportance: should knowfreq 40%

answer

  1. the shebang decides, not your login shell
  2. cron and make still use /bin/sh
  3. trouble starts at 'add this line to your shell config'
  4. no export, no $?, blocks end with end
  5. sit in it, do not ship in it

basics

~20 s

Scripts keep working: a script runs under the interpreter in its shebang, not under your login shell. What breaks is anything meant to be pasted into or sourced by your interactive shell, because fish uses set instead of VAR=value, set -x instead of export, and $status instead of $?.

solid answer

~50 s

The first half of the answer is the reassuring half. `chsh` only changes which program starts when you log in; a file beginning `#!/bin/bash` or `#!/bin/sh` is still executed by that interpreter, and cron, `make`, CI runners and anything calling `system()` still use `/bin/sh`. So "fish will break all my scripts" is wrong. What actually breaks is the material that assumes your *interactive* shell speaks POSIX: installer instructions that tell you to append `export PATH="$HOME/.local/bin:$PATH"` to your shell config, `eval "$(tool init -)"` snippets that emit Bourne syntax, tools like nvm that ship only a bash/zsh init script, and one-liners copied from a README. In fish you write `set -gx PATH $HOME/.local/bin $PATH`, read `$status` rather than `$?`, close blocks with `end`, and there are no backticks and no `[[ ]]`. The usual mitigations are to run the snippet under `bash -c`, or to keep your login shell POSIX and launch fish from your terminal emulator instead.

code

bash · 7 lines
bash
# A perfectly ordinary bash/POSIX snippet from an install guide.
# Every line below is a syntax error or a no-op if pasted into fish.
export PATH="$HOME/.local/bin:$PATH"
if [[ -d "$HOME/.local/bin" ]]; then
  echo "added"
fi
echo "exit status was $?"

go deeper

for a junior

Know that a script runs under the interpreter named in its shebang, so switching your own login shell to fish does not change how existing scripts execute. Know that fish uses set where bash uses export.

for a middle

Explain the split precisely: shebang and /bin/sh paths are unaffected, while sourced init snippets, eval "$(tool init -)" shims and pasted one-liners break. Name the concrete syntax differences rather than saying 'it is not POSIX'.

for a senior

Show the judgment: confine fish to the interactive path by launching it from the terminal rather than via chsh, and refuse to write shipped scripts in a shell that servers and CI images do not have installed.

for a principal

Own the tradeoff for a team. A per-engineer interactive shell is a personal choice, but anything that lands in a repo, an image or a runbook must be POSIX, and your tooling should not require every engineer to run the same shell.

## What chsh actually changes `chsh -s /usr/local/bin/fish` writes a new login-shell field for your account (the shell must be listed in `/etc/shells` first, or `chsh` refuses). From then on, when you log in on a console, over SSH, or open a terminal that starts a login shell, the program that gets started for *you* is fish. That is the entire scope of the change. It is a per-user setting about which interpreter greets you, not a system-wide replacement of the Bourne shell. ## What does not break, and why When the kernel executes a file whose first line is `#!/bin/bash`, it starts `/bin/bash` with that file as an argument. Your login shell is not consulted, so: - every script with a `#!/bin/sh` or `#!/bin/bash` shebang runs exactly as before; - `/bin/sh` is untouched — it is still dash, bash-as-sh, or whatever your distribution ships; - cron jobs, `make` recipes, `ssh host 'command'`, systemd units and library `system()` calls all still go through `/bin/sh`; - CI runners are unaffected, because they never look at a developer's login shell. Stating this clearly is most of the value of the question. The candidate who says "all my automation breaks" has confused the login shell with the system shell. ## What does break: things meant for your interactive shell The damage is concentrated in one place — anything designed to be *sourced into* or *pasted at* your prompt. - **Installer instructions.** "Add this line to your shell configuration": `export PATH="$HOME/.cargo/bin:$PATH"`. fish has no `export` and no `NAME=value` assignment statement; the fish spelling is `set -gx PATH $HOME/.cargo/bin $PATH`, or `fish_add_path $HOME/.cargo/bin` in fish 3.2 and later. - **`eval "$(tool init -)"` shims.** Version managers and prompt tools print Bourne-shell code for you to evaluate. Well-behaved ones detect the shell or offer `tool init fish`; the rest emit bash and simply fail. - **Bash-only init scripts.** nvm's `nvm.sh` is the classic example: it is a large body of bash meant to be sourced, and it cannot be sourced by fish at all. You either use a third-party fish equivalent or run node tooling under bash. - **Copied one-liners.** Anything with backticks (fish has never supported them), `[[ ... ]]`, `$?`, `function foo { ... }`, or a `for ...; do ...; done` loop is a syntax error in fish. ## The syntax differences you will actually hit ```fish set -gx EDITOR nvim # not: export EDITOR=nvim echo $status # not: echo $? set files (ls) # command substitution; $(ls) also works in fish 3.4+ if test -f config.yml echo found end # blocks close with end, not fi/done/esac ``` fish variables are lists and an unquoted expansion never word-splits, which removes a whole class of bash bug and simultaneously makes bash idioms that rely on splitting behave differently. Two things are worth knowing precisely because they are frequently misremembered: `&&`, `||` and `!` **are** valid fish syntax since fish 3.0 (before that you wrote `and`, `or`, `not`), and `$(cmd)` command substitution **is** accepted since fish 3.4 alongside fish's own `(cmd)`. So the gap in 2024-era fish is narrower than the folklore suggests — but it is still a different language, not a dialect. ## Living with it Three strategies, in ascending order of caution: 1. **Translate.** Keep a `config.fish` in fish syntax and translate the snippets you need. Fine for a personal machine. 2. **Shell out.** Run the offending snippet under `bash -c '...'` when you only need its side effects on files, and use `bash -l` when you need a full Bourne environment. 3. **Do not make it your login shell.** Leave the login shell as bash or sh and configure your terminal emulator to run fish as the command it starts. You get fish interactively while every login-time and non-interactive path stays POSIX. This is the recommendation the fish project itself has made, and it is the safest answer on a shared or managed machine. The last rule is separate and non-negotiable: **never write shipped scripts in fish**. Servers, containers and CI images do not have fish installed, so a `#!/usr/bin/env fish` script is a portability liability even though it would run correctly on your laptop. fish is a shell you sit in, not a shell you ship in — and being clear about that boundary is the whole point of the question.

  • A colleague insists fish cannot use && or $(...) at all. Is that still true?
    No, and it is a common stale claim. fish 3.0 made `&&`, `||` and `!` valid syntax alongside the older `and`, `or` and `not`, and fish 3.4 accepted `$(cmd)` next to fish's own `(cmd)`. Backticks remain unsupported, and assignment, `export` and block syntax are still entirely different, so fish is still not a POSIX shell.
  • Why is running fish from your terminal emulator often better than setting it as your login shell with chsh?
    Because it confines fish to the interactive path. Login-time machinery, remote `ssh host 'command'` invocations and anything that sources shell fragments for you keep getting a POSIX shell, so the failures you would otherwise debug at login simply do not occur. You still get fish everywhere you actually type.
  • Would you write a deployment script in fish because you find its syntax cleaner?
    No. fish is rarely installed on servers, containers or CI images, so a `#!/usr/bin/env fish` script fails on exactly the machines that matter, and no reviewer or on-call engineer can be assumed to read it. Cleaner syntax does not outweigh needing an extra package on every host that runs it.
  • Your team standardises on a bash-only tool whose setup is `eval "$(tool init -)"`. What are your options as a fish user?
    Check whether the tool has a fish mode — many print `tool init fish` output. If not, either translate the emitted exports into `set -gx` lines yourself and accept that they drift when the tool updates, or run that toolchain under `bash -c`. Do not push the team to change tools over your shell choice.

saying these in an interview costs you the question

  • Says changing the login shell breaks all existing shell scripts
  • Thinks chsh replaces /bin/sh system-wide
  • Claims fish still has no && or $(...) in fish 3.x
  • Writes deployment scripts in fish because it is their daily shell
  • Assumes export and VAR=value work the same way in fish

context