在 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_jkl
或 1234567_12345_1234567
或 12345d_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,不涉及进程生成,因此速度非常快。
【讨论】:
@SpencerRathbunbash: $$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: filenameecho $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 中,prolystartIndx-$((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】:好的,这里是带有空字符串的纯参数替换。需要注意的是,我已将 someletters 和 moreletters 定义为仅字符。如果它们是字母数字,这将无法正常工作。
filename=someletters_12345_moreletters.ext
substring=$filename//@(+([a-z])_|_+([a-z]).*)
echo $substring
12345
【讨论】:
很棒,但至少需要 bash v4echo "$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 中提取子字符串的主要内容,如果未能解决你的问题,请参考以下文章