▶Prefer to watch? This short video runs this page's main commands on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube.
bash [options] [script [arguments...]]
bash --version
Bash (the Bourne Again SHell) is the default shell on most Linux systems. It is both the command line you type into
and a scripting language. This sheet covers the two halves: working fast at the prompt (shortcuts, history, job control)
and writing scripts (variables, tests, loops, functions, error handling). Bash is not the same as plain sh, and not the same as
fish or zsh. Scripts that start with #!/bin/bash can use everything here. See the Fish sheet for a friendlier interactive shell,
and the AI-Assisted Shell Scripting guide for a safe way to review scripts.
Which shell am I in?
echo $0 # the current shell (-bash means a login shell)
echo $BASH_VERSION # empty if you are not in bash
echo $SHELL # your default login shell
cat /etc/shells # shells installed on this machine
chsh -s /bin/bash # make bash your login shell
bash --version (macOS ships an old bash 3.2. Install a newer one with: brew install bash)
Keyboard shortcuts at the prompt (readline)
Ctrl+A / Ctrl+E Jump to the start / end of the line
Alt+B / Alt+F Move back / forward one word (also Ctrl+Left / Ctrl+Right in many terminals)
Ctrl+U Delete from the cursor to the start of the line
Ctrl+K Delete from the cursor to the end of the line
Ctrl+W Delete the word before the cursor
Alt+D Delete the word after the cursor
Ctrl+Y Paste back what you just deleted (yank)
Ctrl+L Clear the screen (same as the clear command)
Ctrl+C Stop the running command
Ctrl+D Exit the shell (on an empty line), or delete the character under the cursor
Ctrl+Z Suspend the running command (resume with fg or bg)
Ctrl+R Search history backwards as you type. Press Ctrl+R again for the next match
Ctrl+G Cancel a history search
Alt+. Insert the last argument of the previous command. Press again to go further back
Ctrl+X Ctrl+E Edit the current line in your $EDITOR (great for long commands)
Tab Complete a command, file or variable. Press Tab twice to list the choices
Up / Down Walk through history
Ctrl+_ or Ctrl+/ Undo the last edit to the line
Alt+U / Alt+L / Alt+C Uppercase / lowercase / capitalize the word
Ctrl+T Swap the two characters before the cursor
Ctrl+S / Ctrl+Q Pause / resume terminal output (if the screen seems frozen, press Ctrl+Q)
History
history # list history with numbers
history 20 # the last 20 entries
!! # repeat the last command sudo !! repeat it with sudo
!$ # the last argument of the last command mkdir /tmp/a && cd !$
!* # all arguments of the last command
!n # run history entry number n !-2 the command before the last
!git # the last command that started with "git"
!?text # the last command containing "text"
^old^new # repeat the last command, replacing old with new
!!:s/foo/bar/ # same idea, with the substitution written out
history -d 123 # delete one entry history -c clear this session's history
history -w # write history to ~/.bash_history now
Settings for ~/.bashrc:
HISTSIZE=10000 HISTFILESIZE=20000 # how much to keep
HISTCONTROL=ignoreboth:erasedups # ignore duplicates and commands that start with a space (type a space first to keep a secret command out of history)
HISTTIMEFORMAT="%F %T " # add timestamps
shopt -s histappend # append to the history file instead of overwriting it
PROMPT_COMMAND='history -a' # save each command as you go (useful with several terminals)
HISTIGNORE='ls:cd:pwd:exit' # do not record these
Startup files (which one runs when)
Login shell (SSH, console, su -) /etc/profile, then the first found of ~/.bash_profile, ~/.bash_login, ~/.profile
Interactive non-login (a new terminal) ~/.bashrc
Script (bash script.sh) None of the above (only what BASH_ENV points to)
On logout ~/.bash_logout
Common trick: make login shells read .bashrc too. Put this in ~/.bash_profile:
[ -f ~/.bashrc ] && source ~/.bashrc
Put aliases, prompt and interactive settings in ~/.bashrc. Put environment variables you want everywhere in ~/.profile (or ~/.bash_profile).
Reload without logging out: source ~/.bashrc (or: . ~/.bashrc)
Getting help
help cd # help for a builtin
help # list all builtins
man bash # the full manual (search with /, quit with q)
type ls # is it an alias, function, builtin or file? type -a echo show all matches
command -v git # where is it? (better than which in scripts)
compgen -c | sort | head # list all commands you can run compgen -A function your functions
declare -p VAR # show how a variable is defined declare -f myfunc show a function's code
alias # list aliases unalias name remove one
Variables
name="Alice" # NO spaces around the =
echo "$name" # use with a $ (quote it)
echo "${name}_backup" # braces mark where the name ends
export EDITOR=vim # pass to child programs (environment variable)
unset name # remove it
readonly PI=3.14 # cannot be changed
declare -i count=5 # integer declare -r readonly declare -x export declare -l / -u lowercase / uppercase on assignment
local x=1 # inside a function only
env # list environment variables printenv HOME show one
set # list all variables and functions
VAR=value command # set a variable for one command only LANG=C sort file
Common built-in variables:
$HOME $USER $PWD $OLDPWD $PATH $SHELL $HOSTNAME $RANDOM $SECONDS $LINENO $UID $EDITOR $PS1
PATH: add a folder with export PATH="$HOME/bin:$PATH" (put it in ~/.profile or ~/.bashrc)
Special parameters
$0 Name of the script $1 $2 ... $9 ${10} Positional arguments
$# Number of arguments $@ All arguments, each one separate (use "$@")
$* All arguments as one word $? Exit status of the last command (0 = success)
$$ Process ID of this shell $! PID of the last background job
$_ Last argument of the previous command $- Current shell options
shift Drop $1 and move the rest down shift 2 drop two
Quoting
'single quotes' # everything literal. No variables, no escapes
"double quotes" # variables and $(...) still expand. Prevents word splitting and globbing
\$ # escape one character echo "Cost: \$5"
$'line1\nline2\tTab' # ANSI-C quoting: \n, \t and friends work
"$var" and "${array[@]}" and "$(cmd)" ALWAYS quote these, unless you know you want splitting
Unquoted variables split on spaces and expand wildcards. That causes most script bugs (and dangerous rm commands).
Parameter expansion (string tricks without external tools)
${var:-default} # value of var, or "default" if unset or empty
${var:=default} # same, and also assign the default
${var:?message} # exit with an error message if unset or empty
${var:+other} # "other" if var is set and not empty, else nothing
${#var} # length of the string
${var:2} ${var:2:5} # substring: from offset 2, or 5 characters from offset 2
${var#pattern} ${var##pattern} # remove the shortest / longest match from the START ${path##*/} file name
${var%pattern} ${var%%pattern} # remove the shortest / longest match from the END ${file%.txt} drop the extension
${var/old/new} ${var//old/new} # replace the first / every match
${var^} ${var^^} ${var,} ${var,,} # uppercase first letter / all and lowercase first / all
${!prefix*} # names of variables starting with prefix
${!name} # indirect: the value of the variable whose name is in $name
Examples:
f="/var/log/app.log"
echo "${f##*/}" # app.log echo "${f%/*}" /var/log echo "${f%.log}" /var/log/app
echo "${f/var/tmp}" # /tmp/log/app.log
Command substitution and arithmetic
today=$(date +%F) # capture a command's output (prefer $() over `backticks`)
files=$(ls | wc -l)
echo $((3 + 4 * 2)) # arithmetic: + - * / % **
((count++)) # increment (no $ needed inside (( )))
((count += 5)) # also -= *= /=
if (( count > 10 )); then echo big; fi # numeric comparison
result=$((a / b)) # integers only. For decimals use: bc -l or awk
echo "scale=2; 10/3" | bc # 3.33
Expansions and wildcards (globbing)
* Any characters ? One character [abc] One of a, b, c [a-z] A range [!abc] Not those
ls *.txt ls file?.log ls [A-C]*
{a,b,c} # brace expansion: echo file{1,2,3}.txt mkdir -p project/{src,docs,tests}
{1..5} {a..e} {01..10} {1..10..2} # sequences: for i in {1..5}
~ ~user # home directory of you / of user
cp file{,.bak} # cp file file.bak (handy for quick backups)
shopt -s nullglob # a glob with no matches expands to nothing (default: stays as the text *.xyz)
shopt -s dotglob # let * match hidden files too
shopt -s globstar # ** matches directories recursively: ls **/*.py
shopt -s extglob # ?(pat) *(pat) +(pat) @(pat) !(pat): ls !(*.log) rm !(keep|this).txt
shopt -s nocaseglob # case-insensitive globbing
Redirection and pipes
cmd > file # stdout to a file (overwrite) cmd >> file append
cmd 2> errors.log # stderr to a file cmd 2>> errors.log append
cmd &> all.log # stdout and stderr to a file cmd &>> all.log append
cmd > out.log 2>&1 # older, portable form of the same
cmd 2>&1 | less # send both through a pipe cmd |& less shorthand
cmd < input.txt # stdin from a file
cmd > /dev/null # throw output away cmd > /dev/null 2>&1 throw away everything
cmd | tee file # show AND save cmd | tee -a file append
cmd1 | cmd2 | cmd3 # a pipeline: each command's stdout feeds the next
diff <(sort a.txt) <(sort b.txt) # process substitution: treat command output as a file
cmd > >(tee log.txt) # feed a command's output into another command as a file
cat <<'EOF' # here-document: multi-line input (quoted 'EOF' = no expansion)
line one
line two with $literal
EOF
cat <<-EOF # <<- strips leading tabs so you can indent the block (unquoted: variables expand)
Hello $USER
EOF
grep foo <<< "$text" # here-string: feed a string to stdin
set -o noclobber # refuse to overwrite files with > (override with >|)
exec > >(tee -a script.log) 2>&1 # inside a script: log everything the script prints
Order matters: cmd > file 2>&1 is right. cmd 2>&1 > file is not.
Exit status and combining commands
echo $? # 0 = success, anything else = failure (1-255)
cmd1 && cmd2 # run cmd2 only if cmd1 succeeded
cmd1 || cmd2 # run cmd2 only if cmd1 failed
cmd1 ; cmd2 # run both, one after the other
cmd1 & cmd2 & # run in the background
mkdir build && cd build || exit 1 # a common pattern
if cmd; then ... fi # if tests the exit status of a command
! cmd # negate the status
exit 0 exit 1 return 1 # end the script / leave a function with a status
Conventions: 0 success, 1 general error, 2 misuse, 126 not executable, 127 command not found, 130 stopped by Ctrl+C.
Tests: [ ], [[ ]] and (( ))
[[ ... ]] is the bash way (safer: no word splitting, supports && || and patterns). [ ... ] is the old POSIX test.
File tests
[[ -e f ]] exists [[ -f f ]] regular file [[ -d d ]] directory [[ -L f ]] symlink [[ -s f ]] not empty
[[ -r f ]] readable [[ -w f ]] writable [[ -x f ]] executable [[ f1 -nt f2 ]] newer than [[ f1 -ot f2 ]] older than
String tests
[[ -z "$s" ]] empty [[ -n "$s" ]] not empty [[ "$a" == "$b" ]] equal [[ "$a" != "$b" ]] not equal
[[ "$s" == *.txt ]] pattern match (unquoted right side) [[ "$s" =~ ^[0-9]+$ ]] regular expression (matches land in ${BASH_REMATCH[@]})
[[ "$a" < "$b" ]] sorts before
Number tests (use (( )) or these)
(( a == b )) (( a != b )) (( a < b )) (( a <= b )) (( a > b )) (( a >= b )) or in [[ ]]: -eq -ne -lt -le -gt -ge
Combine
[[ -f f && -r f ]] [[ -z $x || $x == y ]] [[ ! -e f ]]
Quote variables inside [ ] always. Inside [[ ]] quoting is safer but not required.
Conditionals
if [[ -f "$file" ]]; then
echo "found"
elif [[ -d "$file" ]]; then
echo "directory"
else
echo "missing"
fi
case "$answer" in
y|Y|yes) echo "yes" ;;
n|N|no) echo "no" ;;
q*) exit 0 ;;
*) echo "unknown" ;;
esac
[[ -f f ]] && echo "yes" || echo "no" # one-line form (beware: the || also runs if the echo fails)
Loops
for i in 1 2 3; do echo "$i"; done
for i in {1..10}; do echo "$i"; done
for ((i = 0; i < 10; i++)); do echo "$i"; done # C-style
for f in *.txt; do echo "$f"; done # over files (quote "$f" inside the body)
for arg in "$@"; do echo "$arg"; done # over script arguments
while [[ $count -lt 5 ]]; do ((count++)); done
until ping -c1 -W1 host >/dev/null 2>&1; do sleep 2; done # repeat until the command succeeds
while IFS= read -r line; do echo "$line"; done < file.txt # read a file line by line (the safe way)
while IFS=, read -r name age city; do echo "$name is $age"; done < data.csv # split fields
command | while read -r line; do ...; done # careful: the loop runs in a subshell (see below)
break continue break 2 # leave the loop / skip to the next round / leave two loops
select choice in start stop quit; do echo "$choice"; break; done # a simple numbered menu
Do NOT loop with: for f in $(ls) It breaks on spaces. Use a glob: for f in * or find ... -print0 | while IFS= read -r -d '' f
Functions
greet() {
local name="${1:-World}" # local variables stay inside the function
echo "Hello, $name"
return 0 # only numbers 0-255. Output the "result" with echo instead
}
greet Alice # call it like a command
message=$(greet Bob) # capture its output
$1, $2, $@, $# work inside a function (the function's own arguments)
declare -f greet # show the code unset -f greet remove it
Export a function to child bash processes: export -f greet
Functions can go in ~/.bashrc, for example:
mkcd() { mkdir -p -- "$1" && cd -- "$1"; }
extract() { case "$1" in *.tar.gz|*.tgz) tar xzf "$1" ;; *.tar.bz2) tar xjf "$1" ;; *.zip) unzip "$1" ;; *) echo "unknown type" ;; esac; }
Arrays
fruits=(apple banana "red grape") # indexed array
echo "${fruits[0]}" # first item (indexes start at 0)
echo "${fruits[@]}" # all items (quote it: "${fruits[@]}" keeps each item whole)
echo "${#fruits[@]}" # how many items
echo "${!fruits[@]}" # the indexes
fruits+=(mango) # append
fruits[1]="cherry" # change one
unset 'fruits[0]' # remove one
echo "${fruits[@]:1:2}" # a slice: 2 items from index 1
for f in "${fruits[@]}"; do echo "$f"; done
mapfile -t lines < file.txt # read a file into an array (one line per item)
read -ra parts <<< "a b c" # split a string into an array
declare -A color # associative array (needs bash 4+)
color[apple]=red; color[banana]=yellow
for k in "${!color[@]}"; do echo "$k is ${color[$k]}"; done
[[ -v color[apple] ]] && echo "key exists"
Strings and text
str="Hello, World"
echo "${#str}" # length
echo "${str:7:5}" # World
echo "${str/World/Bash}" # replace
echo "${str,,} ${str^^}" # lowercase / uppercase
[[ $str == Hello* ]] && echo "starts with Hello"
[[ $str =~ ([A-Z][a-z]+),\ (.*) ]] && echo "${BASH_REMATCH[1]}" # regex capture
printf "%s is %d years old\n" "Alice" 30 # formatted output (more reliable than echo -e)
printf "%-10s|%5.2f|\n" "name" 3.14159 # left-align 10, decimals %x hex %o octal %e scientific %q shell-quoted
printf '%s\n' "${array[@]}" # one array item per line
echo -n "no newline" # -n = no trailing newline. For escapes use printf or echo -e (not portable)
Trim spaces: trimmed="${var#"${var%%[![:space:]]*}"}" or use xargs or sed
Join an array: IFS=,; echo "${arr[*]}"; unset IFS Repeat a character: printf '%.0s-' {1..20}
Input
read name # read a line from the keyboard into $name
read -p "Continue? [y/N] " ans # with a prompt
read -r line # -r: do not treat backslashes specially (use it almost always)
read -s -p "Password: " pw; echo # silent (for passwords)
read -t 5 -n 1 key # 5-second timeout, one character
read -a words <<< "one two three" # into an array
IFS=: read -r user pass uid rest < /etc/passwd # split on a different separator
[[ $ans =~ ^[Yy]$ ]] || exit 1
Script skeleton
#!/usr/bin/env bash
set -Eeuo pipefail # stop on errors, unset variables and pipe failures
IFS=$'\n\t'
trap 'echo "Error on line $LINENO" >&2' ERR
trap 'rm -rf -- "$tmpdir"' EXIT # cleanup when the script ends, however it ends
tmpdir=$(mktemp -d)
usage() { echo "Usage: ${0##*/} [-v] [-o file] input"; exit 2; }
verbose=0; out=""
while getopts ":vo:h" opt; do # getopts: parse short options (o: takes a value)
case "$opt" in
v) verbose=1 ;;
o) out="$OPTARG" ;;
h) usage ;;
:) echo "Option -$OPTARG needs a value" >&2; usage ;;
\?) echo "Unknown option -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1)) # what is left in $@ are the non-option arguments
[[ $# -ge 1 ]] || usage
input="$1"
[[ -r "$input" ]] || { echo "Cannot read $input" >&2; exit 1; }
main() { ... }
main "$@"
Make it runnable: chmod +x script.sh then ./script.sh Shebang: #!/usr/bin/env bash (finds bash on the PATH) or #!/bin/bash
Long options need a manual while/case loop (getopts only does single letters). Use "--" to mark the end of options.
set and shopt: safety and behavior switches
set -e # exit immediately if a command fails (has exceptions: not inside if/while/&&/||)
set -u # error on unset variables (catches typos)
set -o pipefail # a pipeline fails if ANY command in it fails
set -x # print each command before running it (debugging) set +x turn off
set -f # disable globbing set -n read but do not run (syntax check)
set -euo pipefail # the common "strict mode"
shopt # list shell options shopt -s name enable shopt -u name disable
shopt -s checkwinsize cdspell autocd cmdhist # useful interactive ones (autocd: type a directory name to cd into it)
Do not rely on set -e alone: it does not catch every failure. Check the important commands yourself.
Traps and signals
trap 'cleanup' EXIT # run on any exit
trap 'echo Interrupted; exit 130' INT # Ctrl+C
trap 'echo Terminated; exit 143' TERM # kill
trap '' INT # ignore a signal trap - INT restore the default
trap -l # list signals
kill PID kill -9 PID kill -HUP PID # 15 (TERM) asks nicely, 9 (KILL) cannot be caught, 1 (HUP) reload/hangup
pkill -f pattern pgrep -f pattern # find or kill by name
Job control and background work
long_command & # run in the background
jobs # list background jobs jobs -l with PIDs
fg %1 bg %1 # bring job 1 to the foreground / resume it in the background
Ctrl+Z, then bg # suspend a running command and let it continue in the background
disown %1 # detach a job so it survives closing the terminal
nohup command > out.log 2>&1 & # keep running after logout
wait # wait for all background jobs wait $pid wait for one
command1 & command2 & wait # run two things at once, then continue
timeout 30 command # kill it after 30 seconds
sleep 5 # pause
watch -n 2 'df -h' # repeat a command every 2 seconds
For jobs that must outlive your session, use tmux, screen, or a systemd service.
Subshells and scope
( cd /tmp && ls ) # parentheses run in a SUBSHELL: cd and variables do not leak out
{ cd /tmp && ls; } # braces run in the CURRENT shell (note the spaces and the final ;)
count=0; printf 'a\nb\n' | while read -r l; do ((count++)); done; echo "$count" # prints 0! the pipe put the loop in a subshell
Fix: use process substitution or a here-string: while read -r l; do ((count++)); done < <(printf 'a\nb\n') # now $count is 2
Variables set in a child process never reach the parent. export only passes them DOWN.
source script.sh (or . script.sh) # run a script in the current shell (so its variables and cd stay)
Directories and navigation
cd - # back to the previous directory cd or cd ~ home
pushd /var/log; popd; dirs -v # a directory stack: pushd = go there and remember, popd = go back
CDPATH=.:~:~/projects # cd finds directories under these (use with care)
pwd -P # the real path (resolving symlinks)
ls -lah ls -ltr ls -d */ # long/human/hidden oldest first, newest last directories only
mkdir -p a/b/c # create parents rmdir dir remove an EMPTY directory
cd "$(dirname "$0")" # go to the script's own folder script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Aliases and the prompt
alias ll='ls -lah' # in ~/.bashrc: alias ll='ls -lah --color=auto' alias gs='git status -sb'
alias ..='cd ..' alias ...='cd ../..' alias grep='grep --color=auto'
alias update='sudo apt update && sudo apt upgrade'
\ls # run the real command, skipping an alias unalias ll remove an alias
Aliases cannot take arguments. Use a function for that.
The prompt is the PS1 variable:
PS1='\u@\h:\w\$ ' # user@host:directory$ Escapes: \u user \h host \H full host \w directory \W last part \$ $ or # (root) \t time \d date \n newline \j jobs
PS1='\[\e[32m\]\u@\h\[\e[0m\]:\[\e[34m\]\w\[\e[0m\]\$ ' # colors: 32 green, 34 blue, 31 red, 33 yellow, 0 reset. Wrap color codes in \[ \]
PROMPT_COMMAND='echo -ne "\033]0;${USER}@${HOSTNAME}: ${PWD}\007"' # set the terminal title
For a themed prompt with git status and more, see the Oh My Posh sheet, or use starship.
Tab completion
sudo apt install bash-completion # completion for git, ssh, apt, systemctl, docker and hundreds more (often installed already)
source /usr/share/bash-completion/bash_completion
complete -F _longopt mycommand # simple completion for a command
complete -W "start stop status" myservice # complete from a fixed word list
bind 'set completion-ignore-case on' # case-insensitive completion (or put it in ~/.inputrc)
bind 'set show-all-if-ambiguous on' # show choices on the first Tab. Put settings like these in ~/.inputrc without the word bind
Debugging scripts
bash -n script.sh # syntax check only. Runs nothing
bash -x script.sh # trace every command (each line starts with +)
set -x ... set +x # trace only part of a script
PS4='+ ${BASH_SOURCE}:${LINENO}: ' # a more informative trace prefix
shellcheck script.sh # the best linter: catches quoting bugs and bad patterns (sudo apt install shellcheck)
shfmt -d script.sh # formatting differences (shfmt -w to fix them)
echo "DEBUG: var=$var" >&2 # print to stderr so it does not pollute the output
declare -p var # exactly what is in a variable (shows quotes and arrays)
trap 'echo "line $LINENO: $BASH_COMMAND"' ERR # which command failed
Test in a temp folder with fake data first: T=$(mktemp -d)
Common gotchas
name = "x" "command not found": no spaces around = in an assignment. Right: name="x"
[ $a = $b ] Breaks when a value is empty or has spaces. Use [[ "$a" == "$b" ]]
if [$a -eq 1] Needs spaces inside the brackets: [ "$a" -eq 1 ] (or [[ ]])
for f in $(ls) Breaks on spaces in names. Use for f in *
rm -rf $dir/* If $dir is empty this becomes rm -rf /* Use "${dir:?}" and quote: rm -rf -- "${dir:?}"/*
cd somewhere; rm * If cd fails, rm runs in the WRONG place. Use cd somewhere || exit 1
cat file | while read ... The loop runs in a subshell, so variables set inside are lost. Use done < file
echo $var (unquoted) Loses spaces and expands wildcards. Use echo "$var"
$(cmd) result with newlines Trailing newlines are stripped. Fine for most uses
[ "$x" = "a" ] && ... || ... The || part also runs if the && command fails. Use a real if
Using > to update a file cmd file > file truncates it BEFORE reading. Write to a temp file, then mv, or use sponge / sed -i
Windows line endings "bad interpreter: /bin/bash^M". Fix with dos2unix script.sh or sed -i 's/\r$//' script.sh
Script not executable "Permission denied": chmod +x script.sh, or run it with bash script.sh
"./script.sh: not found" Wrong shebang path, or CRLF line endings. Check head -1 script.sh | cat -A
Ran with sh, not bash sh script.sh does not have arrays, [[ ]] or $'..'. Use bash script.sh, or run ./script.sh with a bash shebang
Unset vs empty ${var:-x} treats both as missing. ${var-x} only treats UNSET as missing
Integer math only $((10/3)) is 3. Use bc or awk for decimals
Filenames starting with - rm -- "$f" (the -- ends option parsing)
Globs with no match A glob that matches nothing stays as the literal text. Use nullglob, or test with [[ -e "$f" ]]
Practical one-liners
# Repeat the last command with sudo
sudo !!
# Make a directory and go into it
mkdir -p ~/projects/new && cd $_
# Quick backup of a file
cp file.conf{,.bak}
# Find the 10 biggest things in the current folder
du -sh * .[!.]* 2>/dev/null | sort -rh | head -10
# Count lines in every .py file, largest first
wc -l **/*.py | sort -rn | head (needs shopt -s globstar)
# Run a command for every file
for f in *.jpg; do convert "$f" "${f%.jpg}.png"; done
# Rename many files (replace spaces with underscores)
for f in *\ *; do mv -- "$f" "${f// /_}"; done
# Process a file line by line, safely
while IFS= read -r line; do echo "Got: $line"; done < input.txt
# Ask for confirmation
read -r -p "Delete everything? [y/N] " a; [[ $a == [yY] ]] || exit 1
# Wait until a port is open
until nc -z localhost 8080; do sleep 1; done
# Run something every 5 seconds and log it
while true; do date >> log.txt; sleep 5; done
# Download and check status
if curl -fsS -o out.json https://example.com/api; then echo OK; else echo "failed ($?)"; fi
# Retry a flaky command 5 times
for i in {1..5}; do cmd && break || sleep $((i * 2)); done
# Show the top 5 most used commands in your history
history | awk '{ $1=""; print $2 }' | sort | uniq -c | sort -rn | head -5
# Sum a column of numbers
awk '{ s += $1 } END { print s }' numbers.txt
# Parallel work with xargs (4 at a time)
find . -name '*.log' -print0 | xargs -0 -n1 -P4 gzip
# Print each line of a file with its line number
nl -ba file.txt or awk '{ print NR": "$0 }' file.txt
# Get your public IP and time it
time curl -s ifconfig.me
# Show all listening ports (with the process, if you use sudo)
sudo ss -tulpn
# Lock a script so it cannot run twice at once
exec 9>/tmp/myjob.lock; flock -n 9 || { echo "already running"; exit 1; }
Bash vs other shells
sh (POSIX) The minimal standard. No arrays, no [[ ]], no <( ). Use #!/bin/sh only if you need that portability (Debian's /bin/sh is dash)
bash The default on most Linux systems. Most tutorials and scripts assume it
zsh The default on macOS. Similar to bash, with different globbing and array indexing (starts at 1)
fish Friendly and modern, but NOT compatible with bash scripts. See the Fish sheet
Write scripts in bash (or sh) so they run everywhere. Use fish or zsh interactively if you like them better.
Tips
Quote every variable: "$var", "${array[@]}", "$(command)". Unquoted variables are the source of most script bugs.
Start scripts with #!/usr/bin/env bash and set -Eeuo pipefail, and add a trap to clean up temporary files.
Use [[ ]] and (( )) in bash scripts. They are safer and easier than [ ].
Run shellcheck on every script. It finds real bugs the moment you save.
Use local variables in functions, and pass results back with echo and $(...), not global variables.
Avoid parsing ls. Use globs, find with -print0, and while IFS= read -r.
Test destructive commands with echo first, or use a temp folder. Use cd dir || exit so a failed cd does not run the next command in the wrong place.
Learn the shortcuts: Ctrl+R, Ctrl+A, Ctrl+E, Ctrl+W, Alt+. and !! save more time than any tool.
Keep aliases, functions and the prompt in ~/.bashrc, and keep that file in git so every machine feels the same.
For anything long or complicated, consider Python. Bash is at its best gluing commands together, not doing heavy logic.