CoursesLinux essentialsWildcards, aliases & shortcuts

Wildcards, aliases & shortcuts

Globbing, tab-completion, history, chaining.

Beginner12 min · lesson 12 of 25

Typing every filename by hand is like addressing a hundred envelopes one at a time when a single rubber stamp would cover them all. The shell (the program that reads what you type and turns it into running commands) gives you a handful of features that replace slow, careful typing with something faster and safer. None of them are hard. Each takes a minute to learn and then pays you back on every command you run for the rest of your career. For the security and operations work ahead, a few of them also change what you can see and what you can get badly wrong, so they carry real weight.

Wildcards: one pattern, many files

A wildcard is a stencil for filenames. Instead of naming each file, you describe the shape you want and let the shell fill in the rest. The formal name is globbing, after an old Unix helper program named glob (short for global) that did exactly this expansion. Three symbols do most of the work. An asterisk (*) matches any run of characters, including none of them. A question mark (?) matches exactly one character. Square brackets ([abc]) match any single character from the set inside them. So *.log means every name that ends in .log.

Here is the part that matters most. The shell expands the pattern before the command ever runs. Your command never sees the star. By the time rm or ls starts, the * has already been replaced with the real list of matching names. That is why the same pattern can be a big time-saver and a foot-gun in the same breath.

~/secopslog — bash
$ ls *.log
access.log app.log error.log

The other two symbols narrow things down. The question mark holds a spot for one character, and the brackets let you spell out a small set of allowed characters.

~/secopslog — bash
$ ls log-2026-0?.txt
log-2026-01.txt log-2026-02.txt log-2026-03.txt
$ ls report.[tc]sv
report.csv report.tsv

You can see exactly what a pattern will become before you act on it. Ask the shell to echo it. echo prints its arguments after expansion, so it shows you the literal list the next command would receive. Get in the habit of running this against any wildcard you are about to hand to something destructive.

~/secopslog — bash
$ echo *.log
access.log app.log error.log
What actually happens when you run rm *.log
1You type
rm *.log
2Shell expands the glob
scans the current directory
3Pattern becomes a file list
rm access.log app.log error.log
4Command runs
rm never sees the * at all

One quiet rule both saves you and bites you: a plain * does not match names that begin with a dot. Files like .env (a common place to keep secrets such as database passwords) and .ssh (the directory that holds your login keys) stay invisible to it. That means cp * backup/ will not sweep up your secrets, which is good. It also means a cleanup script that trusts * can leave sensitive files sitting behind, or miss them entirely in an audit, which is worth carrying in your head.

~/secopslog — bash
$ ls -a
. .. .env .ssh config.yaml data.csv
$ echo *
config.yaml data.csv
A wildcard is resolved before your command sees it
Because the shell expands the star first, rm *.log deletes whatever that pattern matches at this exact moment, and a stray space turns rm * .log into rm * (delete everything here) followed by .log. Two habits keep you safe. Run ls or echo with the same pattern first and read the list. And reach for rm -- * or rm ./* so a file cleverly named -rf cannot be read back as a command option. Whatever ls prints is exactly what the next command will act on.

Tab completion: let the shell finish your sentence

Tab completion is the shell's version of the contact list on your phone that fills in a name after you type two letters. Start a command or a path, press the Tab key, and the shell finishes it. If only one thing matches, it fills it straight in. If several match, press Tab twice and it lists your options.

~/secopslog — bash
$ cd /usr/sh<Tab> # press Tab, the shell fills in the rest: cd /usr/share/
$ systemctl re<Tab><Tab> # press Tab twice to see every match:
reboot reenable reload reload-or-restart rescue reset-failed restart revert

It saves keystrokes, and it also does something quieter that matters more. The shell only completes paths and names that actually exist, so a line it finishes for you is a line you have verified. If you type the start of a directory and Tab fills in nothing, that directory is not there, and you have caught the mistake before pressing Enter on something you cannot take back. On a production server, that half-second of feedback is cheap insurance.

History and reverse search

The shell keeps a logbook of what you have run, like the redial list on a phone. The up-arrow walks back through recent commands one at a time. Better, Ctrl-R (hold the Control key and press R) starts a reverse search: type a few letters from any past command and it jumps to the most recent match, so a long command you ran last Tuesday comes back in six keystrokes.

~/secopslog — bash
$ history 5
1013 cd /opt/app 1014 git pull 1015 systemctl restart myapp 1016 journalctl -u myapp -n 50 1017 history 5
$ # press Ctrl-R, then type: nginx (reverse-i-search)`nginx': systemctl restart nginx

That logbook is written to a file, ~/.bash_history (a hidden file of past commands in your home directory), and it is one of the first places both attackers and investigators look. If you ever type a password or an API key (a secret string that authenticates you to a service) directly on the command line, it lands there in plain text, readable by anyone who later gets into that account.

~/secopslog — bash
$ tail -3 ~/.bash_history
git pull systemctl restart myapp mysql -u app -pHunter2 -e "show databases"

There is the leak, sitting in a file that survives every reboot. Two defenses. Put a space in front of any sensitive command, and with HISTCONTROL set to ignorespace (a setting that tells Bash to skip lines that begin with a space) it never reaches the history file. The deeper fix is to stop passing secrets as arguments at all. While a command runs, anyone else on the machine can read its full command line, arguments included, with ps aux (the command that lists every running process). A password in curl -u user:pass or mysql -pSecret is exposed the moment you press Enter, history file or not. Prefer a prompt, a config file with tight permissions, or an environment variable.

~/secopslog — bash
$ export HISTCONTROL=ignorespace mysql -u app -p -e "show databases" # leading space keeps it out of history; -p prompts, no password on the line

Chaining commands with ; && and ||

You can put several commands on one line, and the symbol between them decides how they relate, the way a recipe says bake the cake, and only if it rises, add the frosting. Every command reports a hidden number when it finishes: 0 means success, anything else means failure. You can read the last one with echo $? (the exit status of the previous command). The three connectors work off that number.

A semicolon (;) runs the commands in order no matter what happened. Two ampersands (&&) run the next command only if the previous one succeeded. Two pipes (||) run the next command only if the previous one failed. So build && deploy ships your code only when the build worked, and a failed step stops the line cold before it can do any harm.

~/secopslog — bash
$ cd /opt/app && git pull && sudo systemctl restart myapp
Already up to date.

The pull reported success, so the restart ran (systemd, the service manager on most modern Linux, stays quiet when a restart works). Flip the logic around and || gives you a plain-language alarm that only fires on failure.

~/secopslog — bash
$ ping -c1 db.internal || echo "host unreachable"
ping: db.internal: Name or service not known host unreachable

A semicolon, by contrast, plows ahead even when the first part fails, and that is where it turns into a trap. The line below is a classic way to wreck a machine: the cd fails, the semicolon shrugs, and the delete runs wherever you happened to be standing instead of in the directory you meant.

~/secopslog — bash
$ cd /var/nope ; rm -rf ./*
bash: cd: /var/nope: No such file or directory

Written with && instead of ;, the failed cd would have stopped the whole line and nothing would have been deleted. The rule that follows from this: in scripts, chain steps that depend on each other with &&, not with a semicolon. A step should never run on the assumption that the one before it worked.

Aliases: nickname your common commands

An alias is a nickname (a speed-dial button) for a longer command. You define it once, and from then on the short name stands in for the full thing. Type the nickname, the shell swaps in the real command, and runs it. To make aliases stick around after you close the terminal, put them in ~/.bashrc (a startup file that Bash reads every time it opens a new interactive shell).

~/.bashrc
alias ll='ls -alF' # long listing, all files, with type markers
alias gs='git status'
alias ..='cd ..'
alias rm='rm -i' # ask before deleting each file
# apply changes to the current shell with: source ~/.bashrc

The rm -i alias is a small safety net that makes every delete ask first. Aliases are also how you catch trouble. You can always ask what a name really points to with the type command, and you can force the real command past an alias by putting a backslash in front of it.

~/secopslog — bash
$ type ll type rm \rm old.tmp # the backslash skips the alias, so no -i prompt
ll is aliased to `ls -alF' rm is aliased to `rm -i'

That backslash trick cuts both ways, and it is why ~/.bashrc deserves a security engineer's attention. Anyone who can write to your dotfiles can plant an alias that quietly replaces a command you trust. An alias named sudo pointed at a fake password prompt, or an ls that also copies files out to a stranger, runs every time you use the real name and survives every reboot. It is a favorite way for an intruder to stay on a machine after a break-in, what defenders call persistence. When a box feels off, read ~/.bashrc, ~/.bash_profile, and the files under /etc/profile.d/, and run type on the commands you lean on. Most systems ship a harmless alias or two (ls is usually set to add color), so what you are hunting for is anything you did not put there: an alias, function, or script standing in front of a command you assumed was the real binary.

Quick check
01You are in a directory that contains no files ending in .bak, and you run: ls *.bak. In a default Bash shell, what happens?
Correct — With its default settings, Bash leaves an unmatched pattern untouched, so ls receives the star itself and errors with 'cannot access'.
Incorrect — No. A no-match glob is never widened to everything; that behavior would be far more dangerous than it already is.
Incorrect — No. ls never receives a real filename, so it prints an error and returns a non-zero status.
Incorrect — No. The line is perfectly valid; the surprise is that the unmatched pattern is passed through as plain text.
02Why is putting a password straight into a command, like mysql -pSecret, risky even when you keep it out of your shell history with a leading space?
Incorrect — HISTCONTROL=ignorespace is a Bash feature and does keep the line out of history; the real exposure is elsewhere.
Correct — ps aux (the command that lists every running process) shows each process's arguments, so the password is visible for as long as the command runs, history file or not.
Incorrect — chaining operators do not re-run anything; && only controls whether the next command runs.
Incorrect — HISTCONTROL does not encrypt anything; it just controls which lines get saved.
03A deploy script contains the line: cd /release ; rm -rf ./*. One day /release does not exist. What happens, and how should the line have been written?
Incorrect — a semicolon runs the next command no matter what the previous one did; only && would have stopped the line.
Incorrect — ./* expands against whatever is in the current directory, which is precisely where the danger lies.
Correct — the semicolon plows ahead after the failed cd, so the delete runs wherever you happened to be; && makes each step depend on the one before.
Incorrect — only cd fails; the shell then moves straight on to the rm.

One habit to build starting today: before any rm, cp, or mv that uses a wildcard, run the same pattern through ls or echo and read the result back. It costs two seconds, and it is the whole difference between deleting three log files and deleting three hundred.

Try this

Work through “Aliases: nickname your common commands” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: a wildcard is resolved before your command sees it. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related