How to Customize Your Bash Terminal

The bash prompt is the little run of text that sits to the left of your cursor every time the shell is waiting for a command. Most people accept whatever their system shipped with and never touch it, but the prompt is fully programmable, and a few lines in one config file can turn it into something that quietly tells you where you are, who you are, and what state your project is in before you type a single character. This is a general tour of how that works, from the bare mechanics up to a live git segment, so that by the end you can build a prompt that fits the way you actually work rather than copying someone else’s verbatim.

What PS1 actually is

Bash stores your prompt in an environment variable called PS1. Whenever the shell is ready for input, it reads the current value of PS1, expands anything special inside it, and prints the result. That is the entire model, and the single most useful thing to understand about it: the prompt is just a string, re-read every time, and customizing it means setting that string to whatever you want. You can see your current one right now by running echo "$PS1", which will print something with a few backslash sequences in it that probably look cryptic. By the end of this they will not.

There is also a PS2, the secondary prompt you see when a command spills onto a second line, and a handful of others, but PS1 is the one that matters and the only one most people ever change. To experiment safely, you can set it directly in a running shell — PS1="my-prompt$ " — and the change applies instantly and lasts only until you close that window. Nothing you do at the command line this way is permanent, which makes it the perfect scratchpad. Making it stick comes later, in the section on where to put it.

The prompt escape sequences

Bash gives you a set of backslash escape sequences that stand in for useful pieces of information. You drop them into PS1 and bash substitutes the live value at print time. These are the portable building blocks of any prompt, and the ones worth knowing are a short list:

  • \u — the current username
  • \h — the hostname up to the first dot; \H gives the full hostname
  • \w — the current working directory, with your home folder shown as ~; \W gives just the final folder name instead of the full path
  • \$ — renders as $ for a normal user and # when you are root, a small built-in reminder of when you are operating with elevated privileges
  • \t — the current time in 24-hour format; \d gives the date
  • \n — a newline, if you want your prompt to span two lines so commands always start at the left margin

A clean, portable starting prompt is built almost entirely from these. Try this in a running shell:

PS1="\u@\h \w\$ "

That produces something like you@laptop ~/projects$ . Because it uses \u and \h rather than your name typed out as literal text, the exact same line works on any machine you carry it to and always shows the correct user and host. That portability is the whole reason the escape sequences exist, and it is worth preferring them over hardcoded text wherever you can.

Adding color the right way

Color is where a prompt goes from functional to readable, and also where most broken prompts come from, so it is worth getting the mechanics exactly right. Terminals color text using ANSI escape sequences. A sequence like \033[0;36m switches the text that follows it to cyan: \033 is the escape character that signals “a formatting instruction follows,” and 0;36m is the code that selects the color. A matching \033[0m resets back to the default, and you need it after every colored piece or the color bleeds into everything typed afterward.

The standard foreground codes are easy to remember once you see them lined up:

  • 0;31m red, 0;32m green, 0;33m yellow
  • 0;34m blue, 0;35m purple, 0;36m cyan
  • 0;37m white; the leading 1; instead of 0; gives the bold/bright variant of each

Now the part that trips nearly everyone up the first time. When a color sequence appears in PS1, you must wrap it in \[ and \]. Those two markers tell bash that the bytes between them are invisible and take up zero columns on screen. The color codes print no visible characters, but bash does not know that unless you tell it. Leave the markers out and bash counts the escape codes as if they were printable, miscalculates how wide your prompt is, and you get the classic glitch where long commands wrap back over the prompt or pressing the up arrow to edit history smears the line. So the rule is simple and absolute: every color code in a prompt goes inside \[ \], every visible character stays outside.

Rather than scatter raw escape codes through your prompt, it reads far better to name them once as variables and then refer to the names. This is the conventional way to keep a colored prompt legible:

# Colors — each wraps the ANSI code in \[ \] so bash counts zero width
RED='\[\033[0;31m\]'
GREEN='\[\033[0;32m\]'
YELLOW='\[\033[0;33m\]'
BLUE='\[\033[0;34m\]'
PURPLE='\[\033[0;35m\]'
CYAN='\[\033[0;36m\]'
RESET='\[\033[0m\]'

export PS1="${GREEN}\u${RESET}@${BLUE}\h${RESET} ${YELLOW}\w${RESET}\$ "

That gives you the username in green, the host in blue, and the working directory in yellow, each one reset immediately after so the color stops where it should. Swap the variable names around to recolor any segment; the unused RED and PURPLE are sitting right there to be used.

Going dynamic: putting live information in the prompt

Everything so far has been static text and built-in escapes. The real power of the prompt shows up when you want it to display something that changes — and the trick that makes it possible rests on one detail mentioned at the very start: bash re-reads PS1 every single time it draws a prompt. That means if you can get a shell function to run on each redraw, its output becomes live.

The mechanism is a command substitution, $(some_function), embedded in PS1 — but written so that it runs on each redraw rather than once. The difference comes down to a single escaped character, and it is the most important subtlety in the whole topic. Because PS1 is usually set inside double quotes, anything written plainly is expanded once, at the moment the line is read. Color variables actually want that — you set the codes into the string a single time and you are done. But a function you want to stay live must not be evaluated at set time. By writing it as \$(my_function) with a backslash in front of the dollar sign, you put the literal text $(my_function) into PS1 rather than its result. Then, on every redraw, bash sees that command substitution sitting in the prompt string and runs it fresh. Written unescaped, the function would run exactly once when the config was loaded and its output would be frozen forever after.

So the pattern for any live segment is: write a function that prints what you want, then reference it in PS1 as \$(function_name). The classic and genuinely useful example of this is a git segment, so let us build one properly.

A worked example: a live git segment

The goal is a segment that, whenever you are inside a git repository, shows the current branch and the subject line of the last commit, and that disappears completely when you are not in a repo. Here is the function, and every line earns its place:

git_info() {
  git rev-parse --git-dir >/dev/null 2>&1 || return
  local ref subject
  ref=$(git symbolic-ref --short HEAD 2>/dev/null) \
    || ref="detached @ $(git rev-parse --short HEAD 2>/dev/null)"
  # %s = commit subject; truncate to keep the prompt sane
  subject=$(git log -1 --pretty=%s 2>/dev/null | cut -c1-50)
  printf ' (%s: %s)' "$ref" "$subject"
}

The first line is a guard. git rev-parse --git-dir succeeds only inside a repository; both its normal output and its errors are thrown away with >/dev/null 2>&1, and if it fails the || return bails out of the function immediately, printing nothing. That single line is why the segment cleanly vanishes the moment you step outside a git project.

The next pair of lines works out the branch name. git symbolic-ref --short HEAD resolves to a name like main when you are on a branch. If you have checked out a tag or a specific commit, HEAD is “detached” and that command fails, so the fallback after || builds a label like detached @ 3f2c1ab from the short commit hash instead. Both states the segment can encounter are covered.

Then git log -1 --pretty=%s pulls just the subject line of the most recent commit, and cut -c1-50 trims it to fifty characters so an essay-length commit message cannot swallow your whole terminal width. Finally printf stitches the branch and subject together as (branch: subject) with a leading space so it sits neatly against whatever comes before it.

With the function defined, you wire it into the prompt as a live segment using the escaped-dollar pattern from the previous section:

export PS1="${CYAN}\$(git_info)${RESET} ${GREEN}\u${RESET}@${BLUE}\h${RESET} ${YELLOW}\w${RESET}\$ "

The color variables expand once, baking the cyan and the reset into the string. The \$(git_info) stays literal and runs on every redraw, so the instant you switch branches or make a commit, the very next prompt reflects it. Inside a repo you will see something like (main: fix off-by-one in pager) you@laptop ~/projects/app$ ; outside one, the cyan segment is simply gone and you are left with the clean user-host-directory prompt.

One discipline matters here: everything in git_info runs on every prompt draw, so keep it to fast local commands. The git plumbing used above reads instantly from the local repository. Anything slow or network-bound — a git fetch to check whether you are behind the remote, for instance — would make every new prompt visibly lag, which gets maddening fast. Live prompt segments should only ever do cheap, local work.

Where to put it so it sticks

Setting PS1 at the command line lasts only for that window. To make your prompt permanent, the configuration goes in a startup file that bash reads automatically when it launches. On Linux, that file is ~/.bashrc. Add your color variables, your git_info function, and your PS1 line to it, save, and either open a new terminal or run source ~/.bashrc to apply it to the current one without restarting.

macOS has a wrinkle worth knowing. Terminal there opens what bash calls a login shell, and login shells read ~/.bash_profile rather than ~/.bashrc. The common convention is to keep all your real configuration in ~/.bashrc and have ~/.bash_profile simply pull it in, by putting [ -f ~/.bashrc ] && source ~/.bashrc in the profile. There is a second macOS catch: modern macOS ships zsh as the default shell, so if you want bash at all you will either have switched to it or be running a newer copy installed through Homebrew. None of the prompt mechanics change, but it explains why a bash config can appear to do nothing on a fresh Mac — you may not be in bash to begin with.

A complete starting point

Pulling the whole tutorial together, here is a full block you can drop into ~/.bashrc and then make your own. It uses the portable \u and \h escapes so it travels between machines, colors each segment with the width markers in place, and carries the live git segment:

# --- Prompt colors (the \[ \] markers keep bash's width math correct) ---
RED='\[\033[0;31m\]'
GREEN='\[\033[0;32m\]'
YELLOW='\[\033[0;33m\]'
BLUE='\[\033[0;34m\]'
PURPLE='\[\033[0;35m\]'
CYAN='\[\033[0;36m\]'
RESET='\[\033[0m\]'

# --- Live git segment: "(branch: last commit subject)" inside a repo ---
git_info() {
  git rev-parse --git-dir >/dev/null 2>&1 || return
  local ref subject
  ref=$(git symbolic-ref --short HEAD 2>/dev/null) \
    || ref="detached @ $(git rev-parse --short HEAD 2>/dev/null)"
  subject=$(git log -1 --pretty=%s 2>/dev/null | cut -c1-50)
  printf ' (%s: %s)' "$ref" "$subject"
}

# --- The prompt itself ---
# Colors expand now; \$(git_info), \u, \h, \w, \$ stay live and update each draw
export PS1="${CYAN}\$(git_info)${RESET} ${GREEN}\u${RESET}@${BLUE}\h${RESET} ${YELLOW}\w${RESET}\$ "

From here, every piece is a knob you can turn. Change cut -c1-50 if fifty characters feels too long or too short. Recolor any segment by swapping which variable wraps it. Shorten the path display by trading \w for \W, or push the command onto its own line by adding \n just before the final \$. Add the short commit hash beside the branch with another git rev-parse --short HEAD inside the printf. The prompt is a string that bash rebuilds on every keystroke-ready moment, and once that clicks, the only limit on what it can tell you is what you can print fast from a shell function.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top