Link

Shell Script in 30 Minutes

References

Table of contents

  1. ssh
  2. Variable
  3. Numbers / Strings / Timestamps
  4. Array
  5. Globbing
  6. Pipe
  7. Function
  8. Comparison
  9. Control
  10. Interpreter Files
  11. Best Practices
#!/bin/bash

#######################################
# Delete a file in a sophisticated manner.
# Arguments:
#   File to delete, a path.
# Returns:
#   0 if thing was deleted, non-zero on error.
#######################################
function del_thing() {
  rm "$1"
}

If you are writing a script that is more than 100 lines long, or that uses non-straightforward control flow logic, you should rewrite it in a more structured language now!!!!

ssh

apt-get update
apt-get install -y locales
locale-gen "en_US.UTF-8"
update-locale LC_ALL="en_US.UTF-8"

## Then, add the following lines to ~/.bashrc and ~/.profile: 
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
export LANGUAGE=en_US.UTF-8
ssh-keygen -t rsa -b 4096 -C "wongck@pku.edu.cn"
ssh-keygen -o -a 100 -t ed25519 -f ~/.ssh/id_ed25519

ssh-copy-id id@server -p [port] # by default, use ~/.ssh/id_rsa
ssh-copy-id id@server -p [port] -i ~/.ssh/id_ed25519.pub

Variable

# 'Single' quotes indicate that no substitution is desired.
# "Double" quotes indicate that substitution is required/tolerated.


# This bash script is used to backup a user's home directory to /tmp/.

user=$(whoami)
input=/home/$user
output=/tmp/${user}_home_$(date +%Y-%m-%d_%H%M%S).tar.gz
echo "$user"  # wck
echo '$user'  # $user

tar -czf $output $input
echo "Backup of $input completed! Details about the output backup file:"
ls -l $output

Numbers / Strings / Timestamps

counter=0
let counter+=1
let all=$all+$arch_files+$arch_directories

# substring s[2..]
s=$(echo $s | cut -c2-)

i=64
let i=i/31
echo $i  # 2

# built-in Arithmetic Expansion substitution in POSIX shells
$((0xbc))         # 188
$((1+2+5*2))      # 13
$((16#55))        # 85
$((10#85))        # 55

printf '%x\n' 85  # 55
echo 'obase=16; 9999999999999999999999' | bc  # 21E19E0C9BAB23FFFFF

echo 416E64726F69644C6162 | xxd -r -p         # AndroidLab

filename=$(basename "$fullfile")
extension="${filename##*.}"
spath="$(dirname "$(readlink -f "$0")")"
spath="$(dirname "$(realpath "$0")")"  # preferred

date +"%Y-%m-%d %H:%M:%S"  # 2020-05-13 12:38:13
date +"%s"                 # 1598082285
date -d '04/05/2017 11:13:00' +"%s"  # 1491361980
date -d '04/05/2017 11:13:00'        # depends on the system language
date -d @1491361980 +"%Y-%m-%d %H:%M:%S"  # 2017-04-05 11:13:00

Array

declare -a my_array # indexed array
declare -A my_array # associative array

my_array=(foo bar)
my_array[0]=foo
for i in "${my_array[@]}"; do echo "$i"; done  # => foo bar
for i in "${!my_array[@]}"; do echo "$i"; done # => 0 1
echo "the array contains ${#my_array[@]} elements"

my_array+=(baz)

Globbing

mv *{.py,.sh} folder
touch {foo,bar}/{a..j}

Pipe

Process Substitution

If the >(list) form is used, writing to the file will provide input for list. If the <(list) form is used, the file passed as an argument should be read to obtain the output of list.

2> /dev/null
&> /dev/null   # both stdout and stderr

# <( CMD ) will execute CMD and place the output in a 
# temporary file and substitute the <() with that file’s name. 
diff <(ls foo) <(ls bar) 

# >( CMD ) appears as a file and writing to this file provides
# input for CMD as STDIN
echo "example" | tee >(xargs mkdir) >(wc -c)

# Here Documents, similar to cat file, while the file content contains asdf\nqwerty
cat << END
asdf
qwerty
END

# Here Strings (a simpler form of Here Documents)
cat <<< abc

echo "to stdout"
err "to stderr"

# disable buffer
# -oL means only buffer lines when writing to stdout
( echo "LINE 1" ; sleep 1 ; echo "LINE 2" ; ) | stdbuf -oL grep LINE | cat

Function

function total_files {
  local name="$1"
  
  (( $? == 0 )) || return
  
  find $1 -type f | wc -l
  echo $0  # the name of the function
  echo $1 $2 $4
  echo $#  # number of args
  echo $@  # all args (expands to a separate word)
  echo $*  # all args (expands to a single word)
  echo $$  # the PID
}

total_files $input

A comprehensive list can be found here.

  • $? - Return code of the previous command
  • $$ - PID for the current script
  • !! - Entire last command, including arguments. sudo !!
  • $_, !$ - Last argument from the last command. If you are in an interactive shell, you can also quickly get this value by typing Esc followed by .

Comparison

 numericstring
less than-lt<
 -ne!=
 -eq=
 -leN/A

0 signals true, 1 signals false

test EXPRESSION
[ EXPRESSION ]
[[ EXPRESSION ]]  ## preferred over [ ]
(( EXPRESSION ))  ## preferred over [[ ]]

[ 1 -ne 2 ]
echo $?  # => 0

# http://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Bash-Conditional-Expressions
if [ ! -d "/home/$1" ]; then
[ -z "${MAX_CARNUM}" ] && MAX_CARNUM=18
if [ ! -z "$PULL" ]; then

Control

last_line='NULL'
while read line; do
  if [[ -n "${line}" ]]; then
    last_line="${line}"
  fi
done < <(your_command)
# This will output the last non-empty line from your_command
echo "${last_line}"


if [ $num_a -lt $num_b ]; then
	echo "haha"
elif
	:   # null command (like `pass` in python)
else
  :
fi

for i in 1 2 3; do
for i in $(ls); do
for directory in $*; do
    echo $i
done

until [ $counter -lt 3 ]; do
while [ $a -lt 11 ]; do
        echo $a
        let a+=2
done

while getopts ":o:h" flag; do
    case "${flag}" in
        f) files="${OPTARG}" ;;
        h)
            usage
            exit 0
        ;;
        *)
            usage
            error unknow option
            exit 1
        ;;
    esac
done

Interpreter Files

If we have a file filename start with

#! pathname [ optional-argument ]

Running this file with arguments, acutally you are running command:

pathname [ optional-argument ] filename arguments

Best Practices

trap 'kill $(jobs -p)' EXIT
# Watch out to use single ', to prevent the shell from substituting the $() immediately.

trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
# kill -- -$$ sends a SIGTERM to the whole process group, thus killing also descendants.

set -x  # print before execution
set -e  # die on error

# perl -n assume "while (<>) { ... }" loop around program
# perl -a autosplit mode with -n or -p (splits $_ into @F)
# perl -e evaluate
cat /tmp/sslparams.log | cut -d ' ' -f 2,2 | sort | uniq -c | sort -rn | perl -ane 'printf "%30s %s\n", $F[1], "="x$F[0];'

# tr -s " " Compress a series of identical characters (space here) to a single character
kill -9 $(ps aux | grep h2o | tr -s " " | cut -f 2 -d ' ')

# read: BASH builtin for retrieving data from standard input.
# read -r : Do not let backslash (\) act as an escape character
# read variable: Store data that you type from the keyboard
cat file.txt | sed '/^[[:space:]]*$/d' | while read -r a; do echo $a; done

cat a b | sort | uniq -d > c   # c is a intersect b
cat a b b | sort | uniq -u > c   # c is set difference a - b 

# DEFAULT_PATH=/usr/local/sbin:/usr/local/bin
for path in $(echo "$DEFAULT_PATH" | /bin/sed "s/:/\\n/g"); do

# set default value
[ -z "${MAX_CARNUM}" ] && MAX_CARNUM=18

# install tar
sudo tar xvf package.tar.xz --directory=/usr/local --strip-components=1

# prepend string
awk '{print "prefix " $0}'
sed -e "s/.*/prefix &/"

# Print the fifth and the second column
# Use space as delimiter by default
awk -F , '{print $5,$2}'
# or cut -d',' -f5,2

# sum 
awk{ x += $3 } END { print x }# filter
awk '$3>10'

# -S buffer size
# -n numeric
# -k, --key=POS1[OPTS][,POS2]     start a key at POS1 (origin 1), end it at POS2
#                          (default end of line)
sort -S 4G -k1n,1 -k5n,5

Sed Tutorial:

# subsititution
sed s/day/night/

# use & as the matched string
$ echo "123 abc" | sed 's/[0-9]*/& &/' 
123 123 abc

# extended regex (-E / -r depends on the distribution)
# -n don't print the original lines
sed -rn '/([a-z]+) \1/p' # duplicated words

# command
/g # global
/I # ignore cases
/d # delete
/p # print

# script
#!/bin/sed -f
s/a/A/g
s/e/E/g
s/i/I/g
s/o/O/g
s/u/U/g