# Shell脚本 中文速查表

Shell 是一个应用程序,它连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核,这就是 Shell 的本质。

Shell 也是一种编程语言,它的编译器(解释器)是 Shell 这个程序。我们平时所说的 Shell,有时候是指连接用户和内核的这个程序,有时候又是指 Shell 编程。

# 变量操作

varname=value             # 定义变量
varname=value command     # 定义子进程变量并执行子进程
echo $varname             # 查看变量内容
echo $$                   # 查看当前 shell 的进程号
echo $!                   # 查看最近调用的后台任务进程号
echo $?                   # 查看最近一条命令的返回码
export VARNAME=value      # 设置环境变量(将会影响到子进程)

array[0]=valA             # 定义数组
array[1]=valB
array[2]=valC
array=([0]=valA [1]=valB [2]=valC)   # 另一种方式
array=(valA valB valC)               # 另一种方式

${array[i]}               # 取得数组中的元素
${#array[@]}              # 取得数组的长度
${#array[i]}              # 取得数组中某个变量的长度

declare -a                # 查看所有数组
declare -f                # 查看所有函数
declare -F                # 查看所有函数,仅显示函数名
declare -i                # 查看所有整数
declare -r                # 查看所有只读变量
declare -x                # 查看所有被导出成环境变量的东西
declare -p varname        # 输出变量是怎么定义的(类型+值)

${varname:-word}          # 如果变量不为空则返回变量,否则返回 word
${varname:=word}          # 如果变量不为空则返回变量,否则赋值成 word 并返回
${varname:?message}       # 如果变量不为空则返回变量,否则打印错误信息并退出
${varname:+word}          # 如果变量不为空则返回 word,否则返回 null
${varname:offset:len}     # 取得字符串的子字符串

${variable#pattern}       # 如果变量头部匹配 pattern,则删除最小匹配部分返回剩下的
${variable##pattern}      # 如果变量头部匹配 pattern,则删除最大匹配部分返回剩下的
${variable%pattern}       # 如果变量尾部匹配 pattern,则删除最小匹配部分返回剩下的
${variable%%pattern}      # 如果变量尾部匹配 pattern,则删除最大匹配部分返回剩下的
${variable/pattern/str}   # 将变量中第一个匹配 pattern 的替换成 str,并返回
${variable//pattern/str}  # 将变量中所有匹配 pattern 的地方替换成 str 并返回

${#varname}               # 返回字符串长度

*(patternlist)            # 零次或者多次匹配
+(patternlist)            # 一次或者多次匹配
?(patternlist)            # 零次或者一次匹配
@(patternlist)            # 单词匹配
!(patternlist)            # 不匹配

array=($text)             # 按空格分隔 text 成数组,并赋值给变量
IFS="/" array=($text)     # 按斜杆分隔字符串 text 成数组,并赋值给变量
text="${array[*]}"        # 用空格链接数组并赋值给变量
text=$(IFS=/; echo "${array[*]}")  # 用斜杠链接数组并赋值给变量

A=( foo bar "a  b c" 42 ) # 数组定义
B=("${A[@]:1:2}")         # 数组切片:B=( bar "a  b c" )
C=("${A[@]:1}")           # 数组切片:C=( bar "a  b c" 42 )
echo "${B[@]}"            # bar a  b c
echo "${B[1]}"            # a  b c
echo "${C[@]}"            # bar a  b c 42
echo "${C[@]: -2:2}"      # a  b c 42  减号前的空格是必须的

$(UNIX command)           # 运行命令,并将标准输出内容捕获并返回
varname=$(id -u user)     # 将用户名为 user 的 uid 赋值给 varname 变量

num=$(expr 1 + 2)         # 兼容 posix sh 的计算,使用 expr 命令计算结果
num=$(expr $num + 1)      # 数字自增
expr 2 \* \( 2 + 3 \)     # 兼容 posix sh 的复杂计算,输出 10

num=$((1 + 2))            # 计算 1+2 赋值给 num,使用 bash 独有的 $((..)) 计算
num=$(($num + 1))         # 变量递增
num=$((num + 1))          # 变量递增,双括号内的 $ 可以省略
num=$((1 + (2 + 3) * 2))  # 复杂计算

# 条件判断

兼容 posix sh 的条件判断:man test

statement1 && statement2  # and 操作符
statement1 || statement2  # or 操作符

exp1 -a exp2              # exp1 和 exp2 同时为真时返回真(POSIX XSI扩展)
exp1 -o exp2              # exp1 和 exp2 有一个为真就返回真(POSIX XSI扩展)
( expression )            # 如果 expression 为真时返回真,输入注意括号前反斜杆
! expression              # 如果 expression 为假那返回真

str1 = str2               # 判断字符串相等,如 [ "$x" = "$y" ] && echo yes
str1 != str2              # 判断字符串不等,如 [ "$x" != "$y" ] && echo yes
str1 < str2               # 字符串小于,如 [ "$x" \< "$y" ] && echo yes
str2 > str2               # 字符串大于,注意 < 或 > 是字面量,输入时要加反斜杆
-n str1                   # 判断字符串不为空(长度大于零)
-z str1                   # 判断字符串为空(长度等于零)

-a file                   # 判断文件存在,如 [ -a /tmp/abc ] && echo "exists"
-d file                   # 判断文件存在,且该文件是一个目录
-e file                   # 判断文件存在,和 -a 等价
-f file                   # 判断文件存在,且该文件是一个普通文件(非目录等)
-r file                   # 判断文件存在,且可读
-s file                   # 判断文件存在,且尺寸大于0
-w file                   # 判断文件存在,且可写
-x file                   # 判断文件存在,且执行
-N file                   # 文件上次修改过后还没有读取过
-O file                   # 文件存在且属于当前用户
-G file                   # 文件存在且匹配你的用户组
file1 -nt file2           # 文件1 比 文件2 新
file1 -ot file2           # 文件1 比 文件2 旧

num1 -eq num2             # 数字判断:num1 == num2
num1 -ne num2             # 数字判断:num1 != num2
num1 -lt num2             # 数字判断:num1 < num2
num1 -le num2             # 数字判断:num1 <= num2
num1 -gt num2             # 数字判断:num1 > num2
num1 -ge num2             # 数字判断:num1 >= num2

# 分支控制

if 和经典 test,兼容 posix sh 的条件判断语句

test {expression}         # 判断条件为真的话 test 程序返回0 否则非零
[ expression ]            # 判断条件为真的话返回0 否则非零

test "abc" = "def"        # 查看返回值 echo $? 显示 1,因为条件为假
test "abc" != "def"       # 查看返回值 echo $? 显示 0,因为条件为真

test -a /tmp; echo $?     # 调用 test 判断 /tmp 是否存在,并打印 test 的返回值
[ -a /tmp ]; echo $?      # 和上面完全等价,/tmp 肯定是存在的,所以输出是 0

test cond && cmd1         # 判断条件为真时执行 cmd1
[ cond ] && cmd1          # 和上面完全等价
[ cond ] && cmd1 || cmd2  # 条件为真执行 cmd1 否则执行 cmd2

# 判断 /etc/passwd 文件是否存在
# 经典的 if 语句就是判断后面的命令返回值为0的话,认为条件为真,否则为假
if test -e /etc/passwd; then
    echo "alright it exists ... "
else
    echo "it doesn't exist ... "
fi

# 和上面完全等价,[ 是个和 test 一样的可执行程序,但最后一个参数必须为 ]
# 这个名字为 "[" 的可执行程序一般就在 /bin 或 /usr/bin 下面,比 test 优雅些
if [ -e /etc/passwd ]; then   
    echo "alright it exists ... "
else
    echo "it doesn't exist ... "
fi

# 和上面两个完全等价,其实到 bash 时代 [ 已经是内部命令了,用 enable 可以看到
[ -e /etc/passwd ] && echo "alright it exists" || echo "it doesn't exist"

# 判断变量的值
if [ "$varname" = "foo" ]; then
    echo "this is foo"
elif [ "$varname" = "bar" ]; then
    echo "this is bar"
else
    echo "neither"
fi

# 复杂条件判断,注意 || 和 && 是完全兼容 POSIX 的推荐写法
if [ $x -gt 10 ] && [ $x -lt 20 ]; then
    echo "yes, between 10 and 20"
fi

# 可以用 && 命令连接符来做和上面完全等价的事情
[ $x -gt 10 ] && [ $x -lt 20 ] && echo "yes, between 10 and 20"

# 小括号和 -a -o 是 POSIX XSI 扩展写法,小括号是字面量,输入时前面要加反斜杆
if [ \( $x -gt 10 \) -a \( $x -lt 20 \) ]; then
    echo "yes, between 10 and 20"
fi

# 同样可以用 && 命令连接符来做和上面完全等价的事情
[ \( $x -gt 10 \) -a \( $x -lt 20 \) ] && echo "yes, between 10 and 20"


# 判断程序存在的话就执行
[ -x /bin/ls ] && /bin/ls -l

# 如果不考虑兼容 posix sh 和 dash 这些的话,可用 bash 独有的 ((..)) 和 [[..]]:
https://www.ibm.com/developerworks/library/l-bash-test/index.html

# 流程控制

while / for / case / until

# while 循环

while condition; do
    statements
done

i=1
while [ $i -le 10 ]; do
    echo $i; 
    i=$(expr $i + 1)
done

# for 循环

上面的 while 语句等价

for i in {1..10}; do
    echo $i
done

for name [in list]; do
    statements
done

# for 列举某目录下面的所有文件

for f in /home/*; do 
    echo $f
done

# bash 独有的 (( .. )) 语句,更接近 C 语言,但是不兼容 posix sh

for (( initialisation ; ending condition ; update )); do
    statements
done

# 和上面的写法等价

for ((i = 0; i < 10; i++)); do echo $i; done

# case 判断

case expression in 
    pattern1 )
        statements ;;
    pattern2 )
        statements ;;
    * )
        otherwise ;;
esac

# until 语句

until condition; do
    statements
done

# select 语句

select name [in list]; do
  statements that can use $name
done

# 函数

# 定义一个新函数

function myfunc() {
    # $1 代表第一个参数,$N 代表第 N 个参数
    # $# 代表参数个数
    # $0 代表被调用者自身的名字
    # $@ 代表所有参数,类型是个数组,想传递所有参数给其他命令用 cmd "$@" 
    # $* 空格链接起来的所有参数,类型是字符串
    {shell commands ...}
}

myfunc                    # 调用函数 myfunc 
myfunc arg1 arg2 arg3     # 带参数的函数调用
myfunc "$@"               # 将所有参数传递给函数
myfunc "${array[@]}"      # 将一个数组当作多个参数传递给函数
shift                     # 参数左移

unset -f myfunc           # 删除函数
declare -f                # 列出函数定义

# 有用的函数

# 自动解压

判断文件后缀名并调用相应解压命令

function q-extract() {
    if [ -f $1 ] ; then
        case $1 in
        *.tar.bz2)   tar -xvjf $1    ;;
        *.tar.gz)    tar -xvzf $1    ;;
        *.tar.xz)    tar -xvJf $1    ;;
        *.bz2)       bunzip2 $1     ;;
        *.rar)       rar x $1       ;;
        *.gz)        gunzip $1      ;;
        *.tar)       tar -xvf $1     ;;
        *.tbz2)      tar -xvjf $1    ;;
        *.tgz)       tar -xvzf $1    ;;
        *.zip)       unzip $1       ;;
        *.Z)         uncompress $1  ;;
        *.7z)        7z x $1        ;;
        *)           echo "don't know how to extract '$1'..." ;;
        esac
    else
        echo "'$1' is not a valid file!"
    fi
}

# 自动压缩

判断后缀名并调用相应压缩程序

function q-compress() {
    if [ -n "$1" ] ; then
        FILE=$1
        case $FILE in
        *.tar) shift && tar -cf $FILE $* ;;
        *.tar.bz2) shift && tar -cjf $FILE $* ;;
        *.tar.xz) shift && tar -cJf $FILE $* ;;
        *.tar.gz) shift && tar -czf $FILE $* ;;
        *.tgz) shift && tar -czf $FILE $* ;;
        *.zip) shift && zip $FILE $* ;;
        *.rar) shift && rar $FILE $* ;;
        esac
    else
        echo "usage: q-compress <foo.tar.gz> ./foo ./bar"
    fi
}

# 语法高亮

漂亮的带语法高亮的 color cat ,需要先 pip install pygments

function ccat() {
    local style="monokai"
    if [ $# -eq 0 ]; then
        pygmentize -P style=$style -P tabsize=4 -f terminal256 -g
    else
        for NAME in $@; do
            pygmentize -P style=$style -P tabsize=4 -f terminal256 -g "$NAME"
        done
    fi
}

# 常用技巧

# 列出最常使用的命令

history | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head

# 列出所有网络状态

ESTABLISHED / TIME_WAIT / FIN_WAIT1 / FIN_WAIT2 
netstat -n | awk '/^tcp/ {++tt[$NF]} END {for (a in tt) print a, tt[a]}'

# mount 文件系统

通过 SSH 来 mount 文件系统

sshfs name@server:/path/to/folder /path/to/mount/point

# 显示前十个运行的进程

显示前十个运行的进程并按内存使用量排序

ps aux | sort -nk +4 | tail

# 在右上角显示时钟

while sleep 1;do tput sc;tput cup 0 $(($(tput cols)-29));date;tput rc;done&

# 从网上的压缩文件中解出一个文件来

从网络上的压缩文件中解出一个文件来,并避免保存中间文件

wget -qO - "http://www.tarball.com/tarball.gz" | tar zxvf -

# 测试性能

# 测试处理器性能

python -c "import test.pystone;print(test.pystone.pystones())"

# 测试内存带宽

dd if=/dev/zero of=/dev/null bs=1M count=32768

# Linux下挂载一个 iso 文件

mount /path/to/file.iso /mnt/cdrom -oloop

# 主机 A 直接ssh到主机 B

ssh -t hostA ssh hostB

# 下载一个网站的所有图片

wget -r -l1 --no-parent -nH -nd -P/tmp -A".gif,.jpg" http://example.com/images

# 快速创建项目目录

mkdir -p work/{project1,project2}/{src,bin,bak}

# 按日期范围查找文件

find . -type f -newermt "2010-01-01" ! -newermt "2010-06-01"

# 显示当前正在使用网络的进程

lsof -P -i -n | cut -f 1 -d " "| uniq | tail -n +2

# Vim 中保存一个没有权限的文件

:w !sudo tee > /dev/null %

# 在 .bashrc / .bash_profile 中加载另外一个文件(比如你保存在 github 上的配置)

source ~/github/profiles/my_bash_init.sh

# 反向代理

将外网主机(202.115.8.1)端口(8443)转发到内网主机 192.168.1.2:443

ssh -CqTnN -R 0.0.0.0:8443:192.168.1.2:443  user@202.115.8.1

# 正向代理

将本地主机的 8443 端口,通过 192.168.1.3 转发到 192.168.1.2:443

ssh -CqTnN -L 0.0.0.0:8443:192.168.1.2:443  user@192.168.1.3

# socks5 代理

把本地 1080 端口的 socks5 的代理请求通过远程主机转发出去

ssh -CqTnN -D localhost:1080  user@202.115.8.1

# 终端下正确设置 ALT 键和 BackSpace 键

http://www.skywind.me/blog/archives/2021

# References

https://github.com/skywind3000/awesome-cheatsheets/blob/master/languages/bash.sh
https://github.com/Idnan/bash-guide
http://www.linuxstall.com/linux-command-line-tips-that-every-linux-user-should-know/
https://ss64.com/bash/syntax-keyboard.html
http://wiki.bash-hackers.org/commands/classictest
https://www.ibm.com/developerworks/library/l-bash-test/index.html
https://www.cyberciti.biz/faq/bash-loop-over-file/
https://linuxconfig.org/bash-scripting-tutorial
https://github.com/LeCoupa/awesome-cheatsheets/blob/master/languages/bash.sh
https://devhints.io/bash
https://github.com/jlevy/the-art-of-command-line
https://yq.aliyun.com/articles/68541