13  Customizing Your .bashrc

Every time you log into Hazel, bash reads ~/.bashrc and runs whatever it finds there. That makes it the right place to bake in the small conveniences that compound over hundreds of sessions: a shortcut that jumps to your scratch directory in one keystroke, an alias that turns squeue --me into sq, a function that cds into a project and loads the modules it needs in a single command.

This chapter shows how to build those conveniences without turning ~/.bashrc into an unreadable wall of text. The approach is to keep ~/.bashrc itself nearly empty and put each customization into its own small file under ~/.bashrc.d/.

Important

A broken ~/.bashrc can lock you out of an interactive shell. Test changes in a new SSH session before you log out of your current one — if the new session fails, you can fix the file from the still-working old session.

13.1 Why Split .bashrc Into Pieces?

A single 200-line .bashrc works, but it has problems that get worse over time. Aliases get tangled with PATH edits get tangled with prompt logic. Disabling something temporarily means commenting out fifteen lines and remembering to put them back. Sharing one helper with a labmate means copy-pasting a chunk out of the middle of a file.

The ~/.bashrc.d/ pattern avoids all of that. Each topic — aliases, functions, PATH edits, environment variables — lives in its own file. Disabling something is mv aliases.sh aliases.sh.off. Sharing a function is sending one file. Adding a new shortcut never requires editing .bashrc again.

Tip

You probably have noticed that the .bashrc filename starts with a period (.). These are special files, called dotfiles, that are hidden by default. To list all dotfiles and hidden directories, use the ls -a command.

13.2 Wiring Up ~/.bashrc.d/

It is likely that your ~/.bashrc on Hazel is already formatted to have this code block:

# User specific aliases and functions
if [ -d ~/.bashrc.d ]; then
    for rc in ~/.bashrc.d/*; do
        if [ -f "$rc" ]; then
            . "$rc"
        fi
    done
fi

unset rc

If not, no worries. Open ~/.bashrc in your editor of choice and add the above code block. This is the only edit to ~/.bashrc you’ll make — everything else goes in ~/.bashrc.d/.

What this code does, line by line: if the ~/.bashrc.d/ directory exists, loop over every entry in it; for each one that is a regular file, source it (. "$rc") into the current shell. The trailing unset rc cleans up the loop variable so it doesn’t leak into your environment.

Now create the directory:

$ mkdir -p ~/.bashrc.d

From here on, everything in this chapter is a file you create inside ~/.bashrc.d/. The naming convention used below — aliases.sh, functions.sh, env.sh — is a suggestion; bash will source any regular file in the directory, in alphabetical order. A .sh extension is helpful for editors that use it to enable syntax highlighting.

Tip

After editing any file in ~/.bashrc.d/, the change won’t apply to shells that are already running. Either open a new shell, or re-source your config with source ~/.bashrc.

By the end of this chapter, your home directory will look like this:

~/
├── .bashrc             ← sourced on login; just the loop that reads .bashrc.d/
└── .bashrc.d/          ← every customization lives here
    ├── aliases.sh      ← short names for long commands (sq, ll, ...)
    ├── dirs.sh         ← $SCRATCH/$APPS/$RS1 vars and cd shortcuts
    ├── env.sh          ← PATH edits and tool env vars (Apptainer cache, ...)
    ├── functions.sh    ← helpers that take arguments (jinfo, mkcd, ...)
    └── zz_login.sh     ← interactive-only login banner; sourced last

Each file is a small, self-contained topic. The sections below build them up one at a time.

13.3 Aliases: Short Names for Long Commands

Aliases are the simplest customization: a one-word shortcut for a longer command string. They expand only at the start of a command.

Create ~/.bashrc.d/aliases.sh:

# ~/.bashrc.d/aliases.sh
# Short names for commands you type constantly.

# --- SLURM ---
alias sq='squeue --me'
alias sqlong='squeue --me --format="%.10i %.12j %.8u %.2t %.10M %.6D %R"'
alias scancelme='scancel --user=$USER'

# --- ls family ---
alias ll='ls -lh'
alias la='ls -lah'
alias l.='ls -d .*'   # list dotfiles only

# --- safer file operations ---
alias rm='rm -i'      # prompt before deleting
alias cp='cp -i'
alias mv='mv -i'

# --- quality of life ---
alias h='history'
alias c='clear'
alias ..='cd ..'
alias ...='cd ../..'

A few notes on the choices above. sq is the alias most HPC users reach for first — squeue --me typed twenty times a day adds up. The rm -i / cp -i / mv -i overrides are a safety net for the inevitable typo; you can always bypass with \rm or command rm for a single invocation.

Note

Aliases do not accept arguments in the middle of the command and they don’t work inside scripts that aren’t sourced. If you need either, use a function instead.

Note

If you are working on a Mac or Linux machine, you have a local .bashrc file (sometimes called .zshrc). You can edit your local file as well. For example, you can create an alias for you hazel login info: alias hazel='[unityid]@login.hpc.ncsu.edu'

13.4 Directory Shortcuts: Jumping Around the Cluster

Hazel users typically bounce between four locations: home, scratch, an applications directory, and one or more project directories on RS1. The paths are long and group-specific, which makes them perfect candidates for shortcuts.

The cleanest approach is exported environment variables for the paths plus short functions for the jumps. The variables are useful on their own (you can cd $SCRATCH, cp file $RS1/, etc.) and the functions give you a one-word cd shortcut.

Create ~/.bashrc.d/dirs.sh:

# ~/.bashrc.d/dirs.sh
# Common cluster locations and shortcuts to jump to them.
# Edit GROUP and PROJECT to match your setup.

export GROUP="brc"
export PROJECT="my_project"

export SCRATCH="/share/$GROUP/$USER"
export APPS="/usr/local/usrapps/$GROUP"
export RS1="/rs1/researchers/$GROUP/$PROJECT"

# Shortcut functions
scratch() { cd "$SCRATCH" || return; }
apps()    { cd "$APPS"    || return; }
rs1()     { cd "$RS1"     || return; }

After re-sourcing, typing scratch from anywhere drops you in your scratch directory; rs1 takes you to your project on RS1. The || return at the end of each function means that if the directory doesn’t exist (typo, wrong group), the function exits cleanly instead of leaving you wondering why cd failed silently.

If you work in several projects, define one function per project rather than reassigning $PROJECT:

proj_rnaseq()  { cd "/rs1/researchers/brc/rnaseq_2024" || return; }
proj_genome()  { cd "/rs1/researchers/brc/genome_assembly" || return; }

13.5 Functions: When Aliases Aren’t Enough

Functions are aliases that take arguments. Use them whenever a shortcut needs to do more than substitute a fixed string.

Create ~/.bashrc.d/functions.sh:

# ~/.bashrc.d/functions.sh
# Helpers that need arguments or multiple commands.

# Detailed info on one of your jobs: jinfo <jobid>
jinfo() {
    if [ -z "$1" ]; then
        echo "usage: jinfo <jobid>" >&2
        return 1
    fi
    scontrol show job "$1"
}

# Efficiency report for a finished job: jeff <jobid>
jeff() {
    if [ -z "$1" ]; then
        echo "usage: jeff <jobid>" >&2
        return 1
    fi
    seff "$1"
}

# Make a directory and immediately cd into it: mkcd <path>
mkcd() {
    mkdir -p "$1" && cd "$1" || return
}

# Show disk usage of the current directory, sorted, human-readable
duh() {
    du -sh -- * 2>/dev/null | sort -h
}

# Tail the most recent SLURM output file in the current directory
tailout() {
    local latest
    latest=$(ls -t slurm-*.out 2>/dev/null | head -n 1)
    if [ -z "$latest" ]; then
        echo "no slurm-*.out files found in $PWD" >&2
        return 1
    fi
    echo "==> $latest"
    tail -f "$latest"
}

The pattern in jinfo and jeff — checking $1 is non-empty and printing a usage message to stderr if not — is worth applying to every function that takes arguments. Without it, calling jinfo with no arguments produces a confusing error from scontrol instead of a clear message.

Note

Use local for variables inside a function (as in tailout above). Without it, the variable leaks out into your shell after the function returns and can shadow names you set elsewhere.

13.5.1 Functions That Run at Startup

Anything you put in a ~/.bashrc.d/ file runs every time bash starts an interactive shell — that’s the whole mechanism. So if you want something to happen on login, you don’t need a special hook; just put the command at the bottom of one of your files.

A common use is a brief status banner. Create ~/.bashrc.d/zz_login.sh (the zz_ prefix makes it sort last, so it runs after your variables and functions are defined):

# ~/.bashrc.d/zz_login.sh
# Print a small status banner on login.

# Only run for interactive shells — not for jobs, scripts, or scp.
case $- in
    *i*) ;;
    *) return ;;
esac

echo "Hazel | $(hostname -s) | $(date '+%Y-%m-%d %H:%M')"
echo "scratch: $SCRATCH"

# Show your running and pending jobs, if any.
running=$(squeue --me --noheader 2>/dev/null | wc -l)
if [ "$running" -gt 0 ]; then
    echo "you have $running job(s) in the queue:"
    squeue --me
fi

The case $- block at the top is important. ~/.bashrc is sourced by non-interactive shells too — scp, rsync, and some SLURM operations all start a bash that reads it. Printing output in those contexts can break file transfers and corrupt job logs. The check makes the rest of the file a no-op unless the shell is interactive.

Warning

Never echo unconditionally from ~/.bashrc or anything it sources. Stray output during scp or rsync will produce errors like “received SSH_MSG_DISCONNECT” or corrupted transferred data, and the cause is rarely obvious. Always guard interactive output with the case $- check above.

13.6 PATH and Environment Variables

Two of the most useful customizations have nothing to do with shortcuts: extending PATH so your own scripts are runnable from anywhere, and exporting variables that change how cluster tools behave.

Create ~/.bashrc.d/env.sh:

# ~/.bashrc.d/env.sh
# PATH extensions and environment variables for cluster tools.

# Put your personal scripts in ~/bin and prepend it to PATH.
# Prepending (rather than appending) means your versions win on name conflicts.
if [ -d "$HOME/bin" ]; then
    export PATH="$HOME/bin:$PATH"
fi

# Apptainer caches and bind paths.
# Default cache is in $HOME, which has a 1 GB quota — move it to scratch.
export APPTAINER_CACHEDIR="$SCRATCH/.apptainer/cache"
export APPTAINER_TMPDIR="$SCRATCH/.apptainer/tmp"
mkdir -p "$APPTAINER_CACHEDIR" "$APPTAINER_TMPDIR"

# Conda/pip caches — same reason: keep them off home.
export PIP_CACHE_DIR="$SCRATCH/.cache/pip"
export CONDA_PKGS_DIRS="$SCRATCH/.conda/pkgs"

# History — keep more, drop duplicates, share across sessions.
export HISTSIZE=10000
export HISTFILESIZE=20000
export HISTCONTROL=ignoredups:erasedups
shopt -s histappend

The Apptainer cache redirect is the single most valuable line on this list for most users. By default, Apptainer caches container images under ~/.apptainer, which fills your 1 GB home quota fast and produces baffling failures across unrelated tools when it does. Moving the cache to scratch costs you nothing and prevents the problem permanently.

Note

This file depends on $SCRATCH being defined, so it must load after dirs.sh. Bash sources files in alphabetical order, so as long as dirs.sh precedes env.sh you’re fine. If you rename them, watch the ordering.

Tip

To check what’s currently in your PATH, run echo $PATH | tr ':' '\n' for a one-per-line view. Useful when something runs the wrong version of a tool and you want to see who’s first.

13.7 Putting It Together

After working through the sections above, your home directory should look like this:

$ ls ~/.bashrc.d/
aliases.sh   dirs.sh   env.sh   functions.sh   zz_login.sh

To verify everything loads cleanly, open a new SSH session to Hazel. You should see your login banner, your aliases should expand (type sq will show the alias definition), and cd $SCRATCH should work.

If something goes wrong and a new shell exits immediately or shows errors, your old SSH session is still alive — use it to inspect or rename the offending file:

$ mv ~/.bashrc.d/broken_file.sh ~/.bashrc.d/broken_file.sh.off

Files that don’t end in a recognized name still get sourced (bash sources every regular file in the directory regardless of extension). Renaming with a suffix like .off doesn’t change that — to actually disable a file, move it out of the directory:

$ mkdir -p ~/.bashrc.d.disabled
$ mv ~/.bashrc.d/broken_file.sh ~/.bashrc.d.disabled/

13.8 A Minimal Starting Set

If you want to get the benefit of this chapter in five minutes, the four files below cover most of the day-to-day wins. Drop them in ~/.bashrc.d/, edit the GROUP and PROJECT lines in dirs.sh, and open a new shell.

~/.bashrc.d/aliases.sh:

alias sq='squeue --me'
alias ll='ls -lh'
alias la='ls -lah'
alias rm='rm -i'
alias ..='cd ..'

~/.bashrc.d/dirs.sh:

export GROUP="brc"
export SCRATCH="/share/$GROUP/$USER"
export APPS="/usr/local/usrapps/$GROUP"

scratch() { cd "$SCRATCH" || return; }
apps()    { cd "$APPS"    || return; }

~/.bashrc.d/functions.sh:

mkcd() { mkdir -p "$1" && cd "$1" || return; }
jeff() { seff "$1"; }

~/.bashrc.d/env.sh:

[ -d "$HOME/bin" ] && export PATH="$HOME/bin:$PATH"
export APPTAINER_CACHEDIR="$SCRATCH/.apptainer/cache"
export APPTAINER_TMPDIR="$SCRATCH/.apptainer/tmp"
mkdir -p "$APPTAINER_CACHEDIR" "$APPTAINER_TMPDIR"

That’s enough to make a noticeable dent in the friction of cluster work. Add to it as you find yourself typing the same long command twice in a row.