Learn Linux - Shell, Environment Variables & Customization (.bashrc)
Series/Learn Linux/Episode 12
Episode 12 of 31

Learn Linux - Shell, Environment Variables & Customization (.bashrc)

The shell isn't just a place to type commands — it's the bridge between you and the kernel. This episode dissects environment variables, when .bashrc and .profile are executed, how to customize the prompt and aliases, through to writing your first bash script with variables, conditionals, and loops.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

After episode 11 where we covered package management — apt, dnf, pacman, plus the universal snap/flatpak/AppImage formats — you can now install any application onto the system. But there's one nagging question since the early episodes: when you type nginx, how does Linux know which directory to look in for the nginx program? And why does every newly opened terminal feel "fresh again", even though the configuration was already changed?

The answer lies in a concept that's almost invisible yet works every second: environment variables and startup files. In this episode, we'll dissect how the Bash shell works — from local vs environment variables, key variables like PATH and PS1, when .bashrc/.bash_profile are executed, through to writing your first bash script. By the end of the episode, your terminal will feel "alive" with your own customizations. Let's begin.

Main Discussion

The Shell: The Bridge Between You and the Kernel

Since episode 2 you've known the layered Linux architecture: hardware → kernel → shell → user space. The shell is the program that reads your commands, translates them, and asks the kernel to execute them. Bash (Bourne Again Shell) is the default shell on most distros. It's not just a "place to type" — it has its own programming language that can handle variables, logic, and loops.

One of the best mental models for understanding the shell is a restaurant kitchen. The shell is the head chef receiving orders (commands) from customers (you). On the kitchen counter are recipes already written down (script files) and raw ingredients (variables) ready to use anytime. When you type nginx, the shell looks for its recipe on certain shelves — and those shelves are the contents of the PATH variable.

Variables: Local vs Environment

Variables in the shell are name=value pairs. There are two kinds: local variables (only known to the current shell) and environment variables (inherited by child programs run from that shell).

Local vs exported variables
# Local variable — only known in this shell
NAMA="arman"
echo "Hello, $NAMA"
 
# Still local — a script won't see it
bash -c 'echo "from child: $NAMA"'   # empty output!
 
# Export — now inherited by all child processes
export NAMA="arman"
bash -c 'echo "from child: $NAMA"'   # output: arman
export makes the variable inherited by child processes

This difference is crucial to understand, because it's the root of the "missing export" problem that almost every Linux admin has encountered. When a program (for example a Node.js app) reads an environment variable and doesn't find it, it's usually because the variable was defined without export, so it was never inherited by the child process. Remember the basic pattern: VAR=value is only for this shell, while export VAR=value is for the entire ecosystem of its descendant processes.

Note

The abbreviation VAR=x and VAR = x are two different things! The first is a variable assignment, the second runs a command named VAR with arguments = and x. Always write without spaces around the equals sign.

The Most Important Environment Variables

Some environment variables are set automatically by the system and used constantly. Recognizing them is a big asset for troubleshooting:

VariableFunctionExample value
PATHThe list of directories searched when you type a command/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
HOMEThe active user's home directory/home/arman
USERThe currently logged-in user's namearman
LANGLocalization (language & encoding)en_US.UTF-8
PS1The prompt display template\u@\h:\w\$

PATH is the star player. When you type ls, the shell scans every directory in PATH from left to right until it finds ls. Commands whose path isn't in PATH must be called with a full path, for example ./script.sh or /opt/myapp/bin/tool. This is why you often see ./ before running a script — the dot means "current directory", which isn't always in PATH.

Viewing and adding to PATH
# View the current PATH
echo "$PATH"
 
# Check which directory a command is in
which nginx
 
# Add a custom directory to PATH (permanently via .bashrc)
export PATH="$PATH:$HOME/bin"
PATH is appended, not overwritten — see the pitfall discussion below

Startup Files: .bashrc, .bash_profile, and .profile

This is the part that most often confuses beginners. There are several startup files in the home directory, and each is executed under different conditions. The main difference lies in two kinds of shell sessions:

  • Login shell — the first shell opened when you log in (from a TTY, or SSH to a server).
  • Interactive non-login shell — every new terminal tab/window opened from a graphical session.

The execution map looks roughly like this:

FileLogin shell (TTY/SSH)Non-login (new terminal)Also read by non-interactive shells?
/etc/profileYes (global)NoNo
~/.bash_profileYes (if present)NoNo
~/.profileYes (if .bash_profile is absent)NoNo
~/.bashrcYes (usually called from .bash_profile)YesNo (unless set)

Tip

A safe rule of thumb: put all customizations in ~/.bashrc, then make sure ~/.bash_profile calls it (the line [ -f ~/.bashrc ] && . ~/.bashrc is the default on Ubuntu). That way your aliases and functions are active both in login sessions and every new terminal. Don't put commands that display output in .bashrc excessively — they'll appear in every opened terminal.

Why are there two files? Historically: .profile was designed for login shells (setting up the environment), while .bashrc was for new interactive shells. A login system usually only executes .bash_profile, and because that file is rarely modified, most users don't see their .bashrc customizations at SSH login. The solution — again — is making .bash_profile load .bashrc.

Note that other shells have their own files: .zshrc for Zsh (the modern developer's favorite shell; you got acquainted with Zsh in an earlier episode), and .zshenv/.zprofile as the analogues. The concept is the same: one file for the login environment, one for every interactive session.

Customization: Aliases, Functions, and the Prompt

Now the fun part. Aliases are command shortcuts; functions are mini-scripts inside the shell. Both live in .bashrc.

Example aliases & functions in .bashrc
# Aliases — short shortcuts for common commands
alias ll='ls -lah'
alias gs='git status'
alias c='clear'
alias update='sudo apt update && sudo apt upgrade'
 
# Functions — logic involving arguments & conditions
function mkcd() {
  mkdir -p "$1" && cd "$1"
}
 
function deploy() {
  echo "Deploying to production..."
  # ... deploy logic
}
Aliases for shortcuts, functions for repeated logic

Custom prompts are controlled by PS1. It's a template rendered every time the shell is ready to accept a command. The characters \u, \h, \w are each replaced with the username, hostname, and working directory. An example prompt showing green for a regular user and red for root:

Custom prompt with colors
PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]$ '
PS1 is re-rendered every time a command finishes

For those who want fancier results without writing color codes by hand, the starship or oh-my-bash ecosystems are available — but understanding manual PS1 remains important so you can read the templates they generate.

To see how a .bashrc customization evolves, the diff below adds two aliases and updates the prompt — -- lines are old lines, ++ lines are their replacements:

~/.bashrc (before & after)
alias ll='ls -l'
alias ll='ls -lah'
alias gs='git status'
PS1='\u@\h:\w\$ '
PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]$ '
Diff pattern: remove old lines, add new lines in their place

Important

After changing .bashrc, the changes don't apply automatically to the running session — .bashrc is only executed when a new shell is opened. Don't log out yet; just run source ~/.bashrc (or . ~/.bashrc) to reload the configuration in the current session. It's a lifesaver command you'll use every day.

Writing Your First Bash Script

Functions and aliases are interactive customization; scripts are how you write logic that can be run repeatedly. A bash script is just a text file containing shell commands, with the first line — the shebang #!/usr/bin/env bash — telling the kernel which interpreter to use. Let's build one complete script that uses all the elements: arguments, variables, conditionals, and loops.

hello.sh — your first bash script
#!/usr/bin/env bash
# hello.sh — greet the user and count files
 
NAMA="${1:-friend}"            # first argument, default "friend"
 
if [ -z "$NAMA" ]; then
  echo "No argument provided."
fi
 
echo "Hello, $NAMA!"
 
for file in *.md; do
  echo "Found markdown file: $file"
done
 
echo "Total files in this directory:"
ls | wc -l
From shebang to loops: all the basic elements in one file

Running the script requires execute permission:

Running a bash script
chmod +x hello.sh
./hello.sh arman
 
# Alternative without execute permission: call the interpreter explicitly
bash hello.sh
chmod +x then run with ./ — PATH doesn't include the current directory

The if [ ... ] conditional syntax and the for ... in ... loop use the same structure as other programming languages — the difference is that Bash uses commands as conditions ([ ] is shorthand for the test command). The spaces around the square brackets aren't just style: [ -z "$NAMA" ] needs a space after [ and before ], because [ is a command name separated by spaces.

Conditionals & loops in practice
if [ "$USER" = "root" ]; then
  echo "Running as root"
else
  echo "Running as $USER"
fi
 
count=1
while [ $count -le 5 ]; do
  echo "Iteration $count"
  count=$((count + 1))
done
The if/elif and while patterns most common in admin scripts

Reading Variables with set, env, and printenv

Because variables exist in two "worlds" — local and environment — you also need two different ways to read them. env (or printenv) only shows variables that are inherited; set shows all shell variables including functions. Knowing the difference is the basis of a lot of debugging:

Two ways to read variables
# Only environment variables (those exported)
env
printenv HOME
 
# All shell variables + functions
set
 
# Check a single variable without an error if absent
echo "${HOME:-empty}"
env for environment, set for everything (local + functions)

Common Mistakes in Shell & Environment

The most frequent PATH-related mistake is overwriting it instead of appending. Writing export PATH="$HOME/bin" (without $PATH inside) replaces the entire search list with a single directory — the result is that basic commands like ls, cat, even sudo are no longer found. Always use the append pattern: export PATH="$PATH:$HOME/bin".

The second mistake is editing the wrong startup file — putting aliases in .bash_profile then wondering why new terminals don't load them (remember: .bash_profile is only for login shells), or putting output commands in .bashrc so every terminal tab opens spitting out text.

The third mistake is forgetting export. Defining DB_PASSWORD="secret" in the terminal then running an app that reads process.env.DB_PASSWORD — the app won't find it because the variable was never inherited. Combine this with the habit of checking with env or printenv:

Checking the current environment
# All environment variables
env
 
# Filter for a specific variable
env | grep -i path
printenv PATH
env shows all inherited variables
MistakeSymptomSolution
PATH overwritten, not appendedBasic commands not foundexport PATH="$PATH:$HOME/bin"
Aliases in .bash_profileNot appearing in new terminalsMove them to .bashrc
Forgetting exportChild processes don't see the variableUse export VAR=...
Editing .bashrc without sourceChanges "have no effect"source ~/.bashrc
Space during assignment (VAR = x)Strange "command not found: VAR" errorWrite without spaces
Script without a shebang"permission denied" error or wrong shell usedStart with #!/usr/bin/env bash

Conclusion

In this episode 12 you've built a complete understanding of the shell: distinguishing local and environment variables with the export mechanism, recognizing key variables like PATH, HOME, LANG, and PS1, understanding when .bashrc/.bash_profile are executed based on session type, customizing the prompt and aliases, and writing your first bash script containing arguments, conditionals, and loops. Most importantly, you now know why configuration doesn't take effect immediately, why PATH must be appended, and why export determines who can see a variable.

With a terminal that's now "alive" and capable of scripting, it's time to manage the system's true power: processes. In the next episode 13 we'll discuss process management and resource monitoring — PID and PPID, process states like zombie, ps/top/htop, the signals SIGTERM/SIGKILL/SIGHUP, through to running processes in the background with nohup, screen, and tmux. See you there!

Learn Linux - Shell, Environment Variables & Customization (.bashrc) | Learn Linux