ПРИМІТКА: @ jw013 в наступних коментарях робить таке непідтримуване заперечення:
Основна причина полягає в тому, що самовиправляючий код, як правило, вважається поганою практикою. Ще в часи крихітних програм складання це був розумний спосіб зменшити умовні гілки та підвищити продуктивність, але в наш час ризики для безпеки переважують переваги. Ваш підхід не працював, якби користувач, який керував сценарієм, не мав привілеїв для запису сценарію.
Я відповів на його заперечення щодо безпеки, зазначивши, що будь-які спеціальні дозволи потрібні лише один раз на рік встановлення / оновлення дії для того , щоб встановити / оновити в самоустановлювальний сценарій - який я особисто називаю досить безпечним. Я також вказав йому на man sh
посилання на досягнення подібних цілей подібними засобами. На той час я не намагався зазначити, що які б вади безпеки чи взагалі не були відредаговані практики, які можуть бути або не можуть бути представлені у моїй відповіді, вони, скоріш за все, укорінені в самому питанні, ніж у моїй відповіді на нього:
Як я можу налаштувати шебанг, щоб запуск сценарію як /path/to/script.sh завжди використовував Zsh, доступний у PATH?
Не задоволений, @ jw013 продовжував заперечувати, продовжуючи свій ще непідтримуваний аргумент принаймні парою помилкових тверджень:
Ви використовуєте один файл, а не два файли. У
пакеті [ man sh
посилання] один файл змінює інший файл. У вас є файл, що модифікує себе. Існує чітка різниця між цими двома випадками. Файл, який приймає вхід і видає вихід, добре. Виконаний файл, який змінюється під час роботи, як правило, погана ідея. Приклад, на який ви вказали, цього не робить.
На першому місці:
ЄДИНИЙ ВИКОНАВЧИЙ КОД У БУДЬ-ЯКІЙ ВИКОНАВЧИЙ СКРИПТ СКЛАДУЄ #!
САМО
(хоча навіть #!
є офіційно не визначено )
{ cat >|./file
chmod +x ./file
./file
} <<-\FILE
#!/usr/bin/sh
{ ${l=lsof -p} $$
echo "$l \$$" | sh
} | grep \
"COMMAND\|^..*sh\| [0-9]*[wru] "
#END
FILE
##OUTPUT
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
file 8900 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash
file 8900 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33)
file 8900 mikeserv 0r REG 0,35 108 15496912 /tmp/zshUTTARQ (deleted)
file 8900 mikeserv 1u CHR 136,2 0t0 5 /dev/pts/2
file 8900 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2
file 8900 mikeserv 255r REG 0,33 108 2134129 /home/mikeserv/file
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sh 8906 mikeserv txt REG 0,33 774976 2148676 /usr/bin/bash
sh 8906 mikeserv mem REG 0,30 2148676 /usr/bin/bash (path dev=0,33)
sh 8906 mikeserv 0r FIFO 0,8 0t0 15500515 pipe
sh 8906 mikeserv 1w FIFO 0,8 0t0 15500514 pipe
sh 8906 mikeserv 2u CHR 136,2 0t0 5 /dev/pts/2
{ sed -i \
'1c#!/home/mikeserv/file' ./file
./file
sh -c './file ; echo'
grep '#!' ./file
}
##OUTPUT
zsh: too many levels of symbolic links: ./file
sh: ./file: /home/mikeserv/file: bad interpreter: Too many levels of symbolic links
#!/home/mikeserv/file
Сценарій оболонки це просто текстовий файл - для того , щоб мати якийсь - або ефект на всіх це повинно бути прочитати інший виконуваний файл, його інструкція потім інтерпретована цим іншим виконуваним файлом, поки , нарешті , інший виконуваний файл , то виконує свою інтерпретацію з скрипт оболонки. Це неможливо виконання файлу сценарію оболонки задіяти менше двох файлів. Можливий виняток у zsh
власного компілятора, але з цим у мене мало досвіду, і він тут жодним чином не представлений.
Хешбанг сценарію оболонки повинен вказувати на призначеного інтерпретатора або відкидати його як нерелевантне.
Оболонка має два основні режими розбору та інтерпретації її вводу: або її поточний вхід визначає а, <<here_document
або визначає а { ( command |&&|| list ) ; } &
- іншими словами, оболонка або інтерпретує маркер як роздільник для команди, яку він повинен виконати, як тільки прочитав його в інструкції або як створити файл і відобразити його в дескрипторі файлів для іншої команди. Це воно.
При інтерпретації команд для виконання оболонки розмежовуються лексеми на наборі зарезервованих слів.Коли оболонка виявляє отвір маркерів він повинен продовжувати читати в списку команд , поки перелік не буде або розмежовуються закриттями маркерів , такі як символ нової рядок - коли це може бути застосовано - або закриття фішки як })
для ({
перед виконанням.
Оболонка розрізняє просту команду і складну команду. Команда з'єднання являє собою набір команд , які повинні бути лічені в перед виконанням, але оболонка не виконує $expansion
ні на одному з його складові команд простих до тих пір, поки по окремо виконує кожен з них.
Отже, у наступному прикладі ;semicolon
зарезервовані слова розмежовують окремі прості команди, тоді як \newline
символ, який не уникнув, розмежовує між двома складеними командами:
{ cat >|./file
chmod +x ./file
./file
} <<-\FILE
#!/usr/bin/sh
echo "simple command ${sc=1}" ;\
: > $0 ;\
echo "simple command $((sc+2))" ;\
sh -c "./file && echo hooray"
sh -c "./file && echo hooray"
#END
FILE
##OUTPUT
simple command 1
simple command 3
hooray
Це спрощення керівних принципів. Це набагато складніше, якщо врахувати оболонки, вкладені в оболонки, поточне середовище тощо, але для моїх цілей цього тут достатньо.
І якщо говорити про вбудованих модулях та командних списках,function() { declaration ; }
є лише засобом призначення з'єднання команди до простої команді. Оболонка не повинна виконувати жодну$expansions
заяв декларації - включати <<redirections>
-, а повинна зберігати визначення як єдиний, буквальний рядок і виконувати його як спеціальну вбудовану оболонку, коли її вимагають.
Таким чином, функція оболонки, оголошена у виконавчому скрипті оболонки, зберігається в пам'яті оболонки інтерпретації у її буквальному рядковому вигляді - нерозширюється, щоб включити додані тут документи як вхідні дані - і виконується незалежно від його вихідного файлу кожного разу, коли вона викликається як оболонка, протягом тих пір, поки триває поточне середовище оболонки.
Оператори перенаправлення <<
і <<-
обидва дозволяють перенаправляти рядки, що містяться у вхідному файлі оболонки, відомому як тут-документ, на вхід команди.
Цей документ розглядається як одне слово, яке починається після наступного \newline
і продовжується, поки не з’явиться рядок, що містить лише роздільники та a \newline
, без [:blank:]
проміжків s. Потім починається наступний документ тут , якщо такий є. Формат такий:
[n]<<word
here-document
delimiter
... де необов'язково n
представляє номер дескриптора файлу. Якщо число опущено, цей документ стосується стандартного введення (дескриптор файлу 0).
for shell in dash zsh bash sh ; do sudo $shell -c '
{ readlink /proc/self/fd/3
cat <&3
} 3<<-FILE
$0
FILE
' ; done
#OUTPUT
pipe:[16582351]
dash
/tmp/zshqs0lKX (deleted)
zsh
/tmp/sh-thd-955082504 (deleted)
bash
/tmp/sh-thd-955082612 (deleted)
sh
Розумієш? Для кожної оболонки над оболонкою створюється файл і відображається його в дескрипторі файлу. У zsh, (ba)sh
оболонці створюється звичайний файл у /tmp
, скидається висновок, відображається його в дескрипторі, а потім видаляється /tmp
файл, щоб копія дескриптору ядра була все, що залишилося. dash
уникає всіх цих дурниць і просто переводить свою обробку виводу в анонімний |pipe
файл, спрямований на <<
ціль переадресації .
Це робить dash
:
cmd <<HEREDOC
$(cmd)
HEREDOC
функціонально еквівалентний bash
's:
cmd <(cmd)
в той час як dash
реалізація є принаймні POSIXly портативною.
ЯК РОБИТИ РІЗНІ ФАЙЛИ
Тож у відповіді нижче, коли я це роблю:
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_fn() { printf '#!' ; command -v zsh ; cat
} <<SCRIPT >$0
[SCRIPT BODY]
SCRIPT
_fn ; exec $0
FILE
Буває таке:
Я спочатку cat
вміст будь-який файл оболонки , створений для FILE
в./file
, зробити його виконуваним, а потім виконати його.
Ядро інтерпретує #!
і виклики /usr/bin/sh
з <read
дескриптором файлу , призначеним ./file
.
sh
відображає рядок у пам'ять, що складається з складної команди, що починається на _fn()
і закінчується в SCRIPT
.
Коли _fn
викликається, sh
спочатку слід інтерпретувати, а потім відобразити в дескрипторі файл, визначений в ньому, <<SCRIPT...SCRIPT
перш ніж викликати _fn
як спеціальну вбудовану утиліту, оскільки SCRIPT
це_fn
«s<input.
Виведення рядка з допомогою printf
і command
виписаний на _fn
«s стандартний вихід >&1
- який перенаправляється на поточному оболочечном ARGV0
- або$0
.
cat
об'єднує <&0
стандартний вхідний файл-дескриптор - SCRIPT
- над аргументом >
усіченої поточної оболонки ARGV0
, або $0
.
Виконуючи свою вже прочитану в поточній складовій команді , sh exec
s виконуваний - і знову переписаний - $0
аргумент.
З часу ./file
виклику до тих пір, поки його вміщені інструкції не вказують, що він повинен бути exec
d знову, sh
читає його в одній складеній команді за один раз, коли він виконує їх, а ./file
сам не робить нічого, крім того, щоб радісно прийняти його новий вміст. Файли, які насправді працюють, є/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.
ДЯКУЄМО, ПІСЛЯ ВСІХ
Отже, коли @ jw013 вказує, що:
Файл, який приймає введення та виробляє вихід, добре ...
... серед своєї помилкової критики щодо цієї відповіді він насправді мимоволі потурає єдиному методу, що використовується тут, який, в основному, працює просто:
cat <new_file >old_file
ВІДПОВІДЬ
Усі відповіді тут хороші, але жодна з них не є повністю коректною. Начебто всі стверджують, що ви не можете динамічно та постійно прокладати шлях#!bang
. Ось демонстрація встановлення незалежного шебангу:
DEMO
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_rewrite_me() { printf '#!' ; command -v zsh
${out+cat} ; ${out+:} . /dev/fd/0 >&2
} <<\SCRIPT >|${out-/dev/null}
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
SCRIPT
_rewrite_me ; out=$0 _rewrite_me ; exec $0
FILE
ВИХІД
$0 : ./file
lines : 13
!bang : #!/usr/bin/sh
shell : /usr/bin/sh
1 > #!/usr/bin/sh
2 > _rewrite_me() { printf '#!' ; command -v zsh
...
12 > SCRIPT
13 > _rewrite_me ; out=$0 _rewrite_me ; exec $0
$0 : /home/mikeserv/file
lines : 8
!bang : #!/usr/bin/zsh
shell : /usr/bin/zsh
1 > #!/usr/bin/zsh
2 > printf "
...
7 > sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
8 > sed -e 'N;s/\n/ >\t/' -e 4a\\...
Розумієш? Ми просто змушуємо сценарій перезаписати себе. І це відбувається лише один раз після git
синхронізації. З цього моменту він отримав правильний шлях у рядку #!
Зараз майже все це є просто пух. Для цього безпечно вам потрібно:
Функція, визначена вгорі і викликана внизу, яка виконує написання. Таким чином ми зберігаємо все необхідне в пам'яті і забезпечуємо зачитування всього файлу, перш ніж ми починаємо писати його.
Якийсь спосіб визначення, яким повинен бути шлях. command -v
досить добре для цього.
Гередоки справді допомагають, оскільки вони фактичні файли. Тим часом вони зберігатимуть ваш сценарій. Ви також можете використовувати рядки, але ...
Ви повинні переконатися, що оболонка читає команду, яка перезаписує ваш скрипт у тому ж списку команд, що і той, що виконує його.
Подивіться:
{ cat >|./file
chmod +x ./file
./file
} <<\FILE
#!/usr/bin/sh
_rewrite_me() { printf '#!' ; command -v zsh
${out+cat} ; ${out+:} . /dev/fd/0 >&2
} <<\SCRIPT >|${out-/dev/null}
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
SCRIPT
_rewrite_me ; out=$0 _rewrite_me
exec $0
FILE
Зауважте, що я перемістив exec
команду лише на один рядок. Зараз:
#OUTPUT
$0 : ./file
lines : 14
!bang : #!/usr/bin/sh
shell : /usr/bin/sh
1 > #!/usr/bin/sh
2 > _rewrite_me() { printf '#!' ; command -v zsh
...
13 > _rewrite_me ; out=$0 _rewrite_me
14 > exec $0
Я не отримую другу половину виводу, оскільки сценарій не може прочитати в наступній команді. Але тому, що єдиною командою, яка відсутня, була остання:
cat ./file
#!/usr/bin/zsh
printf "
\$0 :\t$0
lines :\t$((c=$(wc -l <$0)))
!bang :\t$(sed 1q "$0")
shell :\t"$(printf `ps -o args= -p $$`)\\n\\n
sed -n "1,2{=;p};$((c-1)),\${=;p}" "$0" |
sed -e 'N;s/\n/ >\t/' -e 4a\\...
Сценарій вийшов так, як і мав бути - здебільшого тому, що це все було в гередоці - але якщо ви не плануєте його правильно, ви можете усікати свій файл файлів, що саме сталося зі мною вище.
env
, що немає в / bin та / usr / bin? Спробуйтеwhich -a env
підтвердити.