Linux 脚本基础

记录一些平时看到或用过的 shell 脚本技巧和代码片段。

基础

命令行光标跳转及其他方便操作的快捷键

这是一个小 trick,但是我发现有不少人不知道。

1
2
3
4
5
6
Ctrl+a: 跳到命令行首
Ctrl+e: 跳到命令行尾
Ctrl+u: 删除光标至命令行首的内容
Ctrl+k: 删除光标至命令行尾的内容
Ctrl+<- 跳到前一个单词首部
Ctrl+-> 跳到后一个单词尾部
1
2
3
4
5
Ctrl+l  清屏(clear)

Esc+. (键盘按键)上一个命令的最后一个参数

!$ (命令)上一个命令的最后一个参数,用法如:ls !$

管道

重定向>和>>

环境变量

语句和命令

将执行结果赋值给变量

有两种方式:

1
2
3
4
PARM=`echo 'HELLO WORLD'`
echo $PARM
PARM=$(pwd)
echo $PARM

‘与”之间的区别

单引号'和双引号"都可以用于将数值转换为字符串,但如果希望将命令的返回值转换为字符串,则必须使用"

1
2
3
4
huanggaochi@app03:~$ echo '$(date)'
$(date)
huanggaochi@app03:~$ echo "$(date)"
Fri Jan 10 12:32:07 CST 2020

后台运行

有些命令在执行时会不断的在屏幕上输出信息,影响到我们继续输入命令了,此时便可以在执行这条命令前,将这段命令的最后面添加个”&”符号,那么从一开始执行该命令就会是在后台执行。

1
docker run mysql &

写脚本前的准备工作

默认登录 shell

在 Linux 操作系统,/bin/bash是默认登录 shell,是在创建用户时分配的,可以使用 chsh 命令改变默认 shell:

1
2
chsh <用户名> -s <新shell>
chsh huanggaochi -s /bin/sh

#!/bin/bash

位于脚本的第一行,称为释伴(shebang)行,这里#叫作 hash、!叫作 bang,shebang 用于显式声明命令通过/bin/bash 来执行。

脚本执行权

使用 chmod 命令来使脚本可执行:

1
chmod a+x test.sh

禁止使用未定义的变量,避免粗心导致脚本逻辑错误

1
2
3
4
5
#!/usr/bin/env bash
# 在脚本开头声明
set -o nounset
a=1 # 去掉这一行后执行会报错
printf $a

禁止出错后继续执行,避免错误影响面被继续扩大

1
2
3
4
5
6
#!/usr/bin/env bash
set -o errexit
# 执行其他shell脚本,如果出错会直接退出,一个最简单的报错比如没有sp.sh的执行权限,此时会报错:./sp.sh: Permission denied
# 如果没有加set -o errexit,会输出“执行sp结束”
./sp.sh
printf 执行sp结束

一些命令——比如 rm 的-f 参数——可以强制忽略错误,此时脚本无法捕捉到 errexit,这样的参数在脚本中并不推荐使用。

将标准输出和错误输出同时重定向到同一位置

1
2
3
ls /etc/passwd > out.txt 2>$1
或者
ls /etc/passwd &> out.txt

语法

系统定义变量

系统变量是由系统系统自己创建的,这些变量通常由大写字母组成,可以通过“set”命令查看。

用户定义变量

用户变量由系统用户来生成和定义,变量的值可以通过命令“echo $<变量名>”查看。

内建变量

1
2
3
4
5
6
7
$0 命令行中的脚本名字
$1 第一个命令行参数
$2 第二个命令行参数
...
$9 第9个命令行参数
$# 命令行参数的数量
$#### 所有命令行参数,以空格隔开

命令建议:变量名大写、局部变量小写,函数名小写,命名需要体现变量、函数本身的作用,不能随便命名。

取消变量或取消变量的赋值

1
2
3
name=1
unset name
echo $name

执行算数运算

1
2
3
4
# 使用expr命令执行算数运算
expr 5 + 2
# $[]表达式
result=$[16 + 4]

if 语句

可以使用-eq、-ne、-gt、-lt、-ge、-le 比较两个数字的大小。

1
2
3
4
5
6
7
8
x=10
y=10
if [[ $x -gt $y ]]
then
echo 'x > y'
else
echo 'y > x'
fi

判空方法就比较多了:

1
2
3
4
if [[ ! $x ]]
if [[ ! -n "$x" ]]
if [[ test -z $x ]]
if [[ "$x" == "" ]]

case 语句

1
2
3
4
5
6
7
8
9
10
11
12
x=1
case $x in
1)
echo 'x=1'
;;
2)
echo 'x=2'
;;
*)
echo 'nothing'
;;
esac

while 循环语句

1
2
3
4
5
6
i=0
while [[ $i -lt 10 ]]
do
echo $i
i=$(($i+1))
done

遍历一个列表:

1
2
3
4
5
6
7
8
9
i=0
while true; do
# 提取列表中一个item数据,根据数据格式不同,提取方式会有变化,下面以读取json文件为例:
item=$(./jq --argjson i $i -r '.items[$i]' data.json)
if [[ "$item" == "" || "$item" == "null" ]]; then
break
fi
echo $item
done

for 循环语句

1
2
3
4
for i in 1 2 3
do
echo $i
done

function 函数

1
2
3
4
5
6
7
8
9
10
11
function isEmpty() {
if [[ "$1" == "" || "$1" == "null" ]];then
return 1
fi
return 0
}

isEmpty '123'
if [[ $? -eq 1 ]];then
echo "isEmpty"
fi

readonly、local

顾名思义,readonly 限制变量只读,local 限制变量生命周期在函数范围内。

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env bash
set -o nounset
set -o errexit

test() {
readonly x=1
local y=2
echo $x
echo $y
}
test
echo $x
echo $y # 这条语句会报错

使用$()代替`

1
2
echo "A-`echo B-\`echo C-\\\`echo D\\\`\``"
echo "A-$(echo B-$(echo C-$(echo D)))"

$()支持内嵌,且不用转义,而且`和’看起来很像,容易混淆。

使用[[]]代替[]

用单中括号(下面命令只是作为示范,没有实际意义):

1
[ "${name}" \> "a" -o ${name} \< "m" ]

用双中括号:

1
[[ "${name}" > "a" && "${name}" < "m" ]]

[[]]可以避免转义问题,且可以使用以下功能:
||:逻辑OR
&&:逻辑AND
<:字符串比较(不需要转义)
==:通配符匹配
=~:正则表达式匹配

1
2
3
4
5
t="abc123"
[[ "$t" == abc#### ]] # true 通配符
[[ "$t" == "abc*" ]] # false 字面值比较
[[ "$t" =~ [abc]+[123]+ ]] # true 正则表达式
[[ "$t" =~ "abc*" ]] # false 字面值比较

封装,提升内聚性和可读性

以日志函数为例:

1
2
3
4
5
6
7
8
9
[function] log() { # classsic logger
local prefix="[$(date +%Y/%m/%d\ %H:%M:%S)]: "
echo "${prefix} $@" >&2
# return 只返回不会打印出来plainplainplainplainplainplainplainplainplainplainplainplainplain
[return i;]
}

log "INFO" "a message"
echo $?

调试

除了 echo 外 shell 还有以下工具用于调试 shell 脚本。

$?

这个命令可以检查前一命令的结束状态,返回 0 表示执行成功了。

1
2
3
4
huanggaochi@app08:~$ ls /etc/passwd
/etc/passwd
huanggaochi@app08:~$ echo $?
0

脚本调试

bash 提供了一些方便调试的选项:

1
2
3
4
# 跟踪脚本里每个命令的执行,并附加扩充信息
sh -x test.sh
# 对脚本进行语法检查,并跟踪脚本里每个命令的执行
sh -nv test.sh

也可以直接在脚本里设置:

1
2
set -o verbose
set -o xtrace

test

test 命令可以用来测试脚本:

1
2
3
4
5
6
7
-d 如果文件存在并且是目录,返回 true
-e 如果文件存在,返回 true
-f 如果文件存在并且是普通文件,返回 true
-r 如果文件存在并可读,返回 true
-s 如果文件存在并且不为空,返回 true
-w 如果文件存在并可写,返回 true
-x 如果文件存在并可执行,返回 true

应用

输入

1
2
3
echo 'Please enter your name'
read name
echo "My name is $name"

这里不能用单引号’包裹$name,否则会被解析为字符串

排序

如果输出只有一列:

1
sort test.txt

但是默认情况下是按照字典序排序的,如果数据本身是数字,则需要明确指明:

1
sort -n test.txt

按第 2 列排序:

1
sort -n -t ' ' -k 2 test.txt

去重

sort 命令也支持去重:

1
sort -u test.txt

uniq 命令专门用来去重,不过针对的是连续出现的相同记录:

1
sort test.txt | uniq

按行读取文件

写法 1:

1
2
3
4
5
#!/bin/bash
while read line
do
echo $line
done < filename(待读取的文件)

写法 2:

1
2
3
4
5
#!/bin/bash
cat filename(待读取的文件) | while read line
do
echo $line
done

写法 3:

1
2
3
4
for line in `cat filename(待读取的文件)`
do
echo $line
done

for 读取和 while 读取是有区别的:

1
2
3
4
5
6
7
8
9
10
11
12
$ cat file
1
2 3

$ 使用 while 读取
1
2 3

$ 使用 for 读取
1
2
3

遍历一个目录下的所有文件

如果碰到文件夹则递归搜索,碰到文件则执行自定义操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function read_dir() {
for file in `ls $1`; do
if [[ -d $1"/"${file} ]]; then
read_dir $1"/"${file}
elif [[ ${file} != *md ]]; then
# 跳过不符合匹配条件的文件plainplainplainplainplainplainplainplainplainplain
echo '忽略文件:'$1"/"${file}
else
# 处理该文件
echo $1"/"${file}
fi
done
}

read_dir $1

统计每秒

Linux Shell统计每秒钟内文件增加行数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
DATE=$(date +%s)
count=$(grep -c "" short.txt)
while true
do
DATE_New = $(date +%s)
if (( $(date +%s) == DATE+1))
then

DATE = $(date +%s)
count_new = $(grep -c "" short.txt)
add = $((count_new - count))
if [[ ! -n "$add" ]]
then
add = 0
fi
echo add line number is:$add
count = $count_new
fi
done

统计行数

1
2
3
grep "过滤" a.txt b.txt | wc -l
# 直接用 grep 的-c 参数统计可以快一点,而且能分别统计每个文件的行数
grep "过滤" -c a.txt b.txt

正则表达式

捕获 IP 和分组,分组使用括号包围,可以通过数组${BASH_REMATCH}来获得:

1
2
3
4
5
6
7
8
9
if [[ $ip =~ ^([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.([0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$ ]];then
echo "match"
echo ${BASH_REMATCH[1]}
echo ${BASH_REMATCH[2]}
echo ${BASH_REMATCH[3]}
echo ${BASH_REMATCH[4]}
else
echo "Not match"
fi

按行读取并提取:

1
2
3
4
5
6
7
8
9
while read line
do
# 每一行格式为"a,b,c"plain
if [[ $line =~ ^\"(.+)\",\"(.+)\",\"(.+)\"$ ]];then
echo ${BASH_REMATCH[1]}-${BASH_REMATCH[2]}-${BASH_REMATCH[3]}
else
echo "Not match"
fi
done < data.txt

统计机器参数

查看占用 CPU 最多的进程

除了使用 top 命令外,还可以使用 ps 命令导出一份快照:

1
ps H -eo pid,pcpu | sort -rnk2 | head

找最耗 CPU 的进程对应的服务名

可以使用 ps 命令或直接查询 proc 文件:

1
2
ps aux | fgrep {pid}
ll /proc/{pid}

查询某个端口的连接情况

可以使用 netstat 或 lsof 查询已打开的文件列表:

1
2
netstat -lap | fgrep {port}
lsof -l {port}

网络

1
2
# 通过 HTTP 请求查询用户信息
curl -s http://localhost:8080/userinfo?name=Mike\&channel=WECHAT -o userinfo.json

日期操作

1
2
3
4
# 当前时间格式化
date "+%Y-%m-%d %H:%M:%S"
# 把日期中的前缀 0 去掉
date '+%-H:%-M:%-S'

加号表示欲显示的格式,可分为日期和时间两种,日期格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
%a : 星期几 (Sun..Sat)
%A : 星期几 (Sunday..Saturday)
%b : 月份 (Jan..Dec)
%B : 月份 (January..December)
%c : 直接显示日期和时间
%d : 日 (01..31)
%D : 直接显示日期 (mm/dd/yy)
%h : 同 %b
%j : 一年中的第几天 (001..366)
%m : 月份 (01..12)
%U : 一年中的第几周 (00..53) (以 Sunday 为一周的第一天的情形)
%w : 一周中的第几天 (0..6)
%W : 一年中的第几周 (00..53) (以 Monday 为一周的第一天的情形)
%x : 直接显示日期 (mm/dd/yyyy)
%y : 年份的最后两位数字 (00.99)
%Y : 完整年份 (0000..9999)

时间格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
%%: 打印出%
%n : 下一行
%t : 跳格
%H : 小时(00..23)
%k : 小时(0..23)
%l : 小时(1..12)
%M : 分钟(00..59)
%p : 显示本地 AM 或 PM
%P : 显示本地 am 或 pm
%r : 直接显示时间(12 小时制,格式为 hh:mm:ss [AP]M)
%s : 从 1970 年 1 月 1 日 00:00:00 UTC 到目前为止的秒数
%S : 秒(00..61)
%T : 直接显示时间(24 小时制)
%X : 相当于%H:%M:%S %p
%Z : 显示时区

若是不以加号作为开头,则表示要设定时间:

1
date "0110082558.12"

时间格式为:MMDDhhmm[[CC]YY][.ss]

1
2
3
4
5
6
7
MM 为月份
DD 为日
hh 为小时
mm 为分钟
CC 为年份前两位数字
YY 为年份后两位数字
ss 为秒数

当我们使用日期时经常会对日期进行加减操作,比如找下一天:

1
date -d next-day '+%Y-%m-%d %H:%M:%S'

除了next-day,还支持tomorrow、last-day、yesterday(同last-day)、next-month、next-week、next-monday、next-thursday、last-month、next-year、last-year。
除了日期,时间还有hour,minute,second。

下例中,B是一个事先输入的日期:

1
2
3
4
5
6
7
8
9
10
# 转换为时间戳
date -d "$B" +%s
# 5 分钟前
date "+%Y-%m-%d %H:%M:%S" -d "$B -5 minutes"
# 4 天后
date "+%Y-%m-%d %H:%M:%S" -d "$B +4 days"
# 比较两个日期间相差的天数
expr '(' $(date +%s -d "2016-08-08") - $(date +%s -d "2016-09-09") ')' / 86400
# 比较两个时间相差的秒数
expr '(' $(date +%s -d "$(date -d "$B" '+%Y-%m-%d %H:%M:%S')") - $(date +%s -d "2020-01-10 12:20:32") ')'

除了格式化显示外,还可以指定系统时间:

1
2
date -s "2020-01-10 01:01"
date --set="2020-01-10 01:01"

URLEncode

URL编码将一些空格、括号等特殊字符转义为16进制数字,方便网络传输。
可以使用xxd将原字符串编码为16进制,然后每隔两位插入一个%,就可以得到URLEncoded字符串:

1
echo '你好' | tr -d '\n' | xxd -plain | sed 's/\(..\)/%\1/g' | tr -d '\n'

参考

  1. Effective Shell Interlude: Understanding the Shell
  2. Advanced Bash-Scripting Guide
  3. jlevy/the-art-of-command-line