shell编程范例之字符串操作
falcon<zhangjinw@gmail.com>
2007-11-17
忙活了一个礼拜,终于等到周末,可以空下来写点东西。
这次介绍 _字符串操作_ 了,这里先得明白两个东西,什么是字符串,对字符串有哪些操作?
下面是"在线新华字典"的解释:
字符串: 简称“串”。有限字符的序列。数据元素为字符的线性表,是一种数据的逻辑结构。在计算机中可有不同的存储结构。在串上可进行求子串、插入字符、删除字符、置换字符等运算。 |
而字符呢?
字符: 计算机程序设计及操作时使用的符号。包括字母、数字、空格符、提示符及各种专用字符等。 |
照这样说,之前介绍的数值操作中的数字,逻辑运算中的真假值,都是以字符的形式呈现出来的,是一种特别的字符,对它们的运算只不过是字符操作的特例罢了。而这里将研究一般字符的运算,它具有非常重要的意义,因为对我们来说,一般的工作都是处理字符而已。这些运算实际上将围绕上述两个定义来做。
第一、找出字符或者字符串的类型,是数字、字母还是其他特定字符,是可打印字符,还是不可打印字符(一些控制字符)。
第二、找出组成字符串的字符个数和字符串的存储结构(比如数组)。
第三、对串的常规操作:求子串、插入字符、删除字符、置换字符、字符串的比较等。
第四、对串的一些比较复杂而有趣的操作,这里将在最后介绍一些有趣的范例。
1. 字符串的属性
1.1 字符串的类型
字符有可能是数字、字母、空格、其他特殊字符,而字符串有可能是它们任何一种或者多种的组合,在组合之后还可能形成一个具有特定意义的字符串,诸如邮件地址,URL地址等。
概要示例: 下面我们来看看如何判断字符的类型。
// 数字或者数字组合(能够返回结果,即程序退出状态是0,说明属于这种类型,反之不然) $ i=5;j=9423483247234; $ echo $i | grep [0-9]* 5 $ echo $j | grep [0-9]* 9423483247234 $ echo $j | grep [0-9]* >/dev/null $ echo $? 0 // 字符组合(小写字母、大写字母、两者的组合) $ c="A"; d="fwefewjuew"; e="fewfEFWefwefe" $ echo $c | grep [A-Z] A $ echo $d | grep "[a-z]*" fwefewjuew $ echo $e | grep "[a-zA-Z]*" fewfEFWefwefe // 字母和数字的组合 $ ic="432fwfwefeFWEwefwef" $ echo $ic | grep "[0-9a-zA-Z]*" 432fwfwefeFWEwefwef // 空格或者Tab键等 $ echo " " | grep " " $ echo -e "\t" | grep "[[:space:]]" #[[:space:]]会同时匹配空格和TAB键 $ echo -e " \t" | grep "[[:space:]]" $ echo -e "\t" | grep "<tab>" #<tab>为在键盘上按下TAB键,而不是字符<tab> // 匹配邮件地址 $ echo "test2007@lzu.cn" | grep "[0-9a-zA-Z\.]*@[0-9a-zA-Z\.]" test2007@lzu.cn // 匹配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 |
说明:
[1] /dev/null和/dev/zero是非常有趣的两个设备,它们都犹如一个黑洞,什么东西掉进去都会消失殆尽;后者则是一个能源箱,你总能从那里取到0,直到你退出。两者的部分用法见:
关于zero及NULL设备的一些问题
[2] [[:space:]]是grep用于匹配空格或者TAB键类型字符串的一种标记,其他类似的标记请查看grep的帮助,man grep。
[3] 上面都是用grep来进行模式匹配,实际上sed, awk都可以用来做模式匹配,关于匹配中用到的正则匹配模式知识,大家可以参考
正则匹配模式,更多相关资料请看参考资料。
[4] 如果仅仅想判断字符串是否为空,即判断字符串的长度是否为零,那么可以简单的通过test命令的-z选项来判断,具体用法见test命令,man test.
概要示例: 判断字符是否可打印?如何控制字符在终端的显示。
// 用grep判断某个字符是否为可打印字符 $ echo "\t\n" | grep "[[:print:]]" \t\n $ echo $? 0 $ echo -e "\t\n" | grep "[[:print:]]" $ echo $? 1 // 用echo的-e选项在屏幕控制字符显示位置、颜色、背景等 $ echo -e "\33[31;40m" #设置前景色为黑色,背景色为红色 $ echo -e "\33[11;29H Hello, World\!" #在屏幕的第11行,29列开始打印字符串Hello,World! // 在屏幕的某个位置动态显示当前系统时间 $ while :; do echo -e "\33[11;29H "$(date "+%Y-%m-%d %H:%M:%S"); done // 用col命令过滤掉某些控制字符,在处理诸如script,screen等截屏命令的输出结果时,很有用 $ screen -L $ cat /bin/cat $ exit $ cat screenlog.0 | col -b # 把一些控制字符过滤后,就可以保留可读的操作日志 |
更多关于字符在终端的显示控制方法,请参考资料[20]和字符显示实例[21]:用shell实现的一个动态时钟。
1.2 字符串的长度
概要示例: 除了组成字符串的字符类型外,字符串还有哪些属性呢?组成字符串的字符个数。下面我们来计算字符串的长度,即所有字符的个数,并简单介绍几种求字符串中指定字符个数的方法。
// 计算某个字符串的长度,即所有字符的个数[这计算方法是五花八门,择其优着而用之] $ 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 // 如果要统计单词个数,更多相关信息见《shell编程之数值计算》之 _单词统计_ 实例。 $ echo $var | wc -w 5 $ echo "$var" | tr " " "\n" | grep get | uniq -c 1 $ echo "$var" | tr " " "\n" | grep get | wc -l 1 |
说明:
${}操作符在Bash里头一个“大牛”,能胜任相当多的工作,具体就看看网中人的《shell十三问》之《Shell十三问》之"$(( )) 與 $( ) 還有${ } 差在哪?" 吧。
1.3 字符串的存储
在我们看来,字符串是一连串的字符而已,但是为了操作方便,我们往往可以让字符串呈现出一定的结构。在这里,我们不关心字符串在内存中的实际存储结构,仅仅关系它呈现出来的逻辑结构。比如,这样一个字符串:"get the length of me",我们可以从不同的方面来呈现它。
1.3.1 通过字符在串中的位置来呈现它
这样我们就可以通过指定位置来找到某个子串。这在c语言里头通常可以利用指针来做。而在shell编程中,有很多可用的工具,诸如expr,awk都提供了类似的方法来实现子串的查询动作。两者都几乎支持模式匹配(match)和完全匹配(index)。这在后面的字符串操作中将详细介绍。
1.3.2 根据某个分割符来取得字符串的各个部分
这里最常见的就是行分割符、空格或者TAB分割符了,前者用来当行号,我们似乎已经司空见惯了,因为我们的编辑器就这样“莫名”地处理着行分割符(在unix下为\n,在其他系统下有一些不同,比如windows下为\r\n)。而空格或者TAB键经常用来分割数据库的各个字段,这似乎也是司空见惯的事情。
正是因为这样,所以产生了大量优秀的行编辑工具,诸如grep,awk,sed等。在“行内”(姑且这么说吧,就是处理单行,即字符串里头不再包含行分割符)的字符串分割方面,cut和awk提供了非常优越的“行内”(处理单行)处理能力。
1.3.3 更方便地处理用分割符分割好的各个部分
同样是用到分割符,但为了更方便的操作分割以后的字符串的各个部分,我们抽象了“数组”这么一个数据结构,从而让我们更加方便地通过下标来获取某个指定的部分。bash提供了这么一种数据结构,而优秀的awk也同样提供了它,我们这里将简单介绍它们的用法。
概要示例:利用数组存放"get the length of me"的用空格分开的各个部分。
//1. bash提供的数组数据结构,它是以数字为下标的,和C语言从0开始的下标一样 $ 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[5]} 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 //2. 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_arr);}' get the length of me $ echo $var | awk '{split($0, var_arr, " "); for(i in var_arr) printf("%s ",i);}' 1 2 3 4 5 // awk还有更“厉害”的处理能力,它的下标可以不是数字,而可以是字符串,从而变成了“关联”数组,这种“关联”的作用在某些方便将让我们非常方便 // 比如,我们这里就实现一个非凡的应用,把某个文件中的某个系统调用名替换成地址,如果你真正用起它,你会感慨它的“鬼斧神工”的。 // 这就是我在一个场合最好才发现的随好的实现方案:有兴趣看看awk手册帖子中我在3楼回复的实例吧。 $ cat symbol sys_exit sys_read sys_close $ ls /boot/System.map* $ 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还支持二维数组。 |
okay,就介绍到这里啦。为什么要介绍这些内容?再接着看下面的内容,你就会发现,那些有些的工具是怎么产生和发展起来的了,如果累了,看看最后一篇参考资料吧,它介绍了一些linux命令名字的由来,说不定可以帮助你理解本节下面的部分呢。
2. 字符串常规操作
字符串操作包括取子串、查询子串、插入子串、删除子串、子串替换、子串比较、子串排序、子串进制转换、子串编码转换等。
2.1 取子串
概要示例:取子串的方法主要有:直接到指定位置求子串,字符匹配求子串。
// 按照位置取子串,比如从什么位置开始,取多少个字符 $ 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 // 匹配字符求子串 $ echo ${var%% *} #从右边开始计算,删除最左边的空格右边的所有字符 get $ echo ${var% *} #从右边开始计算,删除第一个空格右边的所有字符 get the length of $ echo ${var##* } #从左边开始计算,删除最右边的空格左边的所有字符 me $ echo ${var#* } #从左边开始计算,删除第一个空格左边的所有字符 the length of me $ echo $var | awk '{printf("%s\n", $1);}' # awk把$var按照空格分开为多个变量,依次为$1,$2,$3,$4,$5 get $ echo $var | awk '{printf("%s\n", $5);}' me $ echo $var | cut -d" " -f 5 #差点把cut这个小东西忘记啦,用起来和awk类似, -d指定分割符,如同awk用-F指定分割符一样,-f指定“域”,如同awk的$数字。 $ echo $var | sed 's/ [a-z]*//g' #删除所有 空格+字母串 的字符串,所以get后面的全部被删除了 get $ echo $var | sed 's/[a-z]* //g' me $ echo $var | tr " " "\n" | sed -n 1p #sed有按地址(行)打印(p)的功能,记得先用tr把空格换成行号 get $ echo $var | tr " " "\n" | sed -n 5p me // tr也可以用来取子串哦,它也可以类似#和%来“拿掉”一些字符串来实现取子串 $ echo $var | tr -d " " getthelengthofme $ echo $var | tr -cd "[a-z]" #把所有的空格都拿掉了,仅仅保留字母字符串,注意-c和-d的用法 getthelengthofme |
说明:
[1] %和#的区别是,删除字符的方向不一样,前者在右,后者在左,%%和%,##和#的方向是前者是最大匹配,后者是最小匹配。(好的记忆方法见网中人的键盘记忆法:#$%是键盘依次从左到右的三个键)
[2] tr的-c选项是complement的缩写,即invert,而-d选项是删除的意思,tr -cd "[a-z]"这样一来就变成保留所有的字母啦。
对于字符串的截取,实际上还有一些命令,如果head,tail等可以实现有意思的功能,可以截取某个字符串的前面、后面指定的行数或者字节数。例如:
$ echo "abcdefghijk" | head -c 4 abcd $ echo -n "abcdefghijk" | tail -c 4 hijk |
2.2. 查询子串
概要示例:子串查询包括:返回符合某个模式的子串本身和返回子串在目标串中的位置。
准备:在进行下面的操作之前,请把
http://oss.lzu.edu.cn/blog/blog.php?do_showone/tid_1385.html链接中的内容复制到一个文本text里头,用于下面的操作。
// 查询子串在目标串中的位置 $ var="get the length of me" $ expr index "$var" t #貌似仅仅可以返回某个字符或者多个字符中第一个字符出现的位置 3 $ echo $var | awk '{printf("%d\n", match($0,"the"));}' #awk却能找出字串,match还可以匹配正则表达式 5 // 查询子串,返回包含子串的行(awk,sed都可以实现这些功能,但是grep最擅长) $ grep "consists of" text # 查询text文件包含consists of的行,并打印这些行 $ grep "consists[[:space:]]of" -n -H text # 打印文件名,子串所在行的行号和该行的内容 $ grep "consists[[:space:]]of" -n -o text # 仅仅打印行号和匹配到的子串本身的内容 $ awk '/consists of/{ printf("%s:%d:%s\n",FILENAME, FNR, $0)}' text #看到没?和grep的结果一样 $ sed -n -e '/consists of/=;/consists of/p' text #同样可以打印行号 |
说明:
[1] awk,grep,sed都能通过模式匹配查找指定的字符串,但是它们各有擅长的领域,我们将在后续的章节中继续使用和比较它们,从而发现各自的优点。
[2] 在这里我们姑且把文件内容当成了一个大的字符串,在后面的章节中我们将专门介绍文件的操作,所以对文件内容中存放字符串的操作将会有更深入的分析和介绍。
2.3. 子串替换
子串替换就是把某个指定的子串替换成其他的字符串,实际上这里就蕴含了“插入子串”和“删除子串”的操作。例如,你想插入某个字符串到某个子串之前,就可以把原来的子串替换成”子串+新的字符串“,如果想删除某个子串,就把子串替换成空串。不过有些工具提供了一些专门的用法来做插入子串和删除子串的操作,所以呆伙还是会专门介绍的。另外,要想替换掉某个子串,一般都是先找到子串(查询子串),然后再把它替换掉的,实质上很多工具在使用和设计上都体现了这么一点。
概要示例:下面我们把变量var中的空格替换成下划线看看。
// 用{}运算符,还记得么?网中人的教程。 $ var="get the length of me" $ echo ${var/ /_} #把第一个空格替换成下划线 get_the length of me $ echo ${var// /_} #把所有空格都替换成了下划线了 get_the_length_of_me // 用awk,awk提供了转换的最小替换函数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 |
说明:sed还有很有趣的标签用法呢,下面再介绍吧。
有一种比较有意思的字符串替换是,整个文件行的倒置,这个可以通过tac命令实现,它会把文件中所有的行全部倒转过来。在一定意义上来说,排序实际上也是一个字符串替换。
2.4. 插入子串
插入子串:就是在指定的位置插入子串,这个位置可能是某个子串的位置,也可能是从某个文件开头算起的某个长度。通过上面的练习,我们发现这两者之间实际上是类似的。
公式:插入子串=把"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怎么用来插入字符吧,因为它的标签功能很有趣 $ echo $var | sed -e 's/\( \)/_\1/' #\(和\)将不匹配到的字符串存放为一个标签,按匹配顺序为\1,\2... 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掉换位置后,the和get的位置掉换了 $ echo $var | sed -e 's/\([a-z]*\) \([a-z]*\) /\2 \1 /g' the get of length me // sed还有专门的插入指令,a和i,分别表示在匹配的行后和行前插入指定字符 $ echo $var | sed '/get/a test' get the length of me test $ echo $var | sed '/get/i test' test get the length of me |