Замена заполнителя имени файла содержимым файла в sed

Я пытаюсь написать базовый сценарий для компиляции файлов 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, используя групповую подстановку? Или, возможно, другое решение.

0
0
605
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы можете использовать этот сценарий 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

Я удивлен, что вы предложили цикл оболочки (почему-используется-оболочка-цикл-процесс-текст-считается-плохой-ценой), и этот сценарий awk не обязательно будет производить вывод system () в ожидаемом порядке относительно остальной части основного входного файла.

Ed Morton 12.08.2018 14:23

Если у вас есть GNU sed, вы можете использовать флаг e для команды s, которая выполняет текущее пространство шаблонов как команду оболочки и заменяет ее выводом:

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

   <div>content</div>

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

Обратите внимание, что это не касается отступов, так как во включенных файлах их нет. В этом вам может помочь такой претификатор 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, но имя файла не может быть сгенерировано на лету.


Как указывает Эд в своем комментарии, это уязвимо для инъекции команд оболочки: если у вас есть что-то вроде

@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 в качестве своей оболочки, вы не можете полагаться на существование спецификации формата Bash %q в printf, следовательно, это абсолютный путь к двоичному файлу printf. Для удобства чтения я изменил разделители / команды s на | (так что мне не нужно экранировать \/usr\/bin\/printf).

Наконец, беспорядок с кавычками вокруг \1 состоит в том, чтобы поместить одну кавычку в одну строку в кавычках: '\'' становится '.

Ответ принят как подходящий

Я написал этот 15-20 лет назад для рекурсивного включения файлов, и он включен в статья, которую я написал о том, как / когда использовать 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>

С awk, отличным от GNU, я бы ожидал, что он выйдет из строя примерно после 20 уровней рекурсии с ошибкой «слишком много открытых файлов», так что не торопитесь, если вам нужно пойти глубже, или вам придется написать собственное управление файлами код.

Это действительно отличный awk-скрипт для рекурсивного включения ++

anubhava 12.08.2018 18:49

Другие вопросы по теме