
我想以最有效的方式遞歸地更改數百個文件的第一行。我想要做的一個例子是更改#!/bin/bash
為#!/bin/sh
,所以我想出了這個命令:
find ./* -type f -exec sed -i '1s/^#!\/bin\/bash/#!\/bin\/sh/' {} \;
但是,據我了解,這樣做 sed 必須讀取整個文件並替換原始文件。有沒有更有效的方法來做到這一點?
答案1
是的,sed -i
完整讀取並重寫文件,並且由於行長度發生變化,因此必須這樣做,因為它會移動所有其他行的位置。
……但在這種情況下,線長度實際上不需要改變。我們可以用#!/bin/sh␣␣
兩個尾隨空格來取代 hashbang 行。作業系統將在解析 hashbang 行時刪除這些內容。 (或者,使用兩個換行符,或換行符 + 井號,這兩者都會建立 shell 最終會忽略的額外行。)
我們需要做的就是打開檔案從頭開始寫入,而不是截斷它。通常的重定向>
並>>
不能做到這一點,但在 Bash 中,讀寫重定向<>
似乎可以工作:
echo '#!/bin/sh ' 1<> foo.sh
或使用dd
(這些應該是標準 POSIX 選項):
echo '#!/bin/sh ' | dd of=foo.sh conv=notrunc
請注意,嚴格來說,這兩者都會重寫行尾的換行符,但這並不重要。
當然,上面的內容無條件地覆蓋給定文件的開頭。添加對原始文件是否具有正確的 hashbang 的檢查留作練習...無論如何,我可能不會在生產中執行此操作,顯然,如果您需要將行更改為更長一。
答案2
一種優化是使用{} +
而不是{} \;
.
find . -type f -exec sed -i '1s|^#!/bin/bash|#!/bin/sh|' {} +
您無需為每個找到的檔案呼叫一個 sed 進程,而是將這些檔案作為參數提供給單一 sed 進程。
find on 的 POSIX 規範{} +
(我的粗體):
如果主表達式由 <加號> 標點,則主表達式應始終評估為 true,並且評估主表達式的路徑名應聚合到集合中。對於每組聚合路徑名,應呼叫一次實用程式 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 限定符。 - 我們不會費心去查找那些不夠大以容納要替換的原始 shebang 的文件(我們使用
.
相當於-type f
,因此我們已經從文件中檢索了 inode 信息,因此我們不妨檢查那裡的大小)。 - 我們實際上是在檢查檔案是否以要替換的正確 shebang 開頭,並根據需要讀取盡可能少的位元組(這裡必須如此,
zsh
因為其他 shell 無法處理任意位元組值)。 - 我們使用的
#!/bin/sh -
替代品是/bin/sh
腳本的正確 shebang(順便說一下,#!/bin/bash -
這也是腳本的正確 shebang )。/bin/bash
看為什麼“#! /bin/sh -” shebang 中的“-”?了解詳情。
覆蓋檔案的錯誤會在退出狀態中報告,但不會報告遍歷目錄樹的錯誤,也不會報告讀取檔案的錯誤,儘管可以新增這些錯誤。
無論如何,它只是取代了確切地 #!/bin/bash
,而不是其他用作解釋者的 shebang,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
這裡允許許多不同的 shebang 以及許多受支援的選項,這些選項在新的/bin/sh
shebang 中再現,右填充(帶有r[length]
參數擴展標誌)到與原始大小相同。
答案4
檔案是一長串連續的位元組。替換為bash
本質sh
上需要刪除組成 的兩個位元組(假設是 UTF-8 或類似位元組)ba
。文件中不能有漏洞,因此從開始的所有內容都sh
必須提前兩個位元組寫入文件中。
這需要重寫整個文件,或至少從更改的部分開始。
有一些方法可以代替文件中的字節,例如,如果格式允許,則帶有無辜的空格,而無需重寫整個文件,請參閱接受的答案。