Shell Mastery — Alternate Commands

File Creation (without touch)

touch (the obvious way)
touch file.txt
Redirect (empty file)
> file.txt
echo
echo "hello" > file.txt
printf (more portable than echo)
printf '%s\n' "hello" > file.txt
Heredoc (multi-line config files)
cat << 'EOF' > file.conf
server {
    listen 80;
    root /var/www;
}
EOF
install (set permissions at creation)
install -m 0644 /dev/null file.txt
install -m 0600 /dev/null secret.key
install -d -m 0755 new_directory
dd (create file of specific size)
dd if=/dev/zero of=file.bin bs=1M count=10 2>/dev/null
truncate
truncate -s 0 file.txt     # empty existing file
truncate -s 1M file.bin    # create 1MB sparse file
tee (create + display)
echo "content" | tee file.txt
echo "more" | tee -a file.txt   # append
cp from /dev/null
cp /dev/null file.txt

Reading Files (without cat)

less / more
less file.txt
more file.txt
head / tail
head -20 file.txt
tail -20 file.txt
tail -f log.txt        # follow
awk (print entire file)
awk '{print}' file.txt
awk '1' file.txt           # shortest awk to print all
sed
sed '' file.txt
sed -n '1,20p' file.txt    # lines 1-20
while read loop
while IFS= read -r line; do echo "$line"; done < file.txt
Redirect to stdin
< file.txt grep "pattern"
mapfile/readarray (into array)
mapfile -t lines < file.txt
echo "${lines[0]}"   # first line
echo "${#lines[@]}"  # total lines

Conditionals — [[ ]] vs [ ] vs test

[[ ]] is the bash/zsh enhanced test. Always prefer it over [ ] in scripts.

Why [[ ]] is better
# [ ] is a command — needs careful quoting
[ "$var" = "value" ]         # works
[ $var = "value" ]           # BREAKS if var is empty or has spaces

# [[ ]] is a keyword — handles quoting internally
[[ $var = "value" ]]         # always works, even if var is empty
[[ $var == "value" ]]        # == also works in [[ ]]
Pattern matching (only in [[ ]])
[[ $file == *.txt ]] && echo "text file"
[[ $host == web-* ]] && echo "web server"
[[ $ip =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "valid IP"
Regex matching (only in [[ ]])
if [[ $line =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2}) ]]; then
    year="${BASH_REMATCH[1]}"
    month="${BASH_REMATCH[2]}"
    day="${BASH_REMATCH[3]}"
    echo "Date: $year-$month-$day"
fi
Logical operators
# [ ] uses -a and -o (fragile)
[ "$a" = "1" -a "$b" = "2" ]

# [[ ]] uses && and || (readable)
[[ $a == "1" && $b == "2" ]]
[[ $a == "1" || $b == "2" ]]
String tests
[[ -z $var ]]          # empty
[[ -n $var ]]          # not empty
[[ $a == $b ]]         # equal
[[ $a != $b ]]         # not equal
[[ $a < $b ]]          # alphabetical (no quoting needed in [[ ]])
File tests
[[ -f file.txt ]]      # regular file exists
[[ -d /path ]]         # directory exists
[[ -e anything ]]      # anything exists
[[ -r file.txt ]]      # readable
[[ -w file.txt ]]      # writable
[[ -x script.sh ]]     # executable
[[ -s file.txt ]]      # exists and non-empty
[[ -L link ]]          # is symlink
[[ file1 -nt file2 ]]  # file1 newer than file2
[[ file1 -ot file2 ]]  # file1 older than file2
Arithmetic (use instead)
(( count > 10 )) && echo "too many"
(( count++ ))
(( total = a + b * c ))
(( RANDOM % 6 + 1 ))    # dice roll
Competition patterns — one-liner conditionals
# Check if command exists
[[ $(command -v jq) ]] && echo "jq available" || echo "jq missing"

# Check if file is non-empty before processing
[[ -s data.csv ]] && awk -F, '{print $1}' data.csv || echo "empty file"

# Check if variable looks like an IP
[[ $host =~ ^[0-9]+\. ]] && echo "IP" || echo "hostname"

# Check if running as root
[[ $EUID -eq 0 ]] && echo "root" || echo "not root"

# Check if in a git repo
[[ -d .git ]] || git rev-parse --git-dir > /dev/null 2>&1 && echo "git repo"

Directory Creation (without mkdir)

mkdir (obvious)
mkdir -p /path/to/deep/dir
install -d
install -d -m 0755 /path/to/dir
Using a command that creates dirs as side effect
# rsync creates destination dirs
rsync -a /dev/null /path/to/dir/

# git init creates .git + working dir
git init /new/repo

Process Execution Alternatives

Run a command
ls                    # direct
command ls            # bypass aliases/functions
builtin cd            # use builtin, not function
env ls                # fresh environment
exec ls               # replace shell with command
Background & parallel
cmd &                 # background
cmd1 & cmd2 & wait    # parallel with wait
nohup cmd &           # survive logout
setsid cmd            # new session
Subshell vs current shell
(cd /tmp && ls)        # subshell — doesn't change your pwd
{ cd /tmp && ls; }     # current shell — DOES change your pwd

Permissions (without chmod)

chmod (obvious)
chmod 755 script.sh
chmod u+x script.sh
install (set perms at copy time)
install -m 0755 script.sh /usr/local/bin/
cp with --preserve
cp --preserve=mode source dest
umask (set default for new files)
umask 022    # new files: 644, new dirs: 755
umask 077    # new files: 600, new dirs: 700