【问题标题】:How to Extract Parts of String in Shell Script into Variables如何将Shell脚本中的部分字符串提取到变量中
【发布时间】:2015-10-20 23:31:43
【问题描述】:

我正在尝试在 sh 中执行以下操作。

这是我的文件:

foo
bar
Tests run: 729, Failures: 0, Errors: 253, Skipped: 0
baz

如何将 4 个数字提取到 4 个不同的变量中?我已经在 sed 和 awk 手册页上花费了大约一个小时,并且我正在旋转我的轮子。

【问题讨论】:

  • 文件的精确格式是什么?文件中有一个重要的行吗?数字总是按顺序排列的吗?
  • 文件将是可变的,但带有测试的行将始终存在;那是重要的行。该行中的数字将始终以该格式存在。
  • 使用具有内置正则表达式支持的功能更强大的 shell 而不是 /bin/sh 并且需要使用外部工具进行提取会更有效。我的意思是,是的,这可以在纯 POSIX sh 中完成,但是您将在 awk/sed/whatnot 的启动时间受到性能影响。

标签: regex shell sh


【解决方案1】:

采用我之前的回答来使用@chepner 建议的heredoc 方法:

read run failures errors skipped <<EOF
$(grep -E '^Tests run: ' <file.in | tr -d -C '[:digit:][:space:]')
EOF

echo "Tests run: $run"
echo "Failures: $failures"
echo "Errors: $errors"
echo "Skipped: $skipped"

或者(将其放入 shell 函数以避免在脚本执行期间覆盖“$@”):

unset IFS # assert default values
set -- $(grep -E '^Tests run: ' <in.file | tr -d -C '[:digit:][:space:]')
run=$1; failures=$2; errors=$3; skipped=$4

请注意,这只是安全的,因为当以这种方式运行时,tr 的输出中不会出现任何全局字符; set -- $(something) 通常最好避免这种做法。


现在,如果您编写的是 bash 而不是 POSIX sh,您可以在 shell 内部执行正则表达式匹配(假设在下面您的输入文件相对较短):

#!/bin/bash
re='Tests run: ([[:digit:]]+), Failures: ([[:digit:]]+), Errors: ([[:digit:]]+), Skipped: ([[:digit:]]+)'
while IFS= read -r line; do
  if [[ $line =~ $re ]]; then
    run=${BASH_REMATCH[1]}
    failed=${BASH_REMATCH[2]}
    errors=${BASH_REMATCH[3]}
    skipped=${BASH_REMATCH[4]}
  fi
done <file.in

如果您的输入文件短,则通过 grep 对其进行预过滤可能更有效,因此将最后一行更改为:

done < <(egrep -E '^Tests run: ' <file.in)

【讨论】:

    【解决方案2】:

    给定输入文件的格式,您可以将grep的输出捕获到一个here文档中,然后将其与read拆分成四个部分进行后处理。

    IFS=, read part1 part2 part3 part4 <<EOF
    $(grep '^Tests run' input.txt)
    EOF
    

    然后从每个部分中去掉不需要的前缀。

    run=${part1#*: }
    failures=${part2#*: }
    errors=${part3#*: }
    skipped=${part4#*: }
    

    【讨论】:

    • 不错。很不错。我需要开始更频繁地使用这种技术。
    【解决方案3】:

    假设您的文件中只有一行以Tests run: 开头,并且文件名为foo.txt,以下命令将创建4 个您可以使用的shell 变量:

    eval $(awk 'BEGIN{ FS="(: |,)" }; /^Tests run/{ print "TOTAL=" $2 "\nFAIL=" $4 "\nERROR=" $6 "\nSKIP=" $8 }' foo.txt); echo $TOTAL; echo $SKIP; echo $ERROR; echo $FAIL
    

    echo $TOTAL; echo $SKIP; echo $ERROR; echo $FAIL只是为了证明环境变量存在,可以使用。

    更易读的 awk 脚本是:

    BEGIN { FS = "(: |,)" }
    
    /^Tests run/ {
        print "TOTAL=" $2 "\nFAIL=" $4 "\nERROR=" $6 "\nSKIP=" $8
    }
    

    FS = "(: |,)" 告诉 awk 将“:”或“,”视为字段分隔符。

    然后eval 命令将作为命令读取 awk 脚本的结果并因此创建 4 个环境变量。


    注意:由于使用了eval,您必须信任foo.txt 文件的内容,因为您可能会伪造以Tests run: 开头的行,之后可能会有命令。

    您可以通过在 awk 脚本中使用更严格的正则表达式来改进这一点:/^Tests run: \d+, Failures: \d+, Errors: \d+, Skipped: \d+$/

    完整的命令将是:

    eval $(awk 'BEGIN{ FS="(: |,)" }; /^Tests run: \d+, Failures: \d+, Errors: \d+, Skipped: \d+$/{ print "TOTAL=" $2 "\nFAIL=" $4 "\nERROR=" $6 "\nSKIP=" $8 }' foo.txt); echo $TOTAL; echo $SKIP; echo $ERROR; echo $FAIL
    

    【讨论】:

    • 如果您要在此处使用eval,我建议(强烈!)仅过滤数值,因此攻击者无法将代码插入到您的存储库中放置Tests run: $(rm -rf /) -- 或者下载和运行 shellcode 的东西 -- 到你的测试套件的输出中。能够从将恶意代码检查到 git repo 到在实时基础架构中运行代码进行权限提升并不是一件好事。
    • 你是对的,eval 可能是邪恶的,我会用警告更新我的答案
    • (另一方面,全大写的变量名是不好的做法;参见pubs.opengroup.org/onlinepubs/009695399/basedefs/… 的第四段,记住shell 变量和环境变量共享一个命名空间)。
    【解决方案4】:

    有更短的版本,但这个版本“显示”了每个步骤。

    #!/bin/bash
    declare -a arr=`grep 'Tests ' a | awk -F',' '{print $1 "\n" $2 "\n" $3 "\n" $4}' | sed 's/ //g' | awk -F':' '{print $2}'`
    echo $arr
    for var in $arr
    do
        echo $var
    done
    

    【讨论】:

    • declare -a 在 POSIX sh 中不可用(因为一般不支持数组)。
    • ...另外,declare -a arr=$(...) 仅将任何值分配给 arr 的第一个元素;它需要declare -a arr=( $(...) ) 才能分配给多个元素(使用字符串拆分和全局扩展从从扩展接收到的单个字符串中获取这些元素——这很少是一种可取的做法);或者,在 bash 4.x 中,readarraymapfile 可用于直接填充数组。
    • 还有!不幸的是,我需要在 sh 中执行此操作
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-09
    • 2016-07-09
    • 2020-03-27
    • 2021-02-12
    • 1970-01-01
    • 1970-01-01
    • 2012-11-02
    相关资源
    最近更新 更多