Shell一天入门手册

Posted by mingo on 2019-05-03 16:11

这篇文章不是为了方便大家从入门到rm -rf /跑路的, 而是提高大家工作效率的, 要想回家早, 工具用的好; 学会Shell脚本后, 我们就可以把工作中常见的重复,简单的操作都整成一个个命令, 即能提升效率, 还能装逼

shell是一门脚本语言, 属于解析型语言, 也就是解析器读一行执行一行, 直到读取完脚本里的代码或发生错误中断执行; 常见的解释器有sh, bash, zsh, csh; 本文代码都在bash下测试通过

在shell脚本里可以直接调用*unix系统里其它工具, 如find, grep, sed, awk, cut, sort…, 跟在命令行下调用一样的方式, 没有其它额外学习成本, 这是它最大的优势

语法

本文对于一些数据类型,作用域这些比较枯燥的专业问题没有做解释, 因为这些太无聊了, 遇到问题再去查看其它文章的介绍

也没有对功能A有X种用法进行扩展, 没必要记住所有的用法, 只用记住最常用的用法即可, 我们不需要精通Shell

至于为什么不能返回字符串, 为什么不能并发, 为什么=左右不能有空格等类问题, 这个就是语法的范畴, 他说必须这样你就这样写就行了, 没有道理可讲

对于类似为什么要定义方法, 每个方法的逻辑边界, 变量起名/方法的属于编程经验范畴的问题也没有过多解释, 因为默认你有基本的编程素养, 所以本文适合有一些编程经验, 但不熟悉Shell语法的人; 主要用于编写脚本时忘记了一些语法来查阅, 能尽量用代码说话的我都用代码代替了

总的来讲, shell的语法还是比较少的, 比较简单; 但是有些规定比较死, 看起来比较没道理, 这些地方我都有标注提醒

普通变量

定义变量或修改变量值

1
2
3
4
5
# 定义字符串
name="China"

# 定义数字
age=18

需要注意的是: = 两边不能空格

变量作用域是全局的

使用变量

1
2
3
4
5
6
7
8
9
10
# 直接$+变量名, 就可以读取变量值
$age

# 也可以大括号把变量名包起来
${name}

# 这里就必须要加大括号, 不然就是读取skillScript变量值
echo "I am good at ${skill}Script"

# 使用${}还可以设置默认值, 具体见下面的`字符串的一些常见操作`小节

如果变量不存在, 返回””; 不会报错

普通数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 定义, 所有的元素放在一个括号里, 用空格分开
citys=("深圳" "广州" "珠海")

# 读取某个元素, 下标从0开始; 如果下标越界, 返回""不报错
# output: 深圳
echo "city: ${citys[0]}"

# 添加一个或多个
citys+=("佛山" "北京")

# 按下标删除元素
unset 'citys[0]' 'citys[1]'

# 获取数组全部元素
${citys[@]}

# 获取数组长度, 比上面多了个#
${#citys[@]}

关联数组

类型Python中的Dict, Java中的Map

bash4+才支持, 我懒的升级了, 以后补上

删除变量

1
2
# 只要是变量都可以删除, 包括数组,关联数组
unset name citys

关于为什么要删除变量, 这个关系到变量的作用域问题, 这里不做过度探讨

还有在循环体内定义的数组, 可能需要每次通过重新定义来达到清空旧值的效果

控制流程

顺序结构

从上往下, 一行一行的码, 就是顺序结束, 脚本里最多的就是这样的结构

变量的定义, 读取, 赋值在这里做;

算法术运算, 日志, 输入, 输出等逻辑也是在此实现

方法调用, 系统工具, 系统API调用也是在此

选择结构

怎么解释都显的罗嗦, 直接看下面代码来体会下if的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
if [ "${我爸}" == "马云" ]; then
    echo "垛手不用停"

elif [ "${我爸}" == "Pony马" ]; then
    echo "氪金不用停"

elif [ "${我爸}" == "王健林" ]; then
    echo "嫩模换不停"

else
    echo "工作不能停"

fi

坑爹的地方是: [, ] 两边都要写至少一个空格

上面的代码可以读作, 如果我爸是马云, 就垛手不用停; 如果我爸是Pony马, 就氪金不用停; 如果我爸是王健林, 就嫩模换不停; 否则(都不是以上的人), 就工作不能停

现在换case语法来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case "${我爸}" in
    "马云")
        echo "垛手不用停"
        ;;
    "Pony马")
        echo "氪金不用停"
        ;;
    "王健林")
        echo "嫩模换不停"
        ;;
    *)
        echo "工作不能停"
        ;;
esac    

整个代码块只会匹配其中一个分支, 从上往下匹配, 找到一个就停止匹配

逻辑运算

if, elif 里面的代码必须是一个逻辑条件, 也就是结果必须是bool类似, 也就是0/1, 真/假, 对/错这种

逻辑运算比较常见, 俗称断言, 如: 他是男的, 他有钱, 他来自乡下, 这种结果只能是对和错两种;

  • 关系判断

如果A, B两个变量都是数字, 他们之间的关系就有小于, 小于等于, 等于, 不等于, 大于等于, 大于这6种

由于在*unix里, ><有特殊作用(输出输入重定向), 不能直接使用这些符号, 所以每个操作符分别搞了个别名, 对应关系见下表

操作符 示例 作用
-lt $A -lt $B 判断A是否小于B
-le $A -le $B 判断A是否小于等于B
-eq $A -eq $B 判断A是否等于B
-ne $A -ne $B 判断A是否不等于B
-ge $A -ge $B 判断A是否大于等于B
-gt $A -gt $B 判断A是否大于B

当然在[[里是可以直接使用了>, <, 但是还是有坑, 见test, [], [[]]的区别小节, 推荐算术比较还是用别名

如果A, B两个变量都是字符串, 他们之间的关系就只有等于,不等于,为空, 不为空这4种

操作符 示例 作用
== “$A” == “$B” 判断A是否等于B
!= “$A” != “$B” 判断A是否不等于B
-z -z “$A” 判断A为空
-n -n “$A” 判断A不为空
  • 文件判断

Shell特有的, 用来判断文件是否存在, 是否可执行等类似需求, 具体如下表

符号 示例 作用 助记词
-e -e ${file_name} 判断file_name的的文件是否存在 exist
-f -f ${file_name} 判断file_name的是否是文件 file
-w -w ${file_name} 判断file_name的是否可写 write
-r -r ${file_name} 判断file_name的是否可读 read
-x -x ${file_name} 判断file_name的是否可执行 execute
-s -s ${file_name} 判断file_name的是文件且不空 size
-d -d ${file_name} 判断file_name的是目录 directory
-c -c ${file_name} 判断file_name的是字符文件 char
-b -b ${file_name} 判断file_name的是块文件 block
-L -L ${file_name} 判断file_name的是链接文件 link

更多比较符可见man test手册

  • 逻辑电路

A, B两个都是逻辑值, 他们之间可以有并且(与), 或者(或), 不是(非) 3种比较方式

使用逻辑电路一般是对上面的关系判断, 文件判断进行组合, 达到更精准更强大的逻辑判断

符号 别名 示例 作用
&& -a $A -ge 10 -a $A -lt 20 判断A是否大于等于10并且小于20
|| -o $A == 1 -o $A == 2 判断A是否等于1或2
! ! ! ${结束} 判断有无结束; 对变量结束取反

test, [], [[]]的区别

test expr[ expr ] 作用是一样的, 推荐使用[]

现在是我知道的[[]][]一些区别

  • [是bash的内部命令, [[是bash的关键字, [[结构比[结构更加通用, 功能更强大

    [是内部命令, >, <, &, |这些都是有特殊作用的, 所以在[里不能直接使用>, <, &, |这些字符, 或者说你用了这些符号跟你想要的作用不同

  • [[支持字符串的模式匹配, 使用=~操作
  • [[里逻辑比较可以使用&&, ||, 比较自然; 并且是逻辑短路操作, [不是
  • 同上, [[可以使用>, < 这些符号

    但是[[也比较蛋疼, 有个神坑

1
2
3
4
5
6
7
8
9
10
11
[[ 12 > 5 ]]; echo $?
# output: 1, 也就是假

[[ 52 > 5 ]]; echo $?
# output: 0, 也就是真

[[ 12 -gt 5 ]]; echo $?
# output: 0, 也就是真

# 看懂没, 如果使用>, <这样的符号, 是按字符串的比较方式, 从左到右逐位逐位的比, 所以12>5其实是1跟5比, 而不是12跟5比
# 数字之间的比较还是老实用 -lt, -gt
  • [[字符串比较少了hack写法

    [中, 字符串比较如果直接用 $name == “GZ”, 当没有name没有定义或name为空时, 会报unary operator expected错误;

    通常用”$name” == “GZ”这样的hack写法, 新手遇到问题后, 可能还搞不清楚为什么错了;

    [[中, 就可以$name == “GZ”这样写, 没上面的问题

结论: 优先使用[[

$(), $(())的区别

这2个长的很像, 但区别很大, 简单来讲, $()用来执行命令, $(())用来计算表达式

  • $(expr), expr只能是命令

执行括号里的命令并把结果返回给等号左边的变量

功能上跟``差不多, 但是$()要直观些, ``不注意看还以为是单引号; ``的好处是所有*Unix都支持, $()就不一定了

1
2
3
4
5
6
7
8
9
10
11
# 获取当前目录下所有文件列表, files就是数组, 里面包含了当前目录下每个文件名
files=$(find . -type f maxdepth 1)

# 获取当前时间, 并按yyyy-MM-dd格式化, 用%F更简单点
cur_date=$(date +"%Y-%m-%d")

# 读取abc.txt的内容并赋值给content
content=$(cat abc.txt)

# 作用跟``类似
local_ip=`ifconfig |grep "inet" |grep -v "inet6" |grep -v "127.0.0.1" |awk '{print $2}'`
  • $(( exper ))

POSIX标准的扩展计算, 也就是说只要符合C的运算符都可用在$((exp)),包括三目运算符; 若是逻辑判断,表达式exp为真则为1,假则为0

1
2
3
4
5
6
7
8
9
10
# age + 1
age=$((age+1))

# 另一种方式
let age++

# 与上面等价
let age=age+1

# 其它例子, 发挥想像力吧, 只要C中有的, 都可以整过来试试

看起来let需要敲的字符更少, 所以我推荐用$(()), 因为C

循环结构

主要关键字有whilefor两个, 同C类似

循环结构是用在以下场景, 这些场景本质上都是相同的:

  • 一直做某事直到XXX为止
  • 反复做某事X次
  • 对集合里的每个元素都执行XXX处理

先来浪漫下, 如何用程序表达我爱你直到天荒地老

1
2
3
4
5
6
# 在这个需求下, 使用while比较直观
# 当条件为真, 就一直do
while [[ "${}" != "荒" && "${}" != "老" ]]; 
do
    echo "我爱你"
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 执行X次, 俗称fori
# 使用for来实现执行X次的方式

# 方法一: C风格
for ((i=1; i <= 3; i++))
do
    echo "call with C, times: $i"
done

# 方法二: Python风格
for i in {1..3}
do
    echo "call with Python, times: $i"
done

# 方法三: Seq
for i in `seq 1 3`
do
    echo "call with seq, times: $i"
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 遍历集合, 俗称foreach
# 输出当前目录下的所有文件的大小, 不包含子目录

total=0
for file in $(find . -type f -maxdepth 1)
do
    size=$(du -hk $file |awk '{print $1}')
    total=$((total + size))

    echo "file: $file, size: $size, total: $total"
done

echo "-> total size: $total"

# 写完代码, 再回头看看, 代码里无缝调用find, du, awk, 也没有IO打开/关闭的工作
# 想想如果用Python实现, 
# 首先获取当前目录这个文件句柄, 
# 然后获取当前目录下所有子文件, 如果没有API还要手撸一个, 
# 然后获取文件大小并累加, 最后关闭文件流
# 使用with+API代码量差不多,甚至还要少些
# 只能说, 各有千秋吧

循环配合交互式输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#! /bin/bash

index=0
gos=()  # 用于收集指定目录下所有可用go版本
for _go in $(find /data/services -type d -name "go1.*")
do
    echo "${index}) ${_go}" # 用于给用户展示,输入想要的go版本对应的index
    let index=$((index+1))
    gos+=(${_go})
done

read choice  # 使用read函数获取用户输入
choice_go=${gos[$choice]}  # 得到index对应的go版本
echo "your choice go version: ${choice_go}"
rm -rf /usr/lib/golang  # 删除旧的软链接
ln -s ${choice_go} /usr/lib/golang  // 生成新版本的软链接

break, continue 简单说下, 支持, 跟你想的效果一样

function

把公共的代码抽取出来, 封装成一个方法, 后续只用通过方法名来调用此代码

通过定义方法这个技术, 我们可以提高代码复用率, 逻辑边界划分, 代码更好维护

如果只是定义些工具方法, 还是可以接受的; 如果逻辑的重叠, 抽取公共代码, 就要考虑考虑是不是不应该用Shell来实现了

定义

1
2
3
func_name() {

}

就这么简单, 不用写参数列表, return也可以不写(相当于隐式调用了return 0)

可以显式通过return X指定返回值, 返回值只能是整数且在0~127之间

看起来很容易是吧, 但是真用起来, 一堆坑, 下面会具体会讲

估计当初设计的时候, 也没想让它承担复杂的功能

参数传递

如果有参数, 在方法体内通过$1, $2, $3…这样获取在方法调用时传过来的参数; 跟脚本获取命令行参数是一样一样的

这里又有个神坑:

1
2
3
4
5
6
7
8
# 本来期望的调用方式, 在funcA里把name -> $1, age -> $2
funcA $name $age 

# 但是如果name你传了2个单词, 假设name="pony ma"
# 脚本直接把上面的name替换成pony ma, 变成下面这样
funcA pony ma 50 

# 结果就把pony -> $1, ma -> $2; 这当然不是我们想要的

解决方法, 修改默认内部域分隔符IFS

1
2
3
4
5
6
7
BIFS=$IFS  # 保存当前的IFS
IFS=,      # 临时改成,分隔; 只能指定单个字符

# 这样$name就不会被成2个参数了
funcA $name $age 

IFS=$BIFS   # 恢复到之前的IFS

这个解决方法一句话评价: hack, 还能接受

如果$name里包含”,”就换个其它分隔符, 如果你不知道$name不会包含哪些字符, 这分隔符还没法选了

解决方法就只能在外面把${name}里的空格都替换为其它字符X, 然后在funcA里再把X替换回空格;

这个方法跟修改IFS的方法表面上是一样的, 区别在于IFS只能是一位, 这里的X可以写很长, 这样碰到的概率就可以降到很小很小

这个解决方法一句话评价: hack, 丑陋, 侵入性非常高, 我是接受不了

目前还不知道有更优雅的解决方案

返回值

如果想返回其它类型的数据, 目前优雅的方法是通过全局变量的方式来实现; 还好Shell脚本都是单线程执行, 也无线程安全问题

类似代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 只返回一个数据的情况
ret=""

func_with_single_ret() {
    # xxxx
    ret="a_value"
    # xxxx
}

# call
func_with_single_ret

# get result
v=${ret}

# 返回多个数据的情况
rets=()

func_with_multi_ret() {
    # xxxx

    # 先清空
    rets=()
    rets+=("a" "b" "c")
    # xxxx
}

# call 
func_with_multi_ret

vs=(${rets[@]})

算了, 不想评价了

调用

1
2
# 参数之间默认是空格分隔
func_name param1 param2 ...

这是唯一让我满意的地方的了, 方法的调用跟系统工具的调用是一致的, 体验非常好

for example

1
2
3
4
5
6
7
8
# 我们定义一个类似日志形式的输出
# 需要包含当前日志, 脚本名
log() {
    echo "[$(date +"%F %T")] [$(cd $(dirname .) |pwd)/$0] $@"
}

# 调用
log "业务A" "成功"

一些不太常见的设置/命令/工具, 统一放这里说明

长这样 Usage 作用
set set -e 在”set -e”之后出现的代码,一旦出现了返回值非零,整个脚本就会立即退出, 强迫症
set set +e 关闭-e选项
set set -u 变量未定义就使用, 脚本会报错而退出; 严格模式, 默认是返回””
set set -x 调试模式, 会打印出执行的每行代码及运行时值, 跟bash -x script.sh一样效果

常见特殊符号及意义

一些乱七八糟的符号

$系

主要跟脚本/函数参数相关

长这样 作用
$0 当前脚本名
$n n>0, 表示第n个参数
$# 参数个数
$* 所有参数,不包括$0
$@ 所有参数,不包括$0
$? 上个命令退出状态/函数返回值
$$ 当前Shell脚本的进程ID

$跟$@都是代表所有参数, 区别在于”$”, “$@”

假设参数是a, b, c, 则

”$*” -> “a b c”

”$@” -> “a” “b” “c”

其它

长这样 场景 作用
0 逻辑判断 条件为真; 同理,非0为假
0 函数返回状态 执行成功; 同理, 非0为失败
1 命令执行 标准输出, 经典写法: 2>&1
2 命令执行 标准错误输出
& 命令结尾 后台执行
&& 命令连接 A && B; if(A) B的简写
> 结果保存 输出重定向
< 读取数据 输入重定向
| 命令连接 管道
~ 路径 当前用户的Home目录

唉, 我看了下键盘上每个符号, 好像每个都有特殊功能, 有的还不只一种功能, 以后有空再补充吧

调试

人脑编译

简单的脚本, 肉眼review下, 看看整体逻辑是否完备, 分支是否匹配, 括号是否匹配, 代码缩进, =左右是否有空格, if后面的分号, then关键字有无遗漏, 上面列的坑是否又双叕掉进去了

日志

肉眼看累了, 在关键地方加几条日志, 一般至此, 一些小BUG都能解决了

bash -x

终极大杀器, 打印出执行时每一行的上下文信息; 一般都是语法层面的巨坑了

缺点就是打印的太多了, 你要找到想找的信息稍微有点费力

一些常用的代码片段

字符串转数字

1
2
3
4
5
6
7
a="023"

# 使用(())
a=$((a+0))

#使用let
let a=a

数字转字符串

数字会默认做字符串处理; 变量用单引号’‘变字符串

1
2
3
v=123

t='$v'

变量的一些常见操作, 可以理解为Shell库级API

又是一堆符号组合, 神奇又死板

读取并设置初始值类

写法 含义
${var} 直接返回var的值, 不管var有无定义, 初始化
${var-DEFAULT} 如果var没有被声明, 那么就以DEFAULT作为其值
${var:-DEFAULT} 如果var没有被声明或者其值为空, 那么就以DEFAULT作为其值
${var=DEFAULT} 如果var没有被声明, 那么就以DEFAULT作为其值
${var:=DEFAULT} 如果var没有被声明或者其值为空, 那么就以DEFAULT作为其值
${var+OTHER} 如果var声明了, 那么其值就是OTHER, 否则就为””
${var:+OTHER} 如果var被设置了, 那么其值就是OTHER, 否则就为””
${var?ERR_MSG} 如果var没被声明, 那么就打印ERR_MSG
${var:?ERR_MSG} 如果var没被设置, 那么就打印ERR_MSG
${!varprefix*} 匹配之前所有以varprefix开头进行声明的变量
${!varprefix@} 匹配之前所有以varprefix开头进行声明的变量

就一个变量默认值搞这么多花活, 有这功夫不去填其它的巨坑

字符串操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
local="china GuangDong Sz"
# ${#string}, string的长度
echo ${#local}
# output: 18

# ${string:start}, 在string中, 从位置start开始提取子串
echo "city:${local:16}"
# output: city:Sz

# ${string:start:length}, 在string中, 从位置start开始长度为length, 提取子串
echo "contry:${local:0:5}"
# output: contry:china

# ${string#substring}, 左侧最短删
# ${string##substring}, 左侧最长删
# ${string%substring}, 右侧最短删
# ${string%%substring}, 右侧最长删
# 删除子串, 左边删, 右边删, 最长删, 最短删; 两两组合, 所以有四种方式
# substring可以是个正则表达式; 这里也是坑, 正则表达式也不是标准的, *表示任意个任意字符, 类似ls -lh *
text=abc12342341
echo "${text#a*3}"
# output: 42341
echo "${text##a*3}"
# output: 41
echo "${text%3*1}"
# output: abc12342
echo "${text%%3*1}"
# output: abc12

# ${string/substring/replacement}, 使用replacement替换string中第一个substring
# ${string//substring/replacement}, 使用replacement替换string中全部substring
# ${string/#substring/replacement}, 如果string的前缀匹配substring, 那么就用replacement来代替匹配到的substring
# ${string/%substring/replacement}, 如果string的后缀匹配substring, 那么就用replacement来代替匹配到的substring
# 字符串替换, 其中substring可以是个正则表达式
echo "${text/2/#}"
# output: abc1#342341
echo "${text//2/#}"
# output: abc1#34#341
echo "${text/#abc/xyz}"
# output: xyz12342341
echo "${text/%341/xyz}"
# output: abc12342xyz

其它可能有用的字符串操作

1
2
3
4
5
# 假如有个数据的每行格式很规律, col1:col2:col3:col4:col5, 每列用:分隔
# 现在想只对第3列进行处理, 怎么取出来

line="col1:col2:col3:col4:col5"
f3=$(echo $line |cut -d ":" -f 3)

命令行下输出有颜色的文本

一般用在脚本欢迎界面, 警告, 出错提示等场景

1
2
3
4
5
6
# 格式: echo -e "\033[背景颜色;前景颜色m字符串\033[0m" 
# 显示绿底,红字
echo -e "\033[42;31mHello, Shell\033[0m" 

# 默认背景, 红字
echo -e "\033[31mHello, Shell\033[0m" 

前景色的范围: 30 ~ 37, 背景色的范围: 40 ~ 47, 对应关系如下:

种类 黑色 红色 绿色 黄色 蓝色 紫色 天蓝 白色
前景色 30 31 32 33 34 35 36 37
背景色 40 41 42 43 44 45 46 47

其中\033[0m叫控制选项, 既然是选项, 就不只\033[0m这一个了, 所有选项及作用如下:

optio n 功能
\033[0m 关闭所有设置
\033[1m 设置高亮度
\033[4m 下划线
\033[5m 闪烁
\033[7m 反显
\033[8m 消隐
\033[Fm 设置前景色, F在30~37
\033[B;Fm 设置背,前景色, B在40~47
\033[nA 光标上移n行
\033[nB 光标下移n行
\033[nC 光标右移n行
\033[nD 光标左移n行
\033[y;xH 设置光标位置
\033[2J 清屏
\033[K 清除光标到行尾的内容
\033[s 保存光标位置
\033[u 恢复光标位置
\033[?25l 光标左移n行
\033[?25h 光标左移n行

了解了这些, 就可以使用echo实现各种特效的显示了

关于脚本执行的标准姿势

1
nohup bash /path/to/script.sh arg1 arg2... > /path/to/run.log 2>&1 &

最后的经验

读取外部文件, 输出到外部文件的路径都用绝对路径

你无法预料脚本的执行方式是./script.sh, 还是绝对路径/path/to/script.sh(比如在cron中定时执行)

使用相对路径就很容易出错

长期运行的定时任务脚本适当的打印日志, 关键步骤的返回值

Shell脚本有个”优点”, 死的静悄悄, 没有一点现场痕迹, 我们只能自己保留现场了

忘记字符串处理, 函数, 函数返回值这些高级东西

太恶心了, 典型的, 引入的新问题比解决的问题还多

脚本应该是组合各种工具完成一个更高级, 更复杂的功能;

如果涉及数据的运算和处理, 如split, substr, index等, 就已经超纲了, 果断换Python吧

不要写太长

整个脚本代码不要太长, 短小精干最好, 超过100行的, 都很难懂了

shell的语言表达能力还是偏弱的, 数据类型少, 类型之间转换麻烦, 函数返回值只能是整数, 库函数少, 还有僵硬的语法, 导致稍微复杂点的功能用Shell就会导致代码行数突涨

另外由于*Unix历史原因, 喜欢用各种符号来简写或代表各种设备/功能, 整个脚本充斥着各种符号; 还有自身的BUG导致的hack方法处理

各种因素相互叠加, 导致整个脚本的可读性不是太好

过一段时间就很容易忘记当时为什么要这样hack处理; 短点的脚本, 还可以重头再读一次, 长的脚本, 读起来就很痛苦了

复杂逻辑不适合用shell来实现, 它还是适合做些第一步做XXX, 第二步做YYY,第三步做ZZZ这些简单的逻辑

不要想用Shell实现所有看似简单实则超过他能力的事

俗称强撸, 新手常犯的毛病, 手上拿个锤子, 看什么都是钉子

有些任务看似简单, 比如对字符串的各种花式处理, Shell做起来就很吃力;

有些场景如果能用高级数据结构很容易处理的, 如map, set, 还是换Python吧, 合适的工具来做合适的事吧

典型的应用场景:

  • 项目的脚手架脚本, 编译,打包,部署这个功能
  • 服务端运维类脚本, 各软件bin目录下提供start, stop, restart, reload等操作的脚本