Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Writing Functions in .bashrc: When to Use One Instead of an Alias

Sean

Platform Writer

Aug 14, 2026
8 min read

The rule is simple: use an alias when you want a shorter name for a fixed command, and a function the moment you need an argument anywhere other than the end. An alias is textual substitution with the rest of the line stuck on the end, and that limitation is what sends people to functions.

Writing Functions in .bashrc: When to Use One Instead of an Alias

Most shell configurations accumulate a mix of both, added over years, with no clear reason for which is which. Here is the actual distinction, plus the loading rules that explain why your definition works in one terminal and not another.

Table of contents

The limitation that forces the switch

An alias is a simple text substitution. The shell replaces the alias name with its definition and appends whatever else you typed.

alias ll='ls -alF'
ll /tmp        # becomes: ls -alF /tmp   -- fine, argument goes on the end

That works because the argument belongs at the end. Now try to put one in the middle.

# Broken: you cannot place an argument anywhere but the end.
alias mkcd='mkdir -p "$1" && cd "$1"'
mkcd newdir
# Expands to: mkdir -p "$1" && cd "$1" newdir
# $1 is empty, so mkdir gets nothing and cd gets the wrong argument.

This fails confusingly rather than loudly. Aliases do not take parameters, and $1 inside one refers to the shell’s own positional parameters, which are usually unset.

The function version works, because a function is a real command with its own argument list.

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

So: alias for a shorter name, function for anything with logic, arguments in the middle, conditionals, loops, or more than one statement.

Function syntax and the parts worth getting right

# Preferred form: portable across sh and bash.
extract() {
  local file="${1:?usage: extract <archive>}"

  if [[ ! -f "$file" ]]; then
    echo "extract: no such file: $file" >&2
    return 1
  fi

  case "$file" in
    *.tar.gz|*.tgz) tar xzf "$file" ;;
    *.tar.bz2)      tar xjf "$file" ;;
    *.tar.xz)       tar xJf "$file" ;;
    *.zip)          unzip -q "$file" ;;
    *.gz)           gunzip "$file" ;;
    *)              echo "extract: unknown format: $file" >&2; return 1 ;;
  esac
}

Four habits that separate a function you can rely on from one that surprises you.

  • Declare variables local. Without it, a variable named file inside your function overwrites any variable of that name in your shell session.
  • Send errors to stderr with >&2, so they are visible when output is redirected.
  • Return a non-zero status on failure, so the function composes with && and || correctly.
  • Use ${1:?message} to make a required argument fail with a usable message rather than an empty expansion.

Use the name() { } form rather than the function name { } keyword form. The keyword form is a bashism with no advantage, and the parenthesis form works in any POSIX shell.

Where to put them, and why yours does not load

This is the part that generates the most confusion, and it comes down to the difference between login and interactive shells.

  • ~/.bashrc runs for interactive non-login shells: a new terminal tab on a desktop.
  • ~/.bash_profile runs for login shells: an SSH session, or a terminal on macOS, which treats every window as a login shell.
  • ~/.profile is the fallback that bash reads when .bash_profile does not exist, and which other shells also read.

So a function defined in .bashrc is missing over SSH unless .bash_profile sources it. That is why the standard bridge exists, and if yours is not there, add it.

# In ~/.bash_profile
[[ -f ~/.bashrc ]] && . ~/.bashrc

Rather than growing .bashrc indefinitely, keep functions in their own file and source it. The convention many distributions already support is ~/.bash_aliases, and a directory scales better still.

# In ~/.bashrc
for f in ~/.bashrc.d/*.sh; do
  [[ -r "$f" ]] && . "$f"
done
unset f

After editing, run source ~/.bashrc to apply the change to the current shell. Existing terminals keep the old definition until you do, which is behind most reports that an edit had no effect.

Functions that earn their place

A few that repay the space, chosen because each one is impossible as an alias.

# Make a directory and enter it.
mkcd() { mkdir -p -- "$1" && cd -- "$1"; }

# Go up a variable number of levels.
up() {
  local levels="${1:-1}" path=""
  for ((i = 0; i < levels; i++)); do path="../$path"; done
  cd "$path" || return 1
}

# Search file contents under the current tree, case-insensitively.
ff() { grep -rn --colour=auto -i -- "$1" "${2:-.}"; }

# What is listening on a port?
port() { lsof -nP -iTCP:"$1" -sTCP:LISTEN; }

# Serve the current directory over HTTP.
serve() { python3 -m http.server "${1:-8000}"; }

# Back up a file with a timestamp.
bak() { cp -- "$1" "$1.$(date +%Y%m%d-%H%M%S).bak"; }

The double dash before user-supplied arguments matters more than it looks. Without it, a filename beginning with a hyphen is interpreted as an option, which is both a bug and a small security consideration in a script that processes untrusted names.

Keeping them portable and debuggable

If you use the same dotfiles across machines, guard anything that depends on a specific tool being installed.

if command -v bat >/dev/null 2>&1; then
  cat() { bat --paging=never "$@"; }
fi

# Platform differences, since GNU and BSD tools take different flags.
if [[ "$OSTYPE" == "darwin"* ]]; then
  alias ls='ls -G'
else
  alias ls='ls --color=auto'
fi

Without the guard, a missing tool produces a command-not-found error on every use, on the machine where it is least convenient.

To inspect what is actually defined in the current shell:

type mkcd          # is it a function, alias, or binary?
declare -f mkcd    # print the function body
declare -F         # list every function name
alias              # list every alias
unset -f mkcd      # remove a function from this session

type is the one to reach for when a command behaves unexpectedly. It tells you whether you are running the binary you think you are, or a function someone defined three years ago that shadows it.

How this fits the rest of the stack

Shell functions are personal tooling, and they stop scaling the moment the work needs to happen on a schedule or on a server nobody is logged into. At that point what you want is not a better function but somewhere to run the job with logs attached, so a failure is visible without someone SSHing in to check. The RunxBuild hosting calculator shows the service, managed database, and storage as separate line items, and runtime logs sit alongside the build log for each deploy.

Useful related references:

FAQ

What is the difference between a bash alias and a function?

An alias is text substitution with your extra arguments appended, so it cannot place an argument anywhere but the end. A function is a real command with its own argument list, conditionals, and loops. Use an alias for a shorter name and a function for anything else.

Why is my bashrc function not available over SSH?

SSH starts a login shell, which reads .bash_profile rather than .bashrc. Add a line to .bash_profile that sources .bashrc, and the definitions will be present in both cases.

Do I need to restart my terminal after editing .bashrc?

No. Run source ~/.bashrc to apply changes to the current shell. Terminals already open keep the old definitions until you source it or start a new one.

Should I use local variables in bash functions?

Yes, always. Without local, a variable assigned inside a function overwrites any variable of the same name in your shell session, which causes bugs that appear far from their cause.

Can a bash function have the same name as a command?

Yes, and it will shadow the command. That is useful for wrapping a tool, but call the real binary inside with command name or an absolute path, or the function will recurse into itself.

#bashrc Function#Bash Alias#Shell Configuration#Dotfiles#Linux