shell 命令
语法
全局变量
在 shell 中,我们可以声明变量,就像其他编程语言一样。然后在 shell 中,我们还可以开启子进程,但是我们声明的变量,通常只能在声明它的那个进程中使用,这种变量成为局部变量。
但是还有一些变量,它们可以在任意的进程中使用,是全局通用的,被称为全局变量。
查看全局变量,可以使用 printenv
, env
两个命令。全局变量可以在所有进程中使用
printenv # 打印所有全局变量(全局变量是可以在所有进程中使用的变量,一般用大写变量名)
printenv HOME # 仅仅打印 HOME 这个变量
echo $HOME # 也可以使用 echo 打印某个变量,但是注意要在前面加 $ 号;加 $ 号可以让变量作为命令行参数,被其他命令使用
set
命令可以显示所有的变量,包括全局变量和局部变量。
局部变量
局部变量仅仅在声明这个变量的进程中有效。
声明变量
name='hello world'
不要在
=
前后加空格,否则会报错
使用变量
echo ${name}
${变量名}, 大部分时候
{}
可以省略下面的操作中,有时需要加 $ 符号,有时不用加:加 $ 符号是为了获取变量的值。不加 $ 符号是用来操作变量本身
设置只读变量
readonly name
删除变量(不能删除只读变量)
name2='name2'
unset name2 # 记住不用加 $ 符号
删除一个全局变量,只会影响当面进程,别的进程中依然可以使用这个变量。
设置全局变量
name='name2'
export name # 不用加 $ 符号
设置成全局变量后,就可以在任意进程中使用这个变量了。但是子进程中修改这个变量的值,仅仅在子进程中生效,其他进程中的这个值不会改变。
环境变量
全局变量中,有个变量叫做 PATH
,它里面保存了很多路径,用来查询可执行命令,每个目录之间用 :
分割。如果你想将某个自己的脚本命令放到环境变量中,可以直接拼接:
PATH=$PATH:home/wzt/xxx
export PATH # 导出到全局
声明变量类型
declare [-aixrp] variable
选项与参数:
-a :将后面名为variable 的变数定义成为阵列(array) 类型
-i :将后面名为variable 的变数定义成为整数数字(integer) 类型
-x :用法与export 一样,就是将后面的variable 变成环境变数;+x 可以取消全局变量哦
-r :将变数设定成为readonly 类型,该变数不可被更改内容,也不能unset
-p :查看某个变量的类型
$ declare -i a
$ a=100+20
$ echo $a
120
用户输入
read [-pt] variable
可选选项与参数:
-p :后面可以接提示字符
-t :后面可以接等待的『秒数!』这个比较有趣~不会一直等待使用者啦!
-n :指定接收多长的字符串
-s :隐藏用户输入(不显示输入的内容)
$ read -p "Your name:" name
Your name:Wang
$ echo $name
Wang
等待超时:
$ if read -t 5 name
> then
> echo success
> else
> echo timeout
> fi
timeout
指定接收多少个字符:
$ read -n2 name # 指定接收 2 个字符,达到数量后自动结束输入
wo
隐藏用户输入:
$ read -s -p 'name:' name
name:
$ echo $name
woasldkfoieaklsfiejj;asf
我们可以设置多个变量,来接收多个值:
$ read name age
wang 23
$ echo $name $age
wang 23
如果不指定变量,则会存放了一个特殊变量:REPLY
$ read
hahahah
$ echo $REPLY
hahahah
读取文件
$ cat ./a
#!/bin/bash
# reading data from a file
#
count=1
cat $1 | while read line # cat 读取脚本的第一个参数,将读到的每一行通过管道传递给 read 的 line 变量
do
echo "Line $count: $line"
count=$[ $count + 1]
done
echo "Finished processing the file"
# --------------------- 文件 ------------------
$ cat file
This is line 1
This is line 2
This is line 3
# --------------------- 执行结果 ------------------
$ ./a file
Line 1: This is line 1
Line 2: This is line 2
Line 3: This is line 3
Finished processing the file
字符串
字符串拼接
text1='Wang'
text2="I love ${text1}" #注意要用双引号
echo ${text2}
不放到双引号中也行,可以直接拼接:
text2="I love"${text1}
两个变量也可以直接拼接:
echo ${t1}${t2}
注意:不能使用 '
单引号拼接字符:
>>> t1="I"
>>> echo '${t1}'
${t1}
可以看出,将变量放到单引号中,只会将其当作普通字符串看待。如果你想在双引号中打印原生的
$
符号,可以使用\
转义。
字符串长度
echo ${#text2}
截取字符串
注意,是从某个索引开始,截取几个!而不是截取到几。
aa="012345"
echo ${aa:1:3} # 结果:123
echo ${aa:3:1} # 结果:3
声明数组
数组以圆括号定义,内部的元素以空格分隔
array=('I' 'am' 'beautiful')
数组
读取数组
数组下标以0开始,表示第一个元素
echo ${array[0]}
echo ${array} # 同样只显示第一个元素
修改数组元素
array[0]='He'
获取所有元素 @
echo ${array[@]}
echo ${array[*]} # * 和 @ 都可以
数组长度
整个数组长度
length=${#array[@]}
单个元素长度
echo ${#array[1]}
命令行参数
建一个脚本文件test.sh
echo "0 is the file itself: ${0}"
echo "1 is from the cmd : ${1}"
执行:
./test.sh arg1
结果:
0 is the file itself: ./test.sh
1 is from the cmd : arg1
参数处理 | 说明 |
---|---|
$# | 传递到脚本的参数个数 |
$* | 用一个字符串表示所有参数 |
$$ | 脚本运行的当前进程ID号 |
$! | 后台运行的最后一个进程的ID号 |
$@ | 用数组表示所有参数。 |
$- | 显示Shell使用的当前选项,与set命令功能相同。 |
$? | 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。 |
$ | ${0} 代表脚本本身; ${1},${2},${3}...则代表第1,2,3个参数; 也可以不加{} |
修改test.sh
如下
echo "0 is the file itself: ${0}"
echo "1 is from the cmd : ${1}"
echo "2 is from the cmd : ${2}"
echo "args number: $#"
echo "args is: $*"
echo "args are: $@"
echo "current pid is: $$"
echo "execute state: $?"
执行:
$ ./test.sh arg1 arg2
结果:
$ ./test.sh arg1 arg2
0 is the file itself: ./test.sh
1 is from the cmd : arg1
2 is from the cmd : arg2
args number: 2
args is: arg1 arg2
args are: arg1 arg2
current pid is: 30204
execute state: 0
运算符
算数运算
a=10; b=20
运算符 | 说明 | 举例 |
---|---|---|
+ | 加法 | expr $a + $b 结果为 30。 |
- | 减法 | expr $a - $b 结果为 -10。 |
* | 乘法 | expr $a \* $b 结果为 200。 |
/ | 除法 | expr $b / $a 结果为 2。 |
% | 取余 | expr $b % $a 结果为 0。 |
= | 赋值 | a=$b 将把变量 b 的值赋给 a。 |
== | 相等。用于比较两个数字,相同则返回 true。 | [ $a == $b ] 返回 false。 |
!= | 不相等。用于比较两个数字,不相同则返回 true。 | [ $a != $b ] 返回 true。 |
例子:
a=10
b=20
val=`expr $a + $b`
echo "a + b : $val"
val=`expr $a - $b`
echo "a - b : $val"
val=`expr $a \* $b`
echo "a * b : $val"
val=`expr $b / $a`
echo "b / a : $val"
val=`expr $b % $a`
echo "b % a : $val"
if [ $a == $b ]
then
echo "a 等于 b"
fi
if [ $a != $b ]
then
echo "a 不等于 b"
fi
注意`
expr 表达式
` 是反引号,不是单引号。
注意符号
+
左右一定要加空格
乘号
*
前边必须加反斜杠\
才能实现乘法运算;
条件表达式也要空格:
例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]
关系运算符
关系运算符只支持数字,不支持字符串,除非字符串的值是数字。
下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
-eq | 检测两个数是否相等,相等返回 true。 | [ $a -eq $b ] 返回 false。 |
-ne | 检测两个数是否不相等,不相等返回 true。 | [ $a -ne $b ] 返回 true。 |
-gt | 检测左边的数是否大于右边的,如果是,则返回 true。 | [ $a -gt $b ] 返回 false。 |
-lt | 检测左边的数是否小于右边的,如果是,则返回 true。 | [ $a -lt $b ] 返回 true。 |
-ge | 检测左边的数是否大于等于右边的,如果是,则返回 true。 | [ $a -ge $b ] 返回 false。 |
-le | 检测左边的数是否小于等于右边的,如果是,则返回 true。 | [ $a -le $b ] 返回 true。 |
关系运算符实例如下:
a=10
b=20
if [ $a -eq $b ]
then
echo "$a -eq $b : a 等于 b"
else
echo "$a -eq $b: a 不等于 b"
fi
if [ $a -ne $b ]
then
echo "$a -ne $b: a 不等于 b"
else
echo "$a -ne $b : a 等于 b"
fi
if [ $a -gt $b ]
then
echo "$a -gt $b: a 大于 b"
else
echo "$a -gt $b: a 不大于 b"
fi
if [ $a -lt $b ]
then
echo "$a -lt $b: a 小于 b"
else
echo "$a -lt $b: a 不小于 b"
fi
if [ $a -ge $b ]
then
echo "$a -ge $b: a 大于或等于 b"
else
echo "$a -ge $b: a 小于 b"
fi
if [ $a -le $b ]
then
echo "$a -le $b: a 小于或等于 b"
else
echo "$a -le $b: a 大于 b"
fi
布尔运算符
下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
! | 非运算,表达式为 true 则返回 false,否则返回 true。 | [ ! false ] 返回 true。 |
-o | 或运算,有一个表达式为 true 则返回 true。 | [ $a -lt 20 -o $b -gt 100 ] 返回 true。 |
-a | 与运算,两个表达式都为 true 才返回 true。 | [ $a -lt 20 -a $b -gt 100 ] 返回 false。 |
逻辑运算符
以下介绍 Shell 的逻辑运算符,假定变量 a 为 10,变量 b 为 20:
运算符 | 说明 | 举例 |
---|---|---|
&& | 逻辑的 AND | [[ $a -lt 100 && $b -gt 100 ]] 返回 false |
|| | 逻辑的 OR | [[ $a -lt 100 || $b -gt 100 ]] 返回 true |
字符串运算符
下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":
运算符 | 说明 | 举例 |
---|---|---|
== / = | 检测两个字符串是否相等,相等返回 true。 | [ $a == $b ] 返回 false。 |
!= | 检测两个字符串是否相等,不相等返回 true。 | [ $a != $b ] 返回 true。 |
-z | 检测字符串长度是否为0,为0返回 true。 | [ -z $a ] 返回 false。 |
-n | 检测字符串长度是否不为 0,不为 0 返回 true。 | [ -n "$a" ] 返回 true。 |
$ | 检测字符串是否为空,不为空返回 true。 | [ $a ] 返回 true。 |
> | 比较字符串大小(用于测试时要转义!否则可能会作为重定向) | [ $a \> $b ] |
< | 比较字符串大小(用于测试时要转义!否则可能会作为重定向) | [ $a \< $b ] |
文件测试运算符
文件测试运算符用于检测 Unix 文件的各种属性。
属性检测描述如下:
操作符 | 说明 | 举 |
---|---|---|
-b file | 检测文件是否是块设备文件,如果是,则返回 true。 | [ -b $file ] 返回 false。 |
-c file | 检测文件是否是字符设备文件,如果是,则返回 true。 | [ -c $file ] 返回 false。 |
-d file | 检测文件是否是目录,如果是,则返回 true。 | [ -d $file ] 返回 false。 |
-f file | 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 | [ -f $file ] 返回 true。 |
-g file | 检测文件是否设置了 SGID 位,如果是,则返回 true。 | [ -g $file ] 返回 false。 |
-k file | 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 | [ -k $file ] 返回 false。 |
-p file | 检测文件是否是有名管道,如果是,则返回 true。 | [ -p $file ] 返回 false。 |
-u file | 检测文件是否设置了 SUID 位,如果是,则返回 true。 | [ -u $file ] 返回 false。 |
-r file | 检测文件是否可读,如果是,则返回 true。 | [ -r $file ] 返回 true。 |
-w file | 检测文件是否可写,如果是,则返回 true。 | [ -w $file ] 返回 true。 |
-x file | 检测文件是否可执行,如果是,则返回 true。 | [ -x $file ] 返回 true。 |
-s file | 检测文件是否为空(文件大小是否大于0),不为空返回 true。 | [ -s $file ] 返回 true。 |
-e file | 检测文件是否存在,如果是,则返回 true。 | [ -e $file ] 返回 true。 |
file1 -nt file2 | 文件1是否比文件2新(创建日期)(新返回true) | |
file1 -ot file2 | 文件1是否比文件2旧(创建日期)(旧返回true) |
其他检查符:
- -S: 判断某文件是否 socket。
- -L: 检测文件是否存在并且是一个符号链接。
实例:
file="/var/www/runoob/test.sh"
if [ -r $file ]
then
echo "文件可读"
else
echo "文件不可读"
fi
if [ -w $file ]
then
echo "文件可写"
else
echo "文件不可写"
fi
echo
显示字符
echo "Hello world!"
显示执行命令结果
注意用反引号
echo `ls` # 显示的结果可能没有格式,在同一行
echo "`ls`" # 按照格式显示出来,有换行
显示原字符(不转义)
使用单引号,可以直接显示原字符串
>>> echo '${name}\n'
${name}\n
激活转义 -e
echo -e
可以将转义字符显示
>>> echo -e "abc \n def"
abc
def
•\b 删除前一个字符;
•\c 最后不加上换行符号;
•\f 换行但光标仍旧停留在原来的位置;
•\n 换行且光标移至行首;
•\r 光标移至行首,但不换行;
•\t 插入tab;
•\v 与\f相同;
•\ 插入\字符;
不换行显示
echo -n
可以不换行继续显示
>>> echo -n 'a' ; echo -n 'b' ; echo -n 'c'
abc
定向至文件
echo <result> > file
$ echo "`ls`" > ls.txt
$ cat ls.txt
a.txt
dump_ftrace/
Logs/
Monkey_Doc_Tools/
src/
test.txt
tmp/
utility/
printf
printf是一个输出命令,默认不换行
$ printf "hello " ; printf "world"
hello world
格式化打印
%s 代表字符串,%d 代表整数,%f 代表小数;
$ printf "%s is a %dkg dog" "lili" 6
lili is a 6kg dog
%-10s 指一个宽度为10个字符(-表示左对齐,没有则表示右对齐),任何字符都会被显示在10个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。
%.2f 指格式化为小数,其中.2指保留2位小数。
$ printf "%-6s is a %.4fkg dog" "lili" 6
lili is a 6.0000kg dog
测试 test
test用于判断条件是否成立,也可以测试变量是否有内容
$ if test 1 == 2
> then echo "equal"
> else echo "not"
> fi
not
$ if test; then echo 'True'; else echo 'False'; fi; # test 后面什么参数都没有
False
也可以写成方括号的形式,但是注意:第一个方括号之后和第二个方括号之前必须加上一个空格
$ if [ 1 == 2 ]
> then echo 'equal'
> else echo 'not equal'
> fi
not equal
复合测试
[ condition ] && [ condition2 ]
代表了两种条件同时满足:AND
[ condition ] || [ condition2 ]
代表了两种条件可以只满足一个:OR
流程控制
if then
if <condition> ; then <steps> ; fi
fi
是if
反写的,代表着语句结束
if condition
then
command1
command2
...
commandN
fi
写成一行:
if [ condition ]; then steps; fi
e.g.
$ if [ 1 == 1 ]; then echo "equal" ;fi
equal
if then else
if [ condition ]
then
command
elif [ condition ] # 可以多个elif then语句,也可以不加elif then 语句
then
command
...
else
command
fi
eg.
$ if [ 1 == 2 ]; then echo "equal" ; else echo "Not equal"; fi
Not equal
for 循环
格式:
for var in item1 item2 ... itemN
do
command1
command2
...
commandN
done
*****************************************
for ((init,value,))
do
command
done
eg.
$ for x in 1 2 3 4 5
> do
> printf ${x}
> done
12345
C语言风格
$ n=5
$ for ((i=1; i<=$n; i++))
> do
> echo $i
> done
1
2
3
4
5
还可以同时声明多个变量:
for (( a=1, b=10; a <= 10; a++, b-- ))
do
echo "$a - $b"
done
通配符读取目录
在遍历目录时,如果不知道所有的文件名,可以使用通配符:
for file in /home/test/*
do
if [ -d "$file" ] # 加了双引号,是为了处理带有空格的文件名
then
echo "$file is a directory"
elif [ -f "$file" ]
then
echo "$file is a file"
fi
done
循环结果重定向
for file in /home/*
do
if [ -d "$file" ]
then
echo "$file is a directory"
elif
echo "$file is a file"
fi
done > output.txt # 也可以对它排序等其他操作: done | sort
while 循环
格式:
while condition
do
command
done
eg.
$ a=1
$ while (($a <= 5)); do echo $a ; let "a++"; done
1
2
3
4
5
while 读取文件
假设现有文件a
wang;23;23
zhang;32;32
li;55;55
while IFS=';' read field1 field2 field3 # 文件每行按照分隔符 `;` 分割,分割后的字段分别赋值给 field1, field2 ...
do
echo $field1
done < a # 从文件 a 读取内容
until 循环
until 循环与 while 循环在处理方式上刚好相反。until 循环执行一系列命令直至条件为 true 时停止。
语法:
until condition
do
command
done
实例:
$ a=1
$ until [ $a == 5 ]
> do echo $a
> let "a++"
> done
1
2
3
4
case
case语句为多选择语句。可以用case语句匹配一个值与一个模式,如果匹配成功,执行相匹配的命令。case语句格式如下:
case 值 in
模式1)
command1
command2
...
commandN
;;
模式2)
command1
command2
...
commandN
;;
esac
实例:
$ name="wang"
$ case $name in
> "wang")
> echo "wang"
> ;;
> "zhang")
> echo "zhang"
> ;;
> esac
wang
使用*号:
$ case "haha" in "wang") echo "wang"; ;; "zhang") echo "zhang" ;; *) echo "I don't know what you inputed" ;; esac
I don't know what you inputed
循环控制
break
, continue
可以控制循环
for (( b=1; b<10; b++ ))
do
echo b is $b
for (( a=1; a<10; a++ ))
do
echo = a is $a
if [ $a -eq 2 ]
then
continue
elif [ $a -eq 8 ]
then
break 2 # break 甚至可以控制跳出的循环层数;默认跳出1
fi
done
done
函数
[ function ]是可选的,可写可不写,如果没有返回值,则默认返回最后一个命令的结果,return后跟数值n(0-255)
[ function ] funname [()]
{
action;
[return int;]
}
实例:
$ hello(){
> echo "hello world"
> }
$ hello
hello world
实例2:返回值
# 定义函数
$ hello(){
> echo "recv first arg if $1"
> echo "recv tenth arg is ${10}"
> echo "the sum of $1 and ${10} is $(($1+${10}))"
> return $(($1+${10}))
> }
# 传参
$ hello 1 2 3 4 5 6 7 8 9 10
# 结果:
# recv first arg if 1
# recv tenth arg is 10
# the sum of 1 and 10 is 11
# 使用 $? 获取返回值:
$ echo $?
11
参数处理 | 说明 |
---|---|
$# | 传递到脚本或函数的参数个数 |
$* | 以一个单字符串显示所有向脚本传递的参数 |
$$ | 脚本运行的当前进程ID号 |
$! | 后台运行的最后一个进程的ID号 |
$@ | 数组表示参数 |
$- | 显示Shell使用的当前选项,与set命令功能相同。 |
$? | 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。 |
输入输出重定向
命令 | 说明 |
---|---|
command > file | 将输出重定向到 file。将命令的输出写入文件(如果文件已存在,会覆盖) |
command < file | 将输入重定向到 file。即将文件的内容输入给某个命令 |
command >> file | 将输出以追加的方式重定向到 file。 |
n > file | 将文件描述符为 n 的文件重定向到 file。 |
n >> file | 将文件描述符为 n 的文件以追加的方式重定向到 file。 |
n >&m | 将输出文件 m 和 n 合并。 |
n <&m | 将输入文件 m 和 n 合并。 |
<< tag | 将开始标记 tag 和结束标记 tag 之间的内容作为输入。 |
输出重定向:
$ ls > ls.txt
$ cat ls.txt
~$Stability_Basic.xlsx
22gis/
book/
BUGFIX_622/
BUGFIX_622.7z
chrome.lnk*
CNT-CATS-PLAN-Precondition_definition.xlsx
delUpdate.lnk*
desktop.ini
django.md
追加到文件:
$ ipconfig >> ls.txt
wztshine@GG678S2 MINGW64 ~/Desktop
$ cat ls.txt
~$Stability_Basic.xlsx
22gis/
book/
BUGFIX_622/
BUGFIX_622.7z
chrome.lnk*
CNT-CATS-PLAN-Precondition_definition.xlsx
delUpdate.lnk*
desktop.ini
django.md
Windows IP Configuration
Ethernet adapter Ethernet:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . : cnt01.local
Unknown adapter Local Area Connection:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
Wireless LAN adapter Local Area Connection* 2:
Media State . . . . . . . . . . . : Media disconnected
Connection-specific DNS Suffix . :
Wireless LAN adapter Local Area Connection* 1:
标准输入输出:
一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:
- 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
- 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
- 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。
将错误重定向到文件:
command 2 > file
command 2 >> file
stdout 和 stderr 合并后重定向到文件:
command > file 2>&1
/dev/null 文件
/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃
command > /dev/null
屏蔽 stdout 和 stderr
command > /dev/null 2>&1
导入文件
. filename # 注意点号(.)和文件名中间有一空格
或
source filename
实例:
t1.sh
name="Elias"
t2.sh
. ./t1.sh
name2=$name
echo $name2
执行:
$ ./t2.sh
Elias
其他内容:
括号含义
()
1.子进程; 相当于新开了一个子 shell,在里面执行命令
>>> (a=1;echo $a)
1
>>> (a=1)
>>> (1) # 因为 1 不是一句命令,所以报错
bash: 1: command not found
2.格式为 $(command)
。将括号内的变量的执行结果作为值,赋值给前面的变量
>>> files=$(ls -l) # 将 ls 命令的执行结果,赋值给 files。等同于 files=`ls -l`
$ echo $files
update.pdf MY/ Project/ Redis-x64-3.0.504/ d_lab.lnk desktop.ini
用来执行命令,并将命令的返回值赋值给前面的变量。等同于反引号括起来的:
`命令`
$(命令)
并不是所有 shell 类型都支持这种语法。反引号的`命令`
所有shell都支持。
3.数组
数组用括号来表示,元素用"空格"符号分割开。
array=(1 2 3 4)
(())
1.表达式 $((exp))
,进行数学运算的,如 \+ - * / %
等。
echo $((2*2+(1-2))) # 输出3
a=1
echo $((a++)) # 输出2,且从此之后a=2
2.高级的数学表达式,支持复杂的数学运算,且不需要转义 >, <
等符号
a=1
((a=2))
echo $a # 输出2
3.算数比较
在((exp))
中可以进行算术比较(不能进行字符串比较),双括号中的变量可以省略$
符号前缀,当然也可以带着。
$ a=1
$ if ((a==1))
> then echo 'equal'
> fi
equal
[]
1.条件判断
[]
本质上是 test
语句,[
是调用test
的命令标识,]
是关闭条件判断,第一个方括号之后和第二个方括号之前必须加上一个空格
$ if [ 1 == 1 ]
> then echo 'equal'
> fi
equal
2.数学计算
方括号前面加上 $
,可以用来进行数学计算,但是注意它无法处理浮点数。
$ echo $[1+2]
3
变量内容的替换,删减
格式:
${var [# | ## | % | %%] pattern } # 变量删除
${var [// | /]old/new} # 变量内容的替换
参数:
- #:从左往右删除符合pattern的字符串(非贪婪模式,只匹配最短的那个)
- ##:从左往右删除符合pattern的字符串(贪婪模式,匹配最多的那个)
- %:从右往左删除符合pattern的字符串(非贪婪模式)
- %%:从右往左删除符合pattern的字符串(贪婪模式)
- /:替换第一个匹配到的字符串
- //:替换所有符合的字符串
示例:
$ name='111222333444555'
$ echo ${name#*2} # 从左删除到2,非贪婪模式
22333444555
$ echo ${name##*2} # 贪婪模式
333444555
$ echo ${name%2*} # 从右往左删除到2,非贪婪
11122
$ echo ${name%%2*}
111
替换:
$ echo ${name/1/a}
a11222333444555
$ echo ${name//1/a}
aaa222333444555
变数设定方式 | var1= | var1='' | var1 已设定非为空字串 | 解释 |
---|---|---|---|---|
var=$ | var=var2 | var='' | var=$var1 | var1=None时,var取值var2;否则取值var1 |
var=$ | var=var2 | var=var2 | var=$var1 | var1=None或者=‘’时,var取值var2,否则取值var1 |
var=$ | var= | var=var2 | var=var2 | var1=None时,var取值None,否则取值var2 |
var=$ | var= | var='' | var=var2 | var1=None或者=‘’时,var取值var1,否则取值var2 |
var=$ | var1=var2 | var1="" | var1不变 | 当var1=None时,var和var1都取值var2,否则取值var1 |
var=$ | var1=var2 | var1=var2 | var1不变 | 当var1=None或=“”时,var和var1都取值var2,否则取值var1 |
var=$ | var2 输出至stderr | var='' | var=$var1 | 当var1=None,var2输出到stderr,否则var取值var1 |
var=$ | var2 输出至stderr | var2 输出至stderr | var=$var1 | 当var1=None或=“”时,var2输出到stderr,否则var取值var1 |
编写脚本
下文所有内容和脚本,基本全部来自:《Linux命令行与shell脚本编程大全.第3版》
基础介绍
使用多个命令
想要在同一行使用多个命令,可以在命令之间输入;
:
$ date; date;
Thu Mar 31 13:05:14 CST 2022
Thu Mar 31 13:05:14 CST 2022
编写脚本
编写shell脚本,第一行必须指定要使用的 shell 类型。
test.sh
#!/bin/bash
# this is comment
echo "hello world"
运行上面的脚本:
./test.sh
通常我们的运行目录不在
$PATH
即环境变量里面,因此我们需要手动指定脚本的路径./
即当前目录如果提示
Permission denied
可以使用chmod u+x test.sh
来赋予文件执行的权限。
变量赋值
变量赋值和使用:
var1=10
var2="string"
var2=$var1 # 想要将一个变量赋值给另一个变量,需要加上 $ 符号
命令替换
使用反引号 `
或者 $()
可以提取命令的输出内容,将其赋值给变量。
$ test=`date`
$ test2=$(date)
$ echo $test
Thu Mar 31 12:47:24 CST 2022
$ echo $test2
...
重定向
输出重定向
使用 >
可以将命令的输出重定向到某个文件:
$ date > date.txt # 将命令的输出结果,存入文件中;如果文件已存在,会覆盖原本内容
$ cat date.txt
Thu Mar 31 12:53:25 CST 2022
输入重定向
使用 <
可以将某个文件作为输入,传递给命令:
$ wc < date.txt # 读取文件的内容,并把内容作为输入,传递给 wc 命令来统计单词数(行数,词数,字节数)
1 6 29
追加输出
>>
可以将输出追加到文件末尾,而不是覆盖文件:
$ date >> file
$ date >> file
$ cat file
Wed Apr 13 10:56:47 CST 2022 # 两条记录都在,没有覆盖
Wed Apr 13 10:56:48 CST 2022
内联输入重定向
<<
可以使用一个符号,来作为用户输入的结束符,获取用户输入的内容:
$ cat << m # 使用 m 作为结束符号
> wokd # 用户输入内容,并回车
> woe
> haha
> m # 用户一直输入内容,直到输入 m 后回车
wokd
woe
haha # 可以看到,m 之前输入的内容,都会被 cat 命令作为参数,打印出来。
另一种使用:
$ wc << EOF # 代表 End of file
> my world # 回车后,按下:Ctrl+D 键,也可以代表 EOF
> bash: warning: here-document at line 23 delimited by end-of-file (wanted `EOF')
1 2 9
管道
管道符号:|
,它可以将一个命令的输出,作为参数,传递给另一个命令:
$ date | wc # 将 date 的输出内容,作为 wc 的输入内容
1 6 29
数学运算
expr 命令
expr 可以处理数学表达式,但是依然有一些问题:
$ expr 5 + 2
7
$ expr 4 * 2 # * 需要转义
expr: syntax error
$ expr 4 \* 2 # 需要转义
8
方括号
另一种比较方便的方法,是使用 $[ operation ]
。但是这种方法不支持浮点运算!
$ echo $[1 + 2]
3
$ echo $[3 * 3]
9
$ var1=10
$ echo $[$var1 + 1]
11
$ echo $[10 / 3] # 不支持浮点运算,只能得出整数
3
浮点运算 bc
bc 是一种编程语言,可以用来计算
$ bc
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type 'warranty'.
12 * 5.4 # 输入表达式
64.8
3.156 * (3 + 5)
25.248
quit # 退出
$
bc 想要控制小数点位数,需要手动指定:scale
$ bc -q
3.44 / 5
0
scale=4 # 四位小数
3.44 / 5
.6880
quit
甚至可以赋值和打印变量:
$ bc -q
var1=10
var1 * 4
40
var2 = var1 / 5
print var2
在脚本中使用 bc:
#!/bin/bash
var1=100
var2=45
var3=$(echo "scale=4; $var1 / $var2" | bc)
echo The answer for this is $var3
退出脚本
shell 中每个命令都用 exit status 来表示运行完毕。退出状态码是一个 0~255 的整数。我们可以用 $?
来捕获命令的退出码,命令运行成功,状态码是 0;出现错误,状态码是一个正整数。
$ date
Thu Mar 31 13:47:20 CST 2022
$ echo $?
0
自定义退出码
exit
可以在脚本中自定义退出状态码:
#!/bin/bash
# testing the exit status
var1=10
var2=30
var3=$[$var1 + $var2]
echo The answer is $var3
exit 5
测试 test
见上文 测试 test
if-then 的高级特性
- 用于高级数学表达式的双括号
- 用于高级字符串表达式的双方括号
双括号
双括号的语法:(( expression ))
可以用来表示高级的数学表达式,支持如下:
val++ # 后增
val-- # 后减
++val # 先增
--val # 先减
! # 逻辑求反
~ # 位求反
** # 幂运算
<< # 位左移
>> # 位右移
& # 位布尔和
| # 位布尔或
&& # 逻辑和
|| # 逻辑或
譬如:
#!/bin/bash
# using double parenthesis
#
val1=10
#
if (( $val1 ** 2 > 90 ))
then
(( val2 = $val1 ** 2 ))
echo "The square of $val1 is $val2"
fi
而且在双括号内部的
>,<
等符号,不需要转义。
双方括号
双方括号提供了高级的字符串表达式,还提供了正则表达式:
#!/bin/bash
# using pattern matching
#
if [[ $USER == r* ]]
then
echo "Hello $USER"
else
echo "Sorry, I do not know you"
fi
字段分隔符
IFS
可以用来定义怎么分割字段,譬如:
#!/bin/bash
# changing the IFS value
IFS.OLD=$IFS # 先存储旧的 IFS 值
IFS=$'\n' # 将分隔符改为 换行符
for entry in $(cat /etc/passwd)
do
echo "Values in $entry –"
IFS=: # 改为冒号
for value in $entry
do
echo " $value"
done
done
命令行参数
基础
见上文命令行参数。
移动变量
shift
命令可以用来移动命令行参数。每次使用 shift
命令,除了 $0
以外的其他参数,都会向左移动一位:$2 会替换掉 $1, $3 替换掉 $2 ... ,因此,我们可以用这种方法来遍历命令行参数:
$ cat a
echo
count=1
while [ -n "$1" ] # 判断变量是否为空
do
echo "Parameter #$count = $1" # 一直打印 $1
count=$[ $count + 1 ]
shift # 移动变量,下次循环 $2 会变成 $1 ...
done
# ------------------------------------------------
$ ./a rich barbara katie jessica
Parameter #1 = rich
Parameter #2 = barbara
Parameter #3 = katie
Parameter #4 = jessica
每次都会将最左边的变量删掉,然后将后续的变量左移。
shift
默认移动 1 位。你也可以自定义移动几个:shift 3
左移三个变量
处理选项
一些常见的命令,后面都可以跟随带有 -
的命令行参数。如:ls -l
$ cat a
echo
while [ -n "$1" ]
do
case "$1" in # 可以看出,我们用了 case 语法
-a) echo "Found the -a option" ;;
-b) echo "Found the -b option" ;;
-c) echo "Found the -c option" ;;
*) echo "$1 is not an option" ;;
esac
shift
done
# ------------------------------------------------
$ ./a h -a -b -c -d
h is not an option
Found the -a option
Found the -b option
Found the -c option
-d is not an option
给选项加参数:
$ cat a
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option";;
-b) param="$2" # 先通过 case 找到 -b 选项,然后获取值
echo "Found the -b option, value $param"
shift ;; # 取值完成后向左多移动一次
*) echo "$1 is not an option";;
esac
shift # 移动一次
done
# ------------------------------------------------
$ ./a -a -b BBB
Found the -a option
Found the -b option, value BBB
getopt 命令
基础使用
getopt 可以用来解析命令行选项和参数。
它的语法格式是:
getopt 选项字符串 参数
譬如:
$ getopt ab:cd -a -b test1 -cd test2 test3
-a -b test1 -c -d -- test2 test3
选项字符串:定义了四个有效选项字母:a、b、c和d。冒号(:)被放在了字母b后面,因为 b 选项需要一个参数值
参数:-cd 可以写在一起,也没关系,getopt 会自动分开它们。并且我们额外提供了
test2 test3
选项,getopt 自动在额外选项前面加上了--
因为我们提供了额外的,不在选项字符串中的参数,所以默认会报错:
$ getopt ab:cd -a -b test1 -cde test2 test3
getopt: invalid option -- e
-a -b test1 -c -d -- test2 test3
如果想要忽略错误,可以使用 -q
参数:
$ getopt -q ab:cd -a -b test1 -cde test2 test3 # -q 等自带的选项,需要放在选项字符串之前
-a -b 'test1' -c -d -- 'test2' 'test3'
脚本中使用 getopt
$ cat a
#!/bin/bash
# Extract command line options & values with getopt
set -- $(getopt -q ab:cd "$@")
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
done
# ------------------------------------------------
$ ./a -a -c
Found the -a option
Found the -c option
getopts 命令
更高级的 getopt 。getopts 每次只处理一个参数,处理完所有参数后,它会退出并返回一个大于 0 的退出码。
语法:
getopts optstring variable
optstring:类似 getopt 中用到的选项字符串,如果想要忽略选项错误,可以在整个 选项字符串 前面加上一个
:
,在某个选项后面加冒号意味着选项后面跟着一个选项值。variable:getopts 每次只处理一个选项,variable 保存着当前的参数
其他变量:OPTARG 用来保存参数的值。OPTIND 保存当前参数的索引(第几个参数)
$ cat a
#!/bin/bash
# simple demonstration of the getopts command
while getopts :ab:c opt # opt 是一个变量,代表当前参数
do
case "$opt" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with value $OPTARG";; # OPTARG 变量存放了参数的值
c) echo "Found the -c option" ;;
*) echo "Unknown option: $opt";;
esac
done
# ------------------------------------------------
$ ./a -ab test1 -c
Found the -a option
Found the -b option, with value test1
Found the -c option
$ cat ./a
#!/bin/bash
while getopts :ab:cd opt
do
case "$opt" in
a) echo "Found the -a option" ;;
b) echo "Found the -b option, with value $OPTARG" ;;
c) echo "Found the -c option" ;;
d) echo "Found the -d option" ;;
*) echo "Unknown option: $opt" ;;
esac
done
count=1
for param in "$@" # 依然可以使用 $@ 获取参数列表
do
echo "Parameter $count: $param"
count=$[ $count + 1 ]
done
呈现数据
理解输入和输出
文件描述符
Linux 将每个对象当作文件处理。这包括输入和输出进程。Linux用文件描述符(file descriptor)来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开 的文件。每个进程一次最多可以有九个文件描述符。出于特殊目的,bash shell保留了前三个文件描述符(0、1、2),分别代表:STDIN,STDOUT, STDERR
STDIN
这个描述符代表 shell 的标准输入。对于终端界面来说,通常输入是键盘。shell 从 STDIN 对应的文件描述符对应的键盘中得到输入。
使用输入重定向符号 <
时,Linux 会用指定的文件来替换这个描述符,它会从文件读取内容,就好像是从键盘上输入的一样。
譬如 cat
命令可以从键盘输入,也可以从文件输入:
$ cat # 回车
my world # 我输入的内容
my world # cat 打印的内容
# ----------- 从文件输入 -----------
$ cat < file # file 是一个本地文件
This is line 1
This is line 2
This is line 3
STDOUT
这个文件描述符代表了标准输出。在终端界面上,通常是显示器。shell 中的所有输出都会被定向到标准输出中,也就是显示器上。
我们也可以使用重定向来改变:
$ ls -l > file_list # 重定向到文件中(如果文件已经存在,会覆盖)
$ ls -l >> file_list # 追加到文件中(追加到文件末尾)
STDERR
这个描述符来处理错误消息。代表了执行shell命令时的错误信息。默认情况下错误信息会输出在显示器上。
重定向信息
基础重定向
重定向错误
ls -al basdfwf 2> err.log # 2 是 STDERR 的描述符,2 和 重定向符号> 之间没有空格!
重定向标准错误和标准输出
ls -al test test2 test3 badtest 2> err.log 1> output.log
# 另一种简单写法:
ls -al test test2 test3 badtest &> out.log
/dev/null 空文件
想要阻止某个输出,可以将其定向到一个特殊的 null 文件。所有写入这个文件的内容,都会被丢掉。
$ ls bad_file 2> /dev/null
用它作为输入来源,甚至可以清空文件,因为它本身不含任何内容:
$ cat /dev/null > testfile # 清空 testfile 文件,这是清除日志文件的一个常用方法
脚本中重定向
exec
命令可以在脚本执行期间,重定向某个文件描述符。
临时重定向错误
脚本中,可以临时将某句话重定向到错误
$ cat ./a
echo "this is error" >&2 # 临时重定向,仅仅重定向这一句代码,必须要加上 & 符号(不加 &,代表将输出定向到名字为2的文件)
echo "This is message"
# ----------- 正常执行脚本 -----------
$ ./a
this is error
This is message
# ------------ 重定向错误 ----------------
$ ./a 2> error
This is message
上面的例子中,我们正常执行脚本,可以正常在控制台打印输出;
但是如果我们将脚本的错误重定向到文件,就会发现脚本中临时重定向的那一行,会被记录到文件中,并且不会正常显示在控制台。
永久重定向
在脚本中,我们可以随时改变重定向:
$ cat ./a
exec 1> testout # 将后续的所有标准输出都重定向到 testout 文件中
echo "output"
exec 2> testerror # 将后续所有的标准错误都重定向到 testerror 文件中
ls -asljdfoiejafwwalsdj # 这段代码会报错,错误会定向到 testerror 中
echo "error" # 这是一个标准输出,会定向到 testout 中
# ------------- 执行 ------------
$ ./a # 执行脚本,没有输出,因为输出都定向到文件了
重定向输入
重定向输入,可以在脚本需要的时候,自动从文件读取内容:
$ cat ./a
#!/bin/bash
echo -e 'This is line1\nThis is line 2' > file # 先写入两句话到 file 文件中
exec 0< file # 设置标准输入为 file
count=1
while read line # 这里需要读取数据,会自动从 file 中获取。
do
echo "Line #$count: $line"
count=$[ $count + 1 ]
done
# ------------ 执行 -------------
$ ./a
Line #1: This is line1
Line #2: This is line 2
自定义输出型描述符
在 shell 中最多可以打开9个文件描述符。默认前三个是 0,1,2。其他6个如 3~8 均可用作输入和输出重定向。
$ cat ./a
#!/bin/bash
exec 3>test13out # 将 3 定向到文件 test13out
echo "This should display on the monitor"
echo "and this should be stored in the file" >&3 # 消息定向到 3
echo "Then this should be back on the monitor"
# ------------------------------
$ ./a
This should display on the monitor
Then this should be back on the monitor
恢复重定向
我们在重定向输入或输出以后,如何恢复它的状态:
$ cat ./a
#!/bin/bash
exec 3>&1 # 先将 3 定向到 1,1此时定向到控制台,因此3也定向到控制台
exec 1>test14out # 1 定向到文件 (此时3还是定向到控制台)
echo "This should store in the output file"
exec 1>&3 # 1 又重新定向到 3 即控制台
echo "Now things should be back to normal"
#------------------------------------
$ ./a
Now things should be back to normal
自定义输入型描述符
$ cat testfile
1
2
3
4
5
6
7
# -----------------------------------
$ cat ./a
#!/bin/bash
# redirecting input file descriptors
exec 6<&0 # 将0保存到6上
exec 0< testfile # 将 testfile 作为输入来源
count=1
while read line # 从 testfile 读取内容,每一行都保存到 line 变量中
do
echo "Line #$count: $line"
count=$[ $count + 1 ]
done
exec 0<&6 # 将6保存回到0上(即再次将0作为标准输入)
read -p "Are you done now? " answer
case $answer in
Y|y) echo "Goodbye";;
N|n) echo "Sorry, this is the end.";;
esac
# -----------------------------------
$ ./a
Line #1: 1
Line #2: 2
Line #3: 3
Line #4: 4
Line #5: 5
Line #6: 6
Line #7: 7
Are you done now? y
Goodbye
关闭文件描述符
在脚本中,关闭文件描述符,使用这样的形式:exec 3>&-
#!/bin/bash
# testing closing file descriptors
exec 3> test17file
echo "This is a test line of data" >&3
exec 3>&- # 代表关闭描述符3
echo "This won't work" >&3
& 的作用
上面的例子中,很多地方用到了 &
符号,其实 &
符号是和 <, >
一体的,也就是它们是一个整体,有如下几种形式:
2>&1 # 将 文件描述符2 作为输出型描述符,定向到 描述符1 所定向到的地方:默认终端控制台
command &> file # 将命令的标准输出和标准错误都定向到 file 文件,等同于: command > file 2>&1
6<&0 # 将文件描述符6 作为输入型描述符,定向到 描述符0 所定向到的地方:默认键盘
举个例子吧。把一个命令想象成一个工厂。它默认有三条管线:0 管线是输料管,默认源头是键盘。1 管线是出料管,默认尽头是终端控制台。2 管线是污水管,默认尽头也是终端控制台。
2>&1
代表将 2 管线不再输送到终端控制台,而是输送到 1 管线指定输送的地方:默认是终端控制台。
&> file
代表将 1 和 2 管线都输送到 file 中,而不是输送到终端控制台上。
6<&0
代表新建一条进料管线 6,管线6的源头是管线0的源头:默认都是键盘
思考下面的例子:
$ ls a_not_exist_folder > log 2>&1
# 不会在终端控制台上显示任何信息,原因如下:
# 1. 首先, > log 代表了将标准输出1 定向到文件 log: 1 -> log
# 2. 其次, 2>&1 代表了将标准错误2 定向到 标准输出1, 1 此时定向到 log,因此: 2 -> log
# 3. 所以, 标准输出和标准错误都存入了 log 中
$ $ ls a_not_exist_folder 2>&1 > log
ls: cannot access 'a_not_exist_folder': No such file or directory
# 这次我们改变了一下顺序:结果就和上面不同,标准错误会依然显示在终端控制台上,原因如下:
# 1. 首先,2>&1 将标准错误定向到标准输出,而标准输出此时默认指向控制台,因此: 2 -> terminal
# 2. 其次,> log 将标准输出定向到 log: 1 -> log,但是标准错误依然指向终端: 2 -> terminal
# 3. 所以,标准错误没能都存放到 log 中
罗列出打开的文件描述符
lsof
命令可以查找出某个进程所打开的文件描述符,普通账户使用它时,需要写命令的全路径,如:/usr/sbin/lsof
$ /usr/sbin/lsof -a -p $$ -d 0,1,2
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
bash 3344 rich 0u CHR 136,0 2 /dev/pts/0
bash 3344 rich 1u CHR 136,0 2 /dev/pts/0
bash 3344 rich 2u CHR 136,0 2 /dev/pts/0
参数:
- -a 代表了 AND 运算
- -p 用来指定进程 id, 特殊环境变量
$$
代表了当前进程- -d 指定要显示的文件描述符
显示信息:
COMMAND,PID,USER 代表了命令信息,进程id,用户。
FD 代表了文件描述符号以及访问类型(r代表读,w代表写,u代表读写)
TYPE 文件的类型(CHR代表字符型,BLK代表块型,DIR代表目录,REG代表常规文件)
DEVICE 设备的设备号(主设备号和从设备号)
SIZE 如果有的话,表示文件的大小
NODE 本地文件的节点号
NAME 文件名
创建临时文件
Linux 系统有个特殊的目录,专门存放临时文件:/tmp
有个命令可以创建临时文件,mktemp
命令可以在 /tmp
文件下创建一个唯一的临时文件,只有当前用户和root用户拥有访问它的权限。
当前路径创建临时文件
语法:
mktemp file_name.XXX
# `X` 代表了特殊符号,命令会自动使用随机字符替换掉 X,从而生成唯一的文件名。有几个 X,就会替换掉几个字符
实例:
$ mktemp file.XXX
file.Mqv
$ mktemp file.XXX
file.nGg
$ mktemp file.XXX
file.V33
$ mktemp file.XXX
file.FwG
$ ls file.*
file.FwG file.Mqv file.V33 file.nGg
mktemp 命令的返回值就是文件名,因此你可以将其赋值给某个变量
实例 (在脚本中使用命令):
$ cat a
#!/bin/bash
tempfile=$(mktemp test19.XXXXXX)
exec 3>$tempfile # 将描述符3定向到临时文件
echo "This script writes to temp file $tempfile"
echo "This is the first line" >&3 # 定向到描述符 3,即临时文件
echo "This is the second line." >&3
echo "This is the last line." >&3
exec 3>&- # 关闭描述符
echo "Done creating temp file. The contents are:"
cat $tempfile
rm -f $tempfile 2> /dev/null # 删除临时文件
$ ./a
This script writes to temp file test19.0an4lN
Done creating temp file. The contents are:
This is the first line
This is the second line.
This is the last line.
/tmp 目录创建临时文件
-t
选项可以强制在 /tmp
目录下创建临时文件,并返回文件的全路径。
$ mktemp -t test.XXXXXX
/tmp/test.xG3374
当前路径创建临时目录
-d
选项可以在当前路径下创建临时目录,而不是临时文件。
譬如:
#!/bin/bash
tempdir=$(mktemp -d dir.XXXXXX)
cd $tempdir # 进入目录
tempfile1=$(mktemp temp.XXXXXX) # 在临时目录下创建文件
tempfile2=$(mktemp temp.XXXXXX)
exec 7> $tempfile1
exec 8> $tempfile2
echo "Sending data to directory $tempdir"
echo "This is a test line of data for $tempfile1" >&7
echo "This is a test line of data for $tempfile2" >&8
记录消息
将输出同时发送到显示屏和日志文件,可以使用 tee
命令,它就像一个 T
型管道,从 STDIN 获取数据,分别输出到 STDOUT 和用户指定的地方。
譬如:
$ date | tee log
Wed Apr 13 10:11:12 CST 2022 # 屏幕正常输出
$ cat log
Wed Apr 13 10:11:12 CST 2022 # log 中也有
这个命令默认每次会覆盖原有的文件,如果想要追加到文件,使用 -a
选项:
$ date | tee -a log # 使用了 -a 选项
Wed Apr 13 10:11:40 CST 2022
$ cat log
Wed Apr 13 10:11:12 CST 2022 # 原有的数据没有被覆盖
Wed Apr 13 10:11:40 CST 2022
在脚本中使用:
$ cat test22
#!/bin/bash
# using the tee command for logging
tempfile=test22file
echo "This is the start of the test" | tee $tempfile
echo "This is the second line of the test" | tee -a $tempfile
echo "This is the end of the test" | tee -a $tempfile
$ ./test22
This is the start of the test
This is the second line of the test
This is the end of the test
$ cat test22file
This is the start of the test
This is the second line of the test
This is the end of the test
实例
首先创建一个 members.csv
:
$ cat members.csv
Blum,Richard,123 Main St.,Chicago,IL,60601
Blum,Barbara,123 Main St.,Chicago,IL,60601
Bresnahan,Christine,456 Oak Ave.,Columbus,OH,43201
Bresnahan,Timothy,456 Oak Ave.,Columbus,OH,43201
脚本内容如下:
$cat sql
#!/bin/bash
outfile='members.sql'
IFS=','
while read lname fname address city state zip
do
cat >> $outfile << EOF
INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('$lname', '$fname', '$address', '$city', '$state', '$zip');
EOF
done < ${1}
执行和结果:
$ ./sql members.csv
$ cat members.sql # 生成了这么一个文件
INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Blum', 'Richard', '123 Main St.', 'Chicago', 'IL', '60601');
INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Blum', 'Barbara', '123 Main St.', 'Chicago', 'IL', '60601');
INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Bresnahan', 'Christine', '456 Oak Ave.', 'Columbus', 'OH', '43201');
INSERT INTO members (lname,fname,address,city,state,zip) VALUES ('Bresnahan', 'Timothy', '456 Oak Ave.', 'Columbus', 'OH', '43201');
分析:
首先,脚本第三行的 IFS=','
指定了读取输入时,字段的分隔符要按照 ,
分割,而不是默认的 空格,tab,换行
等分割。
脚本的第四行 while read lname fname address city state zip
来读取 STDIN 内容的每一行,并按照 IFS
分割后将其赋值给各个变量,那么文本来自哪里呢?在脚本的最后一行 done < ${1}
表明了输入来自脚本命令行的第一个参数即: members.csv
文件
在 while
循环中, cat >> $outfile << EOF
有两个小点:
-
>> $outfile
代表了将cat
命令的输出,追加到outfile
变量代表的文件中。 -
<< EOF
代表了内联输入重定向,即将后续的内容都作为输入传递给cat
命令,直到遇到EOF
符号才输入结束(因此后续的INSERT INTO...
都会作为输入内容传递给cat
命令)
控制脚本
处理信号
Linux利用信号与运行在系统中的进程进行通信
信号
信号 | 值 | 描述 |
---|---|---|
1 | SIGHUP | 挂起进程 |
2 | SIGINT | 终止进程 |
3 | SIGQUIT | 停止进程 |
9 | SIGKILL | 无条件终止进程 |
15 | SIGTERM | 尽可能终止进程 |
17 | SIGSTOP | 无条件停止,但不是终止进程 |
18 | SIGTSTP | 停止或暂停,但不终止进程 |
19 | SIGCONT | 继续运行停止的进程 |
停止(stopping)进程跟终止(terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置继续运行
默认情况下,bash shell 会忽略收到的 3,5 信号(防止意外终止),会处理 1,2信号。
如果 bash shell 收到了 SIGHUP 信号,比如你要离开一个交互式 shell,则shell会退出,并且将此信号传递给所有由此shell启动的进程或运行的脚本。
shell 收到 SIGINT 信号会中断。Linux 内核会停止位 shell 分配 CPU 处理时间,shell 会将此信号通知给所有由它启动的进程,来告知情况。
生成信号
中断进程
Ctrl + C 可以发送 SIGINT 信号,并将其发送给所有在当前 shell 中运行的进程。
暂停进程
Ctrl + Z 可以发送 SIGSTSTP 信号,停止 shell 中的任何进程。
捕获信号
trap
命令可以在脚本中捕获从 shell 中拦截的信号,格式:
trap commands signals
- signals 代表了信号们,用空格隔开
- commands 代表了捕获到 signals 中的信号后,要执行的命令
譬如:
$ cat a
#!/bin/bash
# 捕获 SIGINT 信号,并执行 echo '...' 命令
trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT
count=1
while [ $count -le 10 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
#
echo "This is the end of the test script"
# ======================== 执行 =========================
$ ./a
Loop #1
Loop #2
Sorry! I have trapped Ctrl-C # 执行到这里时按下了 Ctrl + C,但是脚本并没有终止
Loop #3
Loop #4
Loop #5
Sorry! I have trapped Ctrl-C
Loop #6
Loop #7
Loop #8
Loop #9
Loop #10
This is the end of the test script
捕获脚本退出的信号:
$ cat test2.sh
#!/bin/bash
trap "echo Goodbye..." EXIT # 脚本退出时,执行 echo ...
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
$ ./test2.sh
Loop #1
Loop #2
Loop #3
Loop #4
Loop #5
Goodbye... # 你也可以在 loop 时手动 Ctrl+C 退出脚本,也能捕获
修改和移除捕获
$ cat test2.sh
#!/bin/bash
trap "echo Goodbye..." SIGINT # 捕获 SIGINT
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
sleep 1
count=$[ $count + 1 ]
done
trap "echo Modified" SIGINT # 修改捕获信号时执行的命令
trap -- SIGINT # trap -- 不再捕获某信号
后台模式运行
后台运行脚本
使用 &
来后台运行脚本,会将命令作为一个独立的后台进程运行。但是注意:后台运行的脚本,它的 STDOUT,STDERR 默认还都是控制台终端。
$ date &
[1] 805 # [1] 是作业号。 805 是进程ID
Wed Apr 13 12:39:39 CST 2022 # date 命令的输出,依然会显示在控制台终端
[1]+ Done date # 后台任务完成时显示的信息
为了不让后台任务的输出显示在控制台终端,最好是将后台脚本的输出进行重定向。
禁止挂起
在后台运行脚本,如果退出 shell,则 shell 中所有启动的进程也会终止。这时可以使用 nohup
命令,它可以阻断所有发送给该进程的 SIGHUP 信号,从而当终端退出时,该进程不会退出。
$ nohup date &
[1] 809
nohup: ignoring input and appending output to 'nohup.out'
[1]+ Done nohup date
因为 nohup 解除了终端和进程的关联,因此该进程会将 STDOUT,STDERR 重定向到 nohup.out 的文件中。
注意:如果使用 nohup 在同一个目录中运行了多个命令,则会将所有命令的输出都放到同一个 nohup.out 文件中。
作业控制
查看作业
jobs
可以列出作业:
$ sleep 100 & # 创建一个后台作业
[1] 813 # 作业号和PID
$ jobs # 查看作业
[1]+ Running sleep 100 & # 作业号后面的 + 代表了它是默认作业,如果是 - 代表它是下一个默认作业
$ jobs -l # -l 选项可以列出作业的进程ID
[1]+ 813 Running sleep 100 &
jobs 的选项:
- -l:列出进程PID和作业号
- -p:只列出作业PID
- -r:只列出运行中的作业
- -s:只列出已暂停的作业
重启停止的作业
后台启动
bg
命令可以以后台的形式,启动已经暂停的作业,后面不加作业号则启动默认作业。
$ sleep 100 # 回车后,按下 Ctrl + Z 发送信号,来暂停作业
[3]+ Stopped sleep 100 # 已暂停
$ jobs
[3]+ Stopped sleep 100
$ bg 3 # 恢复作业 3
[3]+ sleep 100 &
$ jobs
[3]+ Running sleep 100 & # 执行中
前台启动
fg
命令可以以前台的形式,启动作业,语法和 bg
一样。
$ sleep 100
[5]+ Stopped sleep 100 # Ctrl + Z 暂停作业
$ fg 5 # 前台执行
sleep 100 # 这里会阻塞住,因为它在前台执行,会等待执行完毕
脚本优先级
多任务系统中,内核根据优先级,来调整分配给每个进程的CPU时间,优先级从高到低为:-20(最高) 到 +19(最低)。由 shell 启动的所有进程,它们的优先级是一样的,都是0
nice 命令调整优先级
nice 可以在启动命令时,调整进程的优先级。注意:普通用户仅能调低进程的优先级,即将 nice 值调大(越大优先级越低)
$ nice -n 10 sleep 100 &
-n 选项可以忽略,写成: nice -10 sleep 100 &
renice 调整优先级
renice 可以调整已经运行的进程的优先级:
$ renice -n 10 -p 925 # -p 指定PID的进程
普通用户只能将优先级调低
只能调整属于当前用户的进程
root 用户可以任意调整进程优先级
脚本定时执行
at 命令
大部分Linux 在启动时,会运行一个 atd 的守护进程,它会每隔 60 秒检查一下特殊的目录(通常是/var/spool/at
) ,如果有定时作业并且时间和当前时间匹配,就运行此作业。
at 命令只能运行一次命令
语法格式
at [-f file_name] time
-f:用来指定运行哪个脚本
time 是运行脚本的时间,有如下几种格式:
- 标准的小时和分钟格式,比如 10:15
- AM/PM指示符,比如 10:15 PM
- 特定可命名时间,比如 now、noon、midnight 或者 teatime(4 PM)。 除了指定运行作业的时间,也可以通过不同的日期格式指定特定的日期。
- 标准日期格式,比如 MMDDYY、MM/DD/YY 或 DD.MM.YY。
- 文本日期,比如 Jul 4 或 Dec 25,加不加年份均可。
- 你也可以指定时间增量:
- now + 25 min
- tomorrow 10:15 PM
- 10:15 + 7 days
实例:
$ cat test13.sh
#!/bin/bash
echo "This script ran at $(date +%B%d,%T)"
sleep 5
echo "This is the script's end..."
$ at -f test13.sh now # now 代表立刻执行
job 7 at 2015-07-14 12:38
at 命令的输出
at 命令执行的任务,输出内容默认是通过邮件发送给用户的!因此建议你在脚本中进行输出的重定向,否则你可能无法获取输出。
$ at -M -f test13b.sh now # -M 可以屏蔽输出
atq 查看作业
$ atq
atrm 删除作业
$ atrm 18 # 删除作业号 18 的定时任务
cron 定期任务
cron 程序可以每隔多久运行一次任务。
cron 时间表
一种特殊的时间表述方式,来表示每隔多久执行一次某命令:
min hour day month week command
分别代表:分钟,小时,月份中的第几天,几月,周几,命令
week 的取值选项有:(mon、tue、wed、thu、fri、sat、sun)或数值(0~6,0为周日,6为周六)
你可以使用特定值、取值范围(如1~5)、或者 *
来指定时间:
15 10 * * * command # 每天 10:15 执行 command
00 12 1 * * command # 每月第一天的12:00 执行命令
如何在每月的最后一天中午12点运行命令:
00 12 * * * if [`date +%d -d tomorrow` = 01 ] ; then ; command
查看时间表
$ crontab -l
编辑时间表
$ crontab -e
cron 目录
cron 有四个预先配置的脚本目录:hourly, daily, monthly, weekly ,如果你对定期任务的运行时间要求不高的话,可以直接将脚本放到这四个目录中的某个目录下,那么脚本就会定期执行。譬如你想每小时执行一次,可以将其放到 hourly 目录
$ ls /etc/cron.*ly
/etc/cron.daily:
...
/etc/cron.hourly:
...
anacron 程序
cron 程序的缺点是,假如你的 Linux 系统关机了,那么关机期间错过的任务,在开机时会忽略掉,不会执行。而 anacorn 程序就是补充这一缺点的程序:它会去 cron 目录下找那些错过的任务,然后在开机时,尽快的执行这些任务。
anacron 拥有自己的时间表,通常位于 /etc/anacrontab
:
$ sudo cat /etc/anacrontab
# /etc/anacrontab: configuration file for anacron
# See anacron(8) and anacrontab(5) for details.
SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# the maximal random delay added to the base delay of the jobs
RANDOM_DELAY=45
# the jobs will be started during the following hours only
START_HOURS_RANGE=3-22
#period in days delay in minutes job-identifier command
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly
它的时间计划是这样的:
period delay identifier command
period 是多久执行一次
delay 是开机后延迟多久后再执行错过的任务
identifier 没啥用
command条目包含了 run-parts 程序和一个 cron 目录名,代表了它去执行目录下所有的脚本
因此,当 anacron 执行时,会先找出自己的时间表中的记录:
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly
然后它会去比如 /var/spool/anacron/cron.daily
这个文件中查看 daily 这个 cron 目录的执行时间,然后和自己的时间表中的 cron.daily 这个计划任务的 period 字段对比。如果执行时间和当前系统时间的差值大于 period,证明它已经大于 period 没有执行了(即错过了),所以延迟 5 分钟后它就会执行 cron.daily 这个目录下所有的脚本
函数
基本函数
创建函数有两种格式:
function name {
commands
}
# ===============
name() { # () 表明了这是一个函数
commands
}
使用函数
$ cat ./a
#!/bin/bash
function func1 {
echo 'This is func1'
}
func1 # 直接使用,不要加 ()
echo "End of script"
# ================== 执行脚本 =============
$ ./a
This is func1
End of script
函数返回值
默认退出码
默认情况,函数的返回值是函数中最后一条命令的退出状态码。我们可以使用 $?
来获取最近一次命令的退出状态码。
$ cat ./a
#!/bin/bash
function func1 {
echo 'This is func1'
}
func1
echo "Exit code is: $?" # 获取退出状态码
# =============== 执行 ================
$ ./a
This is func1
Exit code is: 0
使用 return
return 命令可以退出函数并返回特定的退出状态码,它允许指定一个整数值来定义函数的退出状态码。
$ cat ./a
#!/bin/bash
function func1 {
echo 'This is func1'
return 2
}
func1
echo "Exit code is: $?"
# ============= 执行 ================
$ ./a
This is func1
Exit code is: 2
记住:退出状态码只能是 0~255
巧用函数输出
我们可以通过 $()
来将函数的输出放到变量里面,从而获取函数的结果:
$ cat ./a
#!/bin/bash
function func1 {
read -p "Enter your name: " name # 让用户输入姓名
echo $name # 输出获取的姓名
}
result=$(func1) # 将函数的输出赋值给变量
echo "The result is: $result"
# ================= 执行 ==================
$ ./a
Enter your name: sunshine
The result is: sunshine
函数中使用变量
函数传参
函数传参,使用如下格式:
func1 arg1 arg2 ... # 要写在同一行
$ cat ./a
#!/bin/bash
add() {
if [ $# -eq 0 ] || [ $# -gt 2 ]
then
echo -1
elif [ $# -eq 1 ]
then
echo $[ $1 + $1 ]
else
echo $[ $1 + $2 ]
fi
}
value=$(add 10 1) # 传参
echo "result is: $value"
value=$(add $value 1)
echo "result is: $value"
# ==================== 执行 ================
$ ./a
result is: 11
result is: 12
函数中处理变量
全局变量
在脚本中,无论是在函数中,还是函数外,出现在脚本中的变量,都是全局变量,这意味着你可以在脚本的任意地方使用变量:
$ cat a
#!/bin/bash
func(){
value=2
}
value=3 # 刚开始是2
func # 执行了函数
echo "value is: $value"
# =========== 执行 ============
$ ./a
value is: 2 # 变成了2
局部变量
全局变量是很危险的行为,这意味着你在脚本中不能声明同样的变量名,否则它们本质是同一个变量,在任意地方改它,都会在全局生效。因此,我们可能需要使用局部变量,使用关键字 local
$ cat ./a
#!/bin/bash
func(){
local value=2 # 声明局部变量
}
value=3
func
echo "value is: $value"
# =============== 执行 ===========
$ ./a
value is: 3
函数传参:数组
将数组作为一个变量传递给函数,函数无法处理,只能获取数组的第一个元素。想要正确处理数组,需要这样做:
#!/bin/bash
func(){
local array=($(echo "$@")) # 重新将参数组合成一个数组
echo "In func: ${array[*]}"
}
array=(1 2 3)
func ${array[@]} # 数组拆开,作为多个参数传递给 func;不能写成:func $array
echo "value is: ${array[*]}"
同样,想要从函数返回一个数组,也得这样做:
$ cat ./a
#!/bin/bash
func(){
local array=(1 2 3 4 5)
echo ${array[@]}
}
arr=($(func)) # 将函数的输出重新作为一个元组
echo "value is: ${arr[*]}"
# =========== 执行 ===========
$ ./a
value is: 1 2 3 4 5
递归
递归,即自己调用自己。下面是一个递归的例子, add 函数可以将多个值累加,在这个过程中会递归调用 add 函数自身。
$ cat ./a
#!/bin/bash
add() {
if [ $# -eq 0 ] # 没有参数,返回0
then
echo 0
elif [ $# -eq 1 ] # 有一个参数,返回本身
then
echo $1
else
local temp=$[ $1 + $2 ] # 多个参数,先将前两个参数相加
shift 2 # 左移两个参数
local result=$(add $temp $*) # 将后续参数继续追加
echo $result
fi
}
add 1 2 3 4
# ====================== run ======================
$ ./a
10
创建库
一个库,即一个脚本文件,我们可以在多个脚本中引用某个脚本库,从而实现脚本的重复使用。
创建库非常简单,一个脚本文件就是一个库。问题在于,如何在其他脚本中引用这个库。
引入库的有一个命令:source
,它的快捷方式是 .
。你如果想要在某个脚本中引入某个库,可以这样写:
. path/to/func # . 代表了 source, 后面跟随你库文件的路径
source 会在当前 shell 上下文中执行命令,而不是创建一个新的 shell,因此需要使用它来导入其他库
实例:
$ cat add.sh # 这是我们等会要引入的库文件
#!/bin/bash
add() {
if [ $# -eq 0 ]
then
echo 0
elif [ $# -eq 1 ]
then
echo $1
else
local temp=$[ $1 + $2 ]
shift 2
local result=$(add $temp $*)
echo $result
fi
}
# ======================================
$ cat test.sh
. ./add.sh # 引入 add 库
add 1 2 3 # 调用 add 库中的 add 函数
# ==================== run ==================
$ ./test.sh
6
.bashrc 中定义函数
在 .bashrc 中定义的函数,会在 shell 启动时自动查找这个文件,从而执行这个函数,因此如果你有一些想要在 shell 启动时就能直接在shell中调用的函数,可以将其写在这个文件中。
$ cat .bashrc
# .bashrc
# Source global definitions
if [ -r /etc/bashrc ]; then
. /etc/bashrc
fi
. /home/rich/libraries/myfuncs # 你可以直接 source 某个库,也可以在这个文件中直接编写函数
这样的话,以后你可以在任意shell,子 shell 中使用库中的函数,而不用导入它了。
图形化脚本
文本菜单
创建文本菜单其实是很容易的,只要使用 read
, echo
, case
三个命令就行了:
echo 可以打印文字,来打印菜单选项
read 可以读取用户输入,来获取用户选了什么
case 可以匹配用户的输入,来绝对执行哪个菜单选项的操作
原理很简单,就不演示了
select 命令
select 命令可以很轻松的创建一个菜单,它的语法是:
select variable in list # list 即要展示的菜单列表,variable 代表了用户选中的选项
do
commands
done
实例:
$ cat ./menu
#!/bin/bash
# using select in the menu
function diskspace {
clear
df -k
}
function whoseon {
clear
who
}
function memusage {
clear
cat /proc/meminfo
}
PS3="Enter option: "
select option in "Display disk space" "Display logged on users" "Display memory usage" "Exit program"
do
case $option in
"Exit program")
break ;;
"Display disk space")
diskspace ;;
"Display logged on users")
whoseon ;;
"Display memory usage")
memusage ;;
*)
clear
echo "Sorry, wrong selection";;
esac
done
clear
# ================== run ==========
$ ./menu
1) Display disk space 3) Display memory usage
2) Display logged on users 4) Exit program
Enter option:
clear
可以用来清空屏幕上的所有内容
窗口和图形
略
初识 sed 和 gawk
文本处理
sed 编辑器
sed 是一款流编辑器。它可以根据之前预设的规则来编辑数据流。sed编辑器并不会修改文本文件的数据,它只会将修改后的数据发送到 STDOUT。
针对输入内容,它会做如下操作:
- 从输入中读取一行数据
- 根据设定的规则匹配数据
- 按照规则修改流中的数据
- 输出新数据到 STDOUT
- 读取下一行,重复以上操作,直到数据流处理完毕
sed 的语法:
sed options script file
options 有如下几种:
- -e script:处理输入时,将 script 中指定的命令添加到当前已有的命令中
- -f file:将 file 中的命令添加到当前已有的命令中
- -n:不产生输出,使用 print 命令来完成输出
处理 STDIN 数据:
$ echo 'I love you' | sed -e 's/love/hate/'
I hate you
s/love/hate/
代表了将 love 替换成 hate,格式为:s/old_str/new_str/
当然你也可以处理文件:
$ echo "I love you" > file
$ sed -e 's/love/hate/' file
I hate you
也可以同时执行多条命令:
$ sed -e 's/love/hate/;s/you/me/' file # 多条命令用 `;` 隔开,分号前面不能有空格
I hate me
或者从文件中读取命令:
$ cat file1
s/love/hate/
s/you/myself/
$ sed -f file1 file
I hate myself
替换命令
替换命令的语法是:s/pattern/replacement/flags
,其中 flags 有如下几种选项:
-
空着:默认只替换发现的第一个字符
-
数字:替换第几处查找的字符
-
g:替换行中所有找到的字符
-
p:打印输出(没啥用)
-
w file:将替换后的结果,写入到文件中
实例:
$ cat file # 要处理的文件
I love you love you # 每一行都有两个 love
You love me love me
$ sed "s/love/hate/" file
I hate you love you # 默认只处理第一个 love
You hate me love me
$ sed "s/love/hate/2" file
I love you hate you # 替换了第二处 love
You love me hate me
$ sed "s/love/hate/g" file
I hate you hate you # 两个 love 全被替换
You hate me hate me
$ sed "s/love/hate/p" file
I hate you love you # 每行都多打印一次
I hate you love you
You hate me love me
You hate me love me
$ sed "s/love/hate/w new_file" file # 将替换后的结果,保存到 new_file 中。注意:只有被替换的行才会保存到 new_file 中。如果某行没有要替换的字符,则不会被保存到 new_file 里面。
I hate you love you
You hate me love me
$ cat new_file
I hate you love you
You hate me love me
替换 /
本身
有两种方式可以替换 /
本身:
# 下面的例子将 /bash 替换成 /bbb
# ============= method 1 =============
$ echo '/bin/bash' | sed 's!/bash!/bbb!' # 使用自定义的 ! 代替语法中的 /
/bin/bbb
# ============== method 2 ============
$ echo '/bin/bash' | sed 's/\/bash/\/bbb/' # 使用 \ 对 / 进行转义
/bin/bbb
限制搜索范围
我们可以从某行到某行之间,进行搜索和替换,譬如在第1~2行之间,替换某字符。
$ cat file
I love you love you
You love me love me
hahahahahha
$ sed '1s/love/hate/' file # 1s 代表只替换第一行中的数据
I hate you love you
You love me love me
hahahahahha
$ sed '1,2s/love/hate/' file # 1,2s 代表替换第 1~2 行之间的数据
I hate you love you
You hate me love me
hahahahahha
$ sed '1,$s/love/hate/' file # 1,$s 代表替换从第一行到最后一行的数据。$ 代表结尾最后一行。
I hate you love you
You hate me love me
hahahahahha
命令组合
如果想在单行数据执行多条命令,可以写成如下形式:
1,2{
s/love/hate/
s/me/you/
}
譬如:
$ cat file
I love you love you
You love me love me
hahahahahha
$ sed '1,2{s/love/hate/;s/you/myself/}' file # {} 括起来,各命令之间使用 ; 分割
I hate myself love you
You hate me love me
hahahahahha
匹配字符
我们可以设置匹配字符,只有当行中匹配到了我们设置的字符,才会尝试对当前行中的数据进行替换。
格式如下:
/pattern/s/old/new/ # 其实就是在 s/old/new/ 前面,加上了一个 /pattern/
实例:
$ cat file
I love you love you
You love me love me
hahahahahha
$ sed '/I love you/s/love/hate/g' file # 先判断当前行是否有字符:I love you,如果有,则正常进行替换操作
I hate you hate you
You love me love me
hahahahahha
删除行
d
命令可以删除数据行:
$ cat file
I love you love you
You love me love me
hahahahahha
$ sed 'd' file # 不指定删除哪行,会全部删除
$ sed '1d' file # 只删除第一行
You love me love me
hahahahahha
$ sed '1,2d' file # 删除 1~2 行
hahahahahha
$ sed '1,$d' file # 删除 1~最后所有行
$ sed '/I love you/d' file # 删除包含了 I love you 字符的所有行
You love me love me
hahahahahha
$ sed '/love/,/haha/d' file # 从 love 字符找到的行开始,一直删除到包含 haha 字符的行。`,` 代表了从某行删除到某行(包括这两行)
$ sed '/love/,/xyz/d' file # 从 love 字符找到的行开始,一直删除到包含 xyz 字符的行(因为到文件末尾也没能发现 xyz,所以将 love 后续的行全部删除
从上面可以看出,要慎用 /x/,/y/d
这种形式,即从某行删除到某行。
插入行
i
命令会在指定的行前插入一行,a
会在指定的行后添加一行。
$ echo 'aaa' | sed 'i\bbb' # 插入在每一行前面
bbb
aaa
$ echo 'aaa' | sed 'a\bbb' # 附加在每一行后面
aaa
bbb
$ echo -e '1\n2\n3\n4' | sed '3i\new' # 第3行前插入
1
2
new
3
4
$ echo -e '1\n2\n3\n4' | sed '3a\new' # 第3行后
1
2
3
new
4
$ echo -e '1\n2\n3\n4' | sed '$a\new' # 最后一行后面
1
2
3
4
new
$ echo -e '1\n2\n3\n4' | sed '$i\new'
1
2
3
new
4
$ echo -e '1\n2\n3\n4' | sed '$i\new\nbbb' # 插入多行,使用 \n
1
2
3
new
bbb
4
修改替换行
c
可以修改某行的数据,相当于将某行替换成自己设置的数据
$ echo -e '1\n2\n3\n4' | sed '3c\new' # 替换第3行
1
2
new
4
$ echo -e '1\n2\n3\n4' | sed '1,3c\new' # 将1~3行替换成 new
new
4
$ echo -e '1\n2\n3\n4' | sed '/3/c\new' # 在行中匹配到 3 后,将此行替换成 new
1
2
new
4
转化单个字符
y
可以转换单个字符,它可以将字符一一对应,用后面的字符替换前面对应位置的字符,并且两个字符集长度要一样:
$ echo -e '1223894124111211349212' | sed 'y/123/xyz/' # 将1,2,3分别转换成 x,y,z
xyyz894xy4xxxyxxz49yxy
打印输出
有三个命令可以打印编辑的行,尽管 sed 命令默认就会打印信息:
- p:打印编辑的行(通常和 -n 选项搭配使用,因为 -n 会阻止打印)
- =:打印行号
- l:小写的L 可以打印数据流中的文本和不可打印的ASCII字符
# ===================== p 命令 ====================
$ echo -e '1\n2\n3\nd' | sed -n '2,3p' # -n 阻止输出,p显示输出,因此只会显示 2,3 行
2
3
$ echo -e '1\n2\n3\nd' | sed -n '1{p; s/1/0/; p}' # 执行多条命令:先打印原始第一行,然后替换,再打印替换后的第一行
1
0
# ----------------- = 命令 ---------------------
$ echo -e 'a\nb\nc\nd' | sed -n '/a/{=}' # 打印文本包含 a 字符的行号
1
# ==================== l 命令 ==============
$ echo -e 'aaalal\taaa' | sed -n '/a/{l}'
aaalal\taaa$ # 打印了 \t 符号
读写文件
w
可以写入文件,r
可以读取文件:
# ================== w ===================
$ echo -e '1\n2\n3\n' | sed '1,2w file' # 将第1,2行写入文件 file
1
2
3
$ cat file
1
# =================== r ==================
$ cat file
1
2
$ echo -e 'aaa' | sed '1r file' # 读取 file 的内容,并将其全部放到第一行后面
aaa
1
2
gawk 程序
gawk 是一种编程语言,同样可以处理流数据。
它的语法是:
gawk options program file
options:
- -F fs:指定数据行中用来划分字段的分隔符
- -f file:从指定的文件中读取程序
- -v var=value:定义变量和其默认值
- -mf N:指定要处理的数据文件中的最大字段数
- -mr N:指定要处理的数据文件的最大数据行数
- -W keyword:指定gawk的兼容模式或警告等级
从命令行读取程序
$ gawk '{print "hello"}' # 会等待用户输入,然后针对每一行数据,执行程序:打印 hello
This is user input
hello
another input
hello
注意:程序必须使用 {}
括起来,并且由于gawk命令行假定脚本是单个文本字符串,所以还要放到单引号''
中。
字段变量
gawk 会自动给一行中的每个数据元素分配一个变量(它会自动使用 IFS 分隔符,来分割每行),其中:
- $0 代表整行
- $1 代表第一个数据字段
- ...
- $n 代表第 n 个数据字段
$ echo "hello word" | gawk '{print $1}'
hello
自定义行分隔符
默认情况下,gawk 会按照特殊变量 IFS
来分割每行数据,IFS 的默认值是空白符,tab符,以及换行符。使用 -F
选项可以指定分隔符
$ echo "name:word's master" | gawk -F ':' '{print $2}'
word's master
-F 后面的分隔符可以加一对双引号或单引号,也可以不加: -F :
编写多条命令
想要编写多条命令,在命令之间加上 ;
$ echo "name:word's master" | gawk -F ":" '{$2="sunshine"; print $0}' # 给 $2 重新赋值
name sunshine
从文件读取程序
-f
选项可以指定从文件读取程序
$ cat f.awk
{$1="sunshine"; print $0} # 不用加单引号
$ echo "name: wang" | gawk -F : -f f.awk
sunshine wang
# ================== 美观 ============
$ cat f.awk # 你也可以格式化一下程序文件,每个命令写在一行,不用加 ;
{
$1="sunshine"
print $0
}
$ echo "name: wang" | gawk -F : -f f.awk
sunshine wang
处理数据前执行
BEGIN
关键字可以在脚本处理数据流之前,先执行一下,如果你处理数据之前有一些前提条件,可以写在这里。并且注意:BEGIN
拥有自己的 block 块,即自己单独有一个 { commands }
$ cat f.awk
BEGIN {
print "This is pre-condition, only run once."
}
{
$1="sunshine"
print $0
}
# =========================================
$ echo -e "name: wang\nname: Li" | gawk -F : -f f.awk
This is pre-condition, only run once.
sunshine wang
sunshine Li
处理数据后执行
END
关键字和 BEGIN
类似,它可以在所有数据行处理完成之后,运行一次,并且它也有用自己的 { commands }
$ cat f.awk
BEGIN {
print "This is pre-condition, only run once."
}
{
$1="sunshine"
print $0
}
END {
print "The End of the script"
}
# ===========================================
$ echo -e "name: wang\nname: Li" | gawk -F : -f f.awk
This is pre-condition, only run once.
sunshine wang
sunshine Li
The End of the script
正则表达式
正则表达式有两种引擎:
- POSIX 基础正则表达式(basic regular expression,BRE)引擎
- POSIX 扩展正则表达式(extended regular expression,ERE)引擎
基础正则
纯文本
正则表达式可以匹配纯文本,它不在于文本出现的位置,只在意数据中有没有此文本(大小写敏感)
$ echo -e 'A\nB' | sed -n '/A/p' # /A/ 就是用来匹配纯文本 A 的
A
特殊字符
正则表达式识别的特殊字符包括: .*[]^${}\+?|()
,如果想要使用特殊字符,需要加 \
进行转义,如转义\?
,想要转义 \
本身,可以这样写:\\
。(注意,尽管/
不是特殊字符,但是如果你要在 sed 等正则表达式中使用 /
,也要转义)
$ echo 'Are you ok?' | sed -n '/\?/p' # 转义 ?
Are you ok?
$ echo '//Are you ok?' | sed -n '/\//p' # 尽管 / 不是特殊字符,但是在sed正则表达式中,还是要转义
//Are you ok?
锚字符
^
^
可以用来定义文本行的行首开始。
$ echo -e 'ABCDE\nBAA' | sed -n '/^A/p' # 只显示以 A 开头的行
ABCDE
$
$
定义文本行的末尾
$ echo -e 'ABCDE\nBAA' | sed -n '/E$/p' # 显示以 E 结尾的行
ABCDE
^$ 组合
$ echo 'ABCDE' | sed -n '/^ABCDE$/p' # 整行为 ABCDE 的行
ABCDE
$ echo -e '\n\n\n\n' | sed '/^$/d' # ^$ 可以匹配空行;这里删除空行
点
.
用来匹配除了换行符以外的任意单个字符(包括空格,制表符等)。
$ echo 'abc' | sed -n '/.bc/p'
abc
字符组 []
譬如:
[Yy]es
可以用来匹配:Yes
或者 yes
[123]0
可以用来匹配:10, 20, 30
[0-9]
可以用来匹配:0~9 之间的任意数字
[A-Z]
可以匹配:A~Z 之间的任意字母
[a-z]
可以匹配:任意小写字母
[^y]es
可以用来排除:yes
,即其他任意 kes, les, ...
都会被匹配,唯独 yes
不会被匹配
[^a-z]
可以用来排除:任意小写字母
...
$ echo 'abc' | sed -n '/[a-z]bc/p'
abc
特殊字符组
[[:alpha:]] 匹配任意字母字符,不管是大写还是小写
[[:alnum:]] 匹配任意字母数字字符09、AZ或a~z
[[:blank:]] 匹配空格或制表符
[[:digit:]] 匹配0~9之间的数字
[[:lower:]] 匹配小写字母字符a~z
[[:print:]] 匹配任意可打印字符
[[:punct:]] 匹配标点符号
[[:space:]] 匹配任意空白字符:空格、制表符、NL、FF、VT和CR
[[:upper:]] 匹配任意大写字母字符A~Z
$ echo "abc" | sed -n '/[[:alpha:]]/p'
abc
*
*
可以匹配它前面的字符0次或多次。
$ echo 'aac' | sed -n '/a*c/p' # 匹配到 aac
aac
$ echo 'aac.txt' | sed -n '/.*/p' # 搭配 . 匹配任意非换行符以外的字符
aac.txt
扩展正则
?
注意:sed 命令不支持扩展正则表达式。
?
类似 *
,但是它只匹配它前面的符号0次或1次。
$ echo -e 'ac\nc' | gawk '/a?c/{print $0}' # a?c 会匹配 ac, c
ac
c
+
+
和 *
类似,它匹配它前面的字符至少1次。
如:a+c
会匹配 ac, aac, aaac
,但不会匹配 c
{}
{n,m}
会匹配它前面的字符至少n次,最多m次。
{m}
它前面出现的字符,必须出现 m 次才会匹配。
$ echo "bt" | gawk --re-interval '/be{1}t/{print $0}'
默认情况下,gawk程序不会识别正则表达式间隔。必须指定gawk程序的
--re- interval
命令行选项才能识别正则表达式间隔。
|
|
代表了 or,即会匹配左右两部分
如:cat|dog
会匹配 cat, dog
[ch]at|dog
会匹配:cat, hat, dog
() 分组
分组的好处是可以将一个表达式作为一个整体,可以对它使用其他特殊字符:
$ echo "Sat" | gawk '/Sat(urday)?/{print $0}'
Sat
$ echo "cat" | gawk '/(c|b)a(b|t)/{print $0}' # 会匹配两个分组中所有字母的排列组合
cat