shell进阶之重定向,管道和过滤器,信号处理,sed和awk

这篇博文介绍shell进阶内容包括shell重定向,管道和过滤器,信号处理,sed和awk,有问题及时在本博客或个人博客留言。

shell重定向

输入输出

默认情况下,linux输入从键盘获取,输出到屏幕,我们也可以从文件读取输入内容,或者输出到文件,改变输入或输出默认路径就叫做重定向。

linux中一切皆文件,所以硬件在linux中也表示为文件

0-标准输入-键盘:从文件(默认是键盘)读取输入

1-标准输出-屏幕:发送数据到文件(默认是屏幕)

2-标准错误-屏幕:发送所有错误信息到一个文件(默认是屏幕)

上面3个数字是标准的POSIX字符,也称为文件描述符,每个linux命令都会使用上述的流与用户或其他系统程序进行交互。

标准输入

标准输入特点:

  • 是默认的输入方法,被所有命令使用来读取输入
  • 用数字0表示,也被称作stdin
  • 默认的标准输入设备是键盘

操作符<是输入重定向操作符,其语法如下:

command < input_filename

标准输出

标准输出特点:

  • 被命令用来写入或显示命令自身的输出
  • 用数字1表示,也被称作stdout
  • 默认的标准输出设备是屏幕

操作符>是输出重定向操作符,语法如下:

command > output_filename || command 1 > output_filename 1表示标准输出

例如将ls的输出保存到文件output.txt:

ls > /tmp/output.txt

如果文件不存在,会自动创建,如果文件存在,会被重写,要想保留源文件内容,使用>>操作符

标准错误

标准错误特点:

  • 是默认的错误输出方法,被用于写入所有系统错误信息
  • 用数字2表示,也被称作stderr
  • 默认的标准错误设备是屏幕或显示器

操作符2>是标准错误重定向操作符,语法如下:

command 2> errors_filename

重定向

从文件输入

重定向简单说就是从文件、命令、程序、脚本或这脚本中的代码块获取输出并把它作为输入发送到另一个文件、程序、命令或脚本。每个打开的文件被指定一个描述符,比如上面的0,1,2,对于打开的另外的文件,余留了文件描述符3-9

重定向操作符只对当前命令有效,下一个命令就会重新输出到标准输出(屏幕)

while循环结合重定向输入或输出可以从文件循环读取内容或者输出内容到文件

从文本或字符串输入

除了上述重定向的方式外,还有一种重定向类型是here-documents,操作符是<<MARKER,语法格式如下:

command <<-MARKER

here document

MARKER

在这种语法中,使用一个单词作为标志,这个单词可以是任何一个,比如MARKER/END/EOF等等,但是需要选择一个不会在数据集合中出现的单词,防止冲突,在第一个标志(如<<MARKER)和第二个标志(MARKER)之间的所有行都会被作为命令的标准输入,而且第二个标志必须独占一行,比如下面将小写字母转换为大写字母的例子(例子中用haha作为标志):

[root@test ~]# tr a-z A-Z <<haha
> one two three
> four five six
> haha
ONE TWO THREE
FOUR FIVE SIX

重定向操作符<<和定界标识符(haha)之间空格可有可无,在<<后面追加减号-将会忽略行首的制表符

#!/bin/bash
tr a-z A-Z <<haha
        one two three
        four five six
haha

tr a-z A-Z <<-haha
        one two three
        four five six
haha
#执行脚本结果如下
./test.sh
        ONE TWO THREE
        FOUR FIVE SIX
ONE TWO THREE
FOUR FIVE SIX

默认情况下,bash替换会在here-documents部分的内容上执行,即内容中的变量和命令会被求值或者运行,要想使替换失效,可以使用单引号或者双引号括起定界符,比如下面例子:

cat <<haha
> 当前路径是:$(pwd)
> haha
当前路径是:/root
cat <<"haha"
> 当前路径是:$(pwd)
> haha
当前路径是:$(pwd)

其实这种重定向最常用的还是用作注释功能,我们再之前已经提到过了,感兴趣可以参考shell脚本注释

还有另外一种重定向方式here-strings是here-documents的一个变种,它由操作符<<<和作为标准输入的字符串构成(被shell认为是一个整体),语法如下:

command <<<WORD

单个单词不需要使用引号,如果是有空格的字符串需要用引号括起来,还可以接收多行字符串,下面是例子:

tr a-z A-Z <<< one
ONE
tr a-z A-Z <<< one two three
tr: extra operand ‘two’
Try 'tr --help' for more information.
tr a-z A-Z <<< "one two three"
ONE TWO THREE
tr a-z A-Z <<< "one two
> three four"
ONE TWO
THREE FOUR

与here-documents相比,here-strings更方便,特别是发送变量内容(而非文件)到像grep或sed这样的过滤程序时。

空文件创建

创建空文件语法为

> filename

/dev/null丢弃不需要的输出

写入到/dev/null的所有数据都将被系统丢弃,所以我们可以将任何不想要的程序或命令的输出发送到/dev/null

重定向命令的标准输出信息到/dev/null的语法如下:

command >/dev/null

重定向命令的标准错误信息到/dev/null的语法如下:

command 2>/dev/null

同时重定向命令的标准输出和标准错误的信息到/dev/null的语法如下:

command &> /dev/null或

command > & /dev/null或

command > /dev/null 2>&1

当我们预料执行脚本可能失败,但并不希望用户被这些失败信息干扰,就可以将这些错误信息重定向到/dev/null

在单命令行进行标准输入输出重定向

可以一条命令中完成标准输入和标准输出的重定向,语法如下:

command input-file output-file或

input-file command output-command

例如我们将一个文件的内容都转换为小写,并将转换后的内容写入新的文件

tr A-Z a-z filename new-filename

文件描述符

文件描述符的范围一般是0-9,大于9的要谨慎使用,可能与shell内部使用的文件描述符冲突。文件描述符可以包含多个数字位,比如,文件描述符001,01和1都是相同的。多种操作(如exec)都可以将文件描述符与特定的文件联系起来

使用exec命令

exec命令的功能之一是允许我们操作文件描述符。如果在exec命令之后没有指定命令,则exec命令之后的重定向将更改当前shell的文件描述符。

比如命令exec 2> file之后运行的所有命令,都会将其产生的错误信息发送到文件file中。

下面是一个示例,假设我们有一个文件at.txt内容如下:

1 2 3 4 5
6 7 8 9 0
a b c d e

现在我们有如下脚本:

#没有指定参数则执行if语句
if [ $# -lt 1  ]; then
  echo "当前脚本使用方法为 $0 filepath"
fi
#将命令行第一个参数赋值给变量file
file=$1
#逐行读取文件内容,并存入变量line中
while read -r line;
do
  echo $line
  #等待用户输入任意键
  read -p "按任意键继续" -n 1
#将while循环的标准输入指向变量file所代表的文件
done < $file

执行脚本:

[root@test ~]# ./test.sh
当前脚本使用方法为 ./test.sh filepath
./test.sh: line 15: $file: ambiguous redirect
[root@test ~]# ./test.sh at.txt
1 2 3 4 5
7 8 9 0
b c d e

上面脚本中read语句并没有执行,因为我们将指定的文件重定向到了while循环的标准输入(文件描述符0),即我们指定的文件将被打开以用于标准输入的读取,而循环中所有的命令包括read也会继承这个文件描述符(这里是标准输入),因此read将从重定向后的标准输入读取,而不是从默认的输入设备(键盘)读取。

我们可以通过exec命令来对脚本稍加改动,来实现功能:

#没有指定参数则执行if语句
if [ $# -lt 1  ]; then
  echo "当前脚本使用方法为 $0 filepath"
  exit
fi
#将脚本的第一个参数作为输入文件,并指定一个文件描述符3
exec 3< $1
#逐行读取文件内容,并存入变量line中,read -u选项表示从指定的文件描述符读取内容,替代从标准输入读取
while read -u 3 line;
do
  echo $line
  #等待用户输入任意键
  read -p "按任意键继续" -n 1
#将while循环的标准输入指向变量file所代表的文件
done
#关闭文件描述符3
exec 3<&-
#执行脚本
[root@test ~]# ./test.sh at.txt
1 2 3 4 5
按任意键继续
6 7 8 9 0
按任意键继续
a b c d e
按任意键继续

read命令的-u选项可以让我们从指定的文件描述符上读取数据,在改动后的脚本上就是从文件描述符3上读取数据。

指定用于输入的文件描述符

给一个输入文件指定一个文件描述符的语法如下:

exec n< file

其中n就是文件描述符,如果不指定,则表示标准输入(0),上述的输入重定向会在文件描述符n上打开一个用于读取的文件file

<&也是一种重定向操作符,用于复制文件描述符,语法如下:

n <&word

如果word是一个数字,则用n表示的文件描述符被作为文件描述符word的副本,如果数字word指定的文件描述符没有打开以用于读取,则会发生重定向错误。如果没有指定n,则默认为标准输入。

比如下面两个命令:

exec 3< /etc/passwd
grep test <& 3

上述grep命令是将文件描述符3复制到了标准输入,而文件/etc/passwd又是在文件描述符3上打开以用于被命令读取,因此grep命令读取的实际是文件描述符3的内容。

指定用于输出的文件描述符

给一个输出文件指定一个文件描述符的语法如下:

exec n> file

其中n就是文件描述符,如果不指定,则表示标准输出(1),上述的输出重定向会在文件描述符n上打开一个用于写入的文件file,如果不存在则会创建,如果存在,则会清空后写入

复制输出文件描述符的语法如下:

n >&word

关闭文件描述符

上面已经涉及到了,语法如下:

n <&-或n>&-

比如关闭标准输入就是<&-,关闭标准错误就是2>&-

打开用于读和写的文件描述符

如下语法可以在文件描述符上打开一个既可读取又可以写入的文件

exec n<>file

其中n就是文件描述符,如果不指定,则表示标准输入,如果file不存在,则会被创建,符号<>用于打开一个可读写的文件。这个语法常用于更新文件,如下

还是我们刚才的文件at.txt其内容如下:

1 2 3 4 5
6 7 8 9 0
a b c d e

接下来执行下面一系列命令

#在文件描述符4上打开用于读写的文件at.txt
[root@test ~]# exec 4<> at.txt
#从文件描述符4读取前三个字符
[root@test ~]# read -n 3 var <& 4
[root@test ~]# echo $var
1 2
#向文件写入内容(一个+号)
[root@test ~]# echo -n + >& 4
#关闭文件描述符4
[root@test ~]# exec 4>&-
[root@test ~]# cat at.txt
1 2+3 4 5
6 7 8 9 0
a b c d e

我们看到+号写在了2和3之间,这是因为我们先用read命令读取了前3个字符,而操作符<>会使后面的读写操作跟随先前读写操作的位置,所以会写在23之间。

管道和过滤器

管道

shell可以将两个或读个程序连接到一起,以使一个程序的输出变为下一个程序的输入,以这种方式连接的两个或多个程序就形成了管道,管道通常用于执行一些复杂的数据处理操作,这些命令之间使用控制操作符(管道符)"|"连接,大部分linux命令都可以用来形成管道。

语法格式如下:

command1|command2

管道也可以连接重定向语句,可以使用重定向操作符>或>>将管道中最后一个命令的标准输出进行重定向,语法如下:

command1 |command2|…|commandN >output.txt

过滤器

将几个命令通过管道符组合在一起就形成一个管道,而这些组成管道的命令通常称为过滤器,过滤器会获取输入,修改内容,然后将其输出。

常被用作过滤器使用的命令如下:

  • awk用于文本处理,通常被作为数据提取和报告的工具
  • cut用于将每个输入文件(如果没有指定文件则为标准输入)的每行的指定部分输出到标准输出
  • grep用于搜索一个或多个文件中匹配指定模式的行
  • tar用于归档文件的应用程序
  • head用于读取文件的开头部分(默认10行),如果没有指定文件,则从标准输入读取
  • paste用于合并文件的行
  • sed用于过滤和转换文本的流编辑器
  • sort用于对文本文件的行进行排序
  • split用于将文件分割成块
  • strings用于打印文件中可打印的字符串
  • tac与cat命令的功能相反,用于倒序的显示文件或连接文件
  • tail用于显示文件的结尾部分
  • tee用于从标准输入读取内容并写入到标准输出和文件,默认重写文件内容,使用-a选项可以追加写入。
  • tr用于转换或删除字符
  • uniq用于报告或忽略冲入的行
  • wc用于打印文件中的总行数、单词数或字节数

下面列出一些例子

#跨网络的复制一个目录的整体结构
tar -cf - /home/test | ssh remote_host "(cd /backup/;tar xf -)"
#替换打印输出的文本的内容,sed是流编辑器的简称,下面命令中使用echo产生一个单词文本流,通过管道发送到sed命令,最后进行替换
echo front | sed 's/front/back/'
back
#显示文件at.txt中除第1-2行的内容
cat -n at.txt |sed '1,2d'
     3  a b c d e
#只显示文件at.txt第1-2行的内容
cat -n at.txt |sed -n '1,2p'
     1  1 2+3 4 5
     2  6 7 8 9 0
#将ll命令列出的文件按照文件大小排序,k5指按照第五列(文件大小数字)排序
 ll |sort -r -n -k5
#将backup目录按每5兆大小进行打包压缩,生成的压缩文件名前缀为backup.tar.gz
tar czf -backup | split -b 5m - backup.tar.gz
#删除前一行命令输出中的所有数字
echo "my age is 18"|tr -d '0-9'
my age is
#显示输出中各重复的行出现的次数,并按次数多少倒序显示
sort testfile |uniq -c |sort -nr

信号处理

子shell

如何在shell脚本中启动一个子shell,语法为(command1,command2)将想要启动子shell的部分用圆括号括起来即可。子shell中的变量在子shell的代码块之外是不可见的,它们不能被传到启动这个子shell的shell(父进程),同样即使是全局变量,也不能在子shell里面进行更改。

捕获

要想使我们编写的脚本比较健壮,其中对于信号捕获处理的能力是需要考虑的,即当我们给执行中的脚本传递一个信号时,脚本能做出针对性的动作。

trap语句

bash的内部命令trap让我们可以在shell脚本内捕获特定的信号并对它们进行处理,其语法如下:

trap command signal [signal…]

其中command可以是函数或者脚本,signal即可以用信号名,也可以用信号值(比如9)指定。也可以不指定任何参数,而直接使用trap命令,将会打印每个要捕获的信号相关的命令的列表。下面通过例子来了解下

#使用mktemp命令创建一个临时文件:使用-u选项表示并不真正创建文件,只是打印生成的文件名,XXXXXX表示生成6位随机字符
[root@test ~]# FILE=`mktemp -u /tmp/testtrap.$$.XXXXXX`
[root@test ~]# echo $FILE
/tmp/testtrap.29566.3nnoUS
#定义捕获错误信号,这里是ERR
[root@test ~]# trap "some error" ERR
#查看已定义的捕获
[root@test ~]# trap
trap -- '' SIGTSTP
trap -- '' SIGTTIN
trap -- '' SIGTTOU
trap -- 'some error' ERR
#当我们删除不存在的文件时,会显示错误
[root@test ~]# rm $FILE
rm: cannot remove ‘/tmp/testtrap.29566.3nnoUS’: No such file or directory
some error

可以看到shell捕获到了我们定义的错误,并打印了相关信息

当调试比较大的脚本时,可能想要赋予某个变量一个踪迹属性,并捕获变量的调试信息,我们可以如下定义:

#声明变量variable,赋予其踪迹属性
declaer -t variable=value
#捕获DEBUG
trap "echo variable is being used" DEBUG

有时接收到一个信号后,可能不想对其做任何处理,可以使用空字符串(""或’’)作为trap的命令参数,那么shell将忽略这些信号,用法如下:

trap ’ ’ SIGHUP SIGINT [ signal … ]

关于SIGHUP SIGINT可以查看posix标准信号表。

移除捕获

如果我们在脚本中应用了捕获,我们通常会在脚本的结尾处,将收到信号时的行为处理重置为默认模式。重置(移除)捕获的语法如下:

trap - signal [signal...]
#比如上面的脚本,可以在结尾移除捕获
trap "echo variable is being used" DEBUG
trap - DEBUG

sed和awk

sedawk是处理文本文件的有力工具,两者有很多共同点:

  • 它们都是用相似的语法来调用
  • 都是面向字符流的,都是从文本文件中每次一行地读取输入,并将输出直接送到标准输出端
  • 都使用正则表达式进行模式匹配,都允许用户将指令放在文件中一起执行

sed编辑器基础

sed是非交互式的面向数据流的编辑器。使用sed可以做如下操作:

  • 自动化的编辑一个或多个文件
  • 简化在多个文件中执行相同编辑的任务
  • 编写转换程序

sed同时只能编辑一行

基本的sed编辑命令

调用sed命令的语法有两种:在命令行指定sed指令,或者将sed指令放入一个文件中并将其文件名作为参数,语法如下:

sed [OPTIONS]… ‘COMMAND’ [FILE]…

sed [OPTIONS] -f SCRIPTFILE [FILE]…

sed命令有如下常用的选项

-e告诉sed将下一个参数解释为sed指令,只有在命令行上给出多个sed指令时才需要使用-e选项

-f指定由sed指令促成的脚本的名称,如果sed脚本的第一行为"#n",则sed的行为与指定-n选项相同

-i直接修改读取的内容,而不是输出到终端

-n取消默认输出。在一般sed用法中,所有来自标准输入的数据一般都会被显示到终端,使用-n参数后,只有经过sed处理的行才会被显示输出。

sed指令的语法形式如下:

[address[,address]][!]command

sed指令由地址和编辑命令组成,其中编辑命令是可选的。它可以是一个模式,被描述为由斜杠、行号或行寻址符号括住的正则表达式。大多数sed命令能接收由逗号分隔的两个地址,这两个地址用来标识行的范围,这些指令的语法格式如下:

[address1,address2]command

有些编辑命令只接收单个地址,不能应用于某个范围的行,语法格式为

[line-address]command

编辑命令还可以用大括号进行分组以使其作用于同一个地址,期语法格式为:

address {

​ command1

​ command2

​ command3

}

上述命令中,第一个命令command1可以与左大括号在同一行,右大括号必须自己单独处于一行。如果命令之间用分号分隔,那么可以将多个sed命令放在同一行,但是不建议这么做。

sed编辑命令有24个,详细信息可以参考sed的man手册,下面介绍几个常用的。

追加、更改、插入编辑命令

追加(a)、更改(c)、插入(i)命令在sed中并不常用,因为它们必须在多行上来指定。语法如下:

[line-address]a\

text

[line-address]i\

text

[line-address]c\

text

追加命令将文本放置在当前行之后,更改命令(c)用所指定的文本取代模式空间(模式空间概念可以网上找下博客)的内容,插入指令将所提供的文本放置在模式空间的当前行之前,这些命令都要求后面跟一个反斜杠用于转义第一个行尾,text必须从下一行开始。如果要输入多行文本,每个连续的行都必须用反斜杠结束,最后一行除外,而且当文本包含一个字面含义的反斜杠时,需要再添加一个反斜杠来转义,如下

#待处理的文本at.txt
<Amy's info>
<Tom's info>
#sed脚本
/<Tom's info>/a\
full name : zhangsan
tel:123456
#使用sed命令处理文本
[root@test ~]# sed -f sed.txt at.txt
#因为没有在文本第一行后面加\,所以需要在full name : zhangsan后面加\
sed: can't find label for jump to `el:123456'
[root@test ~]# vi sed.txt
[root@test ~]# sed -f sed.txt at.txt
<Amy's info>
<Tom's info>
full name : zhangsan
tel:123456

还可以指定在文件结尾处追加一行"any text"

[root@test ~]# cat at.txt
<Amy's info>
<Tom's info>
[root@test ~]# sed '$a"any text"' at.txt
<Amy's info>
<Tom's info>
"any text"

上述命令的$是行寻址符号,用于匹配文件的最后一行。

追加命令和插入命令只应用于单个行地址,而不是一个范围内的行,更改命令可以处理一个范围内的行。使用我们提供的文本替换被寻址行的内容,也就是删除原来行内的内容替换为我们自己的内容。

#待处理的文本at.txt
From:zhangsan@163.com

To:test
Subject:Test
#sed脚本,从FROM开头的文本到行尾替换为<Mail Header Removed>
/^From/,/^$/c\
<Mail Header Removed>
#执行sed脚本
[root@test ~]# sed -f sed.txt at.txt
<Mail Header Removed>
To:test
Subject:Test
删除编辑命令

删除编辑命令采用一个地址,如果行匹配这个地址,就删除模式空间的内容。如果某行匹配这个地址,就删除整个行,而不只是删除行中匹配的部分。

我们可以使用指令"/^$/d"来删除一个文件中的空行

[root@test ~]# cat at.txt
From:zhangsan@163.com

To:test
Subject:Test
[root@test ~]# sed '/^$/d' at.txt
From:zhangsan@163.com
To:test
Subject:Test

删除命令也可以用于删除一个范围内的行,例如,下面的命令删除了文件中从第50行到最后一行的所有行

sed '50,$d' file
替换编辑命令

替换编辑命令(s)语法如下:

[address]s/pattern/replacement/flags

这里flags是替换命令(s)的修饰标志,有如下几个:

  • n1-512之间的数字,表示对文本模式pattern中指定模式第n次出现的情况进行替换
  • g对模式空间的所有出现的情况进行全局更改,没有g时通常只有第一次出现的情况被更改
  • p打印模式空间的内容
  • w file将模式空间的内容写入到文件file中

修饰标志flags可以混合使用,只要有意义。

替换命令应用于与地址匹配的行,如果没有指定地址,就应用于匹配的所有行。地址需要一个作为定界符的斜杠"/",和地址不同的是,正则表达式可以用任意字符来分隔,只有换行符除外。因此,如果模式包含斜杠,那么可以选择另一个字符作为定界符,例如感叹号

s!/usr/lib!/usr/lib64!

上面定界符揣想那了三次,而且在替代字符串之后是必需的。不管使用哪种定界符,如果它出现在正则表达式中,或者在替换文本中,那么就用反斜杠将它转义。

replacement是一个字符串,用来替换与模式(正则表达式)匹配的内容。在replacement部分,只有如下字符具有特殊的含义:

  • &由正则表达式匹配的字符串进行替换

  • \n匹配第n个子字符串(n是一个数字),这个子字符串是在模式中使用"\(“和”\)"指定的。

  • \用于转义字符"&"、反斜线"\"等字符。另外,它用于转义换行符并创建多行replacement字符串。

因此,除了正则表达式中的元字符以外,sed的替换部分也有元字符。

下面一个示例:假设我们有一个文件file,其中每行有3个制表符,然后我们使用换行符取代每行上的第二个制表符,如下:

#要处理的文本at.txt
word1	word2	word3	word4
#sed脚本内容
s/\t/\
/2
#执行处理
[root@test ~]# sed -f sed.txt at.txt
word1   word2
word3   word4

上述sed脚本中反斜杠后面不允许有空格

下面的示例中,我们使用反斜杠来转义&,让它作为一个普通字符出现在替换部分,如下sed替换命令:

s/haha/u & me/g

作用是将文本中的haha替换为u&me

#文本at.txt
wordhahatesthaha
haha12345haha
#执行处理
[root@test ~]# sed 's/haha/u & me/g' at.txt
wordu haha metestu haha me
u haha me12345u haha me
[root@test ~]# sed 's/haha/u \& me/g' at.txt
wordu & metestu & me
u & me12345u & me

从上面可以看到当&前面转义和不转义,得到的内容完全不一样。

打印编辑命令

打印编辑命令(p)输出模式空间的内容,既不清除模式空间,也不改变脚本的控制流。一般会用在改变流控制的命令(d,N,b)之前,除非抑制(使用-n选项)默认的输出,否则打印命令将输出行的重复复制。

下面看一个例子,如何使用打印命令来进行调试,它用于显示在发生改变之前行是什么样的?脚本内容如下:

#要处理的文件at.txt
HAHA -new line
HAHA -my test
HAHA dont replace
YOU CAN DO IT
#sed脚本内容
/^HAHA/{
  p
  s/-//
  s/^HAHA //p
}
#处理后的结果
[root@test ~]# sed -nf sed.txt at.txt
HAHA -new line
new line
HAHA -my test
my test
HAHA dont replace
dont replace

上例子中,打印标志被提供给替换命令,替换命令的打印标志不同于打印命令,替换成功了才打印,所以最后一句YOU CAN DO IT,没有被替换,所以也没有打印出来。上面每个受影响的行被打印了两次,sed脚本的大括号用于在同一个地址应用多个命令。

打印行号编辑命令

跟在地址后面的等号"="用来打印被匹配的行的行号,除非抑制行的自动输出,行号和行本身将被打印,语法如下:

[line-address]=

该命令不能对一个范围内的行做操作

我们可以使用该命令来打印源文件中某些行,比如,下面的脚本会打印源文件中for语句所在的行号和行本身

#要处理的文本at.txt
for i in {1..5}
do
  for i in {1..5} ;
  do
  echo "haha"
  done
    echo i
done
#sed脚本内容
/ *for/{
  =
  p
}
#执行处理
[root@test ~]# sed -nf sed.txt at.txt
1
for i in {1..5}
3
  for i in {1..5} ;
读取下一行编辑命令

读取下一行编辑命令(n)用于读取输入的下一行到模式空间,语法如下:

[address]n

读取下一行命令(n)改变了正常的流控制,导致输入的下一行取代了模式空间中的当前行,脚本中的后续命令应用于替换后的行,而不是当前行。如果没有默认输出,那么在替换发生之前会打印当前行。

下面是一个例子,脚本的作用是删除From之后的一行空格

#要处理的文本at.txt
From:zhangsan@163.com

To:test
Subject:Test

end
#sed脚本
/From/{
  n
  /^$/d
}
#处理过程
[root@test ~]# sed -f sed.txt at.txt
From:zhangsan@163.com
To:test
Subject:Test

end

在sed脚本中,出现在读取下一行命令之前的命令不会应用于模式空间中新的输入行,出现在后面的命令也不应用与模式空间中旧的输入行。

读和写文件编辑命令

读文件编辑命令(r)和写文件编辑命令(w)用于直接处理文件,语法如下:

[line-address]r file

[address]w file

读文件编辑命令和文件名之间必须有空格,如果文件不存在,读文件命令不会报错,写文件命令将创建一个文件,如果文件已经存在,将会被改写。

使用读文件命令对于将一个文件的内容插入到另一个文件的特定位置是很有用的,比如在所有 txt文件尾部插入一个内容,语句如下:

sed -i '$r file' *.txt

其中file中是一些要插入的文本内容(可以使另一个文件)。字符"$"是指定文件最后一行的寻址符号 ,下面看一个示例:

#要修改文本内容at.txt
<html>
<body>
<tag>
</tag>
</body>
</html>
#要添加的文本内容sed.txt
This is a test line.
Hello World!
#sed读文件命令,含义是当sed匹配到以字符串<tag>开始的行时,将文件sed.txt的内容附件在被匹配的行的末尾。
/^<tag>/r sed.txt
#针对文件at.txt执行该指令
[root@test ~]# sed '/^<tag>/r sed.txt' at.txt
<html>
<body>
<tag>
This is a test line.
Hello World!
</tag>
</body>
</html>

可以看到tag标签后面添加了我们要添加的内容。

需要注意,如果上面的指令/^<tag>/r sed.txt还有其他指令,那么后续指令不能对从文件sed.txt中读取的内容做任何改变,比如如果我们在指令后面还有一句,删除以Hello开头的一行(我们可以看到sed.txt中有这么一行),但是这个指令不会对从sed.txt中读取出来的内容有任何改变,也就是最终输出还是上面那样,Hello World那一行不会被删除,如下,我们保持刚才的sed.txt和at.txt不变,但是将sed指令改为使用sedtest.txt。

#使用如下sedtest.txt代替刚才的sed指令/^<tag>/r sed.txt,其实就是在后面追加了一句删除Hello World!行的指令
/^<tag>/r sed.txt
/Hello World!/d
#执行指令
[root@test ~]# sed -f sedtest.txt at.txt
<html>
<body>
<tag>
This is a test line.
Hello World!
</tag>
</body>
</html>

可以看到上面的内容并没有变化,因为虽然sed指令中有一行删除Hello World!行的指令,但是对我们读取的文件内容(sed.txt)不会起作用,这一点演示了读文件后面的编辑命令不会影响读文件命令从文件中读取的行

使用-n选项或#n脚本语法可以取消抑制自动输出,阻止模式空间的初始行被输出,但是读命令的结果仍然会转到标准输出。

下面看一个写文件编辑命令的例子,这个例子的目标是将原文件中的四家公司按照国家分类写到不同的文件中:

#要读取的原文件,分别是四家公司,两家美国公司,两家中国公司
DIDI CHINA
TENCENT CHINA
APPLE USA
GOOGLE USA
#sed脚本,将四家公司分类并写到对应的文件中
/CHINA/w CHINA.txt
/USA/w USA.txt
#现在针对源文件执行sed脚本
[root@test ~]# sed -f sed.txt at.txt
DIDI CHINA
TENCENT CHINA
APPLE USA
GOOGLE USA
[root@test ~]# ll 可以看到多了两个文件CHINA.txt和USA.txt
total 1369068
...
-rw-r--r--. 1 root root         25 Aug 28 10:10 CHINA.txt
-rw-r--r--. 1 root root         21 Aug 28 10:10 USA.txt
[root@test ~]# cat CHINA.txt
DIDI CHINA
TENCENT CHINA
[root@test ~]# cat USA.txt
APPLE USA
GOOGLE USA

写文件命令在被调用时就写出模式空间的内容,而不是等到到达脚本的结尾时才进行写操作。

假如我们想在写入文件之前去掉后面的国家,可以对上面的sed.txt做如下改动:

#sed.txt 匹配与地址相同的模式并删除
/CHINA$/{
s///
w CHINA.txt
}
/USA$/{
S///
w USA.txt
}
#执行新的脚本
[root@test ~]# sed -f sed.txt at.txt
DIDI
TENCENT
APPLE
GOOGLE
[root@test ~]# cat CHINA.txt
DIDI
TENCENT
[root@test ~]# cat USA.txt
APPLE
GOOGLE
退出编辑命令

退出编辑命令(q)会使sed脚本立即退出,停止处理新的输入行,语法如下:

[line-address]q

只适用于单行的地址,一旦找到匹配行,就结束运行。

几个实例:

#使用退出命令打印文件前10行
sed '10q' file(效率更高)
sed -n '1,10p' file

sed与shell

在sed中使用shell变量

在shell中,$用来引用环境变量,而在sed中,用于指示输入文件的最后一行,或是行的末尾(在LHS中),或是字面意义的符号(在RHS中)。sed不能直接访问shell中的变量,所以必须用双引号来扩展shell的变量。LHS(left-hand side )和RHS(right-hand side)分别指sed指令中的左侧部分和右侧部分,比如替换命令"s/LHS/RHS"

要让shell正确的解释sed命令中的shell变量,需要将sed指令放在双引号内,比如:

sed "s/_terminal-type_./$TERM/g" input.file

下面这个需求,假如我们需要将文件中路径为/old/path的内容全部替换为/new/path,当我们用sed脚本实现这个更改的时候,脚本中先定义两个变量:

OLDPATH=/old/path
NEWPATH=/new/path
#运行脚本
sed -i "s/$OLDPATH/$NEWPATH/" file
#此时脚本实际执行的是s//old/path//new/path/ 这样会报错
#替换编辑命令一节中提到过如果模式包含斜杠,那么可以选择另一个字符作为定界符,例如感叹号,所以可以这么写指令
sed -i "s#$OLDPATH#$NEWPATH#" file

下面是sed中使用shell的示例:

移除文件中的空白行

#!/bin/bash
#检查参数,如果没有指定参数,则打印脚本使用方法
if [ -z "$1" ]; then
  echo "当前脚本使用方法为:'basename $0' target-file"
  exit 1
fi

sed - e "/^$/d" "$1"
#-e选项之前说过,表示后面跟一个sed编辑命令
#"^"表示一行的开始,"$"表示一行的结束,匹配在行的开始和结束之间没有任何内容的行
#d是删除命令

#用双引号括起来的命令行参数允许文件名中有空格或特殊字符
#这个脚本并不真正修改目标文件,如果需要修改目标文件,需要重定向脚本输出,或在sed命令中加入-i选项
exit 0

替换文件中的字符串

#!/bin/bash
#定义参数个数
ARGS=3
#检测参数个数是否符合是3个,不是则打印脚本的使用方法
if [ $# -ne "$ARGS" ]; then
  echo "当前脚本使用方法为:$0 oldpattern newpattern filename"
  exit 2
fi
oldpattern=$1
newpattern=$2
#检查指定文件是否存在
if [ -f "$3" ]; then
    filename=$3
else
  echo "文件 \"$3\"不存在"
  exit 3
fi
sed -e "s#$oldpattern#$newpattern/g" $filename
#"s"是sed中的替换命令,/pattren/引用地址匹配 "#"是sed指令的定界符
#"g"会使命令替换每一行中所有匹配$oldpattern的字符串,而不只是第一个匹配的字符串
exit 0
从sed输出中设置shell变量
#!/bin/bash
ARGS=2
#检查传递给脚本的参数个数,不为2则打印使用方法
if [ $# -ne "$ARGS" ]; then
  echo "当前脚本使用方法为:$0 oldpattern newpattern"
fi
number=0
#循环遍历当前目录下所有文件名中包含字符串$1的文件
for filename in *$1* ; do
    #如果指定的文件存在
    if [ -f $filename ]; then
      #去除文件的路径
      fname='basename "$filename"'
      #用新的文件名替换旧的
      newname='echo $fname | sed -e "s/$1/$2/g"'
      #将文件重命名
      mv "$fname" "$newname"
      let "number += 1"
    fi
done
exit 0

awk基础

awk被用于文本处理,通常用作数据提取和报告工具的解释性程序设计语言,awk不能处理非文本文件。和sed类似,awk的基本功能也是搜索文件中包含某些模式的行,当某行匹配一个模式时,awk就在那一行上执行指定的操作,awk持续的使用这种方式处理输入的行,直到处理到输入文件的结尾处为止。

下面是awk的一些功能点:

  • 使用变量操作由文本记录和字段组成的文本文件
  • 具有算术和字符串操作符
  • 具有普通的程序设计结构,例如循环和条件
  • 生成格式化报告
  • 定义函数
  • 从awk脚本中执行linux命令
  • 处理linux命令结果
  • 更加巧妙的处理命令行的参数
  • 更容易的处理多个输入流
awk基本语法

运行awk时,需要指定一个awk程序,这个程序由一系列指令组成,每条指令指定一个用于搜索的模式和找到模式要执行的动作。一个awk指令由一个模式(pattern)后跟一个动作(action)组成。动作被括在花括号内,用来与模式分隔。每个awk指令之间通常用换行符分隔。一个awk程序的语法类似如下:

pattern {action}

pattern {action}

运行awk的方式有两种,如果awk程序很短,可以直接把它写在运行awk的命令行中,其语法如下所示:

awk options – program-text file…

如果程序较长,就放在一个文件中更为方便,语法如下:

awk options -f program-file – file …

一个awk命令行是有选项,awk程序文件或指令以及输入文件名组成的,输入是从指定的文件中读取的,如果没有指定输入文件名或指定为-,那么awk命令将从标准输入中读取。

awk命令中常用的选项如下:

-F fs指定用于输入数据的列分隔符fs

-v var=value在awk程序执行之前指定一个值value给变量var,这些变量值用于awk程序的BEGIN块。

-f program-file指定一个awk程序文件,代替在命令行指定awk指令

--选项根据POSIX参数解析约定,此选项表示命令行选项的结束。利用这个选项可以指定以"-"开头的输入文件,否则它将被解析为一个命令行选项。

下面是awk使用的一个示例:

[root@test ~]# cat at.txt
DIDI CHINA
TENCENT CHINA
APPLE USA
GOOGLE USA
[root@test ~]# awk '{ print }' at.txt
DIDI CHINA
TENCENT CHINA
APPLE USA
GOOGLE USA
[root@test ~]# awk '{ print $0 }' at.txt
DIDI CHINA
TENCENT CHINA
APPLE USA
GOOGLE USA

上面示例中,我们调用awk时,指定at.txt为输入文件,然后awk会在此文件的每一行上按顺序执行print命令,所有输出都送到标准输出,所以我们看到结果和cat命令一样。在awk中使用花括号{}将代码块集合起来,类似于shell函数,在示例中代码块只有一个print函数,在awk中当print不带参数,就会打印当前行的所有内容。

我们看到第三个指定结果和第二个一样,这是因为在awk中,变量$0表示当前的一整行,所以两个指令结果一样。

使用awk打印指定的列
[root@test ~]# cat at.txt
DIDI CHINA
TENCENT CHINA
APPLE USA
GOOGLE USA
#使用awk打印第一列
[root@test ~]# awk '{ print $1 }' at.txt
DIDI
TENCENT
APPLE
GOOGLE

在不指定分隔符的情况下,awk默认采用空白作为分隔符,-F可以用于指定分隔符

awk -F'-' '{print $2}' at.txt
从awk程序文件读取awk指令

语法:

awk -f myscript.awk myfile

#假如我们修改上面的指令为从文件读取awk -F'-' '{print $2}' at.txt,如下,文件名secCol.awk
BEGIN {
	FS="-"
}
{ print $2 }
#执行脚本
awk -f secCol.awk at.txt

主要区别在于设置分隔符的方式不同。

awk的BEGIN和END块

当需要在awk开始处理输入文件中的文本前执行一些初始化代码的时候,可以定义一个BEGIN块;同样在输入文件中所有行都被处理之后想要执行代码,可以定义一个END块,通常,end块用于执行最后的运算或是打印要在输出流的结尾处显示的概要。

awk中使用正则表达式

awk中可以使用正则表达式来对匹配的行做操作,比如下面示例:

#打印输出文件中包含字符串"DIDI"的行
awk '/DIDI/{ print }' at.txt
DIDI CHINA
#打印输出文件中包含字符串"DIDI"的行的第一列
[root@test ~]# awk '/DIDI/{ print $1 }' at.txt
DIDI

正则表达式必须放在斜线内,用法与sed类似,我们也可以使用操作符~!~来指定任意列或变量匹配(或不匹配)一个正则表达式,比如打印文件中匹配字符"-"的行的第一列

awk '$2 ~ /-/ { print $1 }' filename
awk的表达式和块

awk还可以有选择的执行代码块,可以把任意类型的布尔表达式放在代码块之前来控制特定的块什么时候可以执行,即只有当布尔表达式的值为真,awk才会执行此代码块,下面示例输出/etc/passwd中第一列等于root的所有行的第三列

awk 'BEGIN { FS=":" } $1 == "root" { print $3 }' /etc/passwd

awk支持的比较操作符有,"=="、"<"、">"、"<="、">=“和”!=",此外还有"“和”!",下面示例打印文件中匹配字符串"etc"的所有行的第一列

awk 'BEGIN { FS="-" } $2 ~ "etc" { print $1 }' file

表达式可以对打印的,测试的或者传递给一个函数的内容进行求值,一个表达式还可以使用赋值操作符给一个变量或一列赋予一个新的值。

awk条件语句

awk支持条件if语句,比如上面的语句可以改造为if语句如下:

awk 'BEGIN { FS="-" } { if ($2 ~ "etc" ) { print $1 } }' file

上述语句中,块中指定对输入的每一行都执行一次

if-else是awk的决策语句,语法如下:

if (condition) then-body [else else-body]

当条件condition的值为0或者空字符串时,条件被认为假,否则条件为真,同时condition支持||(逻辑或)和&&(逻辑与)来创建更复杂的布尔表达式。

awk中的变量和操作符

awk可以进行整数和浮点数的运算,下面是一个例子,计算文本中空白行的awk程序:

#待统计的文本at.txt
DIDI CHINA

TENCENT CHINA
APPLE USA

GOOGLE USA
#awk脚本文件awk.txt
BEGIN { x=0 }
/^$/ { x=x+1 }
END { print "找到 "x" 个空白行" }
#执行指令
[root@test ~]# awk -f awk.txt at.txt
找到 2 个空白行

当在变量上执行数学运算时,只要变量包含有效的数字串,awk会自动的进行转换(这一点与bash不同)在bash中我们执行任何数学运算,都需要将我们的算式用"$(())"包起来

假如我们在进行运算时,指定的变量不包含数字,awk会将其作为数字0处理。awk支持完整的运算符。

awk中的特殊变量

FS允许你设置希望awk在列之间查找的字符序列,其值并不局限于单个字符,同样可以是正则表达式,或任意长度的字符模式

NF当前记录中列的数量,比如如下脚本将只显示文件中列数是3的行awk 'NF == 3 { print }' file

NR当前记录是第几行,比如awk将第一条记录标记为1,下面的awk指令用于打印输出文件中第三行以后输入的记录:

awk '{ if ( NR > 3 ) { print NR ".\t"$0 } }' file,其中print NR ".\t"$0表示在当前记录编号后面打印字符"."和一个制表符,然后打印当前记录的内容。

awk中的循环结构

while循环,语法如下:

while(condition) body

body代表任意awk语句

do-while循环

do

body

while(condition)

for循环

for (init;condition;increment)

body

for语句中init,condition和increment部分是任意的awk表达式,比如for(i = 1; i <= 3; i++),body是任意的awk语句。

awk中的数组

在awk中数组索引一般从1开始,可以使用如下方式定义一个数组:

arr[1]=“one”

arr[2]=“123”

在第一个赋值语句时awk数组被创建。awk迭代数组语法如下,但是awk迭代数组不保证顺序

for ( x in arr ) {

​ print arr[x]

}

awk的数组除了支持数字索引外,还支持使用字符串索引,比如

arr[“name”]=“Tom”

删除数组中元素

delete arr[1]

查看数组中指定下标是否有元素存在,可以用in布尔操作符:

if( 1 in arr) {

}else{

}

awk与shell

通常我们会将信息传入awk脚本,再将信息以对shell有用的格式传回。

在awk中使用shell

有两种方法来在awk程序中获取shell变量的值:

最常见的方法是使用shell引用来替换变量的值到shell脚本内部的awk程序中,例如下面的脚本:

#从标准输入读取变量pattern的值
read -p "请输入匹配的关键词:" pattern
#打印输出匹配变量pattern的值的行,并记录匹配的次数,在处理完所有行后,打印匹配的总次数
awk "/$pattern/" '{ nmatches++; print } END { print nmatches, "found." }' file

上述awk脚本由两块引用文本连接起来,第一部分用双引号括起来,这样允许引号内的shell变量的替换。

另外的一种方法是使用awk的变量赋值功能将shell变量的值指定为awk变量的值,比如下面这个脚本是对上面脚本的改进版本

#从标准输入读取变量pattern的值
read -p "请输入匹配的关键词:" pattern
#使用awk的-v选项指定变量pat,并将shell变量的值赋值给pat变量,之后执行其他指令
awk -v pat="$pattern" '$0 ~ pat { nmatches++; print } END { print nmatches, "found." }' file

其中-v pat="$pattern"使用双引号主要是防止pattern中有空格,当我们将变量赋值给awk变量后,可以在各个地方使用而不必再加双引号。

awk程序还可以访问shell环境变量,awk解释器会在以环境变量名为索引的ENVIRON数组中存储shell环境变量的一个拷贝,例如下面示例,在awk程序中使用shell的PATH环境变量

awk 'path=ENVIRON["PATH"] { print "The PATH is: " path }'
从awk命令的输出中设置shell变量
#给单个变量赋值
x='awk -F'-' '/Linux/{ print $2 }' file'
#给多个变量赋值
$ z='awk -F "-" '{ if( $1 ~ "Linux" ) print "x="$2; if( $1 ~ "Cool") print "y="$2 }' file'
echo $z
x=123 y=abc
eval $z
echo $z
x=123 y=abc
echo $x
123
echo $y
abc

上例子中,通过awk命令的输出将变量x和y赋值语句放到变量z中,然后通过eval命令将变量z的值作为一条命令被shell执行,这样x,y就成为了shell变量并完成了赋值

我们还可以利用source命令来从awk命令的输出中设置shell变量,修改如下:

awk -F "-" '{ if( $1 ~ "Linux" ) print "x="$2; if( $1 ~ "Cool") print "y="$2 }' file >defvar

上例中我们将awk命令的输出重定向到了文件defVar中,此时查看defVar可以看到

cat defVar
x=123
y=abc

然后使用source命令在当前shell下读取并执行文件defVar中的命令:

source defVar

这样x,y也变成了shell变量,并完成了赋值。

一个通过进程号查看进程启动路径的例子,其中使用awk来获取进程pid和对应的完整路径

#!/bin/bash
#定义脚本的参数个数
ARG=1
#定义退出状态码
WRONGARGS=65
BADPID=66
NOSUCHPROCESS=67
NOPERMISSION=69

PROCFILE=exe
#检查传递给脚本的参数个数
if [ $# -ne $ARG ]; then
  echo "当前脚本使用方法为:$0 pid-no" >&2
  exit WRONGARGS
fi
#确认指定的pid
pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1)
#如果经过上面管道过滤得到字符串长度为0,则指定的pid不存在
if [ -z $pidno ]; then
  echo "输入的进程号对应进程不存在"
  exit NOSUCHPROCESS
fi
#检查文件的读权限
if [ ! -r "/proc/$1/$PROCFILE"  ]; then
  echo "当前进程$1无法读取相关信息"
  #普通用户不能访问/proc目录下的某些文件
  exit NOPERMISSION
fi

exe_file=$( ls -l /proc/$1/exe | awk '{ print $11 }')
#如果软连接/proc/pidno/exe存在,则显示执行对应的可执行文件的全路径
if [ -e "$exe_file" ]; then
  echo "进程 #$1 启动路径为 $exe_file"
else
  echo "没有找到当前进行号对应进程信息"
fi
exit 0
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值