PS1 belongs to bash¶
Level: 101 · you are reading a Linux book on a Mac, and it has just told you to type export PS1="[\d \t \u@\h \w]\$ "
One line: The prompt is drawn by the shell, not by the terminal, so the book's PS1 line is an instruction to bash. bash draws it the same way on Linux and on a Mac. zsh reads the same variable but prints the backslash escapes as they are, because its own escapes start with %. fish never reads PS1 at all: a fish prompt is whatever a function named fish_prompt prints.
Is it valid in fish? No, and nothing tells you so. fish runs the book's export PS1=… line without an error, stores the variable, and goes on drawing the same prompt. To change a fish prompt you define the function fish_prompt, and to keep it you run funcsave fish_prompt, which writes the function to ~/.config/fish/functions/fish_prompt.fish. The book's first prompt, in fish:
function fish_prompt
echo -n "[$USER@"(prompt_hostname)" "(path basename (prompt_pwd -d 0))"]\$ "
end
funcsave fish_prompt
The fish run under In zsh and fish shows both halves: the PS1 line changing nothing, and this function drawing the book's prompt.
Measured¶
The book's page is about bash on Fedora. Its prompt, [donnie@fedora ~]$, comes from the variable PS1, set to [\u@\h \W]\$: the user name, the host name up to its first dot, the last part of the working directory, and $ (# for root). Changing it to [\d \t \u@\h \w]\$ adds the date, the time and the whole path. A prompt set at the command line lasts until that shell ends, and the book keeps it by adding the export line to ~/.bashrc.
A prompt is drawn only when a shell is waiting for someone to type, and CI has nobody at a keyboard, so the script types instead. It pipes lines into bash --norc --noediting -i. -i makes bash interactive although its input is a pipe, so it prints a prompt before it reads each line; --norc keeps ~/.bashrc out; --noediting turns off readline, which would echo each line a second time. Each line prints itself just before it runs, standing in for the terminal showing what you type, and bash prints exit on its own. Every session starts at the prompt $, in a fresh empty HOME that holds one folder, projects/linux-library.
Your user name, host name and clock are not CI's, so the script rewrites them after bash has drawn each prompt: what id -un prints becomes USER, the host name up to its first dot becomes HOST, a date such as Mon Sep 14 becomes Www Mmm dd, and a time becomes HH:MM:SS. Every other character is bash's.
Verified output of ps1_prompts_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ export PS1="[\u@\h \W]\$ "
[USER@HOST ~]$ echo "$PS1"
[\u@\h \W]$
[USER@HOST ~]$ cd projects/linux-library
[USER@HOST linux-library]$ cd /usr/bin
[USER@HOST bin]$ cd /usr
[USER@HOST usr]$ exit
$ export PS1="[\d \t \u@\h \w]\$ "
[Www Mmm dd HH:MM:SS USER@HOST ~]$ cd projects/linux-library
[Www Mmm dd HH:MM:SS USER@HOST ~/projects/linux-library]$ cd /usr/bin
[Www Mmm dd HH:MM:SS USER@HOST /usr/bin]$ exit
$ printf 'echo only this reached stdout\n' | PS1='[\u@\h \W]\$ ' bash --norc --noediting -i 2>/dev/null
only this reached stdout
$ cat ~/.bashrc
export PS1="[\d \t \u@\h \w]\$ "
$ bash
[Www Mmm dd HH:MM:SS USER@HOST ~]$ cd projects/linux-library
[Www Mmm dd HH:MM:SS USER@HOST ~/projects/linux-library]$ exit
\Wis the last part of the path:linux-library,bin,usr. In the home folder it is~, not the folder's name.\wis the whole path, with the home folder written as~.\dand\tare the weekday, month and day (Www Mmm dd) and the 24-hour time (HH:MM:SS).- The book's double quotes eat a backslash.
echo "$PS1"shows[\u@\h \W]$: inside double quotes\$is an escaped dollar sign, so the shell turned it into a plain$beforePS1was set. The prompt still ends in$, but bash's\$escape, the one the bash manual ↗ says turns into#for root, never reaches it. Single quotes,PS1='[\u@\h \W]\$ ', keep it. - The prompt goes to stderr. With
2>/dev/null, the prompts and theexitare gone, and the command's own output is still there. - A prompt lives in the shell that set it. Each session starts at
$again, although the session before it exported the book's prompt. The last one starts with the book's prompt because by then the line was in~/.bashrc, and a new bash reads that file when it starts.
On a Mac¶
The key above is shared: macOS's /bin/bash 3.2 and Ubuntu's bash 5.2 drew every prompt byte for byte the same, so every escape on the book's page works on a Mac. What differs is around the prompt, and this run is recorded on each machine separately:
Verified output of ps1_on_a_mac_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.
$ bash -c 'PS1="[\u@\h \W]\$ "; echo "${PS1@P}"'
[USER@HOST ~]$
(exit status 0)
$ cat ~/.bashrc
export PS1="[\d \t \u@\h \w]\$ "
$ bash -l
USER@HOST:~$ echo "$PS1"
${debian_chroot:+($debian_chroot)}\u@\h:\w\$
USER@HOST:~$ logout
$ cat ~/.bash_profile
[ -r ~/.bashrc ] && . ~/.bashrc
$ bash -l
[Www Mmm dd HH:MM:SS USER@HOST ~]$ cd projects/linux-library
[Www Mmm dd HH:MM:SS USER@HOST ~/projects/linux-library]$ logout
$ bash
$ echo "$BASH_VERSINFO"
5
$ exit
Verified output of ps1_on_a_mac_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.
$ bash -c 'PS1="[\u@\h \W]\$ "; echo "${PS1@P}"'
bash: ${PS1@P}: bad substitution
(exit status 1)
$ cat ~/.bashrc
export PS1="[\d \t \u@\h \w]\$ "
$ bash -l
HOST:~ USER$ echo "$PS1"
\h:\W \u\$
HOST:~ USER$ logout
$ cat ~/.bash_profile
[ -r ~/.bashrc ] && . ~/.bashrc
$ bash -l
[Www Mmm dd HH:MM:SS USER@HOST ~]$ cd projects/linux-library
[Www Mmm dd HH:MM:SS USER@HOST ~/projects/linux-library]$ logout
$ bash
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
$ echo "$BASH_VERSINFO"
3
$ exit
${PS1@P}needs a newer bash than the Mac's. It expands a prompt string without drawing it, the quickest way to try one. bash 5.2 printed the prompt; bash 3.2 calls it a bad substitution and exits 1. On a Mac, try a prompt by setting it, or with zsh'sprint -P, below.- A login shell does not read
~/.bashrc. With the book's line in~/.bashrc,bash -lstarted with a different prompt on each machine, and with the book's on neither.echo "$PS1"shows where each came from:\h:\W \u\$is the prompt macOS's/etc/bashrcsets, and\u@\h:\w\$behind adebian_chroottest is the one Ubuntu's/etc/bash.bashrcsets. Which startup file runs measures which of your own files each kind of bash reads. - Terminal.app opens a login shell. That is documented behaviour, not something CI measures, since CI has no Terminal windows: About bash_profile and bashrc on macOS ↗ explains that a new Terminal window runs
.bash_profile, where a Linux terminal runs.bashrc. On the Mac this page was written on (macOS 26.6, checked withpson 2026-09-13), Terminal's child process waslogin -pfwith the user's name, and its child was-fish: a shell whose name starts with-is a login shell (bash manual, Invoking Bash ↗). So on a Mac, the book's~/.bashrcline is skipped by exactly the shell it was written for. - The fix is one line in
~/.bash_profile:[ -r ~/.bashrc ] && . ~/.bashrc. With it, the login shell drew the book's prompt on both machines. - And bash is not the Mac's shell. An interactive
/bin/bashon macOS first prints a notice that the default interactive shell is now zsh, and its$BASH_VERSINFOis3. (The other runs on this page setBASH_SILENCE_DEPRECATION_WARNINGto hide that notice.) Apple's Terminal guide ↗ gives zsh as the default login shell, so a Mac reader following the book is probably typing into zsh.
In zsh and fish¶
zsh reads PS1 too, and PROMPT is another name for the same variable. The book's line is not an error, but zsh's prompt escapes start with %. The zsh sessions start at the prompt %:
Verified output of ps1_prompts_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
% export PS1="[\u@\h \W]\$ "
[\u@\h \W]$ cd projects/linux-library
[\u@\h \W]$ cd /usr/bin
[\u@\h \W]$ exit
% PROMPT="[%n@%m %1~]%# "
[USER@HOST ~]% cd projects/linux-library
[USER@HOST linux-library]% cd /usr/bin
[USER@HOST bin]% cd /usr
[USER@HOST /usr]% print -r -- "$PS1"
[%n@%m %1~]%#
[USER@HOST /usr]% exit
% PROMPT="[%D{%a %b %d} %D{%H:%M:%S} %n@%m %~]%# "
[Www Mmm dd HH:MM:SS USER@HOST ~]% cd projects/linux-library
[Www Mmm dd HH:MM:SS USER@HOST ~/projects/linux-library]% cd /usr/bin
[Www Mmm dd HH:MM:SS USER@HOST /usr/bin]% exit
$ print -rP -- '[%n@%m %1~]%# '
[USER@HOST ~]%
$ printf 'echo only this reached stdout\n' | PS1='[%n@%m %1~]%# ' zsh -f -i 2>/dev/null
only this reached stdout
$ cat ~/.zshrc
PROMPT="[%D{%a %b %d} %D{%H:%M:%S} %n@%m %~]%# "
$ zsh -d
[Www Mmm dd HH:MM:SS USER@HOST ~]% cd projects/linux-library
[Www Mmm dd HH:MM:SS USER@HOST ~/projects/linux-library]% exit
- The backslash escapes are printed as they are.
[\u@\h \W]$is exactly what was stored, and nothing warns you. - The zsh spelling of the book's first prompt is
[%n@%m %1~]%#:%nis the user,%mthe host up to its first dot,%1~the last part of the path, and%#a%(the zsh manual ↗ gives#for root).print -r -- "$PS1"after settingPROMPTshows that the two names are one variable. %1~is not quite\W. In/usrit printed/usr, where bash printedusr. In/usr/binboth printedbin.- The date and time are
%D{…}withstrftimeletters inside:%D{%a %b %d}for\d,%D{%H:%M:%S}for\t. print -Ptries a prompt without starting a shell. It works in the zsh a Mac ships, where bash's${PS1@P}does not.- The prompt goes to stderr in zsh as well.
~/.zshrckeeps it. A new zsh read the file and started with the prompt. The-donly skips the system-wide startup files, which have nothing to do with this prompt: on GitHub's Ubuntu runner/etc/zsh/zshrcrunscompinit, and that aborts with "not interactive and can't open terminal" when zsh reads its commands from a pipe. A login zsh reads~/.zshrctoo, so in zsh the book's advice works on a Mac once.bashrcis read as.zshrc.
fish has no PS1. Before each command line it calls the function fish_prompt and shows what it printed, so the script calls fish_prompt directly instead of typing into a fish. It runs in a fresh fish --no-config with a throwaway HOME, because funcsave writes into your configuration:
Verified output of ps1_prompts_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ functions fish_prompt | head -n 2
# Defined in embedded:functions/fish_prompt.fish @ line 4
function fish_prompt --description 'Write out the prompt'
$ fish_prompt; echo
USER@HOST ~>
$ export PS1="[\u@\h \W]\$ "; fish_prompt; echo
USER@HOST ~>
$ echo $PS1
[\u@\h \W]$
$ cd projects/linux-library; fish_prompt; echo
USER@HOST ~/p/linux-library>
$ function fish_prompt
echo -n "[$USER@"(prompt_hostname)" "(path basename (prompt_pwd -d 0))"]\$ "
end
$ for dir in ~ ~/projects/linux-library /usr/bin /usr; cd $dir; fish_prompt; echo; end
[USER@HOST ~]$
[USER@HOST linux-library]$
[USER@HOST bin]$
[USER@HOST usr]$
$ fish_is_root_user; and echo "#"; or echo "\$"
$
$ function fish_prompt
echo -n "["(date "+%a %b %d %H:%M:%S")" $USER@"(prompt_hostname)" "(prompt_pwd -d 0)"]\$ "
end
$ for dir in ~ ~/projects/linux-library /usr/bin; cd $dir; fish_prompt; echo; end
[Www Mmm dd HH:MM:SS USER@HOST ~]$
[Www Mmm dd HH:MM:SS USER@HOST ~/projects/linux-library]$
[Www Mmm dd HH:MM:SS USER@HOST /usr/bin]$
$ funcsave fish_prompt
funcsave: wrote ~/.config/fish/functions/fish_prompt.fish
$ cat ~/.config/fish/functions/fish_prompt.fish
function fish_prompt
echo -n "["(date "+%a %b %d %H:%M:%S")" $USER@"(prompt_hostname)" "(prompt_pwd -d 0)"]\$ "
end
$ fish -c fish_prompt; echo
[Www Mmm dd HH:MM:SS USER@HOST ~]$
$ fish --no-config -c fish_prompt; echo
USER@HOST ~>
fish_promptis a function, and fish ships one:functions fish_promptshows its definition, starting with where it came from.- The book's line changes nothing.
export PS1=…ran without complaint, andecho $PS1shows the value (fish's double quotes also turned\$into$), butfish_promptprinted the sameUSER@HOST ~>before and after. - The fish version is a function.
$USERstands for\uandprompt_hostnamefor\h.path basename (prompt_pwd -d 0)is\W,~at home andusrin/usrincluded, which zsh's%1~gets wrong.prompt_pwd -d 0is\w: plainprompt_pwd, as in the default prompt, shortens each folder to one letter,~/p/linux-library. The date and time come fromdate. fish_is_root_useris the test behind bash's\$. It failed here, so the line printed$.funcsave fish_promptwrote~/.config/fish/functions/fish_prompt.fish. A newfishloaded the prompt from that file with noconfig.fishat all, andfish --no-config, which skips your configuration, drew the default prompt again.- fish also ships ready-made prompts. On the Mac this page was written on,
fish_config prompt listnamed thirteen of them,fish_config prompt choose NAMEswitched the running fish to one, andfish_config prompt saveasked before overwritingfish_prompt.fish(with no answer it printedNot overwriting). That was fish 4.3.2 on 2026-09-13, and it is not in the answer key.
The same prompt in the three shells, every cell taken from the runs above:
bash PS1 |
zsh PROMPT |
fish fish_prompt |
|---|---|---|
\u |
%n |
$USER |
\h |
%m |
prompt_hostname |
\W |
%1~ (but /usr, not usr) |
path basename (prompt_pwd -d 0) |
\w |
%~ |
prompt_pwd -d 0 |
\d |
%D{%a %b %d} |
date "+%a %b %d" |
\t |
%D{%H:%M:%S} |
date +%H:%M:%S |
\$ |
%# |
fish_is_root_user |
And where each one is kept: bash's in ~/.bashrc, which a login bash reads only if ~/.bash_profile tells it to; zsh's in ~/.zshrc; fish's in ~/.config/fish/functions/fish_prompt.fish, written by funcsave fish_prompt.
If you are coming from another library¶
- Python.
-cis not the prompt ↗ pipes a line intopython3 -iand finds its>>>prompts on stderr, the same trick this page plays on bash and zsh. - Encodings. A pipe is not a terminal ↗ covers what changes for a program when the other end of a descriptor is a pipe instead of a terminal. An interactive bash reading a pipe is one such program: it prints its prompts, and says it has no job control.
See also¶
- Which startup file runs —
~/.bashrc,~/.bash_profile,~/.zshrcandconfig.fish, measured for login and interactive shells - bash manual, Controlling the Prompt ↗ — every backslash escape,
\$included - zsh manual, Prompt Expansion ↗ —
%n,%m,%~,%#and%D{…} - fish:
fish_prompt↗,prompt_pwd↗,funcsave↗ andfish_config↗