NOTA: @ jw013 hace la siguiente objeción no admitida en los comentarios a continuación:
El voto negativo se debe a que el código de auto modificación generalmente se considera una mala práctica. En los viejos tiempos de los pequeños programas de ensamblaje, era una forma inteligente de reducir las ramas condicionales y mejorar el rendimiento, pero hoy en día los riesgos de seguridad superan las ventajas. Su enfoque no funcionaría si el usuario que ejecutó el script no tuviera privilegios de escritura en el script.
Respondí sus objeciones de seguridad, señalando que cualquier permisos especiales sólo se requiere una vez por instalación / actualización de la acción con el fin de instalar / actualizar el autoinstalable guión - que personalmente yo llamaría bastante segura. También le señalé una man sh
referencia para lograr objetivos similares por medios similares. En ese momento, no me molesté en señalar que, independientemente de las fallas de seguridad o de otras prácticas generalmente desaconsejadas que puedan o no estar representadas en mi respuesta, es más probable que se basaran en la pregunta en sí misma que en mi respuesta:
¿Cómo puedo configurar el shebang para que ejecutar el script como /path/to/script.sh siempre use el Zsh disponible en PATH?
No satisfecho, @ jw013 continuó objetando promoviendo su argumento aún no respaldado con al menos un par de declaraciones erróneas:
Utiliza un solo archivo, no dos archivos. El
paquete [ man sh
referenciado] tiene un archivo para modificar otro archivo. Tienes un archivo que se modifica a sí mismo. Hay una clara diferencia entre estos dos casos. Un archivo que toma entrada y produce salida está bien. Un archivo ejecutable que cambia a sí mismo mientras se ejecuta es generalmente una mala idea. El ejemplo que señaló no hace eso.
En primer lugar:
EL ÚNICO CÓDIGO EJECUTABLE EN CUALQUIER ESCRITO SHELL EJECUTABLE ES EL #!
MISMO
(aunque incluso no #!
está oficialmente especificado )
{ 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 de shell es solo un archivo de texto; para que tenga algún efecto, debe ser leído por otro archivo ejecutable, sus instrucciones luego interpretadas por ese otro archivo ejecutable, antes de que finalmente el otro archivo ejecutable ejecute su interpretación del script de shell No es posible que la ejecución de un archivo de script de shell implique menos de dos archivos. Hay una posible excepción en zsh
el compilador propio, pero con esto tengo poca experiencia y de ninguna manera está representado aquí.
El hashbang de un script de shell debe apuntar a su intérprete previsto o debe descartarse como irrelevante.
El shell tiene dos modos básicos de analizar e interpretar su entrada: o su entrada actual está definiendo a <<here_document
o está definiendo a { ( command |&&|| list ) ; } &
- en otras palabras, el shell interpreta un token como un delimitador para un comando que debe ejecutar una vez que lo ha leído en o como instrucciones para crear un archivo y asignarlo a un descriptor de archivo para otro comando. Eso es.
Al interpretar comandos para ejecutar el shell delimita tokens en un conjunto de palabras reservadas. Cuando la carcasa se encuentra una abertura de contadores debe continuar a leer en una lista de comandos hasta que la lista es o bien delimitada por un cierre de contadores, tal como un salto de línea - en su caso - o el cierre token de como })
para ({
antes de la ejecución.
El shell distingue entre un comando simple y un comando compuesto. El comando compuesto es el conjunto de comandos que deben leerse antes de la ejecución, pero el shell no se ejecuta $expansion
en ninguno de sus comandos simples constituyentes hasta que ejecuta individualmente cada uno.
Entonces, en el siguiente ejemplo, las ;semicolon
palabras reservadas delimitan comandos simples individuales mientras que el carácter no escapado \newline
delimita entre los dos comandos compuestos:
{ 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
Esa es una simplificación de las pautas. Se vuelve mucho más complicado cuando se consideran los componentes integrados de shell, subshell, entorno actual, etc., pero, para mis propósitos aquí, es suficiente.
Y hablando de listas incorporadas y de comandos, a function() { declaration ; }
es simplemente un medio de asignar un comando compuesto a un comando simple. El shell no debe realizar ninguna $expansions
en la declaración de declaración en sí, para incluir <<redirections>
, sino que debe almacenar la definición como una cadena literal única y ejecutarla como un shell especial incorporado cuando se le solicite.
Por lo tanto, una función de shell declarada en un script de shell ejecutable se almacena en la memoria del shell de interpretación en su forma de cadena literal, sin expandir para incluir documentos adjuntos aquí como entrada, y se ejecuta independientemente de su archivo fuente cada vez que se llama como un shell incorporado. durante el tiempo que dure el entorno actual del shell.
Los operadores de redirección <<
y <<-
ambos permiten la redirección de líneas contenidas en un archivo de entrada de shell, conocido como documento aquí, a la entrada de un comando.
El documento aquí se tratará como una sola palabra que comienza después de la siguiente \newline
y continúa hasta que haya una línea que contenga solo el delimitador y a \newline
, sin [:blank:]
s en el medio. Entonces comienza el siguiente documento aquí , si hay uno. El formato es el siguiente:
[n]<<word
here-document
delimiter
... donde lo opcional n
representa el número de descriptor de archivo. Si se omite el número, el documento aquí se refiere a la entrada estándar (descriptor de archivo 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
¿Lo ves? Para cada shell arriba, el shell crea un archivo y lo asigna a un descriptor de archivo. En zsh, (ba)sh
el shell crea un archivo normal /tmp
, descarga la salida, la asigna a un descriptor, luego elimina el /tmp
archivo para que la copia del descriptor del núcleo sea todo lo que queda. dash
evita todas esas tonterías y simplemente deja caer su procesamiento de salida en un |pipe
archivo anónimo dirigido al <<
objetivo de redireccionamiento .
Esto hace que dash
:
cmd <<HEREDOC
$(cmd)
HEREDOC
funcionalmente equivalente a bash
's:
cmd <(cmd)
mientras que dash
la implementación es al menos POSIXly portable.
QUE HACE VARIOS ARCHIVOS
Entonces, en la respuesta a continuación cuando lo hago:
{ 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
Sucede lo siguiente:
La primera vez que cat
el contenido de cualquier archivo de la cáscara creado por FILE
dentro ./file
, hacerlo ejecutable, a continuación, ejecutarlo.
El núcleo interpreta las #!
llamadas y /usr/bin/sh
con un <read
descriptor de archivo asignado ./file
.
sh
asigna una cadena a la memoria que consiste en el comando compuesto que comienza en _fn()
y termina en SCRIPT
.
Cuando _fn
se llama, sh
primero debe interpretar a continuación, asignar a un descriptor de archivo definido en <<SCRIPT...SCRIPT
antes de invocar _fn
como un especial utilidad integrada porque SCRIPT
es _fn
's<input.
La salida de cadenas por printf
y command
se escriben en _fn
's estándar de salida >&1
- que es redirigido a la shell actual de ARGV0
- o $0
.
cat
concatena su descriptor de archivo de <&0
entrada estándarSCRIPT
- sobre el argumento >
del shell actual truncado ARGV0
, o $0
.
Completando su comando compuesto actual ya leído , se encuentra sh exec
el $0
argumento ejecutable y recientemente reescrito .
Desde el momento en que ./file
se llama hasta que sus instrucciones contenidas especifiquen que debe volverse exec
a sh
leer , lo lee en un solo comando compuesto a la vez mientras los ejecuta, mientras que en ./file
sí mismo no hace nada excepto aceptar felizmente sus nuevos contenidos. Los archivos que están realmente en el trabajo son/usr/bin/sh, /usr/bin/cat, /tmp/sh-something-or-another.
GRACIAS, DESPUÉS DE TODO
Entonces cuando @ jw013 especifica que:
Un archivo que toma entrada y produce salida está bien ...
... en medio de su crítica errónea de esta respuesta, en realidad está perdonando involuntariamente el único método utilizado aquí, que básicamente funciona solo para:
cat <new_file >old_file
RESPONDER
Todas las respuestas aquí son buenas, pero ninguna de ellas es completamente correcta. Todo el mundo parece afirmar que no puedes seguir tu camino de forma dinámica y permanente #!bang
. Aquí hay una demostración de cómo configurar un camino independiente shebang:
MANIFESTACIÓN
{ 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
SALIDA
$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\\...
¿Lo ves? Simplemente hacemos que el script se sobrescriba. Y solo ocurre una vez después de una git
sincronización. A partir de ese momento, tiene el camino correcto en la línea #! Bang.
Ahora, casi todo eso allí arriba es solo pelusa. Para hacer esto de manera segura necesitas:
Una función definida en la parte superior y llamada en la parte inferior que realiza la escritura. De esta forma, almacenamos todo lo que necesitamos en la memoria y nos aseguramos de que se lea todo el archivo antes de comenzar a escribir sobre él.
Alguna forma de determinar cuál debería ser el camino. command -v
es bastante bueno para eso
Los heredocs realmente ayudan porque son archivos reales. Almacenarán su guión mientras tanto. También puedes usar cadenas pero ...
Debe asegurarse de que el shell lea el comando que sobrescribe su script en la misma lista de comandos que el que lo ejecuta.
Mira:
{ 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
Tenga en cuenta que solo moví el exec
comando una línea hacia abajo. Ahora:
#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
No obtengo la segunda mitad de la salida porque el script no puede leer en el siguiente comando. Aún así, porque el único comando que faltaba era el último:
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 secuencia de comandos se realizó como debería, principalmente porque estaba todo en el documento heredoc, pero si no lo planifica correctamente, puede truncar su flujo de archivos, que es lo que me sucedió anteriormente.
env
que no está en / bin y / usr / bin? Intentawhich -a env
confirmar.