Skip to content
Shell & TerminalHow-To Published Updated 5 min readViews unavailable

How to Set Up Shell Aliases and Functions Properly

The actual difference between an alias and a function, when each is the right tool, and how to avoid the mistakes that make aliases behave unpredictably.

Aliases and shell functions both let you create shortcuts for longer commands, but they work through genuinely different mechanisms — knowing which one to reach for avoids a specific category of confusing, hard-to-debug behavior.

Step 1: create a basic alias

# in ~/.bashrc or ~/.zshrc
alias ll='ls -lah'
alias gs='git status'

An alias is a simple text substitution — typing ll is, as far as the shell is concerned, exactly equivalent to having typed ls -lah directly.

Step 2: understand exactly where aliases fall short

alias backup='cp $1 $1.bak'
# does NOT work as expected — aliases don't accept
# positional arguments in any meaningful way

Because an alias is pure text substitution happening before argument parsing, it cannot reference $1 or any other positional parameter meaningfully — anything needing actual logic, conditionals, or arguments needs a function instead.

Step 3: write a shell function for anything needing real logic

backup() {
    cp "$1" "$1.bak"
}

Functions are genuine, callable code blocks that accept arguments properly, can include conditionals and loops, and can return values — the right tool the moment your “shortcut” needs to do more than substitute fixed text.

Step 4: use functions for anything involving conditionals

mkcd() {
    mkdir -p "$1" && cd "$1"
}

This function creates a directory and immediately changes into it — genuinely impossible to express as a plain alias, since it requires sequencing two commands conditionally based on the first one’s success.

Step 5: check whether a name you’re about to use is already an alias, function, or command

type ll

type reports whether a given name resolves to an alias, a function, a builtin, or an external command — genuinely useful before defining a new alias or function with a name that might already be doing something else.

Step 6: organize aliases and functions in a separate, sourced file

# in ~/.bashrc:
[ -f ~/.bash_aliases ] && source ~/.bash_aliases

Keeping aliases and functions in a dedicated file, sourced from your main shell config, keeps a large personal collection organized and makes it trivial to share or version-control separately from the rest of your shell configuration.

Step 7: avoid aliasing over standard command names without a safety net

alias rm='rm -i'

Aliasing rm to always prompt for confirmation is common and reasonable — just be aware that if you rely on this safety net, running commands as a different user or through sudo (which doesn’t inherit your aliases by default) bypasses it, so the underlying habit of double-checking destructive commands still matters.

Step 8: check alias definitions when something behaves unexpectedly

alias
type suspicious-command-name

If a familiar command suddenly behaves differently than expected, checking whether it’s actually been aliased (possibly by a plugin, framework, or something you forgot you configured) is a quick, often-overlooked diagnostic step.

Why understanding this distinction prevents a specific class of confusing bugs

Attempting to pass arguments meaningfully to an alias, or expecting conditional logic inside one, produces behavior that looks like it should work but silently doesn’t — recognizing upfront that aliases are text substitution and functions are real code is what prevents time spent debugging a fundamentally mismatched tool choice rather than an actual logic error.

Where the alias concept itself came from

Aliases weren’t part of the original Bourne shell at all — they’re a C shell (csh) invention, one of the genuinely influential interactive features Bill Joy introduced when csh shipped as part of 2BSD in 1979, well before Bash’s own 1989 release. Bash and later shells adopted the same basic concept specifically because it proved useful enough that not having it would have been a real gap relative to csh — a good example of how interactive convenience features have historically migrated across shell family lines even when the underlying scripting language philosophies (Bourne-style versus C-shell-style syntax) remained genuinely distinct and never fully converged.

Step 9: know why aliases silently don’t work inside scripts

Bash disables alias expansion by default in non-interactive shells — which covers essentially any script executed as ./script.sh — so an alias that works perfectly at an interactive prompt fails with “command not found” the instant it’s used inside a script file, unless shopt -s expand_aliases is set before the alias is defined. This is rarely the right fix, though: functions have no such restriction, working identically whether sourced interactively or from within a script, which is itself a strong argument for writing a function instead of an alias the moment there’s any chance the shortcut needs to run somewhere besides your own interactive prompt.

Why neither aliases nor functions cross into a separate script automatically

A closely related surprise catches people even after they’ve switched to a function: neither an alias nor an ordinary function defined in your interactive shell is automatically visible inside a separate script invoked as its own child process, because neither is exported to the environment the way a variable is with export. Bash specifically supports making a function available to child processes with export -f functionname, serializing its definition into the environment for the child to re-parse — aliases have no equivalent mechanism at all. In practice, any shortcut genuinely needed inside scripts you call, not just at your own interactive prompt, should live in a function sourced explicitly from a shared library file wherever it’s needed, rather than assumed to carry over automatically from your shell’s own configuration.

Related:

Sources:

Comments