REMARQUE: @ jw013 émet l' objection non prise en charge suivante dans les commentaires ci-dessous:
Le vote négatif est dû au fait que le code auto-modifiant est généralement considéré comme une mauvaise pratique. À l'époque des petits programmes d'assemblage, c'était un moyen intelligent de réduire les branches conditionnelles et d'améliorer les performances, mais de nos jours les risques de sécurité l'emportent sur les avantages. Votre approche ne fonctionnerait pas si l'utilisateur qui a exécuté le script n'avait pas de privilèges d'écriture sur le script.
J'ai répondu à ses objections de sécurité en soulignant que toutes les autorisations spéciales ne sont requises qu'une seule fois par action d' installation / mise à jour afin d' installer / mettre à jour le script auto-installable - que j'appellerais personnellement assez sécurisé. Je lui ai également fait man sh
référence à une référence à la réalisation d'objectifs similaires par des moyens similaires. À l'époque, je n'ai pas pris la peine de souligner que, quelles que soient les failles de sécurité ou les autres pratiques généralement déconseillées qui peuvent ou non être représentées dans ma réponse, elles étaient plus probablement enracinées dans la question elle-même que dans ma réponse:
Comment puis-je configurer le shebang pour que l'exécution du script en tant que /path/to/script.sh utilise toujours le Zsh disponible dans PATH?
Non satisfait, @ jw013 a continué de s'opposer en approfondissant son argument encore non pris en charge avec au moins quelques déclarations erronées:
Vous utilisez un seul fichier, pas deux fichiers. Le
package [ man sh
référencé] a un fichier modifier un autre fichier. Vous avez un fichier qui se modifie. Il existe une nette différence entre ces deux cas. Un fichier qui prend une entrée et produit une sortie est bien. Un fichier exécutable qui se modifie en cours d'exécution est généralement une mauvaise idée. L'exemple que vous avez cité ne fait pas cela.
En premier lieu:
LE SEUL CODE EXÉCUTABLE DANS TOUT SCRIPT EXECUTABLE SHELL EST LE#!
LUI MÊME
(bien que même #!
est officiellement non précisée )
{ 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
Un script shell est juste un fichier texte - pour qu'il ait un quelconque effet, il doit être lu par un autre fichier exécutable, ses instructions ensuite interprétées par cet autre fichier exécutable, avant que finalement l'autre fichier exécutable n'exécute son interprétation de la script shell. Il n'est pas possible que l'exécution d'un fichier de script shell implique moins de deux fichiers. Il existe une exception possible dans zsh
le propre compilateur de, mais avec cela j'ai peu d'expérience et il n'est en aucune façon représenté ici.
Le hashbang d'un script shell doit pointer vers son interpréteur prévu ou être rejeté comme non pertinent.
Le shell a deux modes de base pour analyser et interpréter son entrée: soit son entrée actuelle définit un <<here_document
soit il définit un { ( command |&&|| list ) ; } &
- en d'autres termes, le shell interprète un jeton comme un délimiteur pour une commande qu'il doit exécuter une fois qu'il l'a lu dans ou comme instructions pour créer un fichier et le mapper à un descripteur de fichier pour une autre commande. C'est ça.
Lors de l'interprétation des commandes à exécuter, le shell délimite les jetons sur un ensemble de mots réservés. Lorsque le shell rencontre un jeton d'ouverture, il doit continuer à lire dans une liste de commandes jusqu'à ce que la liste soit délimitée par un jeton de fermeture tel qu'une nouvelle ligne - le cas échéant - ou le jeton de fermeture comme })
pour({
avant l' exécution.
Le shell fait la distinction entre une commande simple et une commande composée. La commande composée est l'ensemble des commandes qui doivent être lues avant l'exécution, mais le shell ne s'exécute $expansion
sur aucune de ses commandes simples constitutives tant qu'il n'exécute pas chacune individuellement.
Ainsi, dans l'exemple suivant, les ;semicolon
mots réservés délimitent des commandes simples individuelles tandis que le caractère non échappé \newline
délimite entre les deux commandes composées:
{ 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
Il s'agit d'une simplification des lignes directrices. Cela devient beaucoup plus compliqué lorsque vous considérez les commandes intégrées au shell, les sous-coquilles, l'environnement actuel , etc., mais, pour mes besoins ici, c'est suffisant.
Et en parlant de commandes intégrées et de listes de commandes, a function() { declaration ; }
est simplement un moyen d'assigner une commande composée à une commande simple. Le shell ne doit exécuter aucune $expansions
instruction sur la déclaration elle-même - à inclure <<redirections>
- mais doit à la place stocker la définition sous la forme d'une chaîne littérale unique et l'exécuter comme un shell spécial intégré lorsqu'il est appelé.
Ainsi, une fonction shell déclarée dans un script shell exécutable est stockée dans la mémoire du shell interpréteur sous sa forme de chaîne littérale - non développée pour inclure les documents joints ici en entrée - et exécutée indépendamment de son fichier source chaque fois qu'elle est appelée en tant que shell intégré - aussi longtemps que dure l'environnement actuel du shell.
Les opérateurs de redirection <<
et les <<-
deux permettent la redirection des lignes contenues dans un fichier d'entrée shell, appelé ici-document, vers l'entrée d'une commande.
Le document ici doit être traité comme un seul mot qui commence après le suivant \newline
et continue jusqu'à ce qu'il y ait une ligne contenant uniquement le délimiteur et un \newline
, sans [:blank:]
s entre les deux. Ensuite, le prochain document ici commence, s'il y en a un. Le format est le suivant:
[n]<<word
here-document
delimiter
... où l'option n
représente le numéro de descripteur de fichier. Si le nombre est omis, le document ici fait référence à une entrée standard (descripteur de fichier 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
Tu vois? Pour chaque shell au-dessus du shell crée un fichier et le mappe à un descripteur de fichier. Dans zsh, (ba)sh
le shell, crée un fichier normal /tmp
, décharge la sortie, le mappe à un descripteur, puis supprime le /tmp
fichier afin que la copie du noyau du descripteur soit tout ce qui reste. dash
évite toutes ces absurdités et laisse simplement son traitement de sortie dans un |pipe
fichier anonyme destiné à la <<
cible de redirection .
Cela fait dash
:
cmd <<HEREDOC
$(cmd)
HEREDOC
fonctionnellement équivalent à bash
:
cmd <(cmd)
tandis que dash
l'implémentation est au moins POSIXly portable.
QUI FAIT PLUSIEURS FICHIERS
Donc, dans la réponse ci-dessous quand je le fais:
{ 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
Les événements suivants se produisent:
J'ai d' abord cat
le contenu de tout fichier créé pour le shell FILE
dans ./file
, le rendre exécutable, puis l' exécuter.
Le noyau interprète les #!
appels et /usr/bin/sh
avec un <read
descripteur de fichier affecté à ./file
.
sh
mappe une chaîne en mémoire composée de la commande composée commençant à _fn()
et se terminant à SCRIPT
.
Quand _fn
est appelé, il sh
faut d' abord la carte puis à interpréter compte un descripteur du fichier défini dans <<SCRIPT...SCRIPT
avant d' invoquer _fn
comme très spécial , car l' utilité SCRIPT
est _fn
« s<input.
La sortie des chaînes par printf
et command
sont écrites dans _fn
l » standard out >&1
- qui est redirigé vers la coquille de courant ARGV0
- ou $0
.
cat
concatène son descripteur de fichier d' <&0
entrée standard - SCRIPT
- sur l' argument >
du shell courant tronqué ARGV0
, ou $0
.
Complétant sa commande composée actuelle déjà lue , sh exec
s l' $0
argument exécutable - et nouvellement réécrit - .
Du moment où il ./file
est appelé jusqu'à ce que ses instructions contenues spécifient qu'il doit être à exec
nouveau d, le sh
lit dans une seule commande composée à la fois pendant qu'il les exécute, tandis que ./file
lui - même ne fait rien du tout, sauf accepte avec plaisir son nouveau contenu. Les fichiers qui sont réellement au travail sont/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.
MERCI, APRÈS TOUT
Ainsi, lorsque @ jw013 spécifie que:
Un fichier qui prend une entrée et produit une sortie est très bien ...
... au milieu de sa critique erronée de cette réponse, il approuve en fait involontairement la seule méthode utilisée ici, qui fonctionne essentiellement comme suit:
cat <new_file >old_file
RÉPONDRE
Toutes les réponses ici sont bonnes, mais aucune d'entre elles n'est entièrement correcte. Tout le monde semble prétendre que vous ne pouvez pas suivre votre chemin de manière dynamique et permanente #!bang
. Voici une démonstration de la configuration d'un shebang indépendant du chemin:
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
PRODUCTION
$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\\...
Tu vois? Nous faisons juste écraser le script lui-même. Et cela ne se produit qu'une seule fois après une git
synchronisation. À partir de là, il a le bon chemin dans la ligne #! Bang.
Maintenant, presque tout cela n'est que duvet. Pour ce faire, vous avez besoin en toute sécurité:
Une fonction définie en haut et appelée en bas qui fait l'écriture. De cette façon, nous stockons tout ce dont nous avons besoin en mémoire et nous assurons que le fichier entier est lu avant de commencer à l'écrire.
Une façon de déterminer quel devrait être le chemin. command -v
est assez bon pour ça.
Les Heredocs aident vraiment car ce sont de vrais fichiers. Ils entreposeront votre script en attendant. Vous pouvez également utiliser des chaînes mais ...
Vous devez vous assurer que le shell lit dans la commande qui écrase votre script dans la même liste de commandes que celle qui l'exécute.
Regardez:
{ 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
Notez que je n'ai déplacé la exec
commande que d'une ligne. Maintenant:
#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
Je n'obtiens pas la seconde moitié de la sortie car le script ne peut pas lire dans la commande suivante. Pourtant, parce que la seule commande manquante était la dernière:
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\\...
Le script est apparu comme il se doit - principalement parce que tout était dans l'hérédoc - mais si vous ne le planifiez pas correctement, vous pouvez tronquer votre flux de fichiers, ce qui m'est arrivé ci-dessus.
env
ne pas être dans / bin et / usr / bin? Essayezwhich -a env
de confirmer.