回忆之城
生命在于折腾
posts - 575,comments - 9,trackbacks - 0

字符串:

  • 简称“串”。有限字符的序列。数据元素为字符的线性表,是一种数据的逻辑结构。在计算机中可有不同的存储结构。在串上可进行求子串、插入字符、删除字符、置换字符等运算。

字符:

  • 计算机程序设计及操作时使用的符号。包括字母、数字、空格符、提示符及各种专用字符等。

一般字符的运算包括:


  • 第一、找出字符或者字符串的类型,是数字、字母还是其他特定字符,是可打印字符,还是不可打印字符(一些控制字符)。
  • 第二、找出组成字符串的字符个数和字符串的存储结构(比如数组)。
  • 第三、对串的常规操作:求子串、插入字符、删除字符、置换字符、字符串的比较等。
  • 第四、对串的一些比较复杂而有趣的操作,这里将在最后介绍一些有趣的范例。

1. 字符串的属性

字符有可能是数字、字母、空格、其他特殊字符,而字符串有可能是它们任何一种或者多种的组合,在组合之后还可能形成一个具有特定意义的字符串,诸如邮件地址,URL地址等。
概要示例: 下面我们来看看如何判断字符的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 数字或者数字组合(能够返回结果,即程序退出状态是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,直到你退出。
[2] [[:space:]]是grep用于匹配空格或者TAB键类型字符串的一种标记,其他类似的标记请查看grep的帮助,man grep。
[3] 上面都是用grep来进行模式匹配,实际上sed, awk都可以用来做模式匹配,关于匹配中用到的正则匹配模式知识,大家可以参考正则匹配模式,更多相关资料请看参考资料。
[4] 如果仅仅想判断字符串是否为空,即判断字符串的长度是否为零,那么可以简单的通过test命令的-z选项来判断,具体用法见test命令,man test.

概要示例: 判断字符是否可打印?如何控制字符在终端的显示。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用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 字符串的长度

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


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 计算某个字符串的长度,即所有字符的个数[这计算方法是五花八门,择其优着而用之]
$ 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
1
2
stringZ=abcABC123ABCabc
echo `expr "$stringZ" : '.*'`    # 15
说明:

${}操作符在Bash里头一个“大牛”,能胜任相当多的工作,具体就看看网中人的《shell十三问》之《Shell十三问》之"$(( )) 與 $( ) 還有${ } 差在哪?" 吧。

在一个文本文件的段落之间插入空行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash
# paragraph-space.sh
 
# 在一个单倍行距的文本文件中插入空行.
# Usage: $0 <FILENAME
  
MINLEN=45        # 可能需要修改这个值.
#  假定行的长度小于$MINLEN所指定的长度的时候
#+ 才认为此段结束.
 
while read line  # 提供和输入文件一样多的行...
do
   echo "$line"   # 输入所读入的行本身.
  
   len=${#line}
   if [ "$len" -lt "$MINLEN" ]
     then echo    # 在短行(译者注: 也就是小于$MINLEN个字符的行)后面添加一个空行.
   fi 
done
  
exit 0

索引

expr index $string $substring

在字符串$string中所匹配到的$substring第一次所出现的位置.

1
2
3
4
5
6
stringZ=abcABC123ABCabc
echo `expr index "$stringZ" C12`             # 6
                                            # C 字符的位置.
 
echo `expr index "$stringZ" 1c`              # 3
 # 'c' (in #3 position) matches before '1'.

这与C语言中的strchr()函数非常相似.


1.3 字符串的存储

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

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

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


匹配字符串开头的子串长度


expr match "$string" '$substring'

        $substring是一个正则表达式.


expr "$string" : '$substring'

        $substring是一个正则表达式.

1
2
3
4
5
stringZ=abcABC123ABCabc
#       |------|
 
echo `expr match "$stringZ" 'abc[A-Z]*.2'`   # 8
echo `expr "$stringZ" : 'abc[A-Z]*.2'`       # 8


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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//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还支持二维数组。