【Shell】Shell编程之字符串操作 – Blog


Shell编程的字符串操作

  • 字符串的属性
    • 字符串的类型
    • 数字或字母组合
    • 字符组合
    • 字母和数字组合
    • 空格和tab键等
    • 匹配邮箱地址
    • 匹配URL地址
    • 判断字符串是否为可打印字符
    • 字符串的长度
    • 计算某个字符串的长度
    • 计算某些指定字符或字符组合的个数
    • 统计单词个数
  • 字符串的显示
    • 在屏幕控制字符显示位置、颜色、背景等
    • 在屏幕的某个位置动态显示系统时间
    • 过滤掉某些控制字符串
  • 字符串的存储
    • 把字符串拆分成字符串数组
  • 字符串常规操作
    • 取子串
    • 按照位置取子串
    • 匹配字符求子串
    • 查询子串
    • 查询子串在目标串中的位置
    • 查询子串,返回包含子串的行
    • 子串替换
    • 把变量var中的空格替换成下划线
    • 插入子串
    • 在var字符串的空格之前或之后插入一个下划线
    • 删除子串
    • 在var字符串中删除所有的空格
    • 子串比较
    • 子串排序
    • 子串进制转换
    • 子串编码转换
  • 字符串操作进阶
    • 正则表达式
    • 处理URL地址
    • 匹配某个文件中的特定范围的行
    • 处理格式化的文本
    • 选取指定行
    • 文件关联操作
  • 小结

字符串的属性

字符串的类型

字符有可能是数字、字母、空格、其他特殊字符,而字符串有可能是它们中的一种或者多种 的组合,在组合之后还可能形成具有特定意义的字符串,诸如邮件地址,URL地址等。

数字或者数字组合

$ i=5;j=9423483247234;
$ echo $i | grep -q "^[0-9]$" # -q选项是,静默选项,即不打印输出信息
$ echo $?
0
$ echo $j | grep -q "^[0-9]\+$"
$ echo $?
0

字符组合(小写字母、大写字母、两者的组合)

$ c="A"; d="fwefewjuew"; e="fewfEFWefwefe"
$ echo $c | grep -q "^[A-Z]$"   # 这里可以用echo $?进行查看命令执行的退出码
$ echo $d | grep -q "^[a-z]\+$"
$ echo $e | grep -q "^[a-zA-Z]\+$"

字母和数字的组合

$ ic="432fwfwefeFWEwefwef"
$ echo $ic | grep -q "^[0-9a-zA-Z]\+$" # 这里可以用echo $?进行查看命令执行的退出码

空格或者 Tab 键等

$ echo " " | grep " "
$ echo -e "\t" | grep "[[:space:]]" #[[:space:]]会同时匹配空格和TAB键
$ echo -e " \t" | grep "[[:space:]]"
$ echo -e "\t" | grep "" #为在键盘上按下TAB键,而不是字符

匹配邮件地址

$ echo "[email protected]" | grep "[0-9a-zA-Z\.]*@[0-9a-zA-Z\.]*"
[email protected]

匹配 URL 地址 (以 http 链接为例)

$ echo "http://news.lzu.edu.cn/article.jsp?newsid=10135" | grep "^http://[0-9a-zA-Z\./=?]\+$"
http://news.lzu.edu.cn/article.jsp?newsid=10135

说明:

  • /dev/null/dev/zero设备非常有趣,都犹如黑洞,什么东西掉进去都会消失殆尽;后者还是个能源箱,总能从那里取到0,直到退出
  • [[:space:]]grep 用于匹配空格或 TAB 键字符的标记,其他标记请查帮助:man grep.
  • 上面都是用 grep 来进行模式匹配,实际上 sedawk 都可用来做模式匹配,关于匹配中用到的正则表达式知识,会单独写一篇博客进行讲解。
  • 如果想判断字符串是否为空,可判断其长度是否为零,可通过 test 命令的 z 选项来实现,具体用法见 test 命令, man test .

判断字符是否为可打印字符

$ echo "\t\n" | grep "[[:print:]]"
\t\n
$ echo $?
0 $
echo -e "\t\n" | grep "[[:print:]]"
$ echo $?
1

字符串的长度

除了组成字符串的字符类型外,字符串还有哪些属性呢?组成字符串的字符个数。 下面我们来计算字符串的长度,即所有字符的个数,并简单介绍几种求字符串中指定字符个数的方法。

计算某个字符串的长度

即计算所有字符的个数,计算方法五花八门,择其优着而用之:

$ var="get the length of me"
$ echo ${var} # 这里等同于$var
get the length of me
$ echo ${#var}
20
$ expr length "$var"
20
$ echo $var | awk '{printf("%d\n", length($0));}'
20
$ echo -n $var | wc -c
20

计算某些指定字符或者字符组合的个数

$ echo $var | tr -cd g | wc -c
2
$ echo -n $var | sed -e 's/[^g]//g' | wc -c
2
$ echo -n $var | sed -e 's/[^gt]//g' | wc -c
5

统计单词个数

$ echo $var | wc -w
5
$ echo "$var" | tr " " "\n" | grep get | uniq -c
1
$ echo "$var" | tr " " "\n" | grep get | wc -l
1

字符串的显示

在屏幕控制字符显示位置、颜色、背景等

$ echo -e "\033[31;40m" #设置前景色为黑色,背景色为红色
$ echo -e '\033[11;29H Hello, World!' #在屏幕的第11行,29列开始打印字符串Hello,World!

在屏幕的某个位置动态显示当前系统时间

 while :; do echo -e "\033[11;29H "$(date "+%Y-%m-%d %H:%M:%S"); done

过滤掉某些控制字符串

用 col 命令过滤某些控制字符,在处理诸如 script , screen 等截屏命令的输出结果时, 很有用。

$ screen -L
$ cat /bin/cat
$ exit
$ cat screenlog.0 | col -b # 把一些控制字符过滤后,就可以保留可读的操作日志

字符串的存储

在我们看来,字符串是一连串的字符而已,但是为了操作方便,我们往往可以让字符串呈现出一定的结构。在这里,我们不关心字符串在内存中的实际存储结构,仅仅关系它呈现出来的逻辑结构。比如,这样一个字符串: "get the length of me" ,我们可以从不同的方面来呈现它。

  • 通过字符在串中的位置来呈现它

这样我们就可以通过指定位置来找到某个子串。这在 C 语言中通常可以利用指针来做。而在Shell 编程中,有很多可用的工具,诸如 exprawk 都提供了类似方法来实现子串的查询动作。两者都几乎支持模式匹配 match 和完全匹配index。这在后面的字符串操作中将详细介绍。

  • 根据某个分割符来取得字符串的各个部分

这里最常见的就是行分割符、空格或者 TAB 分割符了,前者用来当行号,我们似乎已经司空见惯了,因为我们的编辑器就这样“莫名”地处理着行分割符(在 UNIX 下为\n ,在其他系统下有一些不同,比如 Windows 下为 \r\n ) 。而空格或者 TAB 键经常用来分割数据库的各个字段,这似乎也是司空见惯的事情。正因为这样,所以产生了大量优秀的行编辑工具,诸如 grepawksed 等。在“行内”(姑且这么说吧,就是处理单行,即字符串中不再包含行分割符) 的字符串分割方面,cutawk提供了非常优越的“行内”(处理单行) 处理能力。

  • 更方便地处理用分割符分割好的各个部分

同样是用到分割符,但为了更方便的操作分割以后的字符串的各个部分,我们抽象了“数组”这么一个数据结构,从而让我们更加方便地通过下标来获取某个指定的部分。bash 提供了这么一种数据结构,而优秀的 awk 也同样提供了它,我们这里将简单介绍它们的用法。

把字符串拆分成字符串数组

  • Bash 提供的数组数据结构,以数字为下标的

    `` $ var="get the length of me" $ var_arr=($var) #把字符串var存放到字符串数组var_arr中,默认以空格作为分割符 $ echo ${var_arr[0]} ${var_arr[1]} ${var_arr[2]} ${var_arr[3]} ${var_arr[4]} get the length of me $ echo ${var_arr[@]} #整个字符串,可以用*代替@,下同 get the length of me $ echo ${#var_arr[@]} #类似于求字符串长度,#`操作符也可用来求数组元素个数 5

    ```

    也可以直接给某个数组元素赋值

    ``` $ var_arr[5]="new_element" $ echo ${#var_arr[@]} 6 $ echo ${var_arr[5]} new_element

    ```

    Bash 实际上还提供了一种类似于“数组”的功能,即for i in,它可以很方便地获取某 个字符串的各个部分,例如:

    ``` $ for i in $var; do echo -n $i""; done get_the_length_of_me

    ```

  • awk 里的数组,注意比较它和 Bash 里的数组的异同split 把一行按照空格分割,存放到数组 var_arr 中,并返回数组长度。注意:这里 第一个元素下标不是 0,而是 1

    ``` $ echo $var | awk '{printf("%d %s\n", split($0, var_arr, " "), var_arr[1]);}' 5 get

    ```

    实际上,上述操作很类似 awk 自身的行处理功能: awk 默认把一行按照空格分割为多 个域,并可以通过$1$2$3... 来获取,$0 表示整行

  • 这里的 NF 是该行的域的总数,类似于上面数组的长度,它同样提供了一种通过类似“下标”访问某个字符串的功能。

    ``` $ echo $var | awk '{printf("%d | %s %s %s %s %s | %s\n", NF, $1, $2, $3, $4, $5, $ 0);}' 5 | get the length of me | get the length of me

    ```

    awk 的“数组”功能何止于此呢,看看它的 for 引用吧,注意,这个和Bash里头的 for 不太一样, i不是元素本身,而是下标:

    ``` $ echo $var | awk '{split($0, var_arr, " "); for(i in var_arr) printf("%s ",var_ar r[i]);}' of me get the length 4 5 1 2 3

    ```

    另外,从上述结果可以看到,经过 for 处理后,整个结果没有按照原理的字符顺序排列,不过如果仅仅是迭代出所有元素这个同样很有意义

awk还有更“厉害”的处理能力,它的下标可以不是数字,可以是字符串,从而变成了“关联”数组,这种“关联”在某些方面非常方便。 比如,把某个文件中的某个系统调用名根据另外一个文件中的函数地址映射表替换成地址,可以这么实现:

$ cat symbol
sys_exit
sys_read
sys_close
$ ls /boot/System.map*
/boot/System.map-2.6.20-16-generic
$ awk '{if(FILENAME ~ "System.map") map[$3]=$1; else {printf("%s\n", map[$1])}}' \
/boot/System.map-2.6.20-16-generic symbol
c0129a80
c0177310
c0175d80

另外,awk还支持用delete函数删除某个数组元素。如果某些场合有需要的话,别忘了awk还 支持二维数组。

字符串常规操作

字符串操作包括取子串、查询子串、插入子串、删除子串、子串替换、子串比较、子串排序、子串进制转换、子串编码转换等。

取子串

取子串的方法主要有:

  • 直接到指定位置求子串
  • 字符匹配求子串

按照位置取子串

比如从什么位置开始,取多少个字符

$ var="get the length of me"
$ echo ${var:0:3}
get
$ echo ${var:(-2)} # 方向相反呢
me
$ echo `expr substr "$var" 5 3` #记得把$var引起来,否则expr会因为空格而解析错误
the
$ echo $var | awk '{printf("%s\n", substr($0, 9, 6))}'
length

awk$var按照空格分开为多个变量,依次为$1$2$3$4$5

$ echo $var | awk '{printf("%s\n", $1);}'
get
$ echo $var | awk '{printf("%s\n", $5);}'
me

cut小工具,它用起来和awk类似,-d指定分割符,如同awk-F 指定分割符一样; -f指定“域”,如同awk$数字

$ echo $var | cut -d" " -f 5

匹配字符求子串

用 Bash 内置支持求子串:

$ echo ${var%% *} #从右边开始计算,删除最左边的空格右边的所有字符
get
$ echo ${var% *} #从右边开始计算,删除第一个空格右边的所有字符
get the length of
$ echo ${var##* } #从左边开始计算,删除最右边的空格左边的所有字符
me
$ echo ${var#* } #从左边开始计算,删除第一个空格左边的所有字符
the length of me

删除所有 空格+字母组合 的字符串:

$ echo $var | sed 's/ [a-z]*//g'
get
$ echo $var | sed 's/[a-z]* //g'
me

sed有按地址(行) 打印(p)的功能,记得先用 tr把空格换成行号:

$ echo $var | tr " " "\n" | sed -n 1p
get
$ echo $var | tr " " "\n" | sed -n 5p
me

tr 也可以用来取子串,它可以类似#%来“拿掉”一些字符串来实现取子串:

$ echo $var | tr -d " "
getthelengthofme
$ echo $var | tr -cd "[a-z]" #把所有的空格都拿掉了,仅仅保留字母字符串,注意-c和-d的用法
getthelengthofme

说明:

  • %# 删除字符的方向不一样,前者在右,后者在左,%%%###的方向是前者是最大匹配,后者是最小匹配。(好的记忆方法见网中人的键盘记忆法:#$%是键盘依次从左到右的三个键)
  • trc选项是 complement 的缩写,即 invert ,而d 选项是删除, tr -cd "[a-z]" 这样一来就变成保留所有的字母

对于字符串的截取,实际上还有一些命令,如果 headtail 等可以实现有意思的功能,可以截取某个字符串的前面、后面指定的行数或者字节数。例如:

$ echo "abcdefghijk" | head -c 4
abcd
$ echo -n "abcdefghijk" | tail -c 4
hijk

查询子串

子串查询包括:

  • 返回符合某个模式的子串本身
  • 返回子串在目标串中的位置

查询子串在目标串中的位置

expr index 貌似仅仅可以返回某个字符或者多个字符中第一个字符出现的位置

$ var="get the length of me"
$ expr index "$var" t
3

awk却能找出字串,match还可以匹配正则表达式

$ echo $var | awk '{printf("%d\n", match($0,"the"));}'
5

查询子串,返回包含子串的行

awksed都可以实现这些功能,但是 grep 最擅长

$ grep "consists of" test.txt # 查询文件包含consists of的行,并打印这些行
$ grep "consists[[:space:]]of" -n -H test.txt # 打印文件名,子串所在行的行号和该行的内容
$ grep "consists[[:space:]]of" -n -o test.txt # 仅仅打印行号和匹配到的子串本身的内容
$ awk '/consists of/{ printf("%s:%d:%s\n",FILENAME, FNR, $0)}' text #看到没?和grep的结
果一样
$ sed -n -e '/consists of/=;/consists of/p' text #同样可以打印行号

说明:

  • awkgrepsed 都能通过模式匹配查找指定字符串,但它们各有所长,将在后续章节中继续使用和比较它们,进而发现各自优点
  • 在这里姑且把文件内容当成了一个大的字符串,会单独写一篇关于文件的讲解

子串替换

子串替换就是把某个指定的子串替换成其他的字符串,这里蕴含了“插入子串”和“删除子串”的操作。例如,想插入某个字符串到某个子串之前,就可以把原来的子串替换成”子串+新的字符串“,如果想删除某个子串,就把子串替换成空串。不过有些工具提供了一些专门的用法来做字符串操作插入子串和删除子串的操作,所以呆伙还会专门介绍。另外,要想替换掉某个子串,一般都是先找到子串(查询子串) ,然后再把它替换掉,实质上很多工具在使用和设计上都体现了这么一点。

把变量 var 中的空格替换成下划线

$ var="get the length of me"
$ echo ${var/ /_} #把第一个空格替换成下划线
get_the length of me
$ echo ${var// /_} #把所有空格都替换成下划线
get_the_length_of_me

awkawk 提供了转换的最小替换函数 sub 和全局替换函数 gsub ,类似 ///

$ echo $var | awk '{sub(" ", "_", $0); printf("%s\n", $0);}'
get_the length of me
$ echo $var | awk '{gsub(" ", "_", $0); printf("%s\n", $0);}'
get_the_length_of_me

用 sed ,子串替换可是 sed 的特长:

$ echo $var | sed -e 's/ /_/' #s <= substitude
get_the length of me
$ echo $var | sed -e 's/ /_/g' #看到没有,简短两个命令就实现了最小匹配和最大匹配g <= global
get_the_length_of_me

有忘记 tr 命令么?可以用替换单个字符的:

$ echo $var | tr " " "_"
get_the_length_of_me
$ echo $var | tr '[a-z]' '[A-Z]' # 把所有小写字母都替换为大写字母
GET THE LENGTH OF ME

有一种比较有意思的字符串替换是:整个文件行的倒置,这个可以通过 tac 命令实现,它会把文件中所有的行全部倒转过来。在某种意义上来说,排序实际上也是一个字符串替换

插入子串

在指定位置插入子串,这个位置可能是某个子串的位置,也可能是从某个文件开头算起的某 个长度。通过上面的练习,我们发现这两者之间实际上是类似的。

公式:插入子串=把"old子串"替换成"old子串+new子串"或者"new子串+old子串"

在 var 字符串的空格之前或之后插入一个下划线

用{}:

$ var="get the length of me"
$ echo ${var/ /_ } #在指定字符串之前插入一个字符串
get_ the length of me
$ echo ${var// /_ }
get_ the_ length_ of_ me
$ echo ${var/ / _} #在指定字符串之后插入一个字符串
get _the length of me
$ echo ${var// / _}
get _the _length _of _me

其他的还用演示么?这里主要介绍sed怎么用来插入字符吧,因为它的标签功能很有趣 说明: ( 和 ) 将不匹配到的字符串存放为一个标签,按匹配顺序为\1 , \2 ...

$ echo $var | sed -e 's/\( \)/_\1/'
get_ the length of me
$ echo $var | sed -e 's/\( \)/_\1/g'
get_ the_ length_ of_ me
$ echo $var | sed -e 's/\( \)/\1_/'
get _the length of me
$ echo $var | sed -e 's/\( \)/\1_/g'
get _the _length _of _me

看看 sed 的标签的顺序是不是\1 , \2 ...,看到没?\2\1 调换位置后, theget 的位置掉换了:

$ echo $var | sed -e 's/\([a-z]*\) \([a-z]*\) /\2 \1 /g'
the get of length me

sed 还有专门的插入指令, ai ,分别表示在匹配的行后和行前插入指定字符

$ echo $var | sed '/get/a test'
get the length of me
test
$ echo $var | sed '/get/i test'
test
get the length of me

删除子串

删除子串:应该很简单了吧,把子串替换成“空”(什么都没有) 不就变成了删除么。还是来简单复习一下替换吧

把 var 字符串中所有的空格给删除掉。

鼓励:这样一替换不知道变成什么单词啦,谁认得呢?但是中文却是连在一起的,所以中文有多难,你想到了么?原来你也是个语言天才,而英语并不可怕,你有学会它的天赋,只要有这个打算。

再用 {}

$ echo ${var// /}
getthelengthofme

再用awk

$ echo $var | awk '{gsub(" ","",$0); printf("%s\n", $0);}'

再用 sed

$ echo $var | sed 's/ //g'
getthelengthofme

还有更简单的 tr 命令, tr 也可以把空格给删除掉,看

$ echo $var | tr -d " "
getthelengthofme

如果要删除第一个空格后面所有的字符串该怎么办呢?还记得{}#% 用法么?如果不记得,回到这节的开头开始复习吧。(实际上删除子串和取子串未尝不是两种互补的运算呢,删除掉某些不想要的子串,也就同时取得另外那些想要的子串——这个世界就是一个“二元”的世界,非常有趣)

子串比较

这个很简单:还记得test命令的用法么? man test 。它可以用来判断两个字符串是否相等。另外,有发现“字符串是否相等”和“字符串能否跟另外一个字符串匹配 " 两个问题之间的关系吗?如果两个字符串完全匹配,那么这两个字符串就相等了。所以呢,上面用到的字符串匹配方法,也同样可以用到这里。

子串排序

常见的有按字母序、数字序等正序或反序排列。 sort 命令可以用来做这个工作,它和其他行处理命令一样,是按行操作的,另外,它类似cutawk ,可以指定分割符,并指定需要排序的列。

$ var="get the length of me"
$ echo $var | tr ' ' '\n' | sort #正序排
get
length
me
of
the
$ echo $var | tr ' ' '\n' | sort -r #反序排
the
of
me
length
get

字符串操作进阶

正则表达式

处理 URL 地址

URL 地址(URL(Uniform Resoure Locator:统一资源定位器) 是WWW页的地址)几乎是我们日常生活的玩伴,我们已经到了无法离开它的地步啦,对它的操作很多,包括判断 URL 地址的有效性,截取地址的各个部分(服务器类型、服务器地址、端口、路径等) 并对各个部分进行进一步的操作。

下面我们来具体处理这个URL地址:ftp://anonymous:[email protected]/software/scim-1.4.7.tar.gz

url="ftp://anonymous:[email protected]/software/scim-1.4.7.tar.gz"

匹配URL地址,判断URL地址的有效性

$ echo $url | grep "ftp://[a-z]*:[a-z]*@[a-z\./-]*"

截取服务器类型

$ echo ${url%%:*}
ftp
$ echo $url | cut -d":" -f 1
ftp

截取域名

$ tmp=${url##*@} ; echo ${tmp%%/*}
mirror.lzu.edu.cn

截取路径

$ tmp=${url##*@} ; echo ${tmp%
mirror.lzu.edu.cn/software

截取文件名

$ basename $url
scim-1.4.7.tar.gz
$ echo ${url##*/}
scim-1.4.7.tar.gz

截取文件类型(扩展名)