【问题标题】:Find and replace string and print file directory on change查找和替换字符串并在更改时打印文件目录
【发布时间】:2019-09-03 19:11:24
【问题描述】:

我正在使用findsed 替换多个文件中的字符串。这是我的脚本:

find ./ -type f -name "*.html" -maxdepth 1 -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \; -print

-print 总是打印文件,无论是否有更改。我想看看哪些文件被更改了。理想情况下,我希望输出是这样的(因为文件正在更改):

/path/to/file was changed
  - REPLACE STRING line 9 was changed
  - REPLACE STRING line 12 was changed
  - REPLACE STRING line 26 was changed
/path/to/file2 was changed
  - REPLACE STRING line 1 was changed
  - REPLACE STRING line 6 was changed
  - REPLACE STRING line 36 was changed

有没有办法做这样的事情?

【问题讨论】:

  • 我以前也遇到过这种情况,但从未追求过更优雅的解决方案。然而,一个快速的解决方法是在调用sed -i之前简单地grep文件是否包含${REPLACE_STRING}。为什么? sed -i 将更新文件修改时间,无论是否执行替换。我会简单地做grep -q "${REPLACE_STRING}" "example.html" && sed -i "s/${REPLACE_STRING}/${STRING}/g" {} \; -print。不优雅,但对于单个文件,sed -i 仅在 "example.html 包含 ${REPLACE_STRING} 时才会执行
  • @DavidC.Rankin 我认为您不能在 -exec 子句中使用 &&,您必须将整个命令包装成 sh -c 或类似的。
  • 哦,对了,但是因为这个例子是针对-name "example.html" -maxdepth 1的,所以只需要一个没有find的简单grep。否则,通过-exec 调用一个简单的帮助脚本,而不是将其全部放在-exec 之后。
  • 我正在对多个文件使用此功能,其中一个包含“*.php”,所以 grep 不起作用:/,我现在要更新我的问题。

标签: shell sed replace terminal find


【解决方案1】:

好主意。由于您提到的原因,我认为-print 是死路一条,因此需要在exec 中完成。我认为sed 也是一个死胡同,因为打印到STDOUT 以及修改文件的挑战。所以一个自然的扩展是在它周围包裹一些 Perl。

如果这是你的exec 声明:

perl -p -i -e '$i=1 if not defined($i); print STDOUT "$ARGV, line $i: $_" if s/REPLACE_STRING/STRING/; $i++' {} \;
  • -p 将 Perl 语句包装在标准的 while(<>) 循环中,因此文件像 sed 一样逐行处理。
  • -i 进行就地替换,就像 sed 一样。
  • -e 表示执行以下 Perl 语句。
  • if not defined 是初始化行计数变量的一种偷偷摸摸的方式,即使它是为每一行执行的。
  • STDOUT 告诉 print 输出到控制台而不是文件。
  • <> 读取时,$ARGV 是当前文件名。
  • $_ 是正在处理的行。
  • if 表示 print 只有在找到匹配项时才会执行。

对于包含以下内容的输入文件text.txt

line 1
token 2
line 3
token 4
line 5

perl -p -i -e '$i=1 if not defined($i); print STDOUT "$ARGV, line $i: $_" if s/token/sub/; $i++' text.txt 的声明给了我:

text.txt, line 2: sub 2
text.txt, line 4: sub 4

离开text.txt 包含:

line 1
sub 2
line 3
sub 4
line 5

因此,您不会得到介绍性的“文件已更改”行,但对于单行,我认为这是一个很好的折衷方案。

对几个文件进行操作如下所示:

find ./ -type f -name "*.txt" -maxdepth 1 -exec perl -p -i -e '$i=1 if not defined($i); print STDOUT "$ARGV, line $i: $_" if s/token/sub/; $i++' {} \;
.//text1.txt, line 2: sub 2
.//text1.txt, line 4: sub 4
.//text2.txt, line 1: sub 1
.//text2.txt, line 3: sub 3
.//text2.txt, line 5: sub 5

【讨论】:

    【解决方案2】:

    您可以链接-exec 操作并利用退出状态。例如:

    find . \
        -maxdepth 1 \
        -type f \
        -name '*.html' \
        -exec grep -Hn "$REPLACE_STRING" {} \; \
        -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \;
    

    这会为每个匹配的文件打印路径、行号和行:

    ./file1.html:9:contents of line 9
    ./file1.html:12:contents of line 12
    ./file1.html:26:contents of line 26
    ./file2.html:1:contents of line 1
    ./file2.html:6:contents of line 6
    ./file2.html:36:contents of line 36
    

    对于没有匹配的文件,没有其他事情发生;对于匹配的文件,将调用 sed 命令。

    如果您希望输出更接近您的问题,您可以添加一些操作:

    find . \
        -maxdepth 1 \
        -type f \
        -name '*.html' \
        -exec grep -q "$REPLACE_STRING" {} \; \
        -printf '%p was changed\n' \
        -exec grep -n "$REPLACE_STRING" {} \; \
        -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \; \
        | sed -E "s/^([[:digit:]]+):.*/  - $REPLACE_STRING line \1 was changed/"
    

    现在首先使用grep -q 静默检查文件是否包含字符串,然后打印文件名(-printf),然后打印带有行号(grep -n)的所有匹配行,然后用sed,最后用 sed 稍微修改一下输出。

    由于您使用的是sed -i '',我假设您使用的是 macOS;我不确定那里的股票find 是否支持printf 选项。

    到目前为止,我们已经非常接近在每个匹配的文件上运行复杂的脚本了,所以我们不妨直接这样做:

    shopt -s nullglob
    for f in ./*.html; do
        if grep -q "$REPLACE_STRING" "$f"; then
            printf '%s\n' "$f was changed"
            grep -n "$REPLACE_STRING" "$f" \
                | sed -E "s/^([[:digit:]]+):.*/  - $REPLACE_STRING line \1 was changed/"
            sed -i '' "s/${REPLACE_STRING}/${STRING}/g" "$f"
        fi
    done
    

    【讨论】:

    • 我喜欢它,但就像我的那种“迂回”sed + 助手一样,Ed 的gawk 解决方案确实看起来很诱人:)
    【解决方案3】:

    替换你的 find+sed 命令:

    find ./ -type f -name "*.html" -maxdepth 1 -exec sed -i '' "s/${REPLACE_STRING}/${STRING}/g" {} \; -print
    

    使用这个 GNU awk 命令(需要 gawk 进行就地编辑):

    gawk -i inplace -v old="$REPLACE_STRING" -v new="$STRING" '
        FNR==1 { hdr=FILENAME " was changed\n" }
        gsub(old,new) { printf "%s  - %s line %d was changed\n", hdr, old, FNR | "cat>&2"; hdr="" }
    1' *.html
    

    如果有必要,你还可以使用 awk 使其比使用 sed 更健壮,因为 awk 可以支持文字字符串,而 sed 不能

    【讨论】:

      【解决方案4】:

      好的,为了提高效率,始终遵循 Ed 的 awk 脚本,但继续使用 sed + helper 脚本,使用初步调用 grep 来确定您的文件是否包含要替换的单词,您可以使用一个简短的帮助脚本,将您的 ${REPLACE_STRING}${STRING}filename 作为前三个位置参数,如下所示:

      帮助脚本命名为helper.sh

      #!/bin/sh
      
      test -z "$1" && exit
      test -z "$2" && exit
      test -z "$3" && exit
      
      findw="$1"
      replw="$2"
      fname="$3"
      
      grep -q "$findw" "$fname" || exit
      
      echo "$(readlink -f $fname) was changed"
      grep -n "$findw" "$fname" | {
      while read line; do
          printf -- "  - REPLACE STRING line %d was changed\n" "${line%:*}"
      done }
      
      sed -i "s/$findw/$replw/g" "$fname"
      

      那么您对find 的呼叫可能是,例如:

      find . -type f -name "f*" -exec ./helper.sh "dog" "cat" '{}' \;
      

      使用/输出示例

      从几个名为 f 的文件开始,其中包含:

      $ cat f
      my
      dog
      dog
      has
      fleas
      

      在包含当前目录中的脚本的文件结构中,带有子目录d1f 的多个副本,例如

      $ tree .
      .
      ├── d1
      │   └── f
      ├── f
      └── helper.sh
      

      运行脚本会产生以下结果:

      $ find . -type f -name "f*" -exec ./helper.sh "dog" "cat" '{}' \;
      /tmp/tmp-david/f was changed
        - REPLACE STRING line 2 was changed
        - REPLACE STRING line 3 was changed
      /tmp/tmp-david/d1/f was changed
        - REPLACE STRING line 2 was changed
        - REPLACE STRING line 3 was changed
      

      f的内容也随之改变

      $ cat f
      my
      cat
      cat
      has
      fleas
      

      如果在find 定位的任何文件中都没有找到搜索词,则这些文件的修改时间保持不变。

      现在考虑到所有这些,如果您有 gawk 可用,请听从 Ed 的建议,但是 -- 您可以使用 sed 和助手 :) 来做到这一点

      【讨论】:

        【解决方案5】:

        轻松免费安装Perl,在 bash shell 上定义自己的字符串并在此处测试:

        STRING=
        REPLACE=
        
        perl -ne 'foreach(`find . -maxdepth 1 -type f -iname "*.html"`){ open IH,$_ or die "Error $!"; print "Processing: $_";while (<IH>) {$s=$_;$t=s/$REPLACE/$STRING/; print "$s --> $_" if $t };print "Nothing replaced" if !$t}'
        

        要真正编辑它,请添加 -i 选项,使其成为 perl -i -ne....

        【讨论】:

          猜你喜欢
          • 2018-06-09
          • 1970-01-01
          • 2015-07-23
          • 2011-05-25
          • 2012-04-11
          • 2013-02-19
          • 2020-08-12
          • 1970-01-01
          • 2019-01-29
          相关资源
          最近更新 更多