Bash 字符串操作

警告
本文最后更新于 2023-10-22,文中内容可能已过时。

Bash 字符串操作

Bash 中大部分的变量包括文件都可以当作字符串进行操作,因此本章的内容非常实用

字符串的长度

获取字符串长度的语法如下。

1
${#varname}

下面是一个例子。引号中的字符串也算长度。

1
2
3
$ name="xiashuo.xyz hello world"
$ echo ${#name}
23

大括号{}是必需的,否则 Bash 会将$#理解成脚本的参数个数,将变量名理解成文本。

1
2
$ echo $#myvar
0myvar

上面例子中,Bash 将$#myvar分开解释了。

关于$#的分析,请看《Bash 脚本》

子字符串

字符串提取子串的语法如下。

1
${varname:offset:length}

上面语法的含义是返回变量$varname的子字符串,

首先我们需要明白一个下标定义,从左往右数,即正序的时候,第一个字符的下标是 0,从右往左数,即倒序的时候,第一个数的下标是 -1。

  • offset为正数

    • length为正数的时候,length的意义为字串的长度,表达式的的作用:是返回varname按正序从位置offset开始,包含offset位置的字符,往右数长度为length,的子串。

    • length为负数的时候,length的意义为last_offset,表示子串中最后一个字符的位置,且不包含,表达式的的作用:是返回varname按正序从位置offset开始,包含offset位置的字符,到按倒叙位置为last_offset,不包含last_offset位置的字符,的子串。

    • 如果省略length,则从位置offset开始,一直返回到字符串的结尾。

  • offset为负数,如果offset为负值,表示按倒叙定位。注意,负数前面必须有一个空格,以防止与${variable:-word}的变量的设置默认值语法混淆。关于${variable:-word}的语法,请看《Bash 变量》的检查变量的值是否为空小节。

    • length为正数的时候,length的意义为字串的长度,表达式的的作用:是返回varname按倒叙从位置offset开始,包含offset位置的字符,往右数长度为长度为length,的子串。

    • length为负数的时候,length的意义为last_offset,表示子串中最后一个字符的位置,且不包含,表达式的的作用:是返回varname按倒叙从位置offset开始,包含offset位置的字符,到按倒叙位置为last_offset,不包含last_offset位置的字符,的子串。

    • 如果省略length,则从位置offset开始,一直返回到字符串的结尾。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ echo $name
0123456
$ echo ${name:2:3}
234
$ echo ${name:2:-1}
2345
$ echo ${name:2}
23456
$ echo ${name:9}

$ echo ${name: -4:2}
34
$ echo ${name: -4:-1}
345
$ echo ${name: -4:-5}
-bash: -5: substring expression < 0
$ echo ${name: -4}
3456
$ echo ${name: -9}

$ echo $name
0123456

我们可以通过一个变量来接受子字符串。获取子字符串的操作不会修改原字符串

1
2
3
$ name_sub=${name:3:2}
$ echo $name $name_sub
0123456 34

注意,不能直接对字符串字面量进行获取子字符串的操作,只能对值为字符串的变量执行获取子字符串的操作

1
2
$ echo ${"123456":3:2}
-bash: ${"123456":3:2}: bad substitution

字符串拼接

字符串的拼接很简单,直接写在一起即可

双引号字符串的拼接

1
2
3
4
5
6
7
8
9
# 字符串拼接字符串
str_concatnate="111""22"
echo $str_concatnate
# 字符串拼接变量
delimitor="+"
str_concatnate="111"$delimitor
echo $str_concatnate
str_concatnate="111"$delimitor"22"
echo $str_concatnate

输出

1
2
3
11122
111+
111+22

单引号字符串的拼接

1
2
3
4
5
6
7
8
9
# 字符串拼接字符串
str_concatnate='111''22'
echo $str_concatnate
# 字符串拼接变量
delimitor='+'
str_concatnate='111'$delimitor
echo $str_concatnate
str_concatnate='111'$delimitor'22'
echo $str_concatnate

输出

1
2
3
11122
111+
111+22

查找字符所在的位置

1
2
3
4
5
6
# 查找字符 c 或 8 的位置(哪个字母先出现就计算哪个):
# 输出 3
$ name=abcd123465789
$ echo "$(expr index "$name" c8)"

3

《Bash 的算术运算》的expr 命令小节

搜索和替换

Bash 提供多种模式的匹配和替换的方法

字符串头部的模式匹配

  • ${variable#pattern} 如果 pattern 匹配变量 variable 的开头,删除最短匹配(非贪婪匹配)的部分,返回剩余部分,原字符串不会有变化

  • ${variable##pattern} 如果 pattern 匹配变量 variable 的开头,删除最长匹配(贪婪匹配)的部分,返回剩余部分,原字符串不会有变化

  • ${variable/#pattern/string} 如果 pattern 匹配变量 variable 的开头,就会将最长匹配(贪婪匹配)的部分替换成 string,原字符串不会有变化

不存在语法${variable/##pattern/string}

${variable#pattern}${variable##pattern}可以理解为/string中的string为空字符串。默认将匹配部分替换成空字符串,也就是删除

匹配模式pattern可以使用*?[]等通配符。

如果字符串里面无法匹配模式pattern,则对字符串不会发生任何修改,最终返回原字符串。

简单实践一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ str='xiashuo.xyz hello world!!!'
$ echo $str
xiashuo.xyz hello world!!!
$ echo ${str#*.}
xyz hello world!!!
# 最短匹配
$ echo ${str#*o}
.xyz hello world!!!
# 最长匹配
$ echo ${str##*o}
rld!!!
# 按最长匹配替换
$ echo ${str/#*o/111}
111rld!!!

常用用法,比如只获取文件名称(包括拓展名)、仅获取文件拓展类型、仅获取文件名(不包括拓展类型)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 获取文件名+拓展名
$ echo ${file_path##*/}
Main.java
# 获取拓展名
$ echo ${file_path##*.}
java
# 仅获取文件名
$ file_name_full=${file_path##*/}
$ echo ${file_name_full%%.*}
Main

其实我们可以通过dirname获取文件全路径中的路径部分,通过basename命令获得文件全路径中的文件名(包含文件类型后缀)的部分

  • dirname:该命令可以取给定路径的目录部分(strip non-directory suffix from file name)。

    这个命令很少直接在 shell 命令行中使用,我们一般把它用在 shell 脚本中,用于取得脚本文件所在目录,然后将当前目录切换过去。

    在脚本中可以这样写

    1
    
    cd $(dirname "$0") || exit 1  
  • basename:basename 命令用于去掉文件名的目录和后缀(strip directory and suffix from filenames),对应的 dirname 命令用于截取目录,

    basename也经常用于在脚本中获取脚本的文件名

    1
    
    echo $(basename "$0")   

简单实践:

 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
$ dirname /usr/bin/sort           
/usr/bin
$ dirname stdio.h                 
.
$ dirname /usr/bin                
/usr
$ dirname dir1/file1 dir2/file2    
dir1
dir2
$ dirname -z dir1/file1 dir2/file2    
dir1dir2
$ basename /usr/bin/sort 
sort
# 去除文件名后缀
$ basename /usr/include/stdio.h .h 
stdio
$ basename /usr/include/stdio.h stdio.h 
stdio.h
# 去除文件名后缀方式的另外一种方法
$ basename -s .h /usr/include/stdio.h 
stdio
$ basename -a dir1/file1 dir2/file2
file1
file2
$ basename -a -z dir1/file1 dir2/file2
file1file2

字符串尾部的模式匹配

语法跟字符串头部匹配差不多,就是把#换成了%

  • ${variable%pattern} 如果 pattern 匹配变量 variable 的结尾,删除最短匹配(非贪婪匹配)的部分,返回剩余部分,原字符串不会有变化

  • ${variable%%pattern} 如果 pattern 匹配变量 variable 的结尾,删除最长匹配(贪婪匹配)的部分,返回剩余部分,原字符串不会有变化

  • ${variable/%pattern/string} 如果 pattern 匹配变量 variable 的结尾,就会将最长匹配(贪婪匹配)的部分替换成 string,原字符串不会有变化

不存在语法${variable/%%pattern/string}

${variable%pattern}${variable%%pattern}可以理解为/string中的string为空字符串。默认将匹配部分替换成空字符串,也就是删除

匹配模式pattern可以使用*?[]等通配符。

如果字符串里面无法匹配模式pattern,则对字符串不会发生任何修改,最终返回原字符串。

简单实践:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ str="xiashuo.xzy call my number\"158-4571-2256\""
$ echo $str
xiashuo.xzy call my number"158-4571-2256"
# 最短匹配
$ echo ${str%-*}
xiashuo.xzy call my number"158-4571
# 最长匹配
$ echo ${str%%-*}
xiashuo.xzy call my number"158
# 匹配引号,需要转义
$ echo ${str%%\"*\"}
xiashuo.xzy call my number
# 替换
$ echo ${str/%\"*\"/secret}
xiashuo.xzy call my numbersecret

常用用法,比如只获取文件名称(包括拓展名)、仅获取文件拓展类型、仅获取文件名(不包括拓展类型)

具体实践请看字符串头部的模式匹配小节

任意位置的模式匹配

  • ${variable/pattern/string} 如果 pattern 匹配变量 variable 的一部分,最长匹配(贪婪匹配)的那部分被 string 替换,但仅替换第一个匹配

  • ${variable//pattern/string} 如果 pattern 匹配变量 variable 的一部分,最长匹配(贪婪匹配)的那部分被 string 替换,所有匹配都替换

上面两种语法都是最长匹配(贪婪匹配)下的替换,区别是前一个语法仅仅替换第一个匹配,后一个语法替换所有匹配。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 模式可以包含  / 
$ echo ${fila_path////-}
-opt-shell-ts.sh
# 为了可读性,可以加上引号
$ echo ${fila_path/'/'/-}
-opt/shell/ts.sh
$ echo ${fila_path//'/'/-}
-opt-shell-ts.sh
# 省略第二个 / 后面的内容或者省略第二个 / 都是默认替换成空字符串,也就是删除 
$ echo ${fila_path//'/'/}
optshellts.sh
$ echo ${fila_path//'/'}
optshellts.sh

一个简单的应用是看 PATH 环境变量

:替换成换行,然后可以一行一个路径,看着束缚很多

这个其实给了我们一种将单个字符串拆分成多行的思路,拆分成多行之后,再一行一行地读取到数组中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ echo $PATH
KAFKA_HOME/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/lib/jvm/java/bin:/usr/lib/jvm/java/jre/bin:/root/bin
$ echo -e ${PATH//:/'\n'}
KAFKA_HOME/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/usr/lib/jvm/java/bin
/usr/lib/jvm/java/jre/bin
/root/bin

将单行字符串变成数组

关于数组的知识,请看《Bash 数组》

1
2
3
4
5
6
$ arr_str="111,2222,333"
$ arr=(${arr_str//,/' '})
$ echo ${#arr[@]}
3
$ echo ${arr[@]}
111 2222 333

此外还有两种特殊形式,我们在字符串头部的模式匹配字符串尾部的模式匹配中实际上就已经学习过了

1
2
3
4
5
# 模式必须出现在字符串的开头
${variable/#pattern/string}

# 模式必须出现在字符串的结尾
${variable/%pattern/string}

改变大小写

  • ${varname^^} 转为大写

  • ${varname,,} 转为小写

简单实践

1
2
3
4
5
6
$ tese_var=abcdefg
$ echo ${tese_var^^}
ABCDEFG
$ tese_var=ABCDEFG
$ echo ${tese_var,,}
abcdefg
0%