在Bash中提取子字符串


727

给定格式的文件名someletters_12345_moreleters.ext,我想提取5位数字并将其放入变量中。

因此,为了强调这一点,我有一个文件名,其中包含x个字符,然后是一个五位数的序列,该序列由两侧的单个下划线包围,然后是另一组x个字符。我想使用5位数字并将其放入变量中。

我对实现此目标的许多不同方式非常感兴趣。


5
JB的答案显然是赢得选票-是时候更改接受的答案了吗?
杰夫

3
大多数答案似乎都无法回答您的问题,因为该问题模棱两可。“我的文件名中包含x个字符,然后是一个五位数的序列,在两侧用单个下划线包围,然后是另一组x个字符。” 根据该定义,abc_12345_def_67890_ghi_def是有效输入。你想发生什么?假设只有一个5位数字序列。根据您对输入的定义,您仍然具有abc_def_12345_ghi_jkl1234567_12345_123456712345d_12345_12345e作为有效输入,并且以下大多数答案将无法解决该问题。
gman

2
这个问题的示例输入过于具体。因此,对于这种特殊情况(只有数字,相同的_分隔符,仅包含一次目标字符串的输入等),它有很多具体的答案。在最佳(最普通和最快)的答案了,10年后,只有7 upvotes,而其他有限的答案有数百个。让我失去了开发商😞信仰
达恩·达斯卡莱斯卡

Answers:


691

使用切割

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

更通用:

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

1
更通用的答案正是我想要的答案,谢谢
Berek Bryan 2009年

71
-f标志采用从1开始的索引,而不是程序员习惯的从0开始的索引。
马修·G

2
INPUT = someletters_12345_moreleters.ext SUBSTRING = $(echo $ INPUT | cut -d'_'-f 2)echo $ SUBSTRING
mani deepak 2014/3/24

3
echo除非您确定变量不能包含不规则的空格或外壳元字符,否则应在参数周围使用双引号。详情请参阅stackoverflow.com/questions/10067266/…–
Tripleee,

'-f'之后的数字'2'告诉shell提取第二组子字符串。
Sandun '18

1085

如果x为常数,则以下参数扩展将执行子字符串提取:

b=${a:12:5}

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

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

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

如果还有其他下划线,尽管比较棘手,但这仍然是可行的。如果有人知道如何在一个表达式中执行两个扩展,我也想知道。

提出的两种解决方案都是纯bash,不涉及任何流程生成,因此非常快。


18
@SpencerRathbun bash: ${${a#*_}%_*}: bad substitution在我的GNU的bash 4.2.45。
JB。

2
@jonnyB,过去的某个时间行得通。我的同事告诉我它停止了,他们将其更改为sed命令或其他命令。回顾历史,我正在sh脚本中运行它,可能有点破破烂烂。在这一点上,我不能让它工作了。
Spencer Rathbun 2013年

22
JB,您应该澄清“ 12”是偏移量(从零开始),“ 5”是长度。另外,为@gontard的链接+1做好一切准备!
Doktor J 2014年

1
在“ sh run.sh”脚本中运行此脚本时,可能会出现Bad Substitution错误。为了避免这种情况,请更改run.sh(chmod + x run.sh)的权限,然后以“ ./run.sh”身份运行脚本
Ankur,2015年

2
偏移量参数也可以为负,顺便说一句。您只需要注意不要将其粘在结肠上,否则bash会将其解释为:-“使用默认值”的替代。因此${a: -12:5},从末尾产生5个字符,从12个字符开始,${a: -12:-5}在end-12和end-5之间产生7个字符。
JB。

96

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

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
如果我想从文件的最后一行提取数字/单词怎么办。
撒哈拉(Sahra)

92

只是尝试使用 cut -c startIndx-stopIndx


2
是否有类似startIndex-lastIndex-1的东西?
Niklas 2015年

1
@Niklas在bash中,亲startIndx-$((lastIndx-1))
布朗

3
start=5;stop=9; echo "the rain in spain" | cut -c $start-$(($stop-1))
brown.2179

1
问题是输入是动态的,因为我也使用管道来获取它,所以基本上是这样。git log --oneline | head -1 | cut -c 9-(end -1)
尼克拉斯,2015年

如果分成line=git log --oneline | 分为两部分,可以通过cut来完成。head -1` && echo $ line | cut -c 9-$((($ { git log --oneline | head -1 | sed -e 's/^[a-z0-9]* //g'
#line

34

如果有人需要更严格的信息,您也可以像这样在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}
              子串扩展。扩展到最大长度为
              参数从offset指定的字符开始。如果
              长度被省略,扩展为参数start-的子字符串
              按offset指定的字符。长度和偏移量是
              算术表达式(请参阅下面的算术评估)。如果
              offset的数值小于零,则使用该值
              作为距参数值末尾的偏移量。算术
              以-开头的表达式必须用空格分隔
              与上一个:与使用默认值区分开
              价值扩展。如果长度的计算结果小于
              零,并且参数不是@并且不是索引或关联的
              数组,它被解释为距值结尾的偏移量
              参数而不是多个字符,然后展开
              sion是两个偏移量之间的字符。如果参数是
              @,结果是从off开始的长度位置参数
              组。如果parameter是带下标@或下标的索引数组名
              *,结果是数组的长度成员,以
              $ {parameter [offset]}。相对于负偏移量
              比指定数组的最大索引大1。子
              应用于关联数组的字符串扩展会产生不确定的
              罚款结果。请注意,负偏移必须分开
              远离结肠至少一个空间以避免混淆
              与:-扩展。子字符串索引从零开始,除非
              使用位置参数,在这种情况下,索引
              默认情况下从1开始。如果offset为0,则位置
              使用参数时,$ 0将作为列表的前缀。

2
如上所述,带有负值的一个非常重要的警告:以-开头的算术表达式必须与前面的空格分隔:才能与“使用默认值”扩展区分开。因此,要获取var的最后四个字符:${var: -4}
sshow

26

这是我的做法:

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

说明:

重击特定:

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

  • _ 是要为匹配的字符串划定/锚定匹配边界的文字
  • () 创建一个捕获组
  • [[:digit:]] 是一个角色类,我认为这说明了一切
  • {5} 表示恰好五个先前字符,类(如本例中所示)或组必须匹配

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


3
即使您需要提取多个内容,这也是一种通用方法,就像我所做的那样。
zebediah49

3
确实,这是最通用的答案,应该接受。它适用于正则表达式,不仅适用于固定位置的字符串或相同定界符(启用cut)之间的字符串。它还不依赖于执行外部命令。
丹·达斯卡斯库

1
这个答案被刑事起诉。
chepner

这很棒!我对此进行了修改,以针对我的情况使用不同的开始/停止距离(替换_)和可变长度的数字(对于{5}使用。)。有人可以分解这个黑魔法并解释它吗?
保罗

1
@Paul我在回答中添加了更多详细信息。希望能有所帮助。
nicerobot

21

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

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

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


1
它不是纯bash解决方案,我认为它可以在纯shell(/ bin / sh)中工作
kayn 2014年

5
+1您可以编写另一种方法来避免必须取消设置IFS和位置参数:IFS=_ read -r _ digs _ <<< "$a"; echo "$digs"
kojiro

2
这取决于路径名的扩展!(所以坏了)。
gniourf_gniourf 2015年

20

建立在jor的答案上(这对我不起作用):

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

12
当您遇到一些复杂的事情而仅计算下划线就不会时,正则表达式才是真正的选择cut
2011年

12

遵循要求

我有一个带有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)语法即可。


2
我相信,如今无需使用egrep,该命令本身会警告您:Invocation as 'egrep' is deprecated; use 'grep -E' instead。我已经编辑了您的答案。
神经递质

11

如果我们关注以下概念:
“一个(一个或多个)数字”

我们可以使用几种外部工具来提取数字。
我们可以很容易地擦除sed或tr的所有其他字符:

name='someletters_12345_moreleters.ext'

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

但是,如果$ 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中对大文件进行所有处理快。


10

没有任何子流程,您可以:

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

很小的变体也可以在ksh93中使用。


9

这是一个前缀后缀解决方案(类似于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

7

我喜欢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

所有转义符号\都可以使sedregexp处理工作。


6

我的答案将对您要从字符串中得到的内容进行更多控制。这是有关如何提取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 *”

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


6

给定的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应该提出)的唯一通用解决方案是使用regexp
Dan Dascalescu

3

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

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

2
很棒,但至少需要bash v4
olibre 2015年

2

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

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

这是特定于该输入的。通用问题(OP应该提出)的唯一通用解决方案是使用regexp
Dan Dascalescu,

1

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

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

4
expr不是内置的。
gniourf_gniourf

1
鉴于所=~支持的运算符,也没有必要[[
chepner

1

有点晚了,但我遇到了这个问题,发现了以下内容:

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

1

一个bash解决方案:

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

这将破坏名为的变量xx可以将var 更改为var _

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

1

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

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

别客气。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.