![当我尝试仅匹配脚本中的最后一次出现时,Sed 不尊重 $ 地址字符](https://linux22.com/image/162010/%E5%BD%93%E6%88%91%E5%B0%9D%E8%AF%95%E4%BB%85%E5%8C%B9%E9%85%8D%E8%84%9A%E6%9C%AC%E4%B8%AD%E7%9A%84%E6%9C%80%E5%90%8E%E4%B8%80%E6%AC%A1%E5%87%BA%E7%8E%B0%E6%97%B6%EF%BC%8CSed%20%E4%B8%8D%E5%B0%8A%E9%87%8D%20%24%20%E5%9C%B0%E5%9D%80%E5%AD%97%E7%AC%A6.png)
我创建了一个简单的脚本,我想用它来根据模式重命名文件。
该脚本使用 find 和 sed 来完成其工作
一切基本上都正常工作,除了当我使用 $ 告诉 sed 仅匹配最后一次出现时,它无法匹配任何内容,因此不执行重命名。如果我删除 $ 重命名工作,但所有出现的情况都被重命名,这不是我想要的,我特别想定位最后一个出现的情况。
我已经遵循了 youtube 上的文档、教程,搜索了堆栈溢出、堆栈交换等,但我发现这些东西都不起作用,甚至与我的问题无关,特别是与 $ 地址字符不适合我。
我测试了下面的脚本,它演示了使用带和不带 $ char 的 sed,以便您可以看到我遇到的问题。该逻辑分为 2 个函数:searchReplace() 和 searchReplaceLastMatchOnly()。 searchReplaceLastMatchOnly() 函数是我需要工作的函数,但它无法匹配任何内容。
我的测试目录结构如下:
./bar
./bar/baz
./bar/foobar.txt
./bar/baz/foobar.txt
在测试目录中运行脚本如下:
./script.sh -d "." -s "bar" -r "Bar" -p "*.txt" -e "txt"
应改变:
./bar/foobar.txt to ./bar/fooBar.txt
和:
./bar/baz/foobar.txt to ./bar/baz/fooBar.txt
- - 实际结果 - -
使用searchReplace
:
Renaming ./bar/baz/foobar.txt to ./Bar/baz/fooBar.txt
Renaming ./bar/foobar.txt to ./Bar/fooBar.txt
使用searchReplaceLastMatchOnly
:
Renaming ./bar/baz/foobar.txt to ./bar/baz/foobar.txt
Renaming ./bar/foobar.txt to ./bar/foobar.txt
这是完整的脚本:
# Search and replace using sed.
# $1 target The string to process
# $2 The string to serach for
# $3 The string that will repalce the search string
# Usage Example:
# result=searchReplace "targetToSearch" "Search" "Replace"
# printf "%s" "$result" # should output "targetToReplace"
function searchReplace() {
# spaces are set as defaults
target=${1:- }
search=${2:- }
replace=${3:- }
result="$(printf "%s" "$target" | sed "s/${search}/${replace}/g")"
printf "%s" "$result"
}
# Prints via printf with wrapping newline(\n) chars.
# Note: If no arguments are supplied, pretty print
# will simpy print a default string followed by
# a newline character (\n).
# Usage Exmple:
# txt="Text"
# prettyPrint "$txt"
function prettyPrint() {
# Set default text to print in case no arguments were passed
# (at the moment this is an empty string)
text=${1:-}
[[ -z $text ]] && printf "\n" || printf "\n%s\n" "$text"
#
}
# Get option values
while getopts "d:p:s:r:e:" opt; do
case $opt in
d) dirPath="$OPTARG" ;;
p) pattern="$OPTARG" ;;
s) search="$OPTARG" ;;
r) replace="$OPTARG" ;;
e) fileExt="$OPTARG" ;;
*)
prettyPrint "Error: Invalid flag $opt"
exit 1
;;
esac
done
# Defaults #
dirPath=${dirPath:-.}
pattern=${pattern:-*}
search=${search:- }
replace=${replace:- }
fileExt=${fileExt:-txt}
prettyPrint "Using searchReplace:"
find "$dirPath" -type f -name "$pattern" | while IFS= read -r original; do
modified="$(searchReplace "$original" "$search" "$replace")"
prettyPrint "Renaming $original to $modified"
#mv "$original" "$modified" | This is the goal...no using till renaming is working.
done
# The dev directory is structured as follows:
# .
# ./bar/fooBar.txt
# ./bar/baz/fooBar.txt
#
# This script when run as follows:
#
# ./script.sh -d "." -s "bar" -r "Bar" -p "*.txt" -e "txt"
#
# Should rename:
# ./bar/foobar.txt to ./bar/fooBar.txt
#
# and also rename:
# ./bar/baz/foobar.txt to ./bar/baz/fooBar.txt
#
# However when I run this script it renames as follows:
#
# ./bar/baz/foobar.txt to ./Bar/baz/fooBar.txt
#
# ./bar/foobar.txt to ./Bar/fooBar.txt
#
# As you can see the ./bar directory is also renamed to Bar, i just want the last
# occurence of bar to be renamed to Bar.
#
# I tried modifying the sed command in the searchReplace() function
# from sed "s/${search}/${replace}/g" to sed "s/${search}$/${replace}/g"
# as i read that the $ should tell sed to match only the last occurence,
# but it doesn't work.
# Below is the modified searchReplace() function that uses the $
#
function searchReplaceLastMatchOnly() {
# spaces are set as defaults
target=${1:- }
search=${2:- }
replace=${3:- }
result="$(printf "%s" "$target" | sed "s/${search}$/${replace}/g")"
printf "%s" "$result"
}
prettyPrint "Using searchReplaceLastMatchOnly:"
# here it is running
find "$dirPath" -type f -name "$pattern" | while IFS= read -r original; do
modified="$(searchReplaceLastMatchOnly "$original" "$search" "$replace")"
prettyPrint "Renaming $original to $modified"
#mv "$original" "$modified" | This is the goal...no using till renaming is working.
done
答案1
要仅替换最后一个匹配项,请替换
result="$(printf "%s" "$target" | sed "s/${search}$/${replace}/g")
和
result="$(printf "%s" "$target" | sed "s/\(.*\)${search}/\1${replace}/")"
匹配$
行尾而不是最后一个搜索模式,并且您不需要修饰符,g
因为(最多)有一个替换。
是\(.*\)
贪婪的,sed
在找到最后一个$search
模式之前会匹配所有内容。由于我们不想删除这部分,因此我们必须将其包含\1
在替换中。
笔记:
#!/bin/bash
该脚本第一行缺少一个 shebang 。-e
尚未实施。有很多变量只使用一次。删除它们将使您的代码更加清晰,例如
searchReplaceLastMatchOnly()
函数可以简化为function searchReplaceLastMatchOnly() { printf "%s" "${1:- }" | sed "s/\(.*\)${2:- }/\1${3:- }/" }
答案2
锚$
:
匹配字符串的结尾而不消耗任何字符。如果使用多行模式,这也将匹配紧邻换行符之前的位置。
这与“最后一次出现”不同,因为您的字符串实际上以 结尾,txt$
而不是bar$
不匹配。
仅匹配最后一次出现的一种方法是使用rev
反转字符串并仅替换第一次出现(当然,您的替换也需要反转!)
function searchReplaceLastMatchOnly() {
# spaces are set as defaults
local target=$(rev <<<"${1:- }")
local search=$(rev <<<"${2:- }")
local replace=$(rev <<<"${3:- }")
local esearch=$(printf '%s\n' "$search" | sed 's:[][\/.^$*]:\\&:g')
local ereplace=$(printf '%s\n' "$replace" | sed 's:[\/&]:\\&:g;$!s/$/\\/')
result="$(printf "%s" "$target" | sed "s/${esearch}/${ereplace}/" | rev)"
printf "%s" "$result"
}
注意:g
操作符已从sed
命令中删除,因此只有第一的发生被替换。另外我们是逃避变量这将被传递sed
以防止它们被不期望地解释。