Every time you open a terminal on RHEL 9, Bash reads one or more startup files before presenting you with a prompt. Which files are read depends on whether the shell is a login shell (started by SSH, a console login, or su -) or an interactive non-login shell (a new terminal window in a GUI, or a subshell). Understanding this distinction is essential for configuring environment variables correctly: variables that need to be available system-wide must be in different files than variables needed only for interactive sessions, and both are different from variables set for scripts running non-interactively via cron or systemd.
This guide covers the complete Bash startup file hierarchy on RHEL 9: /etc/profile, /etc/profile.d/, ~/.bash_profile, ~/.bashrc, ~/.bash_logout, setting environment variables correctly, customising the prompt (PS1), creating aliases, managing PATH safely, and using direnv for per-project environment isolation.
Prerequisites
- RHEL 9 server with a user account
- Bash shell (default on RHEL 9)
Step 1 — Understand the Bash Startup File Hierarchy
Login shell startup order:
/etc/profile— system-wide settings, reads all files in/etc/profile.d/~/.bash_profileor~/.bash_loginor~/.profile(first one found)
Interactive non-login shell startup order:
/etc/bashrc— system-wide interactive shell settings~/.bashrc— user’s interactive shell settings
The RHEL 9 convention: ~/.bash_profile sources ~/.bashrc, so most user customisations go in ~/.bashrc and are available in both login and non-login interactive sessions.
# Check which startup file is being used
bash --login --norc -c 'echo $BASH_SOURCE'
# Trace which files are sourced on login
bash -x --login 2>&1 | grep "^+" | head -30
Step 2 — Set System-Wide Environment Variables
For environment variables that all users need, create a file in /etc/profile.d/:
vi /etc/profile.d/custom-env.sh
#!/bin/bash
# Custom system-wide environment variables
# Application home directory
export APP_HOME=/opt/myapp
# Java home (if installed system-wide)
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk
# Add to PATH safely (check it is not already there)
case ":$PATH:" in
*":$APP_HOME/bin:"*) ;;
*) export PATH="$APP_HOME/bin:$PATH" ;;
esac
Make it executable:
chmod +x /etc/profile.d/custom-env.sh
The change takes effect for all users on next login.
Step 3 — Configure User ~/.bashrc
The ~/.bashrc file is read for every interactive shell. It is the right place for aliases, functions, and interactive-only settings:
vi ~/.bashrc
# Source global bashrc (always include this)
if [ -f /etc/bashrc ]; then
. /etc/bashrc
fi
# =====================
# Aliases
# =====================
alias ll='ls -alFh --color=auto'
alias la='ls -A'
alias l='ls -CF'
alias grep='grep --color=auto'
alias ..='cd ..'
alias ...='cd ../..'
alias df='df -h'
alias du='du -h'
alias free='free -h'
alias ps='ps auxf'
alias mkdir='mkdir -pv'
alias cp='cp -iv' # Interactive and verbose
alias mv='mv -iv'
alias rm='rm -Iv --preserve-root' # Verbose, confirm on >3 files
# Git aliases
alias gs='git status'
alias ga='git add'
alias gc='git commit'
alias gp='git push'
alias gl='git log --oneline --graph --decorate'
# Quick directory shortcuts
alias home='cd ~'
alias logs='cd /var/log'
alias web='cd /var/www/html'
# =====================
# Functions
# =====================
# Create and enter a directory
mkcd() {
mkdir -p "$1" && cd "$1"
}
# Show disk usage for current directory, sorted
dusort() {
du -sh ./* 2>/dev/null | sort -h
}
# Extract any archive
extract() {
case "$1" in
*.tar.bz2) tar xjf "$1" ;;
*.tar.gz) tar xzf "$1" ;;
*.bz2) bunzip2 "$1" ;;
*.gz) gunzip "$1" ;;
*.tar) tar xf "$1" ;;
*.zip) unzip "$1" ;;
*.7z) 7z x "$1" ;;
*) echo "'$1' cannot be extracted via extract()" ;;
esac
}
# =====================
# Environment Variables
# =====================
export EDITOR=vim
export VISUAL=vim
export PAGER=less
export LESS='-R --quit-if-one-screen'
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups
export HISTTIMEFORMAT="%F %T "
# =====================
# History settings
# =====================
# Append to history instead of overwriting
shopt -s histappend
# Save and reload history after every command
PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"
Step 4 — Customise the Bash Prompt (PS1)
A well-designed prompt shows the information you need at a glance:
# Add to ~/.bashrc
# Colour variables
RED='[33[0;31m]'
GREEN='[33[0;32m]'
YELLOW='[33[1;33m]'
BLUE='[33[0;34m]'
CYAN='[33[0;36m]'
WHITE='[33[1;37m]'
RESET='[33[0m]'
# Git branch in prompt
git_branch() {
git symbolic-ref --short HEAD 2>/dev/null | sed 's/^/ (/;s/$/)/'
}
# Show user@host in red if root, green if normal user
if [ "$EUID" -eq 0 ]; then
USERCOLOR="$RED"
else
USERCOLOR="$GREEN"
fi
# Format: user@host:/current/dir (git-branch) $
PS1="${USERCOLOR}u@h${RESET}:${BLUE}w${YELLOW}$(git_branch)${RESET}$ "
Step 5 — Configure ~/.bash_profile for Login Sessions
vi ~/.bash_profile
# Source .bashrc for login shells (RHEL convention)
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User-specific environment (not appropriate for .bashrc)
export GPG_TTY=$(tty)
# SSH agent auto-start
if [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519 2>/dev/null
fi
Step 6 — Manage PATH Safely
Appending to PATH carelessly can break things. Always check before adding:
# Safe PATH addition function
add_to_path() {
case ":$PATH:" in
*":$1:"*) ;; # already in PATH
*) export PATH="$1:$PATH" ;;
esac
}
# Usage
add_to_path "$HOME/.local/bin"
add_to_path "$HOME/go/bin"
add_to_path "/opt/node/bin"
Step 7 — Set Environment Variables for Non-Interactive Use
Variables needed by cron jobs, systemd services, or scripts that do not source ~/.bashrc must be set differently:
# For systemd services: use EnvironmentFile in the unit
# /etc/myapp/env:
APP_ENV=production
DATABASE_URL=postgres://localhost/mydb
# For cron jobs: set at the top of the crontab
EDITOR=nano crontab -e
# At the top of the crontab file:
PATH=/usr/local/bin:/usr/bin:/bin
APP_HOME=/opt/myapp
[email protected]
Step 8 — Apply Changes Without Logging Out
# Reload .bashrc in the current session
source ~/.bashrc
# or
. ~/.bashrc
# Reload .bash_profile
source ~/.bash_profile
Verification Checklist
# Show current environment variables
env | sort
# Show PATH
echo $PATH | tr ':' 'n'
# Show current PS1
echo "$PS1"
# Test an alias
type ll
# Verify history settings
echo $HISTSIZE $HISTFILESIZE $HISTCONTROL
Conclusion
Your Bash environment on RHEL 9 is now configured with a logical file hierarchy: system-wide variables in /etc/profile.d/, user aliases and functions in ~/.bashrc, login-session settings in ~/.bash_profile, a colour prompt with Git branch display, safe PATH management, and unlimited history with timestamps. A well-configured shell environment dramatically improves daily administrative efficiency.
Next steps: How to Use tmux for Terminal Multiplexing on RHEL 9, How to Use vim on RHEL 9, and How to Schedule Automated Tasks with cron on RHEL 9.