Table of Contents
- Understanding Bash History
- How History Expansion Works
- Common History Expansion Operators
- Advanced Techniques
- Customization and Configuration
- Best Practices
- Troubleshooting
- Quick Reference Cheat Sheet
1. Understanding Bash History
Before diving into expansion, let’s clarify what “Bash history” is. Bash (Bourne Again SHell) maintains a record of commands you’ve executed in a history list. This list is stored in two places:
- In-memory: Active during your shell session.
- On-disk: Saved to
~/.bash_historywhen the shell exits (by default).
Key Commands to Manage History
history: View the in-memory history list (e.g.,history 10shows the last 10 commands).history -d N: Delete the Nth command from history (replaceNwith the line number).history -c: Clear the entire in-memory history.history -w: Write in-memory history to~/.bash_historyimmediately (instead of waiting for exit).
2. How History Expansion Works
History expansion lets you reference commands from the history list using special syntax, triggered by the ! (exclamation mark) character. Bash replaces these references with the corresponding command or arguments before executing the line.
Basic Recall Syntax
| Syntax | Description | Example |
|---|---|---|
!! | Repeat the last command. | After ls /home/user/docs, !! becomes ls /home/user/docs. |
!N | Repeat the Nth command in history. | If history shows line 500: cd /tmp, !500 runs cd /tmp. |
!-N | Repeat the Nth most recent command. | !-2 repeats the command before the last one. |
!string | Repeat the last command starting with string. | After git commit -m "Fix bug", !git repeats the last git command. |
!?string? | Repeat the last command containing string. | !?bug? finds the last command with “bug” (e.g., the git commit above). |
3. Common History Expansion Operators
Beyond basic recall, Bash provides operators to extract or modify parts of historical commands. These operators use a colon (:) to separate the history reference from the modifier.
Argument Extraction
Extract specific arguments from a historical command:
| Operator | Description | Example |
|---|---|---|
!^ | First argument of the last command. | If last command: cp file1.txt file2.txt /backup, !^ = file1.txt. |
!$ | Last argument of the last command (aka “bang dollar”). | After ls /home/user/docs, cd !$ → cd /home/user/docs. |
!* | All arguments of the last command (excludes the command itself). | After mv a.txt b.txt c.txt /tmp, rm !* → rm a.txt b.txt c.txt. |
!:n | Nth argument (0 = command, 1 = first arg, 2 = second arg, etc.). | Last command: echo "Hello" "World" "!", !:2 = "World". |
Path Manipulation
Modify file paths from historical commands:
| Operator | Description | Example |
|---|---|---|
!:h | ”Head” of the path (directory containing the file). | Last command: vi /etc/nginx/nginx.conf, echo !:h → echo /etc/nginx. |
!:t | ”Tail” of the path (filename without the directory). | Last command: vi /etc/nginx/nginx.conf, echo !:t → echo nginx.conf. |
String Substitution
Replace text in a historical command (like sed for history):
| Operator | Description | Example |
|---|---|---|
!s/old/new/ | Replace the first occurrence of old with new in the last command. | Last command: cd /hom/user (typo), !s/hom/home/ → cd /home/user. |
!gs/old/new/ | Replace all occurrences of old with new (global substitution). | Last command: echo "a a a", !gs/a/b/ → echo "b b b". |
Range and Truncation
Extract ranges of arguments or truncate commands:
| Operator | Description | Example |
|---|---|---|
!::n-m | Extract arguments from position n to m (inclusive). | Last command: echo one two three four, !::1-3 → one two three. |
!:-n | Exclude the last n arguments. | Last command: cp a b c d, !:-2 → cp a b (excludes c d). |
4. Advanced Techniques
Combine operators and modifiers to create powerful, concise commands.
Combining Operators
Chain operators to refine arguments. For example:
- Last command:
scp user@server:/var/logs/app.log /local/backups/ !$:t→ Extracts the filename from the last argument:app.log!^:h→ Extracts the directory from the first argument:user@server:/var/logs
Testing Expansions Safely
Use the :p modifier to print the expanded command without executing it. This is critical for avoiding typos:
$ ls /very/long/path/that/i/dont/want/to/type/again
$ cd !$:p # Prints "cd /very/long/path/that/i/dont/want/to/type/again"
$ cd !$ # Now executes safely
Repeating Command Prefixes
Use !:0 to reference the command name (e.g., ls, cd) from a historical line:
$ grep "error" /var/log/syslog
$ !:0 /var/log/auth.log # Repeats "grep" with a new file: "grep /var/log/auth.log"
5. Customization and Configuration
Tweak Bash history settings to make expansion more useful. Edit your ~/.bashrc or ~/.bash_profile to configure these variables:
HISTSIZE and HISTFILESIZE
Control how many commands are stored in memory and on disk:
HISTSIZE=1000000 # Store 1M commands in memory
HISTFILESIZE=2000000 # Store 2M commands in ~/.bash_history
HISTCONTROL
Avoid clutter in history (and thus expansion) with:
HISTCONTROL=ignoredups:ignorespace # Ignore duplicates and commands with leading spaces
ignoredups: Skip consecutive duplicate commands.ignorespace: Omit commands starting with a space (useful for sensitive commands likessh user@serverwith a password).
HISTTIMEFORMAT
Add timestamps to history for easier reference (critical for !?string? searches):
HISTTIMEFORMAT="%F %T " # Shows "YYYY-MM-DD HH:MM:SS" before each command
Append History Immediately
By default, Bash overwrites ~/.bash_history on exit. Make it append instead:
shopt -s histappend # Append new history to the file instead of overwriting
PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND" # Update history after each command
6. Best Practices
Avoid Sensitive Data in History
Commands with passwords or API keys are stored in plaintext in ~/.bash_history. Mitigate risk with:
- Leading space: Prefix sensitive commands with a space (requires
HISTCONTROL=ignorespace). - Manual deletion:
history -d Nto delete lineNfrom history. - Clear history:
history -c && history -wto clear in-memory and disk history.
Use :p to Validate Expansions
Always test complex expansions with :p before execution. For example:
$ !git:s/commit/push/:p # Prints "git push -m "Fix bug"" (instead of running it)
Learn Reverse Search (Ctrl+R)
Complement history expansion with reverse search: Press Ctrl+R, type a keyword, and Bash finds the last matching command. Press Ctrl+R again to cycle backward.
7. Troubleshooting
History Expansion Not Working?
- Ensure expansion is enabled: Run
set -o | grep histexpand—it should showhistexpand on. If not, enable withset -H. - Check
HISTSIZE: IfHISTSIZE=0, no commands are stored. Set it to a large value (e.g.,10000).
Commands Missing from History?
HISTCONTROL=ignoredupsskips duplicates. UseHISTCONTROL=erasedupsto remove all duplicates, not just consecutive ones.HISTIGNOREexcludes patterns (e.g.,HISTIGNORE="ls:cd"skipslsandcdcommands).
8. Quick Reference Cheat Sheet
| Syntax | Description | Example |
|---|---|---|
!! | Last command | !! → Repeats last command |
!$ | Last argument of last command | cd !$ → cd /path/from/last/arg |
!^ | First argument of last command | echo !^ → echo first_arg |
!string | Last command starting with string | !git → Last git command |
!s/old/new/ | Replace first old with new in last cmd | !s/typo/correct/ → Fixes typo |
!::n-m | Arguments n to m of last command | !::1-3 → Args 1, 2, 3 |
!$:t | Filename from last argument | echo !$:t → echo filename.txt |
!command:p | Print expanded command (no execution) | !ls:p → Prints last ls command |
Conclusion
Bash history expansion is a hidden gem for terminal efficiency. By mastering !!, !$, substitution operators, and customization tricks, you’ll cut down on retyping, reduce errors, and navigate the command line with confidence. Start small—practice with !$ and !!—then gradually incorporate advanced operators. Your future self (and keyboard) will thank you.