【问题标题】:Bash - Search and Replace operation with reporting the files and lines that got changedBash - 搜索和替换操作,报告已更改的文件和行
【发布时间】:2017-12-04 05:40:39
【问题描述】:

我有一个输入文件“test.txt”,如下 -

hostname=abc.com hostname=xyz.com
db-host=abc.com db-host=xyz.com

在每一行中,空格前的值是旧值,需要在名为“test”的文件夹中递归替换为空格后的新值。我可以使用下面的 shell 脚本来做到这一点。

#!/bin/bash

IFS=$'\n' 
for f in `cat test.txt`
do
  OLD=$(echo $f| cut -d ' ' -f 1) 
  echo "Old = $OLD"
  NEW=$(echo $f| cut -d ' ' -f 2)
  echo "New = $NEW"
  find test -type f | xargs sed -i.bak "s/$OLD/$NEW/g"
done

“sed”在 100 个文件中动态替换字符串。

是否有一种技巧或替代方法可以让我获得文件更改的报告,例如文件的绝对路径和更改的确切行?

PS - 我了解 sed 或流编辑器不支持开箱即用的此功能。我不想使用版本控制,因为这对这项任务来说太过分了。

【问题讨论】:

    标签: bash shell sed find


    【解决方案1】:

    来自man sed

       -i[SUFFIX], --in-place[=SUFFIX]
              edit files in place (makes backup if SUFFIX supplied)
    

    这可用于在替换时创建备份文件。然后,您可以查找任何备份文件,这些文件指示哪些文件已更改,diff 那些与原始文件。检查完差异后,只需删除备份文件即可。

    如果您将替换设置为sed 语句而不是自定义格式,您可以更进一步,使用sed shebang 行或将文件传递给-f/--file 以在一次操作中完成所有替换。

    【讨论】:

    • 好主意,我曾想过,但是当单个文件中有多个更改时,管理起来会变得很复杂。 “sed”创建了许多具有多个“.bak”扩展名的文件,例如 1.bak.bak.bak。
    【解决方案2】:

    您的脚本有几个问题,只需将其全部替换为(使用 GNU awk 而不是 GNU sed 进行就地编辑):

    mapfile -t files < <(find test -type f)
    awk -i inplace '
    NR==FNR { map[$1] = $2; next }
    { for (old in map) gsub(old,map[old]) }
    ' test.txt "${files[@]}"
    

    你会发现这比你做的要快几个数量级。

    当“test.txt”字符串包含正则表达式或反向引用元字符并修改先前修改的字符串和处理部分匹配时,您现有的脚本仍然存在失败的问题 - 如果这是一个问题,请告诉我们,因为它很容易工作使用 awk(使用 sed 非常困难!)。

    要获得您想要的任何类型的报告,您只需调整 { for ... } 行以打印它们,例如打印对 stderr 的更改记录:

    mapfile -t files < <(find test -type f)
    awk -i inplace '
    NR==FNR { map[$1] = $2; next }
    {
        orig = $0
        for (old in map) {
            gsub(old,map[old])
        }
        if ($0 != orig) {
            printf "File %s, line %d: \"%s\" became \"%s\"\n", FILENAME, FNR, orig, $0 | "cat>&2"
        }
    }
    ' test.txt "${files[@]}"
    

    【讨论】:

      【解决方案3】:

      让我们从一个简单的脚本重写开始,让它在处理更广泛的替换值时更加健壮,而且速度更快:

      #!/bin/bash
      
      # escape regexp and replacement strings for sed
      escapeRegex() { sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$1"; }
      escapeSubst() { sed 's/[&/\]/\\&/g' <<<"$1"; }
      
      while read -r old new; do
          find test -type f -exec sed "/$(escapeRegex "$old")/$(escapeSubst "$new")/g" -i '{}' \;
      done <test.txt
      

      因此,我们在test.txt 的行中循环遍历由空格分隔的字段对(oldnew),并对使用find 找到的所有文件运行标准的sed 就地替换。

      与您的脚本非常相似,但我们 properly read lines 来自 test.txt(没有分词、路径名/变量扩展等),我们尽可能使用 Bash 内置函数(无需调用像 cat 这样的外部工具, cut, xargs);我们将escape sed metacharacters 中的old/new 值正确用作sed 的正则表达式和替换表达式。

      现在让我们添加logging from sed

      #!/bin/bash
      
      # escape regexp and replacement strings for sed
      escapeRegex() { sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$1"; }
      escapeSubst() { sed 's/[&/\]/\\&/g' <<<"$1"; }
      
      while read -r old new; do
          find test -type f -printf '\n[%p]\n' -exec sed "/$(escapeRegex "$old")/{
              h
              s//$(escapeSubst "$new")/g
              H
              x
              s/\n/ --> /
              w /dev/stdout
              x
          }" -i '{}' > >(tee -a change.log) \;
      done <test.txt
      

      上面的sed 脚本将每个old 更改为new,但它还将old --&gt; new 行写入/dev/stdout(特定于Bash),我们依次将其附加到change.log 文件。 find 中的 -printf 操作为每个处理的文件输出一个带有文件名的“标题”行。

      这样,您的“更改日志”将如下所示:

      [file1]
      hostname=abc.com --> hostname=xyz.com
      
      [file2]
      
      [file1]
      db-host=abc.com --> db-host=xyz.com
      
      [file2]
      db-host=abc.com --> db-host=xyz.com
      

      为了完整起见,快速浏览sed 脚本。我们只对包含old 值的行执行操作。对于每个这样的行,我们将其存储到保持空间(h),将其更改为new,将该新值附加到现在拥有old\nnew 的保持空间(与换行符连接,H)。我们将保持与模式空间 (x) 交换,因此我们可以运行 s 命令将其转换为 old --&gt; new。在使用w 将其写入stdout 之后,我们将new 从保留移回模式空间,因此它被(就地)写入处理的文件。

      【讨论】:

        猜你喜欢
        • 2017-12-15
        • 2011-02-17
        • 2010-09-10
        • 1970-01-01
        • 2022-01-07
        • 1970-01-01
        • 2020-11-26
        • 2021-10-19
        • 2015-11-13
        相关资源
        最近更新 更多