Самый эффективный способ изменить 1 строку в файле

Самый эффективный способ изменить 1 строку в файле

Я хочу изменить первую строку сотен файлов рекурсивно наиболее эффективным способом. Пример того, что я хочу сделать, это изменить #!/bin/bashна #!/bin/sh, поэтому я придумал эту команду:

find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;

Но, насколько я понимаю, делая это таким образом, sed должен прочитать весь файл и заменить оригинал. Есть ли более эффективный способ сделать это?

решение1

Да, sed -iсчитывает и перезаписывает файл полностью, а поскольку длина строки изменяется, то это необходимо, так как при этом перемещаются позиции всех остальных строк.

...но в этом случае длина строки на самом деле не должна меняться. Вместо этого мы можем заменить строку hashbang #!/bin/sh␣␣на два конечных пробела. ОС удалит их при разборе строки hashbang. (В качестве альтернативы используйте два символа новой строки или символ новой строки + решетка, оба из которых создают дополнительные строки, которые оболочка в конечном итоге проигнорирует.)

Все, что нам нужно сделать, это открыть файл для записи с самого начала, не обрезая его. Обычные перенаправления >и >>этого не могут сделать, но в Bash перенаправление чтения-записи, <>похоже, работает:

echo '#!/bin/sh  ' 1<> foo.sh

или используя dd(это должны быть стандартные параметры POSIX):

echo '#!/bin/sh  ' | dd of=foo.sh conv=notrunc

Обратите внимание, что, строго говоря, оба варианта также перезаписывают символ новой строки в конце строки, но это не имеет значения.

Конечно, вышеприведенное безусловно перезаписывает начало указанного файла. Добавление проверки того, что исходный файл имеет правильный хэшбэнг, остается в качестве упражнения... Независимо от этого, я, вероятно, не буду делать это в продакшене, и, очевидно, это не сработает, если вам нужно изменить строку надольшеодин.

решение2

Оптимизацией было бы использование {} +вместо {} \;.

find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +

Вместо того чтобы вызывать один процесс sed для каждого найденного файла, вы предоставляете файлы в качестве аргументов одному процессу sed.

Спецификация POSIX поиска на{} +(выделено мной жирным шрифтом):

Если первичное выражение отмечено знаком <плюс>, первичное выражение всегда будет оцениваться как истинное, а имена путей, для которых первичное выражение оценивается, будут объединены в наборы.Утилита utility_name должна вызываться один раз для каждого набора агрегированных путей.

решение3

Я бы сделал:

#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
shebang_to_replace=$'#!/bin/bash\n'
       new_shebang=$'#!/bin/sh -\n'

length=$#shebang_to_replace

ret=0
for file in **/*(N.L+$((length - 1)));do
  if
    read -u0 -k $length shebang < $file &&
      [[ $shebang = $shebang_to_replace ]]
  then
    print -rn -- $new_shebang 1<> $file || ret=$?
  fi
done
exit $ret

НравитьсяПодход @ilkkachu, файл перезаписывается на месте строкой, которая имеет точно такой же размер. Различия следующие:

  • мы игнорируем скрытые файлы и файлы в скрытых каталогах (например, .gitодин), так как вряд ли вы захотите их учитывать (вы использовали , find ./*который пропустил бы скрытые файлы и каталоги текущего каталога, но не подкаталоги). Добавьте Dквалификатор glob, если они вам нужны.
  • мы не беспокоимся о файлах, которые недостаточно велики, чтобы вместить исходный файл для замены (мы используем .как эквивалент -type f, поэтому мы уже извлекаем информацию об иноде из файла, поэтому мы могли бы также проверить размер там).
  • на самом деле мы проверяем, начинается ли файл с правильного шебанга для замены, считывая столько байтов, сколько необходимо (здесь это необходимо, zshпоскольку другие оболочки не могут работать с произвольными значениями байтов).
  • мы используем #!/bin/sh -в качестве замены, что является правильным шебангом для /bin/shскриптов ( кстати, #!/bin/bash -это было бы правильным шебангом для скриптов). Смотрите/bin/bashПочему "-" в строке "#! /bin/sh -"?для получения подробной информации.

Ошибки при перезаписи файлов сообщаются в статусе выхода, но не ошибки при обходе дерева каталогов и не ошибки при чтении файлов, хотя их можно было бы добавить.

В любом случае, это только заменяет те шалости, которые естьточно #!/bin/bash, а не другие шебанги, которые используют bashв качестве интерпретатора, такие как #! /bin/bash, #! /bin/bash -Oextglob, #! /usr/bin/env bash, #! /bin/bash -efu. Для них вам нужно решить, что делать. -efuесть shварианты, но -Oextglobне имеют shэквивалента, например.

Вы можете расширить его для поддержки самых простых случаев, таких как:

#! /bin/zsh -
LC_ALL=C # work with bytes instead of characters.
zmodload zsh/system || exit

minlength=11 # length of "#!/bin/bash"
maxlength=1024 # arbitrary here.

ret=0
for file in **/*(N.L+$minlength);do
  if
    sysread -s $maxlength buf < $file &&
      [[ $buf =~ $'(^#![\t ]*((/usr)?/bin/env[ \t]+bash|/bin/bash)([ \t]+-([aCefux]*))?[ \t]*)\n' ]]
  then
    shebang=$match[1] newshebang="#!/bin/sh -$match[5]"
    print -r -- ${(r[$#shebang])newshebang} 1<> $file || ret=$?
  fi
done
exit $ret

Здесь допускается несколько различных шебангов с несколькими поддерживаемыми параметрами, которые воспроизводятся в новом /bin/shшебанге, дополненном справа (с r[length]флагом расширения параметров) до того же размера, что и оригинал.

решение4

Файлы — это один длинный непрерывный диапазон байтов. Ваша замена на bashпо shсути потребует удаления двух байтов (предполагая, что UTF-8 или аналогичный), которые составляют ba. Файлы не могут иметь дыр, поэтому все, что начинается с , shдолжно быть записано на два байта раньше в файл.

Для этого необходимо переписать весь файл или, по крайней мере, начать с измененной части.

Есть способызаменятьбайт в файле, например, с невинными пробелами, если формат это позволяет, без необходимости переписывать весь файл, см. принятый ответ.

Связанный контент