用sed中的文件内容替换文件名占位符

时间:2018-08-10 14:08:59

标签: bash awk sed include cat

我正在尝试编写一个基本脚本来编译HTML文件包括。 前提是这样的:

我有3个文件

test.html

<div>
   @include include1.html

   <div>content</div>

   @include include2.html
</div>

include1.html

<span>
   banana
</span>

include2.html

<span>
   apple
</span>

我想要的输出是:

output.html

<div>
   <span>
      banana
   </span>

   <div>content</div>

   <span>
      apple
   </span>
</div>

我尝试了以下操作:

  1. sed "s|@include \(.*)|$(cat \1)|" test.html >output.html
    这将返回cat: 1: No such file or directory

  2. sed "s|@include \(.*)|cat \1|" test.html >output.html
    这可以运行,但给出:

    output.html

    <div>
       cat include1.html
    
       <div>content</div>
    
       cat include2.html
    </div>
    

关于如何使用组替换在cat中运行sed的任何想法?也许是另一种解决方案。

3 个答案:

答案 0 :(得分:3)

我写这个15-20 years ago是为了递归地包含文件,它被包含在the article I wrote about how/when to use getline中的“应用程序”下,然后是“ d)”下。我现在对其进行了调整,以与您的特定“ @include”指令一起使用,提供缩进以匹配“ @include”缩进,并增加了防止无限递归的保护措施(例如,文件A包括文件B,文件B包括文件A):

$ cat tst.awk
function read(file,indent) {
    if ( isOpen[file]++ ) {
        print "Infinite recursion detected" | "cat>&2"
        exit 1
    }

    while ( (getline < file) > 0) {
        if ($1 == "@include") {
             match($0,/^[[:space:]]+/)
             read($2,indent substr($0,1,RLENGTH))
        } else {
             print indent $0
        }
    }
    close(file)

    delete isOpen[file]
}

BEGIN{
   read(ARGV[1],"")
   exit
}

$ awk -f tst.awk test.html
<div>
   <span>
      banana
   </span>

   <div>content</div>

   <span>
      apple
   </span>
</div>

请注意,如果include1.html本身包含一个@include ...指令,那么它也会被接受,依此类推。看:

$ for i in test.html include?.html; do printf -- '-----\n%s\n' "$i"; cat "$i"; done
-----
test.html
<div>
   @include include1.html

   <div>content</div>

   @include include2.html
</div>
-----
include1.html
<span>
   @include include3.html
</span>
-----
include2.html
<span>
   apple
</span>
-----
include3.html
<div>
   @include include4.html
</div>
-----
include4.html
<span>
   grape
</span>

$ awk -f tst.awk test.html
<div>
   <span>
      <div>
         <span>
            grape
         </span>
      </div>
   </span>

   <div>content</div>

   <span>
      apple
   </span>
</div>

对于非GNU awk,我希望它在经过约20个级别的递归后会失败,并显示“打开的文件过多”错误,因此如果您需要更深入的了解,或者要写出自己的代码,请搞清楚自己的文件管理代码。

答案 1 :(得分:0)

您可以使用以下bash脚本,该脚本使用正则表达式来检测以@include开头的行,并使用捕获组来抓取包含文件名:

re="@include +([^[:space:]]+)"

while IFS= read -r line; do
    [[ $line =~ $re ]] && cat "${BASH_REMATCH[1]}" || echo "$line"
done < test.html

<div>
<span>
   banana
</span>

   <div>content</div>

<span>
   apple
</span>
</div>

或者,您可以使用以下awk脚本执行相同的操作:

awk '$1 == "@include"{system("cat " $2); next} 1' test.html

答案 2 :(得分:0)

如果已使用GNU sed,则可以对s命令使用the e flag,该命令将当前模式空间作为shell命令执行,并将其替换为输出:

$ sed 's/@include/cat/e' test.html
<div>
<span>
   banana
</span>

   <div>content</div>

<span>
   apple
</span>
</div>

请注意,这不会处理缩进,因为所包含的文件没有任何缩进。像Tidy这样的HTML修饰词可以进一步帮助您:

$ sed 's/@include/cat/e' test.html | tidy -iq --show-body-only yes
<div>
  <span>banana</span>
  <div>
    content
  </div><span>apple</span>
</div>

GNU拥有读取文件r的命令,但无法即时生成文件名。


Ed在他的评论中指出,这很容易被shell命令注入:如果您有类似的东西

@include $(date)

您会注意到date命令实际上已在运行。可以避免这种情况,但是如果原始解决方案不在窗外,则简洁:

sed 's|@include \(.*\)|cat "$(/usr/bin/printf "%q" '\''\1'\'')"|e' test.html

这仍将@include替换为cat,但另外将行的其余部分包装成用printf "%q"替换的命令,因此一行诸如

@include include1.html

成为

cat "$(/usr/bin/printf "%q" 'include1.html')"

在作为命令执行之前。扩展为

cat include1.html

但是如果文件名为$(date),它将变成

cat '$(date)'

(请注意单引号),以防止执行注入的命令。

由于s///e似乎使用/bin/sh作为其外壳,因此您不能依赖%q中Bash的printf格式规范来存在,因此, printf二进制。为了提高可读性,我将/命令的s分隔符更改为|(因此我不必转义\/usr\/bin\/printf)。

最后,\1周围的引号是将单引号括入一个带引号的字符串中:'\''变为'