这篇文章不是为了方便大家从入门到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
循环结构
主要关键字有while
和for
两个, 同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等操作的脚本