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

fiif反写的,代表着语句结束

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 的高级特性

  1. 用于高级数学表达式的双括号
  2. 用于高级字符串表达式的双方括号

双括号

双括号的语法:(( 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
posted @ 2020-06-23 14:57  wztshine  阅读(537)  评论(0编辑  收藏  举报