Shell aliases in Bash and Zsh: setup, startup files, limits

An alias replaces the first word of a command line before the shell runs it. This article shows how to create aliases in Bash and Zsh, how to store them permanently in ~/.bashrc or ~/.zshrc, how to remove them again, why they do nothing inside scripts without expand_aliases, why they cannot take arguments, and when a shell function is the better choice.

Type alias ll='ls -l' into a terminal, and from then on ll means ls -l. Close the window and it means nothing again. Most of what goes wrong with aliases happens between those two sentences: the line ends up in a file the shell never reads, or it is used in one of the few places where the shell ignores aliases altogether.

This guide covers Bash and Zsh. We reproduced the examples with Bash 5.2.21 on Ubuntu 24.04. The current release is Bash 5.3, published on 30 July 2025, and twenty official patches had appeared for it by September 2026. Where Zsh behaves differently, the text says so on the spot; those cases were checked with Zsh 5.8.1.

Writing the alias line

The form is laid down in POSIX.1-2024, Issue 8 of the Base Specifications, so Bash and Zsh read it the same way:

alias name='Command'

Left of the equals sign is the short name, right of it the text the shell puts in its place. No space is allowed on either side of the =. Not every name is allowed either. The Bash manual excludes slashes, $, backticks and =, as well as blanks, quote characters and shell metacharacters such as | or ;. Try one and Bash answers invalid alias name. The classic first alias:

alias ll='ls -l'

Strictly speaking, the quotes are only needed when the replacement contains a space or a metacharacter, so alias ll=ls works without them. Once there is a space, though, everything after it counts as one more argument to alias. The result is worse than a plain error.

The mistake that half works

alias ll=ls -l

Bash prints alias: -l: not found, because it took -l for a second alias name and tried to look it up. The awkward part is what happens alongside: ll gets defined anyway, as plain ls. We reproduced this for the article, and alias ll afterwards shows alias ll='ls'. You end up with an error message and a working alias, just the wrong one.

Use single quotes rather than double ones. Inside double quotes the shell expands $ and backticks when you define the alias, not when you use it. alias pwd2="echo $PWD" therefore remembers whatever directory you happened to be in at the time.

Checking what is actually defined

After defining anything, look at it. Three commands cover it:

  • alias ll prints that single definition, so you see straight away whether the stored text is what you meant.

  • alias on its own lists every alias in the current shell.

  • type ll tells you whether ll is an alias, a function or a program on disk. Run it on a name before you claim it, and you find collisions while they are still harmless.

Where the line has to go so it survives

A typed alias belongs to one shell process. The next terminal starts without it. To keep it, the line has to go into a file the shell reads at startup, and this is where Bash and Zsh part ways.

Zsh is the simple case. It reads ~/.zshrc for every interactive shell, login shell or not. Since macOS 10.15, Zsh has been the default for newly created accounts on a Mac (Apple Support), so on a Mac that file is almost always the right one.

Bash has two paths. An interactive shell that is not a login shell reads ~/.bashrc. A login shell tries ~/.bash_profile, ~/.bash_login and ~/.profile in that order and reads only the first one that exists (bash man page, section INVOCATION). Ubuntu gives a new user nothing but a ~/.profile, and that file sources ~/.bashrc, so both paths lead to your aliases.

That arrangement breaks easily. Create a ~/.bash_profile of your own, say for an environment variable, and login shells stop reading ~/.profile. Your aliases are gone there without any error message, until the new file sources ~/.bashrc itself. Write the alias into the wrong file and you can spend a long time chasing a bug that does not exist.

An edited file does nothing for shells that are already open. Either start a new terminal or read the file in again, with source ~/.zshrc in Zsh and in Bash with:

source ~/.bashrc

With more than a handful of aliases, a separate file keeps the startup file readable. Ubuntu has planned for that already: /etc/skel/.bashrc, the template for new users on Ubuntu 24.04, contains these three lines.

if [ -f ~/.bash_aliases ]; then
    . ~/.bash_aliases
fi

Anything you put in ~/.bash_aliases is therefore loaded with every new Bash.

What the shell does with an alias, and why sudo ll fails

One sentence from the Bash manual explains nearly every surprise: “Aliases are expanded when a command is read, not when it is executed”. The shell swaps the text in while it is still reading your line, and only then runs whatever came out.

It does not swap just anywhere. According to the bash man page, only the first word of a simple command is checked. That is not the same as the first word of the line: && and ; each start a new simple command, so ll is expanded in cd /tmp && ll and in ls; ll alike. In echo ll it is only an argument and stays as it is. Whatever you type after the alias is left alone and lands behind the replacement, which is why ll /etc runs ls -l /etc.

The same rule is why sudo ll does not work. The first word there is sudo, and the shell never looks at ll. The manual offers a way out: “If the last character of the alias value is a blank, then the shell checks the next command word following the alias for alias expansion.” Define alias sudo='sudo ', with a space before the closing quote, and sudo ll works. If sudo is not set up on your machine yet, How to use Sudo on Debian walks through it.

Scripts: where aliases stop working

An alias that works fine in the terminal can fail in a script for two separate reasons, and both go back to the manual.

The first is a rule: “Aliases are not expanded when the shell is not interactive, unless the expand_aliases shell option is set using shopt”. A script that calls ll prints ll: command not found and keeps going. The line returns status 127, but unless the script uses set -e it moves on to the next one, and the failure scrolls past unnoticed.

The second is less well known. A non-interactive Bash never reads .bashrc; at startup it only reads the file named in BASH_ENV. Even with expand_aliases switched on, the script would have no aliases to expand. It has to turn the option on and load the definitions itself:

shopt -s expand_aliases
source ~/.bash_aliases

Put these lines near the top. Expansion happens as a line is read, so the alias has to exist before the shell reads the line that uses it. The manual adds a stricter version of the same point: a definition that shares a line with another command only takes effect from the next line of input. Defining an alias and using it on one line fails every time.

Zsh handles scripts differently. It does expand aliases there, so an alias defined inside a script is usable from the following line without any option. What it does not do is read ~/.zshrc for a script; the Zsh manual reserves that file for interactive shells. Its section on aliasing goes further and recommends functions instead of aliases for non-interactive code.

An argument in the middle needs a function

An alias can only append. The Bash manual puts it bluntly: “There is no mechanism for using arguments in the replacement text, as in csh. If arguments are needed, use a shell function instead.” Creating a directory and changing into it needs the same name in two places, which is already more than an alias can do:

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

The function sits in the same startup file and is loaded the same way. Unlike an alias it takes parameters and returns an exit status, and in a script it needs no expand_aliases, provided the script defines it or sources the file it lives in. A function that exists only in your .bashrc never reaches a script, for exactly the reason an alias does not. A workable rule: if one word is being replaced, use an alias. Once there is logic involved, write a function.

Changing and removing aliases

There is no edit command. You define the alias again, and the new definition silently replaces the old one, with no warning. To make ll list hidden files as well:

alias ll='ls -la'

That only changes the running shell. If the old line is still in your startup file, the next terminal brings back the old version, so change the file as well.

Removing follows the same pattern. unalias ll drops one alias, unalias -a drops all of them, and both affect the current session only. An alias that is also written into ~/.bashrc or ~/.zshrc comes back at the next start until you delete the line there. That is not a bug. It is simply what separates a temporary alias from a permanent one.

When an alias hides a real command

Nothing stops you from naming an alias after a program that already exists. alias rm='rm -i' is the standard example: rm then asks before every deletion. It feels safe, and after a while you rely on it. On a server without that alias, rm asks nothing at all.

Collisions also happen by accident. A popular naming scheme gives every tool its own first letter, gs, gc and gp for Git, ds and dc for Docker. But dc is already a program, the reverse-Polish calculator from the GNU bc project, which Debian and Ubuntu ship as a separate package called dc. An alias of that name hides it. Running type dc first would have shown the clash.

For a single call there are two ways past an alias that leave it in place. A leading backslash, as in \rm file, skips the alias and runs the real rm. It only gets past the alias, though: if a shell function called rm exists, the backslashed call ends up there. command rm file skips both and always reaches the program. type rm tells you beforehand which of the three you are about to get.

Which aliases are worth keeping

Everyone starts with ll for ls -l and gs for git status. The aliases that really pay off tend to be personal. If you keep heading to the same project folder, alias cdproj='cd ~/projects' earns its line. If you often check which services are listening, shorten the command from How to Show Used Ports in Linux to alias ports='ss -tulnp'. Two caveats apply to that one: ss only shows the processes behind other users’ sockets when it runs as root, and sudo ports only works with the trailing-space sudo alias described above.

A good name is short and still says something. gs does, x7 does not. A shared first letter per tool also makes the list searchable: in Bash, alias | grep "^alias g" shows every alias starting with g. Zsh leaves out the word alias when it lists them, so that filter finds nothing there; use alias | grep '^g' instead.

Shorthand has a price. Your command lines become harder for anyone else to read, and on another machine the aliases are not there at all. Documentation and scripts should keep the long form.

The file itself needs a little care. A one-line comment above each alias answers the question, months later, of why it exists; configuration without comments turns into somebody else’s code within a year. If you use the same aliases on several machines, keep the file in a repository, which also records when each shortcut arrived and why. Then read through it now and again. Workflows change, and an alias for a tool you no longer use is clutter. A long list is never used in full anyway, so add an alias only after the long command has annoyed you for the second or third time.

To get going, pick the two or three commands that bother you most, put the aliases in ~/.bashrc or ~/.zshrc, and check after a few weeks which ones you actually type. Keep those. Delete the rest.

The primary sources for everything above are the Aliases page of the Bash manual and the POSIX specification for alias. Other write-ups worth a look: Ubuntuusers: alias (German), IBM: Creating a Command Alias, Pro-Linux: Der Shell-Befehl alias from 2001 (German), Computerworld: How to Use Aliases in Linux Shell Commands from 2012 and IONOS: Linux Alias Befehl (German).