Skip to content
Published at:

Bash小结

最近项目脚本又开始重构,写了不少Bash脚本,索性写小结归纳下;

脚本并不难

我自己写脚本是早前有个项目是用脚本构建的,就把那个几百行的脚本翻了一下,边看边查,看他做了什么?是怎么一步步构建项目的?后面自己写项目就开始写自己项目的脚本了。我说脚本不难是因为:

  • 我自己命令行用的相对比较熟,自己也写过一些命令;知道敲一条命令大概做了什么(forkexecwait
  • 脚本没有什么抽象能力:不像其它编程语言,有自定义数据类型、接口、多线程这些。脚本就只有一些简单的表达式:if-elseforcasefunction,然后就没有了
  • 脚本有些不太好理解的地方是:还有些单个字符的选项(命令选项之类)、还有很多特殊的符号;对于前者单个选项字符的很多都是单词缩写;如果命令行用的很熟,会解决一部分(用过了),剩下的边学边查
  • 很多时候,不是脚本难不难的问题,是你对脚本中使用的命令熟不熟的问题
  • 最后一个是思维层面的:我们不用把所有的弄懂了在去开始写代码,就像我们不必学完所有的汉字才开始交流和写文章,learning by doing,边学边做

Hello world

bash
#!/bin/bash
echo "hello bash"

在通常的shell脚本中,井号#用作注释行。但这里的第一行是个例外,#后面的惊叹号会告诉shell用哪个shell来运行脚本,它也有特定的语法和称呼,叫Shebang。语法:

bash
#!interpreter [optional-arg]

Shebang工作机制:在类Unix系统上,程序加载器会分析#!后面的内容,并将它做为解释器,在将该文件路径作为该解释器的参数。类似下面这样:

bash
$ bash hello.sh

解释类语言都是可以这样运行,比如python的:python hello.py,js的:node hello.js

运行Hello world

bash
# 方式1
$ bash hello.sh

# 方式2:加可执行权限,运行
$ chmod u+x hello.sh
$ ./hello.sh

变量

bash
# 定义
NAME="John"

# 取值
echo $NAME
echo "$NAME"
echo "${NAME}"

echo "Hi $NAME!"

命令替换

bash
# $()`格式
echo "I'm in $(pwd)"

# 反引号字符(`)
echo "I'm in `pwd`"

WORKING_DIR=$(pwd)
WORKING_DIR=`pwd`
echo "WORKING_DIR: ${WORKING_DIR}"

if-else分支

bash
if [[ -z "$string" ]]; then
  echo "String is empty"
elif [[ -n "$string" ]]; then
  echo "String is not empty"
fi

Case选择

bash
case "$1" in
  start | up)
    vagrant up
    ;;

  *)
    echo "Usage: $0 {start|stop|ssh}"
    ;;
esac

循环

bash
# 遍历目录
for i in /etc/rc.*; do
  echo $i
done

# 遍历文件行
cat file.txt | while read line; do
  echo $line
done

# C-like 循环
for ((i = 0 ; i < 100 ; i++)); do
  echo $i
done

# 范围循环
for i in {1..5}; do
    echo "Welcome $i"
done

# 范围循环,带step size(一次循环后增加step size,下面是5)
for i in {5..50..5}; do
    echo "Welcome $i"
done

# while循环
while true; do
  ···
done

函数

bash
# 函数定义:直接写函数名
myfunc() {
    echo "hello $1"
}

# 函数定义:使用了function关键字
function myfunc() {
    echo "hello $1"
}

myfunc "John"

参数

  • $#:参数个数
  • $*:所有参数(当成一个整体的字符串)
  • $@:所有参数(当成多个独立分开的字符串)
  • $1:第一个参数
  • $2:第二个参数
  • $_:上一个命令的最后一个参数
bash
function foo() {
    echo "Number of arguments: $#"                              #=> 6
    echo "All positional arguments (as a single word): $*"      #=> 1 2 3 4 5 6
    echo "All positional arguments (as separate strings): $@"   #=> 1 2 3 4 5 6
    echo "First argument: $1"                                   #=> 1
    echo "Second argument: $2"                                  #=> 2
    echo "------------------------------"
    echo "last command args"
    echo "Last argument of the previous command: $_"            #=> last command args

    echo "------------------------------"
    for VAR in "$*"; do
        echo "A single word $VAR"                               #=> "1 2 3 4 5 6"
    done

    echo "------------------------------"
    for VAR in "$@"; do
        echo "separate strings $VAR"                            #=> "1" "2" "3" "4" "5" "6"
    done

}

foo 1 2 3 4 5 6

条件执行

bash
git commit && git push              # 前面的命令执行成功,然后执行后面的命令
git commit || echo "Commit failed"  # 前面的命令执行成功,不执行后面的命令

大小写转换

bash
STR="HELLO WORLD!"
echo ${STR,}   #=> "hELLO WORLD!" (lowercase 1st letter)
echo ${STR,,}  #=> "hello world!" (全部转小写)

STR="hello world!"
echo ${STR^}   #=> "Hello world!" (uppercase 1st letter)
echo ${STR^^}  #=> "HELLO WORLD!" (全部转大写)

数字比较

  • [[ NUM -eq NUM ]]:是否相等(Equal)
  • [[ NUM -ne NUM ]]:是否不等(Not equal)
  • [[ NUM -lt NUM ]]:是否小于(Less than)
  • [[ NUM -le NUM ]]:是否小于并等于(Less than or equal)
  • [[ NUM -gt NUM ]]:是否大于(Greater than)
  • [[ NUM -ge NUM ]]:是否大于并等于(Greater than or equal)
  • [[ STRING =~ STRING ]]: (Regexp)
  • (( NUM < NUM )): (Numeric conditions)

字符串比较

  • [[ -z STRING ]]:字符串长度是否为0(表示是空字符串)(Zero)
  • [[ -n STRING ]]:字符串长度是否不为空(Not)
  • [[ STRING == STRING ]]:字符串是否相同(Equal)
  • [[ STRING != STRING ]]:字符串是否不相同(Not Equal)

文件比较

  • [[ -e FILE ]]:检查FILE是否存在(e:Exists)
  • [[ -r FILE ]]:检查FILE是否可读(Readable)
  • [[ -h FILE ]]:检查FILE是否存在 Symlink
  • [[ -d FILE ]]:检查FILE是否存在并是一个目录(Directory)
  • [[ -w FILE ]]:检查FILE是否可写(Writable)
  • [[ -s FILE ]]:检查FILE是否存在并非空(Size)
  • [[ -f FILE ]]:检查FILE是否存在并是一个文件(File)
  • [[ -x FILE ]]:检查FILE是否存在并可执行(Executable)
  • [[ FILE1 -nt FILE2 ]]:检查FILE1是不是比FILE2更新(nt:Newer then)
  • [[ FILE1 -ot FILE2 ]] 检查FILE1是不是比FILE2更老(ot:Older then)
  • [[ FILE1 -ef FILE2 ]] 检查FILE1和FILE2是否相同(ef:Equal file)

其它比较

  • [[ -o noclobber ]]:这个noclobber选项是否开启
  • [[ ! EXPR ]]:Not
  • [[ X && Y ]]:And
  • [[ X || Y ]]:Or

数组

bash
# declarations
Fruits=('Apple' 'Banana' 'Orange')

# assignment
Fruits[0]="Apple"
Fruits[1]="Banana"
Fruits[2]="Orange"

# Working with arrays
echo ${Fruits[0]}           # 获取第0个元素
echo ${Fruits[-1]}          # 获取最后一个元素
echo ${Fruits[@]}           # 获取所有元素,每个是独立分开的
echo ${#Fruits[@]}          # 获取元素个数,数组长度
echo ${#Fruits}             # 获取每一个元素字符串的长度
echo ${#Fruits[3]}          # 获取第三个元素字符串的长度
echo ${Fruits[@]:3:2}       # 获取数组的一段Range范围 (from position 3, length 2)
echo ${!Fruits[@]}          # 获取所有元素的Index索引:0、1、2...

# Operations
Fruits=("${Fruits[@]}" "Watermelon")    # 添加元素
Fruits+=('Watermelon')                  # 也是添加元素
Fruits=( ${Fruits[@]/Ap*/} )            # 通过正则去删除
unset Fruits[2]                         # 删除元素
Fruits=("${Fruits[@]}")                 # 复制到一个新的数组
Fruits=("${Fruits[@]}" "${Veggies[@]}") # 追加数组
lines=(`cat "logfile"`)                 # 从文件中读,一行是一个元素的数组

# Iteration
for i in "${arrayName[@]}"; do
  echo $i
done

选项

https://www.gnu.org/software/bash/manual/html_node/The-Set-Builtin.html

  • set -o noclobber:避免重定向覆盖文件
  • set -o errexit:用于出错时退出,避免级联错误
  • set -o pipefail:在管道连接的命令中,只要有任何一个命令失败(返回值非0),则整个管道操作被视为失败
  • set -o nounset:使用了未定义的变量,立马退出

特殊变量

  • $?:上一个命令的退出状态
  • $!:上一个后台任务的PID
  • $$:执行当前shell的PID
  • $0:执行当前shell的文件名 Filename of the shell script
  • $_:上一个命令的最后一个参数

其它常用命令

  • command
  • pushd、popd
  • basename
  • ...

Ref:

Updated at: