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

Trying to extract a substring and version number from a filename using bash

提问人:cybersnow1989 提问时间:11/11/2023 最后编辑:TylerHcybersnow1989 更新时间:11/23/2023 访问量:175

问:

我目前正在尝试使用 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"

然而,事实证明这很棘手,因为我似乎找不到一种适用于这两种情况的好方法。对于版本包含分支名称的场景,我无法事先知道分支名称将包含多少个单词。

我认为可变扩展可能是要走的路,但无法让它输出我想要的东西。

bash perl awk sed

评论

0赞 Barmar 11/11/2023
而不是 ,用于匹配数字。这样你就不需要担心它后面是什么了。(.*)([0-9.]*)
0赞 Barmar 11/11/2023
顺便说一句,您可以使用扩展正则表达式,而不必转义它。sed -r
0赞 markp-fuso 11/11/2023
你能同时出现两种具有相同前缀的格式吗?如果答案是“是”,那么建议的关联数组赋值将导致单个数组条目(即,第 2 个赋值将覆盖第 1 个赋值),在这种情况下,您需要决定如何存储这两种格式
0赞 markp-fuso 11/11/2023
一个文件可以有多个文件扩展名,例如,而不是你能有吗?*.tgz*.tar.gz
0赞 Ed Morton 11/11/2023
没有理由分支名称不能在分支名称部分包含字符串,例如 因此,您应该在示例输入/输出中至少包含其中之一,因为这很容易在潜在解决方案中出错。您可能还应该想出其他未雨绸缪的情况。-<digits>example-substring-1.1.0-branch-1.2.3.tgz

答:

3赞 Carson 11/11/2023 #1

如果您愿意使用 而不是 ,那么 lookaheads 和 lookbehinds 将允许您定义模式来提取您关心的内容。grepsed

考虑以下模式:这将匹配后面跟着 的任何内容。 标记条件展望,该表达式必须与下一个字符匹配,但被排除在模式的最终匹配之外。 与您的示例一起使用时:.+(?=-\d+\.\d+\.\d+)-<numbers>.<numbers>.<numbers>?=

$ 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

(该标志启用 PCRE2,并且该标志仅打印匹配项)Po

还要考虑模式:它使用后视来断言,在模式之前,有一个 ,并使用前瞻来断言模式以 结尾。 与您的示例一起使用时:(?<=-)\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赞 stevesliva 11/11/2023
我正要开始写一个答案。这正是应该用于提取复杂子字符串 IMO 的内容。可能已经着陆了,但你已经完成了让后视/前视工作的艰苦工作。grep -Po'(?<=-)[\d.]+-.*(?=\.tgz$)'
0赞 Ed Morton 11/13/2023
@stevesliva关于“它正是应该用来提取复杂子字符串的东西”——如果你用于这样的任务,你需要输出多个子字符串,那么你需要多次调用它,如果你需要将不可移植的 GNU 用于 PCRE,那么你也可以直接使用,因为它可以说比 GNU grep 更有可能存在于任何给定的系统上,然后你有 PCRE,而没有需要多次调用该命令。到目前为止,我个人还没有真正遇到过用途,因为您可以使用 sed、awk、bash 或 perl 做任何您需要的事情。grepgrep -Pperlgrep -P
0赞 stevesliva 11/13/2023
@EdMorton 与 .我知道这是可能的,但它更难以理解。(或者,是 prob 更笨拙,更少 sedish)perl -lne 's/regex/print $&/e'grep -Po 'regex'perl -lne 'print $1 if /regex/'
1赞 Ed Morton 11/13/2023
@stevesliva但还不够,因为 OP 需要生成 2 个匹配的字符串,因此它们需要或任何 perl 语法来打印 2 个捕获组。如上面的答案所示,您需要在同一字符串上调用两次才能获得 2 个捕获组输出,这不太理想,使用其他工具也不需要。grep -Po regexperl -lne 's/(regex1).*(regex2)/print $1\n$2/'grep -Po
5赞 Ed Morton 11/11/2023 #2

为了能够真正测试这一点,我们需要包含更多有问题的情况的示例输入,例如,一个看起来像分支名称中出现的版本号的字符串:-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"
2赞 potong 11/11/2023 #3

这可能对你有用 (GNU sed):

sed -E 's/^([^-]+-)+([0-9.]+).*\..*/\2/' file

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

0赞 pjh 11/12/2023 #4

也许可以只使用 Bash 的内置模式匹配来做你需要的事情。以下 Shellcheck-clean 代码演示了这个想法:

#! /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" )
3赞 zdim 11/12/2023 #5

使用 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 脚本中形成所需的结构。测试时没有分支名称,并且有输入字符串的其他一些变体。

由于 can 也可以包含数字(为什么不呢?),分支名称也可以包含 (why not?),因此正则表达式模式没有限制,并且前导部分和(可能的)尾随部分都简单地用 和 匹配。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代码是否需要注释。