在 bash 中的字符串列表中动态提取每个字符串唯一的模式
Dynamically extract pattern unique to each string in a list of strings in bash
我正在尝试从 bash 中的文件名列表中动态提取独特的模式。
输入的文件名列表如下所示
Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt
我想动态提取字符串
Rep1,Rep2,Rep3
此处以图表形式显示:
注意:输入模式每次都可能改变
例如另一个用例可能是
Exp2_DT_10ng_55C_1_User1.png,Exp2_DT_10ng_55C_2_User1.png,Exp2_DT_10ng_55C_3_User1.png
在这种情况下,我想提取
1,2,3
此处以图表形式显示:
在 bash 中实现此目标的最佳方法是什么?
按照评论中的建议,我尝试了以下方法:
declare -p string1 string2
declare -- string1="ER_Rep1"
declare -- string2="ER_Rep2"
diff <(echo "$string1" ) <(echo "$string2")
returns
1c1
< ER_Rep1
---
> ER_Rep2
我要提取的是 Rep1,Rep2。
你可以考虑这个awk
解决方案:
declare -- string1="ER_Rep1"
declare -- string2="ER_Rep2"
awk -F '[_.]+' '{for (i=1; i<=NF; ++i) ++fq[$i]}
END {for (w in fq) if (fq[w] == 1) print w}' <(echo "$string1" ) <(echo "$string2")
Rep1
Rep2
此 awk
解决方案使用 _
或 .
作为字段分隔符,并将每个字段存储在关联数组 fq
中,值作为表示频率的数字那个词的出现。
在 END
块中,我们迭代 fq
数组中的每个词,如果频率等于 1
则打印该词,表明该词的唯一出现。
您可以将 GNU awk
与 sort
& uniq
结合使用
echo 'Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt' | awk -v RS='[_.,]' '1' | sort | uniq -u
或
tr
结合 sort
& uniq
echo 'Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt' | tr '_.,' '\n' | sort | uniq -u
产生输出
Rep1
Rep2
Rep3
我确信有更好的编码方法,但我在这里所做的是针对任意数量的输入字符串的通用解决方案。
找出下划线分隔子串的最长公共前缀
longestCommonPrefix() {
local i prefix file found
local -a pieces
IFS=_ read -ra pieces <<<""
for ((i = ${#pieces[@]} - 1; i > 0; i--)); do
prefix=$(IFS=_; echo "${pieces[*]:0:i}_")
found=true
for file in "${@:2}"; do
if [[ $file != "$prefix"* ]]; then
found=false
break
fi
done
if $found; then
echo "$prefix"
return
fi
done
}
找到最长的公共后缀(普通字符)
longestCommonSuffix() {
local i suffix file found
for ((i = ${#1}; i > 0; i--)); do
suffix=${1: -i}
found=true
for file in "${@:2}"; do
if [[ $file != *"$suffix" ]]; then
found=false
break
fi
done
if $found; then
echo "$suffix"
return
fi
done
}
把它们放在一起
uniqueStrings() {
local prefix=$(longestCommonPrefix "$@")
set -- "${@/#"$prefix"/}"
local suffix=$(longestCommonSuffix "$@")
printf '%s\n' "${@/%"$suffix"/}"
}
然后
$ uniqueStrings Exp1_ML_Rep1.txt Exp1_ML_Rep2.txt Exp1_ML_Rep3.txt
Rep1
Rep2
Rep3
和
$ uniqueStrings Exp2_DT_10ng_55C_1_User1.png Exp2_DT_10ng_55C_2_User1.png Exp2_DT_10ng_55C_3_User1.png
1
2
3
其他几个例子:
# nothing in common, should return the input strings
$ uniqueStrings foo bar baz
foo
bar
baz
$ uniqueStrings x_foo13 x_bar13 x_baz13 x_qux13
foo
bar
baz
qux
适用于 bash v3.2+
查看与@glennjackman 提出的解决方案类似的解决方案:
- 找到共同前缀
- 找到共同后缀
- 去掉共同点prefix/suffix剩下的就是不同点
假设:
- 文件名列表以逗号分隔的字符串形式提供
- 可变数量的文件名
- 逐个字符比较
- 无分隔符
- 假设单个 'difference' 由连续字符组成,例如,在比较
aBcDe
和 aXcYe
时,我们不认为 c
是常见的,因此差异将被报告为 BcD
和 XcY
一个使用 awk
的想法,它应该比 bash
级循环有一些性能改进:
awk '
# function to return an absolute value of a number
function abs(v) { return v < 0 ? -v : v }
# function to determine if each string has the same character at a given offset;
# return 0 if "no", return 1 if "yes"
function equal() {
for ( i=1; i<=n; i++ ) {
pos = offset <= 0 ? length(fname[i]) + offset : offset
x = substr(fname[i],pos,1)
if ( i == 1 ) curr = x
if ( x != curr ) return 0
}
return 1
}
# for now assume strings input using a here-string, and strings are delimited by a comma
FNR==1 { n=split([=10=],fname,",")
exit # skip to END processing
}
END {
# twice through the outer "for" loop:
# op = 1 => prefix processing
# op = -1 => suffix processing
# "op" will be used to increment/decrement our offset pointer to
# perform the character-by-character comparison
for ( op=1; op>=-1; op=op-2 ) {
offset = op == 1 ? 1 : 0 # determine initial offset based on op (prefix vs suffix)
# if all strings have the same character @ a given offset then update our pfx/sfx pointers
while ( equal() && abs(offset) <= length(fname[1]) ) {
if ( op == 1 ) pfx = offset
else sfx = offset
offset = offset + op # go to next offset
}
}
if ( pfx == "" ) pfx=0 # if no common prefix, default to 0
if ( sfx == "" ) sfx=1 # if no common suffix, default to 1
# use substr() and our pfx/sfx offsets to display the difference
for ( i=1; i<=n; i++ )
print substr(fname[i], pfx+1, length(fname[i]) - pfx - 1 + sfx )
}' <<< "${in}"
备注:
- 此时有点冗长;或许可以精简一点...
- 代码可以修改为直接与 'normal' 文件列表一起工作(例如,从
find
到 awk
的管道输出);一个想法是只处理第一条记录 (FNR==1
) 并将 FILENAME
填充到数组
测试结果:
# in='Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt'
1
2
3
# in='Exp2_DT_10ng_55C_1_User1.png,Exp2_DT_10ng_55C_2_User1.png,Exp2_DT_10ng_55C_3_User1.png'
1
2
3
# in='x_foo13,x_bar13,x_baz13,x_qux13'
foo
bar
baz
qux
# in='x_foo13,x_bar13,x_baz13,x_abcde23'
foo1
bar1
baz1
abcde2
# in='abcde.123,abcde.123,abcde.123' # identical
# three
# blank
# lines
# in='abc,def,123456,xyz$$' # nothing in common
abc
def
123456
xyz$$
我正在尝试从 bash 中的文件名列表中动态提取独特的模式。
输入的文件名列表如下所示
Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt
我想动态提取字符串
Rep1,Rep2,Rep3
此处以图表形式显示:
注意:输入模式每次都可能改变 例如另一个用例可能是
Exp2_DT_10ng_55C_1_User1.png,Exp2_DT_10ng_55C_2_User1.png,Exp2_DT_10ng_55C_3_User1.png
在这种情况下,我想提取
1,2,3
此处以图表形式显示:
在 bash 中实现此目标的最佳方法是什么?
按照评论中的建议,我尝试了以下方法:
declare -p string1 string2
declare -- string1="ER_Rep1"
declare -- string2="ER_Rep2"
diff <(echo "$string1" ) <(echo "$string2")
returns
1c1
< ER_Rep1
---
> ER_Rep2
我要提取的是 Rep1,Rep2。
你可以考虑这个awk
解决方案:
declare -- string1="ER_Rep1"
declare -- string2="ER_Rep2"
awk -F '[_.]+' '{for (i=1; i<=NF; ++i) ++fq[$i]}
END {for (w in fq) if (fq[w] == 1) print w}' <(echo "$string1" ) <(echo "$string2")
Rep1
Rep2
此 awk
解决方案使用 _
或 .
作为字段分隔符,并将每个字段存储在关联数组 fq
中,值作为表示频率的数字那个词的出现。
在 END
块中,我们迭代 fq
数组中的每个词,如果频率等于 1
则打印该词,表明该词的唯一出现。
您可以将 GNU awk
与 sort
& uniq
echo 'Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt' | awk -v RS='[_.,]' '1' | sort | uniq -u
或
tr
结合 sort
& uniq
echo 'Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt' | tr '_.,' '\n' | sort | uniq -u
产生输出
Rep1
Rep2
Rep3
我确信有更好的编码方法,但我在这里所做的是针对任意数量的输入字符串的通用解决方案。
找出下划线分隔子串的最长公共前缀
longestCommonPrefix() { local i prefix file found local -a pieces IFS=_ read -ra pieces <<<"" for ((i = ${#pieces[@]} - 1; i > 0; i--)); do prefix=$(IFS=_; echo "${pieces[*]:0:i}_") found=true for file in "${@:2}"; do if [[ $file != "$prefix"* ]]; then found=false break fi done if $found; then echo "$prefix" return fi done }
找到最长的公共后缀(普通字符)
longestCommonSuffix() { local i suffix file found for ((i = ${#1}; i > 0; i--)); do suffix=${1: -i} found=true for file in "${@:2}"; do if [[ $file != *"$suffix" ]]; then found=false break fi done if $found; then echo "$suffix" return fi done }
把它们放在一起
uniqueStrings() { local prefix=$(longestCommonPrefix "$@") set -- "${@/#"$prefix"/}" local suffix=$(longestCommonSuffix "$@") printf '%s\n' "${@/%"$suffix"/}" }
然后
$ uniqueStrings Exp1_ML_Rep1.txt Exp1_ML_Rep2.txt Exp1_ML_Rep3.txt
Rep1
Rep2
Rep3
和
$ uniqueStrings Exp2_DT_10ng_55C_1_User1.png Exp2_DT_10ng_55C_2_User1.png Exp2_DT_10ng_55C_3_User1.png
1
2
3
其他几个例子:
# nothing in common, should return the input strings
$ uniqueStrings foo bar baz
foo
bar
baz
$ uniqueStrings x_foo13 x_bar13 x_baz13 x_qux13
foo
bar
baz
qux
适用于 bash v3.2+
查看与@glennjackman 提出的解决方案类似的解决方案:
- 找到共同前缀
- 找到共同后缀
- 去掉共同点prefix/suffix剩下的就是不同点
假设:
- 文件名列表以逗号分隔的字符串形式提供
- 可变数量的文件名
- 逐个字符比较
- 无分隔符
- 假设单个 'difference' 由连续字符组成,例如,在比较
aBcDe
和aXcYe
时,我们不认为c
是常见的,因此差异将被报告为BcD
和XcY
一个使用 awk
的想法,它应该比 bash
级循环有一些性能改进:
awk '
# function to return an absolute value of a number
function abs(v) { return v < 0 ? -v : v }
# function to determine if each string has the same character at a given offset;
# return 0 if "no", return 1 if "yes"
function equal() {
for ( i=1; i<=n; i++ ) {
pos = offset <= 0 ? length(fname[i]) + offset : offset
x = substr(fname[i],pos,1)
if ( i == 1 ) curr = x
if ( x != curr ) return 0
}
return 1
}
# for now assume strings input using a here-string, and strings are delimited by a comma
FNR==1 { n=split([=10=],fname,",")
exit # skip to END processing
}
END {
# twice through the outer "for" loop:
# op = 1 => prefix processing
# op = -1 => suffix processing
# "op" will be used to increment/decrement our offset pointer to
# perform the character-by-character comparison
for ( op=1; op>=-1; op=op-2 ) {
offset = op == 1 ? 1 : 0 # determine initial offset based on op (prefix vs suffix)
# if all strings have the same character @ a given offset then update our pfx/sfx pointers
while ( equal() && abs(offset) <= length(fname[1]) ) {
if ( op == 1 ) pfx = offset
else sfx = offset
offset = offset + op # go to next offset
}
}
if ( pfx == "" ) pfx=0 # if no common prefix, default to 0
if ( sfx == "" ) sfx=1 # if no common suffix, default to 1
# use substr() and our pfx/sfx offsets to display the difference
for ( i=1; i<=n; i++ )
print substr(fname[i], pfx+1, length(fname[i]) - pfx - 1 + sfx )
}' <<< "${in}"
备注:
- 此时有点冗长;或许可以精简一点...
- 代码可以修改为直接与 'normal' 文件列表一起工作(例如,从
find
到awk
的管道输出);一个想法是只处理第一条记录 (FNR==1
) 并将FILENAME
填充到数组
测试结果:
# in='Exp1_ML_Rep1.txt,Exp1_ML_Rep2.txt,Exp1_ML_Rep3.txt'
1
2
3
# in='Exp2_DT_10ng_55C_1_User1.png,Exp2_DT_10ng_55C_2_User1.png,Exp2_DT_10ng_55C_3_User1.png'
1
2
3
# in='x_foo13,x_bar13,x_baz13,x_qux13'
foo
bar
baz
qux
# in='x_foo13,x_bar13,x_baz13,x_abcde23'
foo1
bar1
baz1
abcde2
# in='abcde.123,abcde.123,abcde.123' # identical
# three
# blank
# lines
# in='abc,def,123456,xyz$$' # nothing in common
abc
def
123456
xyz$$