¿$ 0 siempre incluirá la ruta al script?

11

Quiero obtener el script actual para poder imprimir la ayuda y la información de la versión desde la sección de comentarios en la parte superior.

Estaba pensando en algo como esto:

grep '^#h ' -- "$0" | sed -e 's/#h //'

Pero luego me pregunté qué pasaría si el script se ubicara en un directorio que estaba en PATH y se llamara sin especificar explícitamente el directorio.

Busqué una explicación de las variables especiales y encontré las siguientes descripciones de $0:

  • nombre del shell o programa actual

  • nombre de archivo del script actual

  • nombre del guión en sí

  • comando como se ejecutó

Ninguno de estos aclara si el valor de $0incluiría o no el directorio si se invocara el script sin él. El último realmente me implica que no lo haría.

Prueba en mi sistema (Bash 4.1)

Creé un archivo ejecutable en / usr / local / bin llamado scriptname con una línea echo $0y lo invoqué desde diferentes ubicaciones.

Estos son mis resultados:

> cd /usr/local/bin/test
> ../scriptname
../scriptname

> cd /usr/local/bin
> ./scriptname
./scriptname

> cd /usr/local
> bin/scriptname
bin/scriptname

> cd /tmp
> /usr/local/bin/scriptname
/usr/local/bin/scriptname

> scriptname
/usr/local/bin/scriptname

En estas pruebas, el valor de $0siempre es exactamente cómo se invocó el script, excepto si se invoca sin ningún componente de ruta. En ese caso, el valor de $0es la ruta absoluta . Entonces parece que sería seguro pasar a otro comando.

Pero luego encontré un comentario sobre Stack Overflow que me confundió. La respuesta sugiere usar $(dirname $0)para obtener el directorio del script actual. El comentario (votado 7 veces) dice "eso no funcionará si el script está en su camino".

Preguntas

  • ¿Es correcto ese comentario?
  • ¿Es el comportamiento diferente en otros sistemas?
  • ¿Hay situaciones en las $0que no incluiría el directorio?
toxalot
fuente
La gente ha respondido sobre situaciones en las que $0hay algo más que el guión, que responde el título de la pregunta. Sin embargo, también estoy interesado en situaciones en las que se $0encuentra el script en sí, pero no incluye el directorio. En particular, estoy tratando de entender el comentario hecho sobre la respuesta SO.
toxalot

Respuestas:

17

En los casos más comunes, $0contendrá una ruta, absoluta o relativa a la secuencia de comandos, por lo que

script_path=$(readlink -e -- "$0")

(suponiendo que haya un readlinkcomando y lo admita -e) generalmente es una forma suficientemente buena de obtener la ruta de acceso absoluta canónica al script.

$0 se asigna a partir del argumento que especifica el guión que se pasa al intérprete.

Por ejemplo, en:

the-shell -shell-options the/script its args

$0consigue the/script.

Cuando corres:

the/script its args

Su caparazón hará un:

exec("the/script", ["the/script", "its", "args"])

Si el script contiene un #! /bin/sh -she-bang, por ejemplo, el sistema lo transformará a:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "the/script", "its", "args"])

(si no contiene un she-bang, o más generalmente si el sistema devuelve un error ENOEXEC, entonces es su shell el que hará lo mismo)

Hay una excepción para los scripts setuid / setgid en algunos sistemas, donde el sistema abrirá el script en algunos fd xy se ejecutará en su lugar:

exec("/bin/sh", ["/bin/sh" or "the/script", "-", "/dev/fd/x", "its", "args"])

para evitar condiciones de carrera (en cuyo caso $0contendrá /dev/fd/x).

Ahora, puede argumentar que /dev/fd/x es un camino hacia ese script. Sin embargo $0, tenga en cuenta que si lee , romperá el script a medida que consume la entrada.

Ahora, hay una diferencia si el nombre del comando de script como invocado no contiene una barra inclinada. En:

the-script its args

Su cáscara buscará the-scripten $PATH. $PATHpuede contener rutas absolutas o relativas (incluida la cadena vacía) a algunos directorios. Por ejemplo, si $PATHcontiene /bin:/usr/bin:y the-scriptse encuentra en el directorio actual, el shell hará un:

exec("the-script", ["the-script", "its", "args"])

que se convertirá en:

exec("/bin/sh", ["/bin/sh" or "the-script", "-", "the-script", "its", "args"]

O si se encuentra en /usr/bin:

exec("/usr/bin/the-script", ["the-script", "its", "args"])
exec("/bin/sh", ["/bin/sh" or "the-script" or "/usr/bin/the-script",
     "-", "/usr/bin/the-script", "its", "args")

En todos los casos anteriores, excepto el caso de la esquina setuid, $0contendrá una ruta (absoluta o relativa) al script.

Ahora, un script también se puede llamar como:

the-interpreter the-script its args

Cuando the-scriptcomo arriba no contiene caracteres de barra diagonal, el comportamiento varía ligeramente de un shell a otro.

Las kshimplementaciones antiguas de AT&T realmente buscaban el script incondicionalmente $PATH(lo que en realidad era un error y un agujero de seguridad para los scripts setuid), por lo que en $0realidad no contenía una ruta al script a menos que la $PATHbúsqueda realmente se encontrara the-scripten el directorio actual.

Los nuevos AT&T kshintentarían interpretar the-scripten el directorio actual si es legible. Si no, buscaría un archivo legible y ejecutablethe-script en $PATH.

Para bash, comprueba si the-scriptestá en el directorio actual (y no es un enlace simbólico roto) y si no, las operaciones de búsqueda para un legible (no necesariamente ejecutable) the-scripten $PATH.

zshen shla emulación como lo haría bashexcepto que si the-scriptes un enlace simbólico roto en el directorio actual, no sería buscar un the-scripten $PATHy en su lugar informar de un error.

Todos los otros proyectiles tipo Bourne no miran the-scripthacia adentro $PATH.

De todos modos, para todos esos proyectiles, si encuentra que $0no contiene /ay no es legible, entonces probablemente se haya buscado $PATH. Luego, dado que $PATHes probable que los archivos sean ejecutables, es probable que sea una aproximación segura de usar command -v -- "$0"para encontrar su ruta (aunque eso no funcionaría si $0también fuera el nombre de un shell incorporado o una palabra clave (en la mayoría de los shells)).

Entonces, si realmente quieres cubrir ese caso, puedes escribirlo:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}""; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

(lo que se ""adjunta $PATHes para preservar un elemento vacío final con conchas que $IFSactúa como delimitador en lugar de separador ).

Ahora, hay formas más esotéricas de invocar un script. Se podría hacer:

the-shell < the-script

O:

cat the-script | the-shell

En ese caso, $0será el primer argumento ( argv[0]) que recibió el intérprete (arriba the-shell, pero eso podría ser cualquier cosa, aunque generalmente sea el nombre base o una ruta a ese intérprete).

Detectar que estás en esa situación en función del valor de $0no es confiable. Podrías mirar la salida de ps -o args= -p "$$"para obtener una pista. En el caso de la tubería, no hay una forma real de volver a la ruta del script.

También se podría hacer:

the-shell -c '. the-script' blah blih

Entonces, excepto en zsh(y alguna implementación antigua del shell Bourne), $0sería blah. Nuevamente, es difícil llegar al camino del guión en esos shells.

O:

the-shell -c "$(cat the-script)" blah blih

etc.

Para asegurarse de que tiene el derecho $progname, puede buscar una cadena específica como:

progname=$0
[ -r "$progname" ] || progname=$(
    IFS=:; set -f
    for i in ${PATH-$(getconf PATH)}:; do
      case $i in
        "") p=$progname;;
        */) p=$i$progname;;
        *) p=$i/$progname
      esac
      [ -r "$p" ] && exec printf '%s\n' "$p"
    done
    exit 1
  ) && progname=$(readlink -e -- "$progname") ||
  progname=unknown

[ -f "$progname" ] && grep -q 7YQLVVD3UIUDTA32LSE8U9UOHH < "$progname" ||
  progname=unknown

Pero de nuevo, no creo que valga la pena el esfuerzo.

Stéphane Chazelas
fuente
Stéphane, no entiendo tu uso "-"en los ejemplos anteriores. En mi experiencia, se exec("the-script", ["the-script", "its", "args"])convierte exec("/the/interpreter", ["/the/interpreter", "the-script", "its", "args"]), por supuesto, en la posibilidad de una opción de intérprete.
jrw32982 es compatible con Monica el
@ jrw32982, #! /bin/sh -es el "uso cmd -- somethingsiempre que no pueda garantizar que somethingno comenzará con -" el adagio de buenas prácticas aquí aplicado /bin/sh(donde -el marcador de fin de opción es más portátil que --) con somethingser la ruta / nombre del guión. Si no usa eso para scripts setuid (en sistemas que los admiten pero no con el método / dev / fd / x mencionado en la respuesta), entonces puede obtener un shell raíz creando un enlace simbólico a su script llamado -io -spara ejemplo.
Stéphane Chazelas
Gracias Stéphane. Me había perdido el guión final de su línea shebang de ejemplo. Tuve que buscar dónde está documentado que un solo guión es equivalente a un guión doble pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html .
jrw32982 es compatible con Monica el
Es muy fácil olvidar el guión simple / doble en la línea shebang para un script setuid; de alguna manera el sistema debería encargarse de usted. No permita los scripts setuid por completo o de alguna manera /bin/shdebería deshabilitar su propio procesamiento de opciones si puede detectar que se está ejecutando setuid. No veo cómo usar / dev / fd / x por sí solo soluciona eso. Todavía necesitas el guión simple / doble, creo.
jrw32982 es compatible con Monica el
@ jrw32982, /dev/fd/xcomienza con /, no -. Sin execve()embargo, el objetivo principal es eliminar la condición de carrera entre los dos s (entre el execve("the-script")cual eleva los privilegios y el posterior execve("interpreter", "thescript")donde interpreterabre el script más tarde (que bien podría haber sido reemplazado por un enlace simbólico a otra cosa mientras tanto). Sistemas que implementan los scripts de suid hacen correctamente un execve("interpreter", "/dev/fd/n")lugar donde se ha abierto n como parte del primer execve ().
Stéphane Chazelas
6

Aquí hay dos situaciones en las que el directorio no se incluiría:

> bash scriptname
scriptname

> bash <scriptname
bash

En ambos casos, el directorio actual tendría que ser el directorio donde se encontraba el nombre del script .

En el primer caso, el valor de $0todavía podría pasarse grepya que supone que el argumento FILE es relativo al directorio actual.

En el segundo caso, si la información de ayuda y versión solo se imprime en respuesta a una opción de línea de comando específica, no debería ser un problema. No estoy seguro de por qué alguien invocaría un script de esa manera para imprimir la ayuda o la información de la versión.

Advertencias

  • Si el script cambia el directorio actual, no querrá usar rutas relativas.

  • Si el script tiene su origen, el valor de $0usualmente será el script de la persona que llama en lugar del script de origen.

toxalot
fuente
3

Se puede especificar un argumento arbitrario cero cuando se usa la -copción para la mayoría de los shells (¿todos?). P.ej:

sh -c 'echo $0' argv0

De man bash(elegido únicamente porque tiene una mejor descripción que mi man sh: el uso es el mismo independientemente):

-C

Si la opción -c está presente, los comandos se leen desde el primer argumento sin opción command_string. Si hay argumentos después de command_string, se asignan a los parámetros posicionales, comenzando con $ 0.

Graeme
fuente
Creo que esto funciona porque -c 'comando' son operandos, y argv0es su primer argumento no operativo y de línea de comandos.
mikeserv
@mike correcto, he actualizado con un fragmento de hombre.
Graeme
3

NOTA: Otros ya han explicado la mecánica de, $0así que me saltearé todo eso.

Por lo general, paso por alto todo este problema y simplemente uso el comando readlink -f $0. Esto siempre te devolverá el camino completo de lo que sea que le des como argumento.

Ejemplos

Digamos que estoy aquí para empezar:

$ pwd
/home/saml/tst/119929/adir

Hacer un directorio + archivo:

$ mkdir adir
$ touch afile
$ cd adir/

Ahora comienza a presumir readlink:

$ readlink -f ../adir
/home/saml/tst/119929/adir

$ readlink -f ../
/home/saml/tst/119929

$ readlink -f ../afile 
/home/saml/tst/119929/afile

$ readlink -f .
/home/saml/tst/119929/adir

Trucos adicionales

Ahora, con un resultado coherente que se devuelve cuando interrogamos a $0través de readlink, podemos usar simplemente dirname $(readlink -f $0)para obtener la ruta absoluta al script, o basename $(readlink -f $0)para obtener el nombre real del script.

slm
fuente
0

Mi manpágina dice:

$0: se expande a la namede shello shell script.

Parece que esto se traduce en argv[0]el shell actual, o en el primer argumento de línea de comandos no operativo y al que se alimenta el shell de interpretación actual cuando se invoca. He dicho anteriormente que el sh ./somescriptcamino sería la variable de a , pero esto era incorrecto porque el es un nuevo proceso de su propia e invocado por una nueva .$0 $ENV shshell$ENV

De esta manera sh ./somescript.shdifiere de lo . ./somescript.shque se ejecuta en el entorno actual y $0ya está configurado.

Puede verificar esto comparando $0con /proc/$$/status.

echo 'script="/proc/$$/status"
    echo $0
    cat "$script"' \
    > ./script.sh
sh ./script.sh ; . ./script.sh

Gracias por la corrección, @toxalot. Aprendí algo

mikeserv
fuente
En mi sistema, si es de origen ( . ./myscript.sho source ./myscript.sh), entonces $0es el shell. Pero si se pasa como argumento al shell ( sh ./myscript.sh), entonces $0es la ruta al script. Por supuesto, shen mi sistema está Bash. Entonces no sé si eso hace la diferencia o no.
toxalot
¿Tiene un hashbang? Creo que la diferencia es importante, y puedo agregar que, sin ella, no debería serlo execy, en su lugar, buscarlo, pero con él, debería execserlo.
mikeserv
Con o sin hashbang, obtengo los mismos resultados. Con o sin ser ejecutable, obtengo los mismos resultados.
toxalot
¡Yo también! Creo que es porque es una concha incorporada. Estoy comprobando ...
mikeserv
Las líneas de explosión son interpretadas por el núcleo. Si se ./somescriptejecuta con el punto de referencia #!/bin/sh, sería equivalente a correr /bin/sh ./somescript. De lo contrario, no hacen ninguna diferencia en el caparazón.
Graeme
0

Quiero obtener el script actual para poder imprimir la ayuda y la información de la versión desde la sección de comentarios en la parte superior.

Si bien $0contiene el nombre del script, puede contener una ruta prefijada basada en la forma en que se llama el script, siempre he usado ${0##*/}para imprimir el nombre del script en la salida de ayuda que elimina cualquier ruta principal $0.

Tomado de la guía avanzada de secuencias de comandos Bash - Sección 10.2 sustitución de parámetros

${var#Pattern}

Eliminar de $varla parte más corta $Patternque coincida con el extremo frontal de $var.

${var##Pattern}

Eliminar de $varla parte más larga de $Patterneso coincide con el extremo frontal de $var.

Entonces, la parte más larga de $0esas coincidencias */será el prefijo de ruta completo, devolviendo solo el nombre del script.

sambler
fuente
Sí, también lo hago por el nombre del script que se utiliza en el mensaje de ayuda. Pero estoy hablando de grepping el script, así que necesito tener al menos una ruta relativa. Quiero poner el mensaje de ayuda en la parte superior de los comentarios y no tener que repetir el mensaje de ayuda más tarde al imprimirlo.
toxalot
0

Para algo similar uso:

rPath="$(dirname $(realpath $0))"
echo $rPath 

rPath=$(dirname $(readlink -e -- "$0"))
echo $rPath 

rPath Siempre tiene el mismo valor.

Anónimo
fuente