【问题标题】:Howto split a string on a multi-character delimiter in bash?如何在bash中的多字符分隔符上拆分字符串?
【发布时间】:2017-04-02 21:15:10
【问题描述】:

为什么下面的 bash 代码不起作用?

for i in $( echo "emmbbmmaaddsb" | split -t "mm"  )
do
    echo "$i"
done

预期输出:

e
bb
aaddsb

【问题讨论】:

  • ...嗯?这根本不是split 所做的。如,完全与其实际功能无关。
  • 知道如何在 bash 中的任意多字符分隔符上拆分任意字符串吗?如果这是您真正想知道的,为什么不编辑您的问题来提出这个问题?
  • split 将一个文件拆分为一堆较小的文件。不像您的脚本所期望的那样写入标准输出的名称,而是实际文件。 -t 提供了一个字符,用于确定记录的开始和结束位置,从而在记录边界上进行分割。
  • 当然不是,因为您希望将名称写入标准输出。我已经告诉过你它不会将名称写入标准输出。
  • 如果没有任何内容写入标准输出,则命令替换不会捕获任何内容。

标签: bash shell ubuntu gnu-coreutils


【解决方案1】:

由于您需要换行符,您可以简单地将字符串中的所有 mm 实例替换为换行符。在纯原生 bash 中:

in='emmbbmmaaddsb'
sep='mm'
printf '%s\n' "${in//$sep/$'\n'}"

如果您想对较长的输入流进行这种替换,最好使用awk,因为 bash 的内置字符串操作不能很好地扩展到超过几千字节的内容。 BashFAQ #21 中给出的gsub_literal shell 函数(后端为awk)适用:

# Taken from http://mywiki.wooledge.org/BashFAQ/021

# usage: gsub_literal STR REP
# replaces all instances of STR with REP. reads from stdin and writes to stdout.
gsub_literal() {
  # STR cannot be empty
  [[ $1 ]] || return

  # string manip needed to escape '\'s, so awk doesn't expand '\n' and such
  awk -v str="${1//\\/\\\\}" -v rep="${2//\\/\\\\}" '
    # get the length of the search string
    BEGIN {
      len = length(str);
    }

    {
      # empty the output string
      out = "";

      # continue looping while the search string is in the line
      while (i = index($0, str)) {
        # append everything up to the search string, and the replacement string
        out = out substr($0, 1, i-1) rep;

        # remove everything up to and including the first instance of the
        # search string from the line
        $0 = substr($0, i + len);
      }

      # append whatever is left
      out = out $0;

      print out;
    }
  '
}

...在此上下文中用作:

gsub_literal "mm" $'\n' <your-input-file.txt >your-output-file.txt

【讨论】:

    【解决方案2】:

    推荐的字符替换工具是sed 的命令s/regexp/replacement/ 用于一次正则表达式或全局s/regexp/replacement/g,你甚至不需要循环或变量。

    管道echo 输出并尝试用换行符\n 替换字符mm

    echo "emmbbmmaaddsb" | sed 's/mm/\n/g'

    输出是:

    e
    bb
    aaddsb
    

    【讨论】:

    • “推荐”?有关在 bash 中进行字符串操作的最佳实践指南,请参阅 BashFAQ #100。您会注意到,参数扩展通常被认为是短输入的最佳实践方法(而echo | sed 方法虽然简洁,但在如何在后台实现方面有很大的开销——需要,通常,两个 fork、一个 mkfifo、一个需要链接和加载的外部工具的 execv 等)。
    • ...例如,如果您在一个紧密的循环中逐行处理输入(或迭代具有数百或数千个文件名的全局结果),则为每一行调用 echo | sed 绝对是一种反模式。 (相比之下,调用sed仅一次来处理整个传入流通常是合适的)。
    【解决方案3】:

    下面给出了一个更一般的示例,而不用单字符分隔符替换多字符分隔符:

    使用参数扩展:(来自@gniourf_gniourf 的评论)

    #!/bin/bash
    
    str="LearnABCtoABCSplitABCaABCString"
    delimiter=ABC
    s=$str$delimiter
    array=();
    while [[ $s ]]; do
        array+=( "${s%%"$delimiter"*}" );
        s=${s#*"$delimiter"};
    done;
    declare -p array
    

    一种更粗暴的方式

    #!/bin/bash
    
    # main string
    str="LearnABCtoABCSplitABCaABCString"
    
    # delimiter string
    delimiter="ABC"
    
    #length of main string
    strLen=${#str}
    #length of delimiter string
    dLen=${#delimiter}
    
    #iterator for length of string
    i=0
    #length tracker for ongoing substring
    wordLen=0
    #starting position for ongoing substring
    strP=0
    
    array=()
    while [ $i -lt $strLen ]; do
        if [ $delimiter == ${str:$i:$dLen} ]; then
            array+=(${str:strP:$wordLen})
            strP=$(( i + dLen ))
            wordLen=0
            i=$(( i + dLen ))
        fi
        i=$(( i + 1 ))
        wordLen=$(( wordLen + 1 ))
    done
    array+=(${str:strP:$wordLen})
    
    declare -p array
    

    参考 - Bash Tutorial - Bash Split String

    【讨论】:

    • 这已损坏(如果字符串包含全局字符或空格等,则会失败)。此外,您没有使用现代 Bash 习语,这使得代码看起来非常奇怪。您只需要一个简单的循环:str="LearnABCtoABCSplitABCaABCString" delimiter=ABC s=$str$delimiter array=(); while [[ $s ]]; do array+=( "${s%%"$delimiter"*}" ); s=${s#*"$delimiter"}; done; declare -p array。就是这样。
    • 感谢@gniourf_gniourf 的评论。我刚刚开始使用 Bash 脚本,您的建议对以惯用的方式思考非常有帮助。
    • 感谢 @MallikarjunM 发布您的解决方案(来自 Bash 新手)。它帮助我解决了将字符串解析为具有多字符分隔符的数组的问题,其中 IFS / 读取数组不适合。
    • @gniourf_gniourf 对于str="Nope:" delimiter="::",您的“简单循环”失败
    • @gniourf_gniourf 这应该可以工作:s="a::b:" delimiter="::" array=(); while [[ $s ]]; do array+=( "${s%%"$delimiter"*}" ); c="${array[@]: -1}"; s="${s:${#c}}"; [[ $s != "$delimiter" ]] || { array+=(""); break; }; s="${s#"$delimiter"}"; done; declare -p array
    【解决方案4】:

    使用 awk,您可以使用 gsub 替换所有正则表达式匹配项。

    正如您的问题,要将两个或多个“m”字符的所有子字符串替换为新行,请运行:

    echo "emmbbmmaaddsb" | awk '{ gsub(/mm+/, "\n" ); print; }'
    

    e

    bb

    aaddsb

    gsub() 中的“g”代表“全局”,意思是到处替换。

    您也可以要求只打印 N 匹配项,例如:

    echo "emmbbmmaaddsb" | awk '{ gsub(/mm+/, " " ); print $2; }'
    

    bb

    【讨论】:

      猜你喜欢
      • 2010-10-29
      • 1970-01-01
      • 1970-01-01
      • 2019-08-21
      • 1970-01-01
      • 2013-03-03
      • 1970-01-01
      • 2011-10-03
      • 1970-01-01
      相关资源
      最近更新 更多