4. 字符串的存储

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

  • 通过字符在串中的位置来呈现它
    • 这样我们就可以通过指定位置来找到某个子串。这在 C 语言中通常可以利用指针来做。而在 Shell 编程中,有很多可用的工具,诸如 exprawk 都提供了类似方法来实现子串的查询动作。两者都几乎支持模式匹配 match 和完全匹配 index。这在后面的字符串操作中将详细介绍。
  • 根据某个分割符来取得字符串的各个部分
    • 这里最常见的就是行分割符、空格或者 TAB 分割符了,前者用来当行号,我们似乎已经司空见惯了,因为我们的编辑器就这样“莫名”地处理着行分割符(在 UNIX 下为 \n,在其他系统下有一些不同,比如 Windows 下为 \r\n )。而空格或者 TAB 键经常用来分割数据库的各个字段,这似乎也是司空见惯的事情。
    • 正因为这样,所以产生了大量优秀的行编辑工具,诸如 grepawksed 等。在“行内”(姑且这么说吧,就是处理单行,即字符串中不再包含行分割符)的字符串分割方面,cutawk 提供了非常优越的“行内”(处理单行)处理能力。
  • 更方便地处理用分割符分割好的各个部分
    • 同样是用到分割符,但为了更方便的操作分割以后的字符串的各个部分,我们抽象了“数组”这么一个数据结构,从而让我们更加方便地通过下标来获取某个指定的部分。 bash 提供了这么一种数据结构,而优秀的 awk 也同样提供了它,我们这里将简单介绍它们的用法。

范例:把字符串拆分成字符串数组

  • 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[@]}
    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_arr[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还支持二维数组。