【问题标题】:Unix shell script find out which directory the script file resides?Unix shell script 找出脚本文件所在的目录?
【发布时间】:2010-09-19 13:02:32
【问题描述】:

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

【问题讨论】:

  • 真的是复制品吗?这个问题是关于“unix shell 脚本”的,另一个是关于 Bash 的。
  • @BoltClock:此问题未正确关闭。链接的问题是关于 Bash 的。这个问题是关于 Unix shell 编程的。请注意,接受的答案完全不同!
  • Google 把我带到了这里,因为我正在寻找解决方案的纯 posix 实现。经过多一点搜索后,我发现了这个非常详细的答案,解释了为什么它不能完成,以及通过填充对流行 shell 的支持来解决它的方法。 *.com/a/29835459/816584

标签: shell unix


【解决方案1】:

基础版:

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")")"

【讨论】:

    【解决方案2】:

    使用 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/

    【讨论】:

      【解决方案3】:

      假设你正在使用 bash

      #!/bin/bash
      
      current_dir=$(pwd)
      script_dir=$(dirname "$0")
      
      echo $current_dir
      echo $script_dir
      

      这个脚本应该打印你所在的目录,然后是脚本所在的目录。例如,当使用/home/mez/中的脚本从/调用它时,它会输出

      /
      /home/mez
      

      请记住,在从命令的输出中分配变量时,请将命令包装在 $() 中 - 否则您将无法获得所需的输出。

      【讨论】:

      • 当我从当前目录调用脚本时,这将不起作用。
      • @EricWang 你总是在当前目录中。
      • 对我来说,$current_dir 确实是我调用脚本的路径。但是,$script_dir 不是脚本的目录,它只是一个点。
      • @Michael script_dir 是相对于 current_dir 的。因此,如果您从存储脚本的目录运行脚本,您将在 script_dir 中简单地得到一个点。
      • $(pwd)$PWD 相比开销很大,POSIX explicitly requires 将在 shell 启动和 cd 上设置。
      【解决方案4】:

      这里回答了这个问题的最佳答案:
      Getting the source directory of a Bash script from within

      它是:

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

      无论从哪里调用脚本,它都会为您提供脚本的完整目录名称。

      要了解它的工作原理,您可以执行以下脚本:

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

      【讨论】:

        【解决方案5】:

        如果您想获取实际的脚本目录(无论您是使用符号链接还是直接调用脚本),请尝试:

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

        这适用于 linux 和 macOS。我在这里看不到有人提到realpath。不确定这种方法是否有任何缺点。

        在 macOS 上,您需要安装 coreutils 才能使用 realpath。例如:brew install coreutils.

        【讨论】:

        • 这似乎在符合 POSIX 的 shell 上运行良好,例如 ashdash(例如在 Alpine Linux 上)
        【解决方案6】:
        BASE_DIR="$(cd "$(dirname "$0")"; pwd)";
        echo "BASE_DIR => $BASE_DIR"
        

        【讨论】:

        • 我所知道的最可靠的非 bash 特定方式。
        【解决方案7】:

        之前对某个答案的评论说过,但在所有其他答案中很容易被忽略。

        使用 bash 时:

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

        Bash Reference Manual, 5.2 Bash Variables

        【讨论】:

        • 只有这一项与环境路径中的脚本一起使用,投票最多的不起作用。谢谢!
        • 您应该使用 dirname "$BASH_SOURCE" 来处理 $BASH_SOURCE 中的空格。
        • 根据 ShellCheck 工具,打印目录的更明确的方法是:"$(dirname "${BASH_SOURCE{0}}")" 因为 BASH_SOURCE 是一个数组,并且没有下标,默认取第一个元素。
        • @AdrianM。 ,您需要括号而不是大括号作为索引:"$(dirname "${BASH_SOURCE[0]}")"
        • 对我没好处..只打印一个点(即当前目录,大概)
        【解决方案8】:

        让它成为一个 POSIX oneliner:

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

        在许多与 Bourne 兼容的 shell 上进行了测试,包括 BSD 的。

        据我所知,我是作者,我把它放到了公共领域。有关更多信息,请参阅: https://www.jasan.tk/posts/2017-05-11-posix_shell_dirname_replacement/

        【讨论】:

        • 如所写,cd: too many arguments 如果路径中有空格,则返回$PWD。 (一个明显的修复,但只显示实际有多少边缘情况)
        • 会投票。除了来自@michael 的评论,它在路径中出现空格时失败......有解决办法吗?
        • @spechter 是的,有一个解决方法。查看更新的jasan.tk/posix/2017/05/11/posix_shell_dirname_replacement
        • @Der_Meister - 请更具体。或者在 jasan.tk 上给 jasan 写(并可能加密)电子邮件
        • ln -s /home/der/1/test /home/der/2/test && /home/der/2/test => /home/der/2(应该显示原来的脚本)
        【解决方案9】:

        这一行告诉你 shell 脚本在哪里,不管你是运行它还是获取它。此外,如果是这种情况,它还会解析所涉及的任何符号链接:

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

        顺便说一句,我想你正在使用 /bin/bash

        【讨论】:

          【解决方案10】:

          简介

          这个答案更正这个线程的非常破碎但令人震惊的最高投票答案(由 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
          

          你会得到 dirname $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
          

          【讨论】:

          • 我会说“非常破碎”是一种令人震惊的夸大其词。是的,您以调用脚本的方式获得路径。怎么样?在您调用它的上下文中,它应该仍然是正确的。这可能是您最关心的..但是如果您需要某种具有绝对路径的规范化版本,因为您想要例如将其附加到PATH,然后您只需将cd 添加到文件夹并获取pwd。但是我猜想获取路径的通常需要是能够调用相对于当前路径的其他脚本,在这种情况下,我看不到任何这些情况如何导致任何问题。
          • ...除了符号链接 ofc。在解析脚本的实际位置的同时支持它们有点棘手。但是您上面的主要抱怨没有提到符号链接。
          【解决方案11】:

          如此多的答案,都是合理的,每个都有优点和缺点以及略有不同的目标(可能应该针对每个人说明)。这是另一个解决方案,它满足了在所有 bash 上清晰和跨所有系统工作的主要目标(没有关于 bash 版本或readlinkpwd 选项的假设),并且可以合理地执行您期望发生的事情(例如,解决符号链接是一个有趣的问题,但通常不是您真正想要的),处理路径中的空格等边缘情况,忽略任何错误并在有任何问题时使用合理的默认值。

          每个组件都存储在一个单独的变量中,您可以单独使用:

          # 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)"
          

          【讨论】:

            【解决方案12】:

            在 Bash 中,你应该得到你需要的东西:

            #!/usr/bin/env bash
            
            BASEDIR=$(dirname "$0")
            echo "$BASEDIR"
            

            【讨论】:

            • 如果您通过不同目录中的符号链接调用脚本,这将不起作用。要完成这项工作,您还需要使用readlink(请参阅下面的回答)
            • 在 bash 中使用$BASH_SOURCE 代替$0 更安全,因为$0 并不总是包含被调用的脚本的路径,例如在“采购”脚本时.
            • $BASH_SOURCE 是特定于 Bash 的,问题一般是关于 shell 脚本的。
            • @auraham: CUR_PATH=$(pwd)pwd 一定要返回当前目录(不必是脚本的父目录)!
            • 我尝试了@mklement0 推荐的方法,使用$BASH_SOURCE,它返回了我需要的内容。我的脚本被另一个脚本调用,$0 返回.$BASH_SOURCE 返回正确的子目录(在我的情况下为scripts)。
            【解决方案13】:

            这应该可以解决问题:

            echo `pwd`/`dirname $0`
            

            根据调用方式和 cwd 的不同,它可能看起来很丑,但应该可以让你到达你需要去的地方(或者如果你关心它的外观,你可以调整字符串)。

            【讨论】:

            • * 逃逸问题在这里:它肯定应该是这样的:`pwd`/`dirname $0` 但在符号链接上仍然可能失败
            【解决方案14】:

            原始帖子包含解决方案(忽略响应,它们不会添加任何有用的东西)。有趣的工作是由上面提到的带有选项-f 的unix 命令readlink 完成的。当脚本被绝对路径和相对路径调用时有效。

            对于 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
            

            对于 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
            

            另请参阅:https://*.com/a/246128/59087

            【讨论】:

            • 注意:并非所有系统都有readlink。这就是为什么我推荐使用 pushd/popd(bash 内置)。
            • -f 选项对readlink 的作用在 OS X (Lion) 和可能的 BSD 上有所不同。 *.com/questions/1055671/…
            • 澄清@Ergwun 的评论:OS X 根本不支持-f(从Lion 开始);在那里,您可以删除-f 以解决最多一个间接级别,例如pushd "$(dirname "$(readlink "$BASH_SOURCE" || echo "$BASH_SOURCE")")",或者您可以滚动您自己的递归符号链接跟踪脚本,如链接帖子中所示。
            • 我还是不明白,为什么 OP 需要绝对路径。报告“。”如果您想访问相对于脚本路径的文件并且您调用了类似 ./myscript.sh 的脚本,那么应该可以正常工作
            • @StefanHaberl 我认为如果您在当前工作目录与脚本位置不同的情况下运行脚本将是一个问题(例如sh /some/other/directory/script.sh),在这种情况下. 将是您的密码,不是/some/other/directory
            【解决方案15】:

            灵感来自blueyed’s answer

            read < <(readlink -f $0 | xargs dirname)
            cd $REPLY
            

            【讨论】:

              【解决方案16】:

              正如 theMarko 建议的那样:

              BASEDIR=$(dirname $0)
              echo $BASEDIR
              

              除非您从脚本所在的同一目录执行脚本,否则此方法有效,在这种情况下,您将获得 '.' 的值

              要解决该问题,请使用:

              current_dir=$(pwd)
              script_dir=$(dirname $0)
              
              if [ $script_dir = '.' ]
              then
              script_dir="$current_dir"
              fi
              

              您现在可以在整个脚本中使用变量 current_dir 来引用脚本目录。但是,这可能仍然存在符号链接问题。

              【讨论】:

                【解决方案17】:
                cd $(dirname $(readlink -f $0))
                

                【讨论】:

                  【解决方案18】:

                  如果你使用 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}"
                  

                  【讨论】:

                  • 您可以将pushd/popd 替换为cd $(dirname "${0}")cd - 以使其在具有pwd -L 的其他shell 上工作。
                  • 为什么要在这里使用 pushd 和 popd?
                  • 所以我不必将原始目录存储在变量中。这是我在函数等中经常使用的一种模式。它嵌套得很好,这很好。
                  • 它仍然存储在内存中——在一个变量中——无论你的脚本中是否引用了一个变量。此外,我相信执行 pushd 和 popd 的成本远远超过不在脚本中创建本地 Bash 变量所节省的成本,无论是 CPU 周期还是可读性。