
Я хочу изменить первую строку сотен файлов рекурсивно наиболее эффективным способом. Пример того, что я хочу сделать, это изменить #!/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
должно быть записано на два байта раньше в файл.
Для этого необходимо переписать весь файл или, по крайней мере, начать с измененной части.
Есть способызаменятьбайт в файле, например, с невинными пробелами, если формат это позволяет, без необходимости переписывать весь файл, см. принятый ответ.