¿Unix shell script descubre en qué directorio reside el archivo de script?

511

Básicamente, necesito ejecutar el script con rutas relacionadas con la ubicación del archivo de script de shell, ¿cómo puedo cambiar el directorio actual al mismo directorio donde reside el archivo de script?

William Yeung
fuente
12
¿Es eso realmente un duplicado? Esta pregunta es sobre un "script de shell de Unix", la otra específicamente sobre Bash.
michaeljt
2
@BoltClock: esta pregunta se cerró incorrectamente. La pregunta vinculada es sobre Bash. Esta pregunta es sobre la programación de shell de Unix. ¡Tenga en cuenta que las respuestas aceptadas son bastante diferentes!
Dietrich Epp
@Dietrich Epp: Tienes razón. Parece que la elección del autor de la respuesta aceptada y la adición de la etiqueta [bash] (probablemente en respuesta a eso) me llevaron a marcar la pregunta como un duplicado en respuesta a una bandera.
BoltClock
2
Creo que esta respuesta es mejor: obtener el directorio fuente de un script Bash desde dentro
Alan Zhiliang Feng

Respuestas:

568

En Bash, deberías obtener lo que necesitas así:

#!/usr/bin/env bash

BASEDIR=$(dirname "$0")
echo "$BASEDIR"
TheMarko
fuente
17
Esto no funciona si ha llamado al script a través de un enlace simbólico en un directorio diferente. Para que eso funcione, también debe usarlo readlink(consulte la respuesta de todos a continuación)
AndrewR
44
En bash es más seguro usarlo $BASH_SOURCEen lugar de $0, porque $0no siempre contiene la ruta del script que se invoca, como cuando se 'obtiene' un script.
mklement0
2
$BASH_SOURCEes específico de Bash, la pregunta es sobre el script de shell en general.
Ha-Duong Nguyen
77
@auraham: ¡ CUR_PATH=$(pwd)o pwddevuelve el directorio actual (que no tiene que ser el directorio principal de los scripts)!
Andreas Dietrich
55
Probé el método @ mklement0 recomendado, usando $BASH_SOURCE, y devuelve lo que necesitaba. Mi secuencia de comandos se llama desde otra secuencia de comandos y $0regresa .mientras que $BASH_SOURCEdevuelve el subdirectorio correcto (en mi caso scripts).
David Rissato Cruz
402

La publicación original contiene la solución (ignore las respuestas, no agregan nada útil). El interesante trabajo lo realiza el mencionado comando unix readlinkcon opción -f. Funciona cuando el guión es invocado por una ruta absoluta o relativa.

Para bash, sh, ksh:

#!/bin/bash 
# Absolute path to this script, e.g. /home/user/bin/foo.sh
SCRIPT=$(readlink -f "$0")
# Absolute path this script is in, thus /home/user/bin
SCRIPTPATH=$(dirname "$SCRIPT")
echo $SCRIPTPATH

Para tcsh, csh:

#!/bin/tcsh
# Absolute path to this script, e.g. /home/user/bin/foo.csh
set SCRIPT=`readlink -f "$0"`
# Absolute path this script is in, thus /home/user/bin
set SCRIPTPATH=`dirname "$SCRIPT"`
echo $SCRIPTPATH

Ver también: https://stackoverflow.com/a/246128/59087

Alabama.
fuente
12
Nota: No todos los sistemas tienen readlink. Es por eso que recomendé usar pushd / popd (incorporados para bash).
docwhat
23
La -fopción de readlinkhacer algo diferente en OS X (Lion) y posiblemente BSD. stackoverflow.com/questions/1055671/…
Ergwun
99
Para aclarar el comentario de @ Ergwun: -fno es compatible con OS X (a partir de Lion); allí puede soltar la -ftarea de resolver como máximo un nivel de indirección, por ejemplo pushd "$(dirname "$(readlink "$BASH_SOURCE" || echo "$BASH_SOURCE")")", o puede rodar su propio script recursivo de seguimiento de enlaces simbólicos como se demuestra en la publicación vinculada.
mklement0
2
Todavía no entiendo por qué el OP necesitaría el camino absoluto. Reportando "." debería funcionar bien si desea acceder a los archivos relativos a la ruta de los scripts y llamó al script como ./myscript.sh
Stefan Haberl
10
@StefanHaberl Creo que sería un problema si ejecutaras el script mientras tu directorio de trabajo actual fuera diferente de la ubicación del script (por ejemplo sh /some/other/directory/script.sh), en este caso .sería tu pwd, no/some/other/directory
Jon z
51

Un comentario anterior sobre una respuesta lo dijo, pero es fácil pasarlo por alto entre todas las otras respuestas.

Al usar bash:

echo this file: "$BASH_SOURCE"
echo this dir: "$(dirname "$BASH_SOURCE")"

Manual de referencia de Bash, 5.2 Variables de Bash

Daniel
fuente
3
Solo este funciona con script en la ruta del entorno, los más votados no funcionan. ¡gracias!
julio
En su dirname "$BASH_SOURCE"lugar, debe usar para manejar espacios en $ BASH_SOURCE.
Mygod
1
Según la herramienta ShellCheck, una forma más explícita de imprimir el directorio sería: "$ (dirname" $ ​​{BASH_SOURCE {0}} ")" porque BASH_SOURCE es una matriz y, sin el subíndice, el primer elemento se toma de forma predeterminada .
Adrian M.
1
@AdrianM. , quiere corchetes, no llaves, para el índice:"$(dirname "${BASH_SOURCE[0]}")"
Hashbrown
No es bueno para mí ... imprime solo un punto (es decir, el directorio actual, presumiblemente)
JL_SO
40

Asumiendo que estás usando bash

#!/bin/bash

current_dir=$(pwd)
script_dir=$(dirname $0)

echo $current_dir
echo $script_dir

Este script debe imprimir el directorio en el que se encuentra, y luego el directorio en el que se encuentra el script. Por ejemplo, al llamarlo /con el script en /home/mez/, se genera

/
/home/mez

Recuerde, cuando asigne variables desde la salida de un comando, envuelva el comando $(y )- o no obtendrá la salida deseada.

Mez
fuente
3
Esto no funcionará cuando invoque el script desde el directorio actual.
Eric Wang
1
@EricWang siempre estás en el directorio actual.
ctrl-alt-delor
Para mí, $ current_dir es de hecho la ruta desde la que llamo al script. Sin embargo, $ script_dir no es el directorio del script, es solo un punto.
Michael
31

Si estás usando bash ...

#!/bin/bash

pushd $(dirname "${0}") > /dev/null
basedir=$(pwd -L)
# Use "pwd -P" for the path without links. man bash for more info.
popd > /dev/null

echo "${basedir}"
docwhat
fuente
44
Puede reemplazar el pushd/ popdcon cd $(dirname "${0}")y cd -hacer que funcione en otros shells, si tienen un pwd -L.
docwhat
¿Por qué usarías pushd y popd aquí?
qodeninja
1
Por lo tanto, no tengo que almacenar el directorio original en una variable. Es un patrón que uso mucho en funciones y demás. Anida realmente bien, lo cual es bueno.
docwhat
Todavía se está almacenando en la memoria, en una variable, ya sea que se haga referencia a una variable en su script o no. Además, creo que el costo de ejecutar pushd y popd supera con creces los ahorros de no crear una variable Bash local en su script, tanto en ciclos de CPU como en legibilidad.
ingyhere
20

Como sugiere el Marko:

BASEDIR=$(dirname $0)
echo $BASEDIR

Esto funciona a menos que ejecute el script desde el mismo directorio donde reside el script, en cuyo caso obtendrá un valor de '.'

Para solucionar ese problema, use:

current_dir=$(pwd)
script_dir=$(dirname $0)

if [ $script_dir = '.' ]
then
script_dir="$current_dir"
fi

Ahora puede usar la variable current_dir a lo largo de su script para referirse al directorio del script. Sin embargo, esto aún puede tener el problema del enlace simbólico.

ranamalo
fuente
20

La mejor respuesta para esta pregunta se respondió aquí:
Obtener el directorio fuente de un script Bash desde dentro

Y es:

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

One-liner que le dará el nombre completo del directorio de la secuencia de comandos sin importar de dónde se llame.

Para entender cómo funciona, puede ejecutar el siguiente script:

#!/bin/bash

SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
  TARGET="$(readlink "$SOURCE")"
  if [[ $TARGET == /* ]]; then
    echo "SOURCE '$SOURCE' is an absolute symlink to '$TARGET'"
    SOURCE="$TARGET"
  else
    DIR="$( dirname "$SOURCE" )"
    echo "SOURCE '$SOURCE' is a relative symlink to '$TARGET' (relative to '$DIR')"
    SOURCE="$DIR/$TARGET" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
  fi
done
echo "SOURCE is '$SOURCE'"
RDIR="$( dirname "$SOURCE" )"
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
if [ "$DIR" != "$RDIR" ]; then
  echo "DIR '$RDIR' resolves to '$DIR'"
fi
echo "DIR is '$DIR'"
Alexandro de Oliveira
fuente
14
cd $(dirname $(readlink -f $0))
azulado
fuente
10

Hagámoslo un POSIX oneliner:

a="/$0"; a=${a%/*}; a=${a#/}; a=${a:-.}; BASEDIR=$(cd "$a"; pwd)

Probado en muchos shells compatibles con Bourne, incluidos los BSD.

Hasta donde sé, soy el autor y lo puse en dominio público. Para obtener más información, consulte: https://www.jasan.tk/posts/2017-05-11-posix_shell_dirname_replacement/

revs Ján Sáreník
fuente
1
como está escrito, cd: too many argumentssi espacios en la ruta, y vuelve $PWD. (una solución obvia, pero solo muestra cuántos casos extremos realmente hay)
michael
1
Votaría. Excepto por el comentario de @michael de que falla con espacios en la ruta ... ¿Hay alguna solución para eso?
Spechter
1
@spechter sí, hay una solución para eso. Ver actualizado jasan.tk/posix/2017/05/11/posix_shell_dirname_replacement
Ján Sáreník
@Der_Meister - por favor sea más específico. O escriba (y posiblemente cifre) un correo electrónico a jasan en jasan.tk
Ján Sáreník
2
ln -s / home / der / 1 / test / home / der / 2 / test && / home / der / 2 / test => / home / der / 2 (debería mostrar la ruta al script original)
Der_Meister
10
BASE_DIR="$(cd "$(dirname "$0")"; pwd)";
echo "BASE_DIR => $BASE_DIR"
Searchrome
fuente
3
La forma más confiable no específica de bash que conozco.
eddygeek
10

Si desea obtener el directorio de script real (independientemente de si está invocando el script usando un enlace simbólico o directamente), intente:

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

Esto funciona tanto en Linux como en macOS. No pude ver a nadie aquí mencionar realpath. No estoy seguro de si hay algún inconveniente en este enfoque.

en macOS, necesita instalar coreutilspara usar realpath. Por ejemplo: brew install coreutils.

Rohith
fuente
6

INTRODUCCIÓN

Esta respuesta corrige la respuesta muy rota pero sorprendentemente mejor votada de este hilo (escrita por TheMarko):

#!/usr/bin/env bash

BASEDIR=$(dirname "$0")
echo "$BASEDIR"

¿POR QUÉ EL USO DE dirname "$ 0" EN SU PROPIO NO FUNCIONA?

dirname $ 0 solo funcionará si el usuario inicia el script de una manera muy específica. Pude encontrar varias situaciones en las que esta respuesta falla y bloquea el script.

En primer lugar, comprendamos cómo funciona esta respuesta. Está obteniendo el directorio de script haciendo

dirname "$0"

$ 0 representa la primera parte del comando que llama al script (es básicamente el comando ingresado sin los argumentos:

/some/path/./script argument1 argument2

$ 0 = "/ some / path /./ script"

dirname básicamente encuentra el último / en una cadena y lo trunca allí. Entonces si lo haces:

  dirname /usr/bin/sha256sum

obtendrá: / usr / bin

Este ejemplo funciona bien porque / usr / bin / sha256sum es una ruta formateada correctamente pero

  dirname "/some/path/./script"

no funcionaría bien y te daría:

  BASENAME="/some/path/." #which would crash your script if you try to use it as a path

Digamos que está en el mismo directorio que su script y lo inicia con este comando

./script   

$ 0 en esta situación será ./script y dirname $ 0 dará:

. #or BASEDIR=".", again this will crash your script

Utilizando:

sh script

Sin ingresar la ruta completa también dará un BASEDIR = "."

Usando directorios relativos:

 ../some/path/./script

Da un nombre de directorio $ 0 de:

 ../some/path/.

Si está en el directorio / some y llama al script de esta manera (observe la ausencia de / al principio, nuevamente una ruta relativa):

 path/./script.sh

Obtendrá este valor para dirname $ 0:

 path/. 

y ./path/./script (otra forma de la ruta relativa) da:

 ./path/.

Las únicas dos situaciones en las que trabajará con base $ 0 es si el usuario usa sh o touch para iniciar un script porque ambas generarán $ 0:

 $0=/some/path/script

que le dará una ruta que puede usar con dirname.

LA SOLUCIÓN

Debería tener en cuenta y detectar cada una de las situaciones mencionadas anteriormente y aplicar una solución si surge:

#!/bin/bash
#this script will only work in bash, make sure it's installed on your system.

#set to false to not see all the echos
debug=true

if [ "$debug" = true ]; then echo "\$0=$0";fi


#The line below detect script's parent directory. $0 is the part of the launch command that doesn't contain the arguments
BASEDIR=$(dirname "$0") #3 situations will cause dirname $0 to fail: #situation1: user launches script while in script dir ( $0=./script)
                                                                     #situation2: different dir but ./ is used to launch script (ex. $0=/path_to/./script)
                                                                     #situation3: different dir but relative path used to launch script
if [ "$debug" = true ]; then echo 'BASEDIR=$(dirname "$0") gives: '"$BASEDIR";fi                                 

if [ "$BASEDIR" = "." ]; then BASEDIR="$(pwd)";fi # fix for situation1

_B2=${BASEDIR:$((${#BASEDIR}-2))}; B_=${BASEDIR::1}; B_2=${BASEDIR::2}; B_3=${BASEDIR::3} # <- bash only
if [ "$_B2" = "/." ]; then BASEDIR=${BASEDIR::$((${#BASEDIR}-1))};fi #fix for situation2 # <- bash only
if [ "$B_" != "/" ]; then  #fix for situation3 #<- bash only
        if [ "$B_2" = "./" ]; then
                #covers ./relative_path/(./)script
                if [ "$(pwd)" != "/" ]; then BASEDIR="$(pwd)/${BASEDIR:2}"; else BASEDIR="/${BASEDIR:2}";fi
        else
                #covers relative_path/(./)script and ../relative_path/(./)script, using ../relative_path fails if current path is a symbolic link
                if [ "$(pwd)" != "/" ]; then BASEDIR="$(pwd)/$BASEDIR"; else BASEDIR="/$BASEDIR";fi
        fi
fi

if [ "$debug" = true ]; then echo "fixed BASEDIR=$BASEDIR";fi
thebunnyrules
fuente
4

Esta línea le dice dónde está el script de shell, no importa si lo ejecutó o si lo obtuvo . Además, resuelve los enlaces simbólicos involucrados, si ese es el caso:

dir=$(dirname $(test -L "$BASH_SOURCE" && readlink -f "$BASH_SOURCE" || echo "$BASH_SOURCE"))

Por cierto, supongo que estás usando / bin / bash .

Richard Gomes
fuente
2

Tantas respuestas, todas plausibles, cada una con ventajas y desventajas y objetivos ligeramente diferentes (que probablemente deberían establecerse para cada una). Aquí hay otra solución que cumple con el objetivo principal de ser claro y trabajar en todos los sistemas, en todos los bash (sin suposiciones sobre las versiones readlinku pwdopciones de bash ), y hace razonablemente lo que esperaría que suceda (por ejemplo, resolver enlaces simbólicos es un problema interesante, pero generalmente no es lo que realmente quieres), maneja casos extremos como espacios en las rutas, etc., ignora cualquier error y usa un valor predeterminado sensato si hay algún problema.

Cada componente se almacena en una variable separada que puede usar individualmente:

# script path, filename, directory
PROG_PATH=${BASH_SOURCE[0]}      # this script's name
PROG_NAME=${PROG_PATH##*/}       # basename of script (strip path)
PROG_DIR="$(cd "$(dirname "${PROG_PATH:-$PWD}")" 2>/dev/null 1>&2 && pwd)"
Miguel
fuente
-4

Eso debería hacer el truco:

echo `pwd`/`dirname $0`

Puede parecer feo dependiendo de cómo se invocó y el cwd, pero debería llevarlo a donde necesita ir (o puede ajustar la cadena si le importa cómo se ve).

kenorb
fuente
1
problema de escape de stackoverflow aquí: seguramente debería verse así: `pwd`/`dirname $0`pero aún puede fallar en enlaces simbólicos
Andreas Dietrich