在Bash中提取子字符串

1105

给定一个形如 someletters_12345_moreleters.ext 的文件名,我想提取其中的 5 个数字并将其赋值给一个变量。

为了强调一下,我有一个由 x 个字符组成的文件名,然后是由单个下划线包围的五位数字序列,在另一组 x 个字符之后。我想要取出这个五位数并将其存入一个变量中。

我非常感兴趣的是有多少种方法可以实现这个目标。


7
大多数答案似乎没有回答你的问题,因为问题含义不明确。根据你的定义,例如abc_12345_def_67890_ghi_def是一个有效的输入,那么你想要发生什么?假设只有一个5位数字序列,基于你对输入的定义,仍然可以将abc_def_12345_ghi_jkl1234567_12345_123456712345d_12345_12345e视为有效输入。大多数下面的答案都无法处理这种情况。 - gman
7
这个问题的示例输入太具体了,因此它得到了很多针对这种特定情况(仅数字、相同的 _ 分隔符、输入字段中只包含目标字符串等)的具体答案。而最佳(最通用和最快速)答案在10年后只有7个赞,而其他有限的答案则有数百个赞。这让我失去了对开发者的信心。 - Dan Dascalescu
标题党。子字符串函数的含义已经很明确了,意味着通过数字位置获取一部分内容。所有其他内容(indexOf、regex)都与搜索有关。一个3个月前的问题在准确询问bash中的substring,得到了相同的回答,但没有在标题中包含"substring"。不是误导,但命名不当。结果:关于大多数投票的内置函数的答案被埋藏在5个屏幕之下进行排序;更早且更精确的问题标记为重复。 https://dev59.com/gnVC5IYBdhLWcg3wqzHV - user9999
2
我要注意到,我非常清楚在变量扩展中使用正则表达式的效率和实用性,但我来这里是因为我忘记了如何获取bash字符串变量的第i到第j个索引子字符串。根据答案的赞数,这也是我们大多数人来到这里的原因。 OP的具体问题最终被正则表达式实现更优雅地回答并不重要。 - Steven Lu
26个回答

1591
你可以使用参数扩展来实现这个功能。
如果a是常量,下面的参数扩展将执行子字符串提取:
b=${a:12:5}

其中12是偏移量(从零开始),5是长度。

如果输入中下划线是唯一的字符,则可以通过两个步骤去除前缀和后缀:

tmp=${a#*_}   # remove prefix ending in "_"
b=${tmp%_*}   # remove suffix starting with "_"

如果有其他下划线,可能仍然可行,但会更加棘手。如果有人知道如何在单个表达式中执行两个扩展,我也想知道。

所提供的两个解决方案都是纯bash,没有涉及进程生成,因此非常快。


30
在我的GNU bash 4.2.45上,出现了bash: ${${a#*_}%_*}: bad substitution的错误提示。 - JB.
2
@jonnyB,过去有一段时间它是有效的。我的同事告诉我它停止了,并且他们将其更改为sed命令或其他内容。从历史记录中查看,我在一个sh脚本中运行它,这可能是破折号。现在我无法让它再次工作。 - Spencer Rathbun
24
JB,你应该澄清一下,“12”是偏移量(从0开始计算),而“5”是长度。另外,感谢@gontard提供的链接,其中详细解释了所有内容! - Doktor J
2
在脚本中运行“sh run.sh”时,可能会出现Bad Substitution错误。为了避免这种情况,请更改run.sh的权限(chmod +x run.sh),然后作为“./run.sh”运行脚本。 - Ankur
9
顺便提一句,偏移参数也可以是负数。只需注意不要将其与冒号连在一起,否则Bash会将其解释为“使用默认值”替换中的:-。因此,${a: -12:5}会输出距离末尾12个字符的5个字符,而${a: -12:-5}会输出末尾倒数第12个字符到倒数第5个字符之间的7个字符。 - JB.
显示剩余15条评论

926

使用 cut 命令:

echo 'someletters_12345_moreleters.ext' | cut -d'_' -f 2

更加通用:

INPUT='someletters_12345_moreleters.ext'
SUBSTRING=$(echo $INPUT| cut -d'_' -f 2)
echo $SUBSTRING

5
更通用的回答正是我在寻找的,谢谢。 - Berek Bryan
113
-f 标志使用基于 1 的索引,而不是程序员通常使用的基于 0 的索引。 - Matthew G
2
输入=一些字母_12345_更多字母.ext 子字符串=$(echo $输入| cut -d'_' -f 2) echo $子字符串 - mani deepak
7
除非你确定变量不包含异常空格或shell元字符,否则应该在echo的参数周围正确使用双引号。更多信息请参见https://dev59.com/NWkw5IYBdhLWcg3wMHum。 - tripleee
4
在“-f”后面加上数字“2”是告诉Shell提取第二个子字符串集合。 - Sandun
显示剩余6条评论

135

尝试使用cut -c 开始索引-结束索引


3
有类似于 startIndex-lastIndex - 1 的东西吗? - Niklas
1
@Niklas 在Bash中,可能是startIndx-$((lastIndx-1)) - brown.2179
3
start=5;stop=9; echo "the rain in spain" | cut -c $start-$(($stop-1)) - brown.2179
2
问题在于输入是动态的,因为我也使用管道来获取它,所以基本上是这样的:git log --oneline | head -1 | cut -c 9-(end -1) - Niklas
这可以通过将其分为两部分并使用cut来完成,如 line=git log --oneline | head -1 && echo $line | cut -c 9-$((${#line}-1)),但在这种特定情况下,最好使用 sed ,如 git log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g' - brown.2179
这个命令非常适合从像stat这样的命令中获取时间戳等信息!省时! - Sean Halls

128
通用解决方案,其中数字可以出现在文件名的任何位置,使用第一个这样的序列:
number=$(echo "$filename" | egrep -o '[[:digit:]]{5}' | head -n1)

另一种提取变量的部分的解决方案:
number="${filename:offset:length}"

如果您的文件名始终具有格式stuff_digits_...,您可以使用awk:
number=$(echo "$filename" | awk -F _ '{ print $2 }')

另一种除去除数字以外的所有内容的解决方案是使用
number=$(echo "$filename" | tr -cd '[[:digit:]]')

2
如果我想从文件的最后一行提取数字/单词怎么办? - A Sahra
我的需求是删除文件名的最后几个字符。 fileName="filename_timelog.log" number=${filename:0:-12} echo $number 输出:filename - Purushoth.Kesav
1
echo $filename | is itself broken -- it should be echo "$filename" | .... See I just assigned a variable, but echo $variable shows something else!. Or, for a bash-only more-efficient approach (at least, more efficient if your TMPDIR is stored on tmpfs, as is conventional on modern distros), <<<"$filename" egrep ... - Charles Duffy
egrepе·Іиў«ејѓз”ЁпјЊиЇ·дЅїз”Ёgrep -Eд»Јж›їгЂ‚ - soundflix

60

这是我会做的方式:

FN=someletters_12345_moreleters.ext
[[ ${FN} =~ _([[:digit:]]{5})_ ]] && NUM=${BASH_REMATCH[1]}

解释:

针对Bash:

正则表达式(RE):_([[:digit:]]{5})_

  • _是字面字符,用于标记匹配边界
  • ()创建一个捕获组
  • [[:digit:]]是一个字符类,我认为这个已经很清楚了
  • {5}意味着之前的字符、类别(本例中)或组必须恰好匹配五次

可以将其视为以下英文行为:迭代FN字符串的每个字符,直到我们看到一个_,此时捕获组被打开,并尝试匹配五个数字。如果到目前为止匹配成功,则捕获组保存遍历的五个数字。如果下一个字符是_,则条件成功,捕获组在BASH_REMATCH中可用,并且下一个NUM=语句可以执行。如果任何匹配的部分失败,则保存的细节将被处理并且字符处理将在_之后继续进行。例如,如果FN_1 _12 _123 _1234 _12345_,则在找到匹配项之前会出现四次错误尝试。


4
这是一种通用的方法,即使您需要提取多个内容(就像我所做的那样),它也可以起作用。 - zebediah49
6
这确实是最通用的答案,应该被接受。它适用于正则表达式,而不仅仅是固定位置或在相同分隔符之间的字符字符串(这使得cut生效)。它也不依赖于执行外部命令。 - Dan Dascalescu
1
太好了!我将其适应为使用不同的起始/停止界定符(用 _ 替换)和可变长度数字(. 代表 {5})以适应我的情况。有人可以解释一下这黑魔法吗? - Paul
3
@Paul,我在我的回答中添加了更多细节。希望有所帮助。 - nicerobot
@nicerobot(或任何有见解的人),为什么你使用[[:digit:]]而不是[0-9]?我是否忽略了某些区域设置问题?我认为数字组始终应该是[0-9],而[0-9]仍然比[[:digit:]]更清晰、更节省空间。我一直对这个字符类的存在有点好奇。当它可以用\d表示时,我理解它,但不理解它的完整形式。这与非扩展正则表达式有关吗? - UrsineRaven
1
@UrsineRaven 个人偏好。我通常更喜欢使用 POSIX 类名,因为我认为它们使正则表达式更易读。 - nicerobot

38

如果有人需要更为严谨的信息,您也可以像这样在 man bash 中搜索

$ man bash [press return key]
/substring  [press return key]
[press "n" key]
[press "n" key]
[press "n" key]
[press "n" key]
结果为: ${parameter:offset} ${parameter:offset:length} 子字符串扩展。从偏移量所指定的字符开始,将参数的最多length个字符扩展出来。如果省略了length,则将参数的子字符串从指定偏移处开始扩展出来。length和offset是算术表达式(参见下面的算术计算)。如果offset计算为一个小于零的数字,则该值被用作从参数值结尾处的偏移量。以-开头的算术表达式必须与前面的冒号用空格分隔,以便与默认值扩展进行区分。如果length计算为一个小于零的数字,并且parameter不是@也不是一个索引或关联数组,则将其解释为从parameter值的结尾处的偏移量而不是字符数,扩展为两个偏移之间的字符。如果参数是@,则结果是从offset开始的长度位置参数。如果参数是由@或*订阅的索引数组名称,则结果是以${parameter[offset]}开头的数组成员的长度。负偏移相对于指定数组的最大索引加1。应用于关联数组的子字符串扩展会产生未定义的结果。请注意,负偏移必须与冒号至少用一个空格分隔,以避免与:-扩展混淆。除非使用位置参数,则子字符串索引从0开始。如果偏移为0且使用了位置参数,则$0将作为列表的前缀。

7
负数值的一个非常重要的注意事项如上所述:以a -开头的算术表达式必须通过空格与前面的:分开,以便与使用默认值扩展区分开来。因此,要获取变量的最后四个字符:${var: -4} - sshow
如果空格在您的命令中看起来不协调,您也可以使用${var:0-4} - Vopel

25

我很惊讶这个纯Bash解决方案没有被提出:

a="someletters_12345_moreleters.ext"
IFS="_"
set $a
echo $2
# prints 12345

您可能希望将IFS重置为先前的值,或在之后取消设置IFS


1
这不是纯Bash解决方案,我认为它在纯Shell(/bin/sh)中可以工作。 - kayn
6
你可以用另一种方式来避免取消设置 IFS 和位置参数:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs"。这样写也可以达到同样的效果,并且更为简洁易懂。 - kojiro
4
这句话涉及到路径名扩展!(所以它已经损坏了)。 - gniourf_gniourf

23

在 jor 的答案的基础上进行改进(但对我来说并不起作用):

substring=$(expr "$filename" : '.*_\([^_]*\)_.*')

12
当你需要处理复杂的内容时,正则表达式是真正的利器,简单地计算下划线数量可能不够用。 - Aleksandr Levchuk
1
嗨,为什么不用[:digit:] *代替[^ _] * - YoavKlein

14

如果我们聚焦于这个概念:
"一串(一个或多个)数字"

我们可以使用几个外部工具来提取数字。
我们可以使用sed或tr轻松地删除所有其他字符:

name='someletters_12345_moreleters.ext'

echo $name | sed 's/[^0-9]*//g'    # 12345
echo $name | tr -c -d 0-9          # 12345

但如果 $name 包含多个数字序列,上述方法会失败:

比如当 "name=someletters_12345_moreleters_323_end.ext" 时,会出现问题:

echo $name | sed 's/[^0-9]*//g'    # 12345323
echo $name | tr -c -d 0-9          # 12345323

我们需要使用正则表达式 (regex)。
在 sed 和 perl 中只选择第一次运行 (12345 而不是 323):

echo $name | sed 's/[^0-9]*\([0-9]\{1,\}\).*$/\1/'
perl -e 'my $name='$name';my ($num)=$name=~/(\d+)/;print "$num\n";'

但我们也可以直接在 bash(1) 中完成:

regex=[^0-9]*([0-9]{1,}).*$; \
[[ $name =~ $regex ]] && echo ${BASH_REMATCH[1]}

这使我们能够提取任意长度的数字组成的首个连续序列,该序列可能被其他文本/字符包围。

注意regex=[^0-9]*([0-9]{5,5}).*$; 仅匹配刚好有5个数字组成的序列。 :-)

(1): 比为每个短文本调用外部工具更快。对于大文件内的所有处理来说,不如在sed或awk中进行处理。


echo $name 更改为 echo "$name",否则 name=' * 12345 *' 将导致您的输出包含文件名中的数字。 - Charles Duffy

13

根据要求:

我有一个由x个字符组成的文件名,然后是一个由单个下划线包围的五位数字序列,再然后是另一组由x个字符组成的字符。我想取出这个5位数字并将其放入变量中。

我找到了一些可能有用的 grep 方法:

$ echo "someletters_12345_moreleters.ext" | grep -Eo "[[:digit:]]+" 
12345

更好。
$ echo "someletters_12345_moreleters.ext" | grep -Eo "[[:digit:]]{5}" 
12345

使用-Po语法:

$ echo "someletters_12345_moreleters.ext" | grep -Po '(?<=_)\d+' 
12345

或者,如果你希望它恰好适合5个字符:

$ echo "someletters_12345_moreleters.ext" | grep -Po '(?<=_)\d{5}' 
12345

最后,要将其存储在变量中,只需使用var=$(command)语法。


3
我认为现在没有必要使用egrep命令,因为该命令本身会警告你:“调用‘egrep’已被弃用,请改用‘grep -E’”。我已编辑了你的回答。 - Neurotransmitter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接