NOTA: @ jw013 formula la seguente obiezione non supportata nei commenti seguenti:
Il downvote è perché il codice auto-modificante è generalmente considerato una cattiva pratica. Ai vecchi tempi di piccoli programmi di assemblaggio era un modo intelligente per ridurre i rami condizionati e migliorare le prestazioni, ma oggigiorno i rischi per la sicurezza superano i vantaggi. Il tuo approccio non funzionerebbe se l'utente che eseguiva lo script non avesse i privilegi di scrittura sullo script.
Ho risposto le sue obiezioni di sicurezza sottolineando che eventuali permessi speciali sono richiesti solo una volta per installare / aggiornare azione al fine di installare / aggiornare l'autoinstallante sceneggiatura - che io personalmente chiamo abbastanza sicuro. Gli ho anche indicato un man sh
riferimento al raggiungimento di obiettivi simili con mezzi simili. A quel tempo non mi preoccupavo di sottolineare che qualsiasi difetto di sicurezza o altre pratiche generalmente sconsigliate che potevano o non potevano essere rappresentate nella mia risposta, erano più probabilmente radicati nella domanda stessa che nella mia risposta ad essa:
Come posso impostare shebang in modo che l'esecuzione dello script come /path/to/script.sh usi sempre lo Zsh disponibile in PATH?
Non soddisfatto, @ jw013 ha continuato a obiettare promuovendo il suo argomento non ancora supportato con almeno un paio di affermazioni errate:
Si utilizza un singolo file, non due file. Il
pacchetto [ man sh
referenziato] ha un file per modificare un altro file. Hai un file che si modifica da solo. C'è una netta differenza tra questi due casi. Un file che accetta input e produce output va bene. Un file eseguibile che cambia se stesso mentre viene eseguito è generalmente una cattiva idea. L'esempio che hai indicato non lo fa.
Innanzitutto:
L'UNICO CODICE ESEGUIBILE IN QUALSIASI SCRIPT DI SHELL ESECUTABILE È IL#!
STESSO
(anche se #!
è ufficialmente non specificato )
{ 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
Uno script di shell è solo un file di testo - affinché abbia qualche effetto, deve essere letto da un altro file eseguibile, le sue istruzioni vengono quindi interpretate da quell'altro file eseguibile, prima che l'altro file eseguibile esegua quindi la sua interpretazione del script di shell. Non è possibile che l'esecuzione di un file di script shell coinvolga meno di due file. C'è una possibile eccezione nel zsh
compilatore, ma con questo ho poca esperienza e qui non è in alcun modo rappresentato.
L'hashbang di uno script di shell deve indicare l' interprete previsto o essere scartato come irrilevante.
La shell ha due modalità di base per analizzare e interpretare il suo input: o il suo input corrente sta definendo un <<here_document
oppure sta definendo un { ( command |&&|| list ) ; } &
- in altre parole, la shell interpreta un token come delimitatore per un comando che dovrebbe eseguire dopo averlo letto in o come istruzioni per creare un file e mapparlo a un descrittore di file per un altro comando. Questo è tutto.
Quando si interpretano i comandi per eseguire la shell, delimita i token su una serie di parole riservate. Quando la shell incontra un token di apertura, deve continuare a leggere in un elenco di comandi fino a quando l'elenco non viene delimitato da un token di chiusura come una nuova riga - se applicabile - o dal token di chiusura come })
per({
prima dell'esecuzione.
La shell distingue tra un comando semplice e un comando composto. Il comando composto è l'insieme di comandi che devono essere letti prima dell'esecuzione, ma la shell non esegue $expansion
su nessuno dei suoi comandi costituenti semplici fino a quando non esegue singolarmente ciascuno di essi.
Quindi, nell'esempio seguente, le ;semicolon
parole riservate delimitano i singoli comandi semplici mentre il carattere senza escape \newline
delimita tra i due comandi composti:
{ 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
Questa è una semplificazione degli orientamenti. Diventa molto più complicato se si considerano i built-in della shell, i subshells, l'ambiente attuale e così via, ma, per i miei scopi qui, è abbastanza.
E parlando di built-in e liste di comandi, a function() { declaration ; }
è semplicemente un mezzo per assegnare un comando composto a un comando semplice. La shell non deve eseguire alcuna $expansions
istruzione sulla dichiarazione stessa - per includere <<redirections>
- ma deve invece memorizzare la definizione come singola stringa letterale ed eseguirla come shell speciale incorporata quando viene chiamata.
Quindi una funzione di shell dichiarata in uno script di shell eseguibile viene memorizzata nella memoria della shell di interpretazione nella sua forma letterale di stringa - non espansa per includere qui i documenti allegati come input - ed eseguita indipendentemente dal suo file sorgente ogni volta che viene chiamata come shell incorporata - fino a quando dura l'attuale ambiente della shell.
Gli operatori di reindirizzamento <<
ed <<-
entrambi consentono il reindirizzamento delle righe contenute in un file di input della shell, noto come documento here, all'input di un comando.
Il documento qui deve essere trattato come una singola parola che inizia dopo la successiva \newline
e continua fino a quando non vi è una riga contenente solo il delimitatore e una \newline
, senza [:blank:]
s in mezzo. Quindi inizia il prossimo documento qui , se ce n'è uno. Il formato è il seguente:
[n]<<word
here-document
delimiter
... dove l'opzione facoltativa n
rappresenta il numero del descrittore di file. Se il numero viene omesso, il documento qui si riferisce all'input standard (descrittore di file 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
Vedi? Per ogni shell sopra la shell crea un file e lo mappa su un descrittore di file. Nella zsh, (ba)sh
shell crea un file normale /tmp
, scarica l'output, lo mappa su un descrittore, quindi elimina il /tmp
file in modo che la copia del descrittore del kernel sia tutto ciò che rimane. dash
evita tutte queste sciocchezze e semplicemente elimina la sua elaborazione dell'output in un |pipe
file anonimo finalizzato al <<
target di reindirizzamento .
Questo rende dash
:
cmd <<HEREDOC
$(cmd)
HEREDOC
funzionalmente equivalente a bash
's:
cmd <(cmd)
mentre dash
l'implementazione è almeno POSIXly portabile.
CHE RENDE MOLTI FILE
Quindi nella risposta qui sotto quando lo faccio:
{ 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
Succede quanto segue:
I primi cat
il contenuto di qualsiasi file di guscio realizzato per FILE
in ./file
, renderlo eseguibile, quindi eseguirlo.
Il kernel interpreta #!
e chiama /usr/bin/sh
con un <read
descrittore di file assegnato a ./file
.
sh
mappa una stringa in memoria costituita dal comando composto che inizia _fn()
e termina in SCRIPT
.
Quando _fn
viene chiamato, sh
deve prima interpretano poi mappare un descrittore di file definito nella <<SCRIPT...SCRIPT
prima di invocare _fn
come una speciale utilità incorporata perché SCRIPT
è _fn
s'<input.
L'uscita stringhe da printf
e command
sono scritti fuori a _fn
's standard di-out >&1
- che viene reindirizzato alla corrente shell ARGV0
- o $0
.
cat
concatena il suo descrittore di file di <&0
input standard - SCRIPT
- sull'argomento >
della shell corrente troncata ARGV0
, oppure $0
.
Completando il comando composto corrente già letto , sh exec
è l' $0
argomento eseguibile - e appena riscritto - .
Dal momento in cui ./file
viene chiamato fino a quando le sue istruzioni contenute specificano che dovrebbe essere exec
nuovamente d, lo sh
legge in un singolo comando composto alla volta mentre li esegue, mentre ./file
se stesso non fa nulla se non accetta felicemente il suo nuovo contenuto. I file che sono effettivamente al lavoro sono/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.
GRAZIE, DOPO TUTTO
Quindi quando @ jw013 specifica che:
Un file che accetta input e produce output va bene ...
... tra le sue errate critiche a questa risposta, in realtà sta inconsapevolmente perdonando l'unico metodo usato qui, che in pratica risolve semplicemente:
cat <new_file >old_file
RISPOSTA
Tutte le risposte qui sono buone, ma nessuna di esse è completamente corretta. Tutti sembrano affermare che non puoi seguire il tuo percorso in modo dinamico e permanente #!bang
. Ecco una dimostrazione di impostazione di un percorso indipendente shebang:
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
PRODUZIONE
$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\\...
Vedi? Facciamo semplicemente sovrascrivere lo script. E succede sempre e solo una volta dopo una git
sincronizzazione. Da quel momento in poi ha preso la strada giusta nella linea #! Bang.
Adesso quasi tutto lassù c'è solo lanugine. Per farlo in sicurezza devi:
Una funzione definita nella parte superiore e chiamata nella parte inferiore che esegue la scrittura. In questo modo archiviamo tutto ciò di cui abbiamo bisogno in memoria e assicuriamo che l'intero file venga letto prima di iniziare a sovrascriverlo.
Qualche modo per determinare quale dovrebbe essere il percorso. command -v
è abbastanza buono per questo.
Heredocs aiuta davvero perché sono file reali. Nel frattempo memorizzeranno la tua sceneggiatura. Puoi usare anche le stringhe ma ...
Devi assicurarti che la shell legga nel comando che sovrascrive il tuo script nella stessa lista di comandi di quello che lo esegue.
Guarda:
{ 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
Notare che ho spostato il exec
comando solo di una riga. Adesso:
#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
Non ottengo la seconda metà dell'output perché lo script non può leggere nel comando successivo. Tuttavia, poiché l'unico comando mancante era l'ultimo:
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\\...
La sceneggiatura è arrivata come avrebbe dovuto - principalmente perché era tutto in eredità - ma se non la pianifichi correttamente puoi troncare il tuo filestream, che è quello che è successo a me sopra.
env
non sia in / bin e / usr / bin? Provawhich -a env
a confermare.