在 Bash 中提取子字符串

Posted

技术标签:

【中文标题】在 Bash 中提取子字符串【英文标题】:Extract substring in Bash 【发布时间】:2021-10-31 02:16:09 【问题描述】:

给定一个someletters_12345_moreleters.ext 形式的文件名,我想提取这 5 个数字并将它们放入一个变量中。

为了强调这一点,我有一个包含 x 个字符的文件名,然后是一个五位数字序列,两边各有一个下划线,然后是另一组 x 个字符。我想将 5 位数字放入变量中。

我对可以实现这一点的不同方法的数量非常感兴趣。

【问题讨论】:

大部分答案似乎都没有回答您的问题,因为问题含糊不清。 “我有一个包含 x 个字符的文件名,然后是一个五位数字序列,两边各有一个下划线,然后是另一组 x 个字符”。根据该定义,abc_12345_def_67890_ghi_def 是有效输入。你想发生什么?假设只有一个 5 位序列。根据您对输入的定义,您仍然有 abc_def_12345_ghi_jkl1234567_12345_123456712345d_12345_12345e 作为有效输入,并且下面的大多数答案都无法处理此问题。 这个问题的示例输入太具体了。正因为如此,它为 这种特殊情况 得到了很多具体的答案(仅限数字,相同的_ 分隔符,只包含一次目标字符串的输入等)。 best (most generic and fastest) answer 10 年后只有 7 个赞,而其他有限的答案有数百个。让我对开发者失去信心???? 点击诱饵标题。子字符串函数的含义已经确立,意味着通过数字位置获取部分。所有其他的东西,(indexOf, regex) 都是关于搜索的。一个 3 个月前的问题精确询问 bash 中的子字符串,答案相同,但标题中没有“子字符串”。没有误导,但没有正确命名。结果:在投票最多的问题中,关于内置函数的答案被活动排序隐藏了 5 个屏幕;较旧且更精确的问题,标记为重复。 ***.com/questions/219402/… 【参考方案1】:

您可以使用Parameter Expansion 来执行此操作。

如果a为常数,则下面的参数展开进行子串提取:

b=$a:12:5

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

如果数字周围的下划线是输入中唯一的下划线,则可以分两步(分别)去除前缀和后缀:

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

如果还有其他下划线,无论如何它可能是可行的,尽管更棘手。如果有人知道如何在一个表达式中执行两种扩展,我也想知道。

提供的两种解决方案都是纯 bash,不涉及进程生成,因此速度非常快。

【讨论】:

@SpencerRathbun bash: $$a#*_%_*: bad substitution 在我的 GNU bash 4.2.45 上。 @jonnyB,过去一段时间有效。我的同事告诉我它停止了,他们将其更改为 sed 命令或其他东西。在历史中查看它,我在 sh 脚本中运行它,这可能是破折号。在这一点上,我不能让它工作了。 JB,您应该澄清“12”是偏移量(从零开始),“5”是长度。此外,+1 为 @gontard 的链接提供了全部内容! 在脚本中将其作为“sh run.sh”运行时,可能会出现错误替换错误。为避免这种情况,请更改 run.sh 的权限(chmod +x run.sh),然后将脚本作为“./run.sh”运行 偏移量参数也可以是负数,顺便说一句。您只需要注意不要将其粘贴到冒号上,否则 bash 会将其解释为 :-“使用默认值”替换。所以$a: -12:5 产生距离末尾 12 个字符的 5 个字符,$a: -12:-5 产生 end-12 和 end-5 之间的 7 个字符。【参考方案2】:

使用cut:

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

更通用:

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

【讨论】:

更通用的答案正是我想要的,谢谢 -f 标志采用基于 1 的索引,而不是程序员习惯的基于 0 的索引。 INPUT=someletters_12345_moreleters.ext SUBSTRING=$(echo $INPUT| cut -d'_' -f 2) echo $SUBSTRING 您应该在echo 的参数周围正确使用双引号,除非您确定变量不能包含不规则空格或shell 元字符。进一步查看***.com/questions/10067266/… '-f'后面的数字'2'是告诉shell提取第二组子串。【参考方案3】:

数字可以位于文件名中的任何位置的通用解决方案,使用此类序列中的第一个:

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:]]')

【讨论】:

如果我想从文件的最后一行提取数字/单词怎么办。 我的要求是最后删除几个字符 fileName="filename_timelog.log" number=$filename:0:-12 echo $number O/P: filename echo $filename | 本身已损坏 - 它应该是 echo "$filename" | ...。见I just assigned a variable, but echo $variable shows something else!。或者,对于仅 bash 更有效的方法(至少,如果您的 TMPDIR 存储在 tmpfs 上,则效率更高,这在现代发行版中是传统的),<<<"$filename" egrep ...【参考方案4】:

尝试使用cut -c startIndx-stopIndx

【讨论】:

有没有类似 startIndex-lastIndex - 1 的东西? @Niklas 在 bash 中,proly startIndx-$((lastIndx-1)) start=5;stop=9; echo "the rain in spain" | cut -c $start-$(($stop-1)) 问题是输入是动态的,因为我也使用管道来获取它,所以基本上是这样。 git log --oneline | head -1 | cut -c 9-(end -1) 如果分成line=git log --oneline | 两部分,这可以通过 cut 来完成head -1` && echo $line | cut -c 9-$(($#line-1))` 但在这种特殊情况下,将sed 用作git log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g' 可能会更好【参考方案5】:

我会这样做:

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

解释:

特定于 Bash 的:

[[ ]]indicates a conditional expression =~indicates the condition is a regular expression && chains the commands 如果前面的命令成功了

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

_ 是用于为要匹配的字符串划分/锚定匹配边界的文字 () 创建捕获组 [[:digit:]] 是一个字符类,我认为它不言自明 5 表示前一个字符、类(如本例中)或组中的五个必须匹配

在英语中,你可以认为它的行为是这样的:FN 字符串逐个字符地迭代,直到我们看到一个_,此时捕获组打开,我们尝试匹配五个数字。如果此时匹配成功,则捕获组保存遍历的五个数字。如果下一个字符是_,则条件成功,捕获组在BASH_REMATCH 中可用,并且可以执行下一个NUM= 语句。如果匹配的任何部分失败,保存的详细信息将被处理掉,并在_ 之后继续逐字符处理。例如如果FN where _1 _12 _123 _1234 _12345_,在找到匹配之前会有四次错误开始。

【讨论】:

这是一种通用的方法,即使您需要提取不止一个东西,就像我一样。 这确实是最通用的答案,应该被接受。它适用于正则表达式,而不仅仅是固定位置的字符串,或同一分隔符之间的字符串(启用cut)。它也不依赖于执行外部命令。 这太棒了!我对此进行了调整,以根据我的情况使用不同的开始/停止测距仪(替换 _)和可变长度数字(. for 5)。有人可以分解这个黑魔法并解释一下吗? @Paul 我在答案中添加了更多详细信息。希望对您有所帮助。【参考方案6】:

如果有人想要更严格的信息,你也可以像这样在 man bash 中搜索

$ man bash [press return key]
/substring  [press return key]
[press "n" key]
[press "n" key]
[press "n" key]
[press "n" key]

结果:

$参数:偏移量 $参数:偏移量:长度 子串扩展。扩展到最多长度字符 参数从 offset 指定的字符开始。如果 length 被省略,展开为参数 start- 的子字符串 在由 offset 指定的字符处。长度和偏移量是 算术表达式(见下面的算术评估)。如果 offset 计算为小于零的数字,使用该值 作为参数值末尾的偏移量。算术 以 - 开头的表达式必须用空格分隔 与前面的:要区别于使用默认值 价值观扩张。如果长度计算结果小于 零,并且参数不是@,也不是索引或关联 数组,它被解释为从值末尾的偏移量 参数而不是字符数,以及扩展 sion 是两个偏移量之间的字符。如果参数是 @,结果是从 off 开始的长度位置参数 放。如果参数是由@ 或下标的索引数组名称 *,结果是以数组开头的长度成员 $参数[偏移]。相对于 比指定数组的最大索引大一。子 应用于关联数组的字符串扩展会产生不正确的 罚款结果。请注意,必须将负偏移量分开 与冒号相距至少一个空格以避免混淆 使用 :- 扩展。子字符串索引是从零开始的,除非 使用位置参数,在这种情况下,索引 默认从 1 开始。如果偏移量为 0,则位置 使用参数,$0 是列表的前缀。

【讨论】:

一个非常重要的关于负值的警告,如上所述:以 - 开头的算术表达式必须用空格与前面的 : 分隔,以区别于使用默认值扩展。 所以要获取 var 的最后四个字符:$var: -4【参考方案7】:

我很惊讶这个纯 bash 解决方案没有出现:

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

您可能希望将 IFS 重置为之前的值,或者之后将 unset IFS 重置!

【讨论】:

它不是纯 bash 解决方案,我认为它可以在纯 shell (/bin/sh) 中工作 +1 您可以用另一种方式编写此代码以避免取消设置 IFS 和位置参数:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs" 这取决于路径名扩展! (所以它坏了)。【参考方案8】:

基于 jor 的回答(这对我不起作用):

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

【讨论】:

当你有一些复杂的事情并且简单地计算下划线不会cut它时,正则表达式是真正的交易。 嗨,为什么不用[[:digit:]]* 而不是[^_]* @YoavKlein [[:digit:]] 对于簿记而言无疑是更好的选择。【参考方案9】:

如果我们专注于以下概念: “一连串(一个或几个)数字”

我们可以使用几个外部工具来提取数字。 我们可以很容易地删除所有其他字符,无论是 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 *' 将导致您的输出包含文件名中的数字。【参考方案10】:

遵守要求

我有一个包含 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 '(?<=_)\d5' 
12345

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

【讨论】:

我相信现在没有必要使用 egrep,命令本身会警告你:Invocation as 'egrep' is deprecated; use 'grep -E' instead。我已经编辑了你的答案。【参考方案11】:

无需任何子流程即可:

shopt -s extglob
front=$input%%_+([a-zA-Z]).*
digits=$front##+([a-zA-Z])_

一个非常小的变体也可以在 ksh93 中工作。

【讨论】:

【参考方案12】:

这里有一个前缀-后缀解决方案(类似于JB和Darron给出的解决方案),匹配第一个数字块,不依赖于周围的下划线:

str='someletters_12345_morele34ters.ext'
s1="$str#"$str%%[[:digit:]]*""   # strip off non-digit prefix from str
s2="$s1%%[^[:digit:]]*"            # strip off non-digit suffix from s1
echo "$s2"                           # 12345

【讨论】:

【参考方案13】:

我的回答将更好地控制你想要从你的字符串中得到什么。这是有关如何从字符串中提取12345 的代码

str="someletters_12345_moreleters.ext"
str=$str#*_
str=$str%_more*
echo $str

如果您想提取包含abc 等任何字符或_- 等任何特殊字符的内容,这将更有效。例如:如果您的字符串是这样的,并且您想要 someletters__moreleters.ext 之前的所有内容:

str="someletters_123-45-24a&13b-1_moreleters.ext"

使用我的代码,您可以说出您想要什么。 说明:

#* 它将删除前面的字符串,包括匹配的键。这里我们提到的关键是_ % 它将删除以下包含匹配键的字符串。这里我们提到的关键是'_more*'

自己做一些实验,你会发现这很有趣。

【讨论】:

echo $var 更改为echo "$var",否则var=' * 12345 *' 将导致您的输出包含文件名中的数字。【参考方案14】:

我喜欢sed 处理正则表达式组的能力:

> var="someletters_12345_moreletters.ext"
> digits=$( echo "$var" | sed "s/.*_\([0-9]\+\).*/\1/p" -n )
> echo $digits
12345

一个稍微更通用的选项是假设您有一个下划线_ 标记您的数字序列的开始,因此例如剥离您在序列之前获得的所有非数字:s/[^0-9]\+\([0-9]\+\).*/\1/p.


> man sed | grep s/regexp/replacement -A 2
s/regexp/replacement/
    Attempt to match regexp against the pattern space.  If successful, replace that portion matched with replacement.  The replacement may contain the special  character  &  to
    refer to that portion of the pattern space which matched, and the special escapes \1 through \9 to refer to the corresponding matching sub-expressions in the regexp.

如果您对正则表达式不太自信,请对此进行详细说明:

s 代表_s_ubstitute [0-9]+ 匹配 1+ 个数字 \1 链接到正则表达式输出的第 n.1 组(第 0 组是整个匹配项,在这种情况下第 1 组是括号内的匹配项) p 标志用于 _p_rinting

所有转义\ 都是为了使sed 的正则表达式处理工作。

【讨论】:

echo $var 更改为echo "$var",否则var=' * 12345 *' 将导致您的输出包含文件名中的数字。【参考方案15】:

假设 test.txt 是一个包含“ABCDEFGHIJKLMNOPQRSTUVWXYZ”的文件

cut -b19-20 test.txt > test1.txt # This will extract chars 19 & 20 "ST" 
while read -r; do;
> x=$REPLY
> done < test1.txt
echo $x
ST

【讨论】:

这对于特定的输入是极其特殊的。一般问题(OP应该问)的唯一一般解决方案是use a regexp。【参考方案16】:

shell cut - 从字符串中打印特定范围的字符或给定部分

#method1) 使用 bash

 str=2020-08-08T07:40:00.000Z
 echo $str:11:8

#method2) 使用剪切

 str=2020-08-08T07:40:00.000Z
 cut -c12-19 <<< $str

#method3) 使用 awk 时

 str=2020-08-08T07:40:00.000Z
 awk 'time=gensub(/.11(.8).*/,"\\1","g",$1); print time' <<< $str

【讨论】:

【参考方案17】:

类似于php中的substr('abcdefg', 2-1, 3):

echo 'abcdefg'|tail -c +2|head -c 3

【讨论】:

这对那个输入来说是非常特殊的。一般问题(OP应该问)的唯一一般解决方案是use a regexp。【参考方案18】:

好的,这里是带有空字符串的纯参数替换。需要注意的是,我已将 somelettersmoreletters 定义为仅字符。如果它们是字母数字,这将无法正常工作。

filename=someletters_12345_moreletters.ext
substring=$filename//@(+([a-z])_|_+([a-z]).*)
echo $substring
12345

【讨论】:

很棒,但至少需要 bash v4 echo "$substring",或者如果有人有IFS=12345,则输出将完全为空。【参考方案19】:

还有 bash 内置的“expr”命令:

INPUT="someletters_12345_moreleters.ext"  
SUBSTRING=`expr match "$INPUT" '.*_\([[:digit:]]*\)_.*' `  
echo $SUBSTRING

【讨论】:

expr 不是内置的。 鉴于[[ 支持的=~ 运算符也没有必要。【参考方案20】:

bash 解决方案:

IFS="_" read -r x digs x <<<'someletters_12345_moreleters.ext'

这将破坏一个名为 x 的变量。 var x 可以更改为 var _

input='someletters_12345_moreleters.ext'
IFS="_" read -r _ digs _ <<<"$input"

【讨论】:

【参考方案21】:

Inklusive 端,类似于 JS 和 Java 的实现。如果您不希望这样做,请删除 +1。

function substring() 
    local str="$1" start="$2" end="$3"
    
    if [[ "$start" == "" ]]; then start="0"; fi
    if [[ "$end"   == "" ]]; then end="$#str"; fi
    
    local length="(($end-$start+1))"
    
    echo "$str:$start:$length"
 

例子:

    substring 01234 0
    01234
    substring 012345 0
    012345
    substring 012345 0 0
    0
    substring 012345 1 1
    1
    substring 012345 1 2
    12
    substring 012345 0 1
    01
    substring 012345 0 2
    012
    substring 012345 0 3
    0123
    substring 012345 0 4
    01234
    substring 012345 0 5
    012345

更多示例调用:

    substring 012345 0
    012345
    substring 012345 1
    12345
    substring 012345 2
    2345
    substring 012345 3
    345
    substring 012345 4
    45
    substring 012345 5
    5
    substring 012345 6
    
    substring 012345 3 5
    345
    substring 012345 3 4
    34
    substring 012345 2 4
    234
    substring 012345 1 3
    123

【讨论】:

function funcname() 以与旧版 ksh​​ 和 POSIX sh 不兼容的方式合并旧版 ksh​​ 语法 function funcname 和 POSIX sh 语法 funcname() 。见wiki.bash-hackers.org/scripting/obsolete【参考方案22】:

也许这可以帮助您获得所需的输出

代码:

your_number=$(echo "someletters_12345_moreleters.ext" | grep -E -o '[0-9]5')
echo $your_number

输出:

12345

【讨论】:

【参考方案23】:

有点晚了,但我刚刚遇到这个问题,发现如下:

host:/tmp$ asd=someletters_12345_moreleters.ext 
host:/tmp$ echo `expr $asd : '.*_\(.*\)_'`
12345
host:/tmp$ 

我用它在没有 %N 日期的嵌入式系统上获得毫秒分辨率:

set `grep "now at" /proc/timer_list`
nano=$3
fraction=`expr $nano : '.*\(...\)......'`
$debug nano is $nano, fraction is $fraction

【讨论】:

expr 是 1970 年代的产物;作为需要作为子进程分叉的外部命令,与现代 shell 内置程序相比,它的效率非常低。【参考方案24】:

这是一个 substring.sh 文件

用法

`substring.sh $TEXT 2 3` # characters 2-3

`substring.sh $TEXT 2` # characters 2 and after 

substring.sh 遵循这一行

#echo "starting substring"
chars=$1
start=$(($2))
end=$3

i=0
o=""
if [[ -z $end ]]; then
  end=`echo "$chars " | wc -c`
else
  end=$((end))
fi
#echo "length is " $e
a=`echo $chars | sed  's/\(.\)/\1 /g'`
#echo "a is " $a
for c in $a
do
  #echo "substring" $i $e $c
  if [[ i -lt $start ]]; then
    : # DO Nothing
  elif [[ i -gt $end ]]; then
    break;
  else
    o="$o$c"
  fi
  i=$(($i+1))
done
#echo substring returning $o
echo $o

【讨论】:

您使用旧的反引号命令替换是否有原因?它产生了一些现代$() 语法没有的相当讨厌的错误(特别是关于反斜杠在反引号中的解释方式)。 (除此之外,当 bash 具有内置的 $varname:start:length 功能时,为什么有人会这样做,哪些预先存在的答案已经显示了如何使用?) ...这里还有 shellcheck.net 将标记的错误。大量未加引号的扩展(这会将输入中的 * 更改为文件名列表)等。

以上是关于在 Bash 中提取子字符串的主要内容,如果未能解决你的问题,请参考以下文章

如何在Bash中提取多个唯一子字符串

在普通bash中使用正则表达式提取子字符串

在 Bash 中提取子字符串

有没有办法在bash中的特定子字符串之后提取子字符串?

[在python中使用正则表达式搜索字符串子字符串

如何更改python字符串子字符串信息