尝试使用bash从文件名中提取子字符串和版本号

7
我目前正在尝试使用bash从文件名中提取子字符串和版本号。
文件名有两种格式:
example-substring-1.1.0.tgz
example-substring-1.1.0-branch-name.tgz

对于第一个场景,我能够使用sed提取版本号,像这样:
echo example-substring-1.1.0.tgz | sed "s/.*-\(.*\)\.[a-zA-Z0-9]\{3\}$/\1/"

然而,这对于第二种情况是行不通的。
最终,我希望创建一个脚本,将第一个子字符串和版本存储在一个类似下面的关联数组中。
example_array["example-substring"]="1.1.0"
example_array["example-substring"]="1.1.0-branch-name"

这证明是棘手的,因为我似乎找不到适用于两种情况的好方法。而对于包含分支名称的情况,我事先无法知道分支名称将包含多少个单词。
我认为变量扩展可能是解决的方法,但是我无法得到我想要的输出。

使用([0-9.]*)来匹配数字,而不是使用(.*)。这样你就不需要担心它后面的内容了。 - undefined
顺便说一句,你可以使用sed -r来使用扩展正则表达式,而无需太多转义。 - undefined
你能同时使用相同前缀的两种格式吗?如果答案是“是”,那么提议的关联数组赋值将导致一个数组条目(即,第二个赋值将覆盖第一个赋值),在这种情况下,你需要决定如何存储这两种格式。 - undefined
一个文件可以有多个文件扩展名吗?例如,可以使用*.tar.gz代替*.tgz吗? - undefined
分支名称中可以包含带有“-<数字>”的字符串,例如example-substring-1.1.0-branch-1.2.3.tgz,因此您应该在示例输入/输出中包含至少一个这样的字符串,因为在潜在的解决方案中,这可能是一个容易出错的匹配。您还应该考虑其他可能出现的特殊情况。 - undefined
如果 example-substring 也可能包含 -<数字>,那么你也需要包含那种情况的示例,并解释如何区分它与实际的版本号。 - undefined
5个回答

6
为了真正测试这个,我们需要包含更多问题情况的样本输入,例如一个字符串-1.2.3,它看起来像是分支名称中的版本号。
$ cat file
example-substring-foo-1.1.0.tgz
example-substring-bar-1.1.0-branch-name.tgz
example-substring-rainy-1.1.0-branch-1.2.3.tgz

通常我会在sed或awk中进行模式匹配部分,例如使用任何awk:
$ awk 'match($0,/-([0-9].*)\.[^.]+$/) {
    printf "\"%s\" \"%s\"\n", substr($0,1,RSTART-1), substr($0,RSTART+1)
}' file
"example-substring-foo" "1.1.0.tgz"
"example-substring-bar" "1.1.0-branch-name.tgz"
"example-substring-rainy" "1.1.0-branch-1.2.3.tgz"

不过既然你无论如何都想要用shell数组来填充结果,那不如使用一个shell循环。
$ cat tst.sh
#!/usr/bin/env bash

declare -A example_array

while IFS= read -r ver; do
    if [[ $ver =~ -([0-9].*)\.[^.]+$ ]]; then
        example_array["${ver::-${#BASH_REMATCH[0]}}"]="${BASH_REMATCH[1]}"
    fi
done < "$@"

for idx in "${!example_array[@]}"; do
    printf 'example_array["%s"]="%s"\n' "$idx" "${example_array[$idx]}"
done

$ ./tst.sh file
example_array["example-substring-rainy"]="1.1.0-branch-1.2.3"
example_array["example-substring-bar"]="1.1.0-branch-name"
example_array["example-substring-foo"]="1.1.0"


3
如果你愿意使用grep而不是sed,那么前瞻和后顾将允许你定义模式来提取你关心的内容。
考虑以下模式:.+(?=-\d+\.\d+\.\d+) 这将匹配任何后面跟着-<数字>.<数字>.<数字>的内容。?=表示条件前瞻,它是一个必须匹配下一个字符的表达式,但不包括在模式的最终匹配中。 当与你的示例一起使用时:
$ echo example-substring-1.1.0.tgz | grep -Po '.+(?=-\d+\.\d+\.\d+)'
example-substring
$ echo example-substring-1.1.0-branch-name.tgz | grep -Po '.+(?=-\d+\.\d+\.\d+)'
example-substring

P标志启用PCRE2,o标志仅打印匹配项)
还要考虑模式:(?<=-)\d+\.\d+\.\d+.*(?=\.tgz$) 它使用向后查找来断言,在模式之前立即有一个-,并使用向前查找来断言模式以.tgz结尾。 当与您的示例一起使用时:
echo 'example-substring-1.1.0.tgz' | grep -Po '(?<=-)\d+\.\d+\.\d+.*(?=\.tgz$)'
1.1.0
$ echo 'example-substring-1.1.0-branch-name.tgz' | grep -Po '(?<=-)\d+\.\d+\.\d+.*(?=\.tgz$)'
1.1.0-branch-name

1
我刚要开始写一个 grep -Po 的答案。在我看来,这正是用来提取复杂子字符串的最佳选择。我可能会用到 '(?<=-)[\d.]+-.*(?=\.tgz$)',但你已经做了让后顾/前瞻正则表达式工作的辛苦努力。 - undefined
@stevesliva 关于“它正是用于提取复杂子字符串的最佳选择” - 如果你在这样的任务中使用 grep,需要输出多个子字符串,那么你需要多次调用它。如果你需要使用非便携式的 GNU grep -P 来处理 PCREs,那么你可能会考虑直接使用 perl,因为相比 GNU grep,perl 在任何给定的系统上更有可能存在,并且你可以使用 PCREs,而不需要多次调用命令。到目前为止,我个人还没有真正遇到过需要使用 grep -P 的情况,因为你可以使用 sed、awk、bash 或 perl 来完成你所需的任何操作。 - undefined
@EdMorton perl -lne 's/regex/print $&/e'grep -Po 'regex' 的区别在于,前者更加晦涩难懂。虽然可以实现相同的功能,但不容易理解。另外,perl -lne 'print $1 if /regex/' 更类似于 awk,而不太像 sed。 - undefined
1
@stevesliva 但是grep -Po regex并不足够,因为楼主需要找到2个匹配的字符串,所以他们需要使用perl -lne 's/(regex1).*(regex2)/print $1\n$2/'或者其他perl语法来打印2个捕获组。正如上面的答案所示,你需要在同一个字符串上两次调用grep -Po来获取2个捕获组的输出,这不是最理想的方法,也不是其他工具所必需的。 - undefined

3
使用Perl
echo "example-substring-1.1.0-branch-name.tgz" |
    perl -wne'print join " ", /(.+)\-([0-9]+\.[0-9]+\.[0-9]+.*)\.tgz/'

打印两个词
example-substring 1.1.0-branch-name

这样就返回到了shell脚本中,我猜这是从中调用的,然后可以在shell脚本中形成所需的结构。 也测试了没有分支名称以及输入字符串的其他几种变化。
由于example-substring也可以包含数字(为什么不呢?),分支名称也可以(为什么不呢?),所以正则表达式模式没有限制,前导部分和(可能的)尾部部分都可以通过.+.*简单匹配。
但是,对于版本号,我们需要更具体的内容,我假设它始终由用点分隔的三个数字组成。我还假设了字符串的固定部分,即文件扩展名.tgz。如果需要,这些可以稍作放宽。
† 可以直接将一个列表(键 值 键 值...)读入一个关联数组中。
#!/bin/bash

eval declare -A ver=( $( 
    echo "example-substring-1.1.0-branch-name.tgz" | 
    perl -wnE'say join " ", /(.+)\-([0-9]+\.[0-9]+\.[0-9]+.*)\.tgz/' ))

echo ${ver["example-substring"]}

或者将其分配给变量可能更合适
str="example-substring-1.1.0-branch-name.tgz"

read -r str val <<< $( 
perl -wE'say join " ", $ARGV[0] =~ /(.+)\-([0-9]+\.[0-9]+\.[0-9]+.+)\.tgz/' 
    -- "$str" )

ver[$str]=$val

甚至只需使用位置参数
set -- $(
    perl -wE'say join " ", $ARGV[0] =~ /(.+)\-([0-9]+\.[0-9]+\.[0-9]+.+)\.tgz/' 
        -- "$str" )

ver[$1]=$2

当然,还有其他方法可以向Perl脚本或命令行程序(“一行代码”)传递参数,以及在bash中获取其输出的其他方法。
如果这段Perl代码需要注释,请告诉我。

2
这可能适合你(GNU sed):
sed -E 's/^([^-]+-)+([0-9.]+).*\..*/\2/' file

匹配由一个或多个单词以-分隔,后跟由.分隔的数字,然后以.前缀结尾的扩展名的文件名,并返回由.分隔的数字。

0
可能可以只使用Bash的内置模式匹配来完成你所需的操作。这段Shellcheck清晰的代码展示了这个想法。
#! /bin/bash -p

shopt -s extglob

files=( example-substring-1.1.0.tgz example-substring2-1.1.0-branch-name.tgz )

declare -A example_array

for f in "${files[@]}"; do
    base=${f%.*}    # remove suffix
    substring=${base%%-+([0-9]).*}
    example_array["$substring"]=${base#"$substring-"}
done

declare -p example_array

这将输出:
declare -A example_array=([example-substring2]="1.1.0-branch-name" [example-substring]="1.1.0" )

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接