【问题标题】:Extract substring in Bash在 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 个屏幕;较旧且更精确的问题,标记为重复。 stackoverflow.com/questions/219402/…

标签: string bash shell substring


【解决方案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 元字符。进一步查看stackoverflow.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 的:

正则表达式 (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 '(?<=_)\d{5}' 
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 将标记的错误。大量未加引号的扩展(这会将输入中的 * 更改为文件名列表)等。
            猜你喜欢
            • 2015-04-26
            • 1970-01-01
            • 2023-03-17
            • 2012-09-24
            • 2019-06-22
            • 2021-01-21
            相关资源
            最近更新 更多