shell编程笔记

[TOC]

Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。
Shell 脚本(shell script),是一种为 shell 编写的脚本程序,业界所说的 shell 通常都是指 shell 脚本。

基本语法

变量

shell中的变量是弱类型的,允许对变量进行算数运算和比较,决定因素是变量值是否含有数字

1
2
3
4
5
6
a=1
b=$a+1 //变量赋值 结果b:1+1
b=$(($a+1)) //变量赋值 结果b:2 $(()) 用来做整数运算
c=${a}hello //变量赋值 当引用变量时,可以用{}定界变量名
unset a b c //删除变量a b c

单引号双引号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
`单引号`: 所见即所得,不做扩展解析
`双引号`: 对内容中命令、变量做解析
`不加引号`: 同双引号(不会将包含空格的字符串整体解析输出)
> 双引号可以通过转义字符\避免扩展
$ echo '$SHELL'
$SHELL
$ echo "$SHELL"
/bin/zsh
$ echo $SHELL
/bin/zsh
$ echo "\$SHELL"
$SHELL

字符串

1
2
b=$(s^) 首字母大写
${i:0:2} 截取字符

数组

声明定义

1
2
3
4
5
6
7
变量=(
元素1
元素2
元素3
)
变量=(a b c)

数组处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 数组长度
${#变量[@]}
# 数组索引列表
${!变量[@]}
# 获取范围元素
${数组名[@]:起始位置:长度}
data=(a b c)
echo ${data[@]:0:2} //输出: a b
# 替换
${数组名[@]/查找字符/替换字符}
data=(a b c)
echo ${data[@]/c/b} //a b b
echo ${data[@]} //a b c]
字符串分割为数组
1
2
3
4
5
IN="张三|李四"
list=$(echo $IN|tr "|" "\n")
for p in list;do
echo $p
done

遍历

1
2
3
4
5
# 遍历 (${变量[@]}将数组转为列表)
for 元素 in ${变量[@]}
do
...
done

示例

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
---------------------------示例-------------------------------
elements=(
a
b
c
)
echo ${elements[0]}
for element in ${elements[@]}
do
echo $element
done
for element in \
a b c
do
echo $element
done
for element in {0..100};do
echo $element
done

函数

定义及使用

1
2
3
4
5
6
7
#定义函数
funcname(){
...
}
#调用函数
funcname arg1 arg2

条件判断

条件判断test[]效果等价

1
2
3
if [ -n "$1" ];
等同于
if test -n "$1";
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
if [表达式];then
...
elif [表达式];then
...
fi
----------------------示例-----------------------------
sh demo.sh sxk
#!/bin/bash
name=$1
read -p '请输入年龄: ' age
if [ $name = 'sxk' ];then
echo 'my name is sxk';
elif [ $name = 'xxx' ];then
echo 'my name is xxx'
else
echo 'other';
fi
if [ ! $age ];then
echo '年龄不能为空'
fi
a=1
b=0
if [ $a -a $b ];then 会执行,因为$a$b
if [ $a -gt 0 -a $b -gt 0 ];then 不会执行

比较运算

文件比较运算符

1
2
3
4
5
6
7
-e filename 是否存在,可检测文件和目录是否存在
-d filename 是否是目录
-f filename 是否是文件
-r filename 是否可读
-w filename 是否可写
-x filename 是否可执行

字符串比较运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-z string 如果string长度为0,则为真 通常用于判断变量是否为空
-n string 如果string长度非0,则为真 通常用于判断变量是否有值
==/= 等于
//例1
if [ -n "$1" ];then
echo 111
fi
//例2
if [ $str == "hello" ];then
fi
# 判断变量是否为空
if [ -z "$p" ];then
...
fi
# 判断变量是否有值
if [ -n "$p"];then
...
fi

算数比较运算符

1
2
3
4
5
6
-ne num 不等于
-eq num 等于
-gt num 大于
-lt num 小于
-ge num 大于等于
-le num 小于等于

ne、eq等使用于数字,==/=适用于字符串比较
ps: ==/=在[]中效果相同,在[[]]中效果不同

逻辑运算符

1
2
-a 非
-o 或

环境相关

环境变量

变量可以划分为局部变量环境变量,其中局部变量只在当前shell进程中有效,环境变量在所有子进程中也有效,简单来说环境变量可以传递给子孙进程

默认变量都是局部变量,通过export使其变为环境变量

1
2
a=1 # 局部变量
export a # 环境变量

source: 在当前环境下运行并执行file中的命令,别名.。常用于合并配置信息,类似php中的include,比如向环境变量中添加内容

1
2
3
4
5
6
#config.sh
a=1
$ source config.sh
等同于
$ . config.sh
特殊变量
1
2
3
4
5
6
7
8
$$ 当前进程的pid
$# 参数个数
$? 最后运行命令的结束代码
$0 命令本身
$1 $2...$n 参数
解析并引用命令执行结果
``等同于$(...)

历史命令

1
2
#快速使用历史命令
ctrl+r 输入命令关键字

输入输出

获取命令参数

  • 利用参数变量
  • 利用for循环

for循环获取参数变量

当for循环没有指定列表时,默认读取位置参数列表。
Nginx就是利用这种方式获取configure参数的

1
2
3
4
5
6
7
8
9
10
11
12
13
$ options p1 p2 p3
#options
for option;do
echo $option
done
---
结果:
p1
p2
p3

格式化输出

1
2
$ printf "%5d\t%s\n" 1 hello
1 hello

输出多行文本

1
2
3
4
5
6
7
8
9
cat << END
[xdebug]
zend_extension=/usr/local/php/lib/php/extensions/no-debug-non-zts-20151012/xdebug.so
xdebug.remote_connect_back=0
xdebug.remote_host=docker.for.mac.host.internal
xdebug.remote_port=10000
xdebug.remote_enable=1
xdebug.remote_log=/tmp/xdebug.log
END >> a.txt

输出多行文本至文件

1
2
3
4
5
6
7
8
9
cat << END >> /usr/local/php/lib/php.ini
[xdebug]
zend_extension=/usr/local/php/lib/php/extensions/no-debug-non-zts-20151012/xdebug.so
xdebug.remote_connect_back=0
xdebug.remote_host=docker.for.mac.host.internal
xdebug.remote_port=10000
xdebug.remote_enable=1
xdebug.remote_log=/tmp/xdebug.log
END

命令行输入

1
2
3
#!/bin/bash
read -p "请输入姓名: " name
echo $name

重定向

每个Linux命令运行时都会打开三个文件:

  • 标准输入文件(stdin): 文件描述符为0,Linux程序默认从stdin读取数据
  • 标准输出文件(stdout): 文件描述符为1,Linux程序默认向stdout输出数据
  • 标准错误文件(stderr): 文件描述符为2,Linux程序默认向stderr写入错误信息
1
2
#将错误信息与标准输出合并后输出到/dev/null 起到禁止输出的效果
sh process.sh > /dev/null 2>&1

subshell

在bash 脚本中,subshells (写在圆括号里的) 是一个很方便的方式来组合一些命令。一个常用的例子是临时地到另一个目录中,例如:

1
2
3
# do something in current dir
(cd /some/other/dir; other-command)
# continue in original dir

<(some command)

通过 <(some command) 可以把某命令当成一个文件。

示例:比较一个本地文件和远程文件 /etc/hosts:

1
diff /etc/hosts <(ssh somehost cat /etc/hosts)

数据处理

集合交集、并集、差集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 集合操作
cat a b | sort | uniq > ``c # c is a union b 并集
cat a b | sort | uniq -d > c # c is a intersect b 交集
cat a b b | sort | uniq -u > c # c is set difference a - b 差集
uniq
-d only print duplicate lines, one for each group
-u only print unique lines
- 以分号为分隔符,按第一列排序
-- 默认按字母排序
cat groups.txt | sort -t ';' -k1
-- -n按数字排序
cat groups.txt | sort -t ';' -n -k1
-- 倒排
cat groups.txt | sort -t ';' -r -k1

awk和sed

awk分4段 选择段|起始段|处理段|结束段
filter BEGIN{} {} END{}

例如求第三列数字之和

awk '{x+=$3} END {print x}'

NR 行号
指定多个分隔符 awk -F [;|]

1
sed -i '1d' filename 删除第一行

awk中变量类型转换

1
2
3
4
5
6
7
8
9
10
11
比如对于文件data.txt
02
2
2西瓜
2
$ cat data.txt |awk '$1==2{print $0}'
02
2
2

为什么02没有被过滤掉?是因为$1发生了隐式转换变为了2。
那么如何才能精确的取出值为2的行的呢?我们需要对比较运算符右侧的2做显示类型转换

1
2
3
$ cat data.txt|awk '$1==2""{print $0}'
2
2

2""将数字2转换为字符串”2”,此时$1与之对比时也会转换为字符串,逐字进行比较

字符串转数字 strtonum($str) 或 $str+0
数字转字符串 $num””

时间戳转换

1
2
3
转换前: 29481091 1585699588
head -n10 xxx.txt |awk '{aa=strftime("%Y-%m-%d %H:%M:%S",$2);print $1" "aa}'
转换后: 29481091 2020-04-01 08:06:28

文件分割

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
43
44
45
46
47
48
49
50
51
52
53
54
55
按照需求处理文本,内容格式如下:
102;"说文解字";"2012-03-19 18:10:47+08"
示例来源
1. 输出文本
- 显示前十条
head -n 10 groups.txt
- 显示后十条
tail -n 10 groups.txt
2. 数据过滤
- 只显示包含"技术"两字的行
cat groups.txt|grep 技术
- 显示既包含"技术"又包含"灰机"的行
cat groups.txt|grep 技术|grep 灰机
- 显示小组id大于30且小于100的行
- awk -F 分隔符
- \$0代表整行
cat groups.txt|awk -F ';' '$1>30 && $1<100 {print $0}'
- 限制字段输出
cat groups.txt|awk -F ';' '{print $1 "\t" $2}'
- 聚合
-- 获取总行数(使用wc)
cat groups.txt|wc -l
-- 获取总行数(使用awk)
cat groups.txt|awk '{l+=1} END{print l}'
-- 获取分组后每个分组的统计行数(使用uniq)
cat groups.txt|sort -n|uniq -c
-- 获取按照字段2字数分组后统计信息(awk)
cat groups.txt|awk -F ';' '{print length($2)-2}'|awk -F ';' '{g[$1]+=1} END{for (l in g) print l,"=",g[l]}'
-- 获取日期Y-m-d
cat groups.txt|awk -F ';' '{split($3,a," "); gsub("\"","",a[1]);print a[1]}'
split将字符串$3" "分割为数组a
gsub将"替换为空
-- 获取按照字段2字数分组后统计信息(sort uniq)
cat groups.txt|awk -F ';' '{print length($2)-2}'|sort -n|uniq -c
-- 按照第一个字段排序
cat groups.txt|sort -k 1
-- 引用环境变量
a=1
cat groups.txt|awk -vmyvar="$a" '{print myvar}'

文件系统

查找文件

1
2
3
4
find / -name 'httpd.conf'
find ./ -type f 查找当前目录(包含子目录)下所有文件
find ./ -type f -maxdepth 0 查找当前目录(不包含子目录)下所有文件
find ./* -type f -maxdepth 0 ! -name data_0.txt 查找当前目录下名字不等于data_0的文件

过滤文件grep/pgrep

1
2
3
4
5
6
7
grep -rni string .
grep -v string 反向过滤
grep ^hello 查找以hello开头的行
pgrep -P 123 列出所有ppid为123的进程pid
grep -F <string> 非正则表达式匹配
grep -E '张三|李四' 或匹配
grep '张三' filename|grep '李四' 与匹配

其他

路径相关

1
2
3
4
5
# 获取相对路径
rPath=$(dirname $0)
# 获取绝对路径
shellPath=$(cd $(dirname $0);pwd)

日期时间

1
2
3
4
date "+%Y-%m-%d:%H:%M:%S" 2017-07-27:15:36:32
# 睡眠1秒
sleep 1

ssh

1
2
3
4
5
# 在远程机器上执行命令
ssh user@server cmd
# 在远程机器上执行shell脚本
ssh user@server bash < /path/to/local/script.sh

压缩

1
2
# 压缩解压
tar -zxvf xxx.tar.gz -C mydir # 将温江解压到mydir中

other

1
2
3
4
5
6
7
8
9
10
#终止运行
exit
exit 1
# 获取md5
md5 filepath|cut -d ' ' -f1
#xargs
xargs的作用是获取stdin 并将stdin内容分割为arguments
ll|xargs echo

FAQ

1.为什么关闭会话后,正在执行的命令也会被关闭

原因: 关闭会话操作会将当前会话相关的进程全部关闭

衍生:为什么用php的exec方法执行的命令 在页面关闭后还会继续执行?

因为命令是使用web服务器的启动用户来进行的,并不涉及会话关闭,所以进程依旧存在,命令依旧执行

参考:
解决Linux关闭终端(关闭SSH等)后运行的程序自动停止
Linux—进程组、会话、守护进程
linux 关闭远程回话之后保持进程继续

2.删除当前目录下文件名不等于a的所有文件

1
2
find . -type f ! -name 'a'|xargs rm -rf
find . -type f ! -name 'a' -and ! -name 'b'