スクリプトで最後の項目だけを一致させようとすると、Sedは$ address文字を尊重しません。

スクリプトで最後の項目だけを一致させようとすると、Sedは$ address文字を尊重しません。

パターンに基づいてファイル名を変更するために使用したい簡単なスクリプトを作成しました。

スクリプトは find と sed を使用して操作を実行します。

基本的にすべてがうまくいきます。ただし、$を使用してsedに最後の項目のみを一致させるように指示すると、何も一致しないため、名前の変更は行われません。 $を削除すると名前変更が機能しますが、すべてのアイテムの名前が変更され、私が望むものとは異なり、特に最後のアイテムをターゲットにしたいと思います。

私はドキュメント、YouTubeチュートリアル、スタックオーバーフロー、スタック交換などを検索しましたが、これらのいずれも機能しないか、私の問題とは関係ありませんでした。特に$アドレス文字が機能しない場合は、さらにそうです。

$ charを使用したり使用したりせずにsedを使用する方法を示す以下のスクリプトをテストして、私が経験している問題を特定できます。ロジックは searchReplace() と searchReplaceLastMatchOnly() という 2 つの関数に分けられます。 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(最大)1つの代替項目があるため、修飾子は必要ありません。

貪欲\(.*\)sed最後のパターンが見つかる$searchまですべてを一致させます。この部品を取り外したくないので、\1交換品に含める必要があります。

メモ:

  • #!/bin/bashスクリプトの最初の行にshebangはありません。
  • -eまだ実装されていません。
  • 一度だけ使用される変数がたくさんあります。これを削除すると、コードがより明確になります。たとえば、関数はsearchReplaceLastMatchOnly()次のように単純化できます。

    function searchReplaceLastMatchOnly() {
      printf "%s" "${1:- }" | sed "s/\(.*\)${2:- }/\1${3:- }/"
    }
    

答え2

アンカー$:

文字を使用せずに文字列の末尾と一致します。複数行モードを使用している場合は、改行文字の直前の位置とも一致します。

これは、文字列が実際に矛盾txt$ではなく次に終わるという点で、「最後の発生」とは異なります。bar$

最後の項目だけを一致させる1つの方法は、逆方向の文字列を使用して最初の項目のみを置き換える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予期しない解釈を防ぐために渡されます。

関連情報