ПРИМЕЧАНИЕ: @ jw013 высказывает следующие неподдерживаемые возражения в комментариях ниже:
Недостаток в том, что самоизменяющийся код обычно считается плохой практикой. В старые времена крошечных программ сборки это был умный способ уменьшить условные переходы и повысить производительность, но в настоящее время риски безопасности перевешивают преимущества. Ваш подход не сработает, если пользователь, запустивший сценарий, не имеет прав на запись в сценарий.
Я ответил на его возражения безопасности, указывая на то , что какие - либо специальные разрешения требуется только один раз за установки / обновления действия для того , чтобы установить / обновить в самоустанавливающийся сценарий - который я лично называю довольно безопасным. Я также указал ему наman sh
ссылку на достижение аналогичных целей с помощью аналогичных средств. В то время я не удосужился указать, что какие бы ни были недостатки в безопасности или вообще неучтенные действия, которые могут или не могут быть представлены в моем ответе, они были скорее укоренены в самом вопросе, чем в моем ответе на него:
Как я могу настроить shebang так, чтобы при запуске сценария как /path/to/script.sh всегда использовался Zsh, доступный в PATH?
Не удовлетворенный, @ jw013 продолжал возражать, дополняя свой пока еще не поддерживаемый аргумент хотя бы парой ошибочных утверждений:
Вы используете один файл, а не два файла. Пакет [ man sh
ссылочный]
содержит один файл для изменения другого файла. У вас есть файл, модифицирующий себя. Между этими двумя случаями есть четкая разница. Файл, который принимает входные данные и производит выходные данные, в порядке. Исполняемый файл, который изменяется сам по себе во время работы, обычно является плохой идеей. Пример, на который вы указали, не делает этого.
На первом месте:
ЕДИНСТВЕННАЯ EXECUTABLE КОД В ЛЮБОЙ EXECUTABLE SHELL SCRIPT ЯВЛЯЕТСЯ#!
ОБОЛОЧКИ
(хотя даже #!
это официально не определено )
{ 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
собственном компиляторе, но с этим у меня мало опыта, и он никоим образом не представлен здесь.
Hashbang сценария оболочки должен указывать на предполагаемый интерпретатор или отбрасываться как не относящийся к делу.
Оболочка имеет два основных режима синтаксического анализа и интерпретации своих входных данных: либо ее текущий ввод определяет, <<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
и продолжается до тех пор , пока не будет строка , содержащая только разделитель и А \newline
, без каких - либо [:blank:]
с между ними. Затем начинается следующий документ здесь , если он есть. Формат выглядит следующим образом:
[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
реализация «S, по крайней мере 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
синхронизации. С этого момента он получил правильный путь в строке #! Bang.
Теперь почти все это просто пух. Для этого вам необходимо:
Функция, определенная вверху и вызываемая внизу, которая выполняет запись. Таким образом, мы сохраняем все необходимое в памяти и гарантируем, что весь файл будет прочитан, прежде чем мы начнем перезаписывать его.
Какой-то способ определить, каким должен быть путь. command -v
довольно хорошо для этого.
Heredocs действительно помогают, потому что они настоящие файлы. Тем временем они сохранят ваш сценарий. Вы можете использовать строки, но ...
Вы должны убедиться, что оболочка считывает команду, которая перезаписывает ваш скрипт в том же списке команд, что и тот, который его исполняет.
Посмотрите:
{ 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
подтвердить.