当我尝试仅匹配脚本中的最后一次出现时,Sed 不尊重 $ 地址字符

当我尝试仅匹配脚本中的最后一次出现时,Sed 不尊重 $ 地址字符

我创建了一个简单的脚本,我想用它来根据模式重命名文件。

该脚本使用 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以防止它们被不期望地解释。

相关内容