Unix shell脚本如何查找脚本文件所在的目录?

676

基本上我需要使用与shell脚本文件位置相关的路径来运行脚本,如何将当前目录更改为与脚本文件所在目录相同?

您可以使用以下命令将当前工作目录更改为与脚本文件所在目录相同:
``` cd "$(dirname "$0")" ```
其中 `$0` 是脚本文件的名称,`$(dirname "$0")` 获取该文件所在的目录,并使用 `cd` 命令将当前工作目录切换到那个目录。

15
这真的是一个重复的问题吗?这个问题是关于“Unix shell脚本”的,而另一个问题则特别涉及Bash。 - michaeljt
3
@BoltClock:这个问题被错误地关闭了。链接的问题是关于Bash的。而这个问题是关于Unix shell编程的。请注意,被接受的答案是非常不同的! - Dietrich Epp
3
我认为这个答案更好:从Bash脚本内获取源目录 - Alan Zhiliang Feng
2
可能是从内部获取Bash脚本的源目录的重复问题。 - dulgan
1
谷歌把我带到了这里,因为我正在寻找一个纯粹的 POSIX 实现方案。经过更多的搜索,我发现了这个非常详细的答案,解释了为什么它不能被实现,并通过支持流行 shell 的 shimming 方法来解决它。https://dev59.com/fl0a5IYBdhLWcg3w48JP#29835459 - Steve Buzonas
显示剩余2条评论
19个回答

17
BASE_DIR="$(cd "$(dirname "$0")"; pwd)";
echo "BASE_DIR => $BASE_DIR"

4
我知道的最可靠的非bash特定方法。 - eddygeek

8

介绍

这篇答案纠正了这个帖子中最高票的但非常错误的答案(由TheMarko撰写):

#!/usr/bin/env bash

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

为什么只使用dirname "$0"不能正常工作?

dirname $0只有在用户以非常特定的方式启动脚本时才能起作用。我发现了几种情况,这个答案会失败并使脚本崩溃。

首先,让我们了解一下这个答案是如何工作的。他通过执行以下命令来获取脚本目录:

dirname "$0"

$0 代表调用脚本的第一部分命令(它基本上是输入的命令而没有参数:

/some/path/./script argument1 argument2

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

dirname 基本上是在字符串中找到最后一个 / 并将其截断。因此,如果您执行以下操作:

  dirname /usr/bin/sha256sum

你将得到:/usr/bin。 这个例子很好,因为/usr/bin/sha256sum是一个格式正确的路径。
  dirname "/some/path/./script"

无法正常工作,会给您带来以下问题:
  BASENAME="/some/path/." #which would crash your script if you try to use it as a path

假设你的脚本和命令在同一个目录下,你可以使用以下命令启动它:

./script   

在这种情况下,$0 将是 ./script,dirname $0 将会给出:
. #or BASEDIR=".", again this will crash your script

使用:

sh script

没有输入完整路径也会给出BASEDIR="。"

使用相对目录:

 ../some/path/./script

给出$0的目录名:

 ../some/path/.

如果您在 /some 目录中,并以这种方式调用脚本(请注意开头没有 /,再次强调这是相对路径):

 path/./script.sh

您将得到$0的目录名的值:
 path/. 

而 ./path/./script(另一种相对路径形式)则会返回:

 ./path/.

只有两种情况下, basedir $0 才能正常工作,即用户使用 sh 或 touch 启动脚本,因为这两者都会导致 $0:
 $0=/some/path/script

这将为您提供一个可以与dirname一起使用的路径。

解决方案

您需要考虑并检测上述每种情况,并在出现问题时应用修复措施:

#!/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

1
我认为“非常破碎”是一个令人震惊的夸张说法。是的,你按照调用脚本的方式得到了路径。那又怎样?在你调用它的上下文中,它应该仍然是正确的。这可能是你最关心的事情...但是,如果你需要一些规范化的版本和绝对路径,因为你想将其附加到PATH,那么你只需cd到文件夹并获取pwd。但我想通常需要获取路径的原因是为了能够相对于当前路径调用其他脚本,在这种情况下,我无法看出任何这些情况会导致任何问题。 - Timo
除了符号链接外,当解析脚本的实际位置时支持它们有点棘手。但是你上面的主要抱怨没有提到符号链接。 - Timo

7

这个一行命令可以告诉你脚本所在的位置,无论你运行它还是引用它。而且,如果涉及符号链接,它还会解析它们:

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

顺便提一下,我想你正在使用/bin/bash。

4
基本版本:
dir=$(dirname $0)

如果脚本可能通过$PATH被调用,则:
dir=$(dirname $(which $0))

如果脚本可能被这样调用:bash script.sh,那么:
dir=$(dirname $(which $0 2>/dev/null || realpath $0))

如果你感到非常不安全,那么:
dir="$(dirname -- "$(which -- "$0" 2>/dev/null || realpath -- "$0")")"

这个可能可以在不使用_realpath_的情况下完成:dir=$(dirname $(which $0 2>/dev/null || echo $0)) - anton_rh
可能可以在没有_realpath_的情况下完成:dir=$(dirname $(which $0 2>/dev/null || echo $0)) - undefined

2
有很多答案,每个都有道理,各自有不同的目标(每个目标都应该说明)。下面是另一种解决方案,它符合清晰和跨所有系统工作的主要目标,适用于所有bash(不假设bash版本或readlink或pwd选项),并且通常会按预期运行(例如,解析符号链接是一个有趣的问题,但通常不是您实际想要的),处理路径中的空格等边缘情况,忽略任何错误,并在出现问题时使用合理的默认值。
每个组件都存储在单独的变量中,您可以单独使用:
# 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)"

1

0

我发现最稳健的方法之一是

#!/bin/sh
relative_dir=`perl -e 'use Cwd "realpath";$pwd = realpath(shift); $pwd =~ s/\/[^\/]*$//; print $pwd' $0`
cd $relative_dir

可以使用符号链接,并且无论同事们选择哪种shell类型,都能正常工作。


0

使用tcsh,您可以使用:h变量修饰符来检索路径。

一个注意点是,如果脚本作为tcsh myscript.csh执行,则只会获取脚本名称。解决方法是按下面所示验证路径。

#!/bin/tcsh

set SCRIPT_PATH = $0:h
if ( $SCRIPT_PATH == $0 ) then
        set SCRIPT_PATH = "."
endif

$SCRIPT_PATH/compile.csh > $SCRIPT_PATH/results.txt

有关变量修饰符的更多信息可以在https://learnxinyminutes.com/docs/tcsh/找到。


-4

应该就可以了:

echo `pwd`/`dirname $0`

根据调用方式和当前工作目录,它可能看起来很丑,但应该能带你到你需要去的地方(或者如果你在意它的外观,你可以调整字符串)。


1
stackoverflow逃逸问题在这里:它肯定应该像这样:\pwd`/`dirname $0``,但可能仍然会在符号链接上失败。 - Andreas Covidiot

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接