bash脚本获取完整路径的可靠方法

Reliable way for a Bash script to get the full path to itself

本问题已经有最佳答案,请猛点这里访问。

我有一个bash脚本需要知道它的完整路径。我正试图找到一种广泛兼容的方法来实现这一点,但最终却没有找到相对的或看起来很古怪的路径。我只需要支持bash,而不需要sh、csh等。

到目前为止我所发现的:

  • 从地址中获取bash脚本的源目录的公认答案,通过dirname $0获取脚本的路径,这很好,但这可能返回相对路径(如.),如果您想更改脚本中的目录,并且路径仍然指向脚本的目录,这是一个问题。不过,dirname将是难题的一部分。

  • 接受的对bash脚本绝对路径的回答带有OS X(OS X特定,但不管答案如何都有效)提供了一个函数,该函数将测试$0是否看起来相对,如果相对,则会预先将$PWD挂起。但是结果中仍然可以有相对位(尽管总体上是绝对的);例如,如果脚本在目录/usr/bin中是t,而您在/usr中,并且键入bin/../bin/t来运行它(是的,这很复杂),则最终会得到/usr/bin/../bin作为脚本的目录路径。这是可行的,但是…

  • 本页中的readlink解决方案如下:

    1
    2
    3
    4
    # Absolute path to this script. /home/user/bin/foo.sh
    SCRIPT=$(readlink -f $0)
    # Absolute path this script is in. /home/user/bin
    SCRIPTPATH=`dirname $SCRIPT`

    readlink不是posix,显然解决方案依赖于GNU的readlink,因为某种原因,BSD无法工作(我无法访问类似于BSD的系统进行检查)。

  • 所以,做这件事的各种方法,但他们都有自己的注意事项。

    有什么更好的办法?其中,"更好"是指:

    • 给了我绝对路径。
    • 即使是以复杂的方式调用,也会去掉一些奇怪的位(参见上面的注释2)。(例如,至少适度规范路径。)
    • 只依赖于bash isms或几乎可以确定是最流行的*nix系统(GNU/Linux、BSD和BSD,如OS ;X等系统)的东西。
    • 尽可能避免调用外部程序(例如,首选bash内置程序)。
    • (更新后,感谢大家的提醒,wich)它不需要解析符号链接(事实上,我更愿意让它们单独使用,但这不是一个要求)。


    以下是我的想法(编辑:加上一些由sfstewman、levigroker、kyle strand和rob kennedy提供的调整),这似乎最符合我的"更好"标准:

    1
    SCRIPTPATH="$( cd"$(dirname"$0")" ; pwd -P )"

    SCRIPTPATH线看起来特别迂回,但我们需要它而不是SCRIPTPATH=`pwd`,以便正确处理空格和符号链接。

    还要注意,一些深奥的情况,比如执行一个完全不来自可访问文件系统中的文件的脚本(这是完全可能的),在那里(或者在我看到的任何其他答案中)是不适合的。


    我很惊讶,这里没有提到realpath命令。我的理解是它是广泛的可移植/移植的。

    您的初始解决方案是:

    1
    2
    SCRIPT=`realpath $0`
    SCRIPTPATH=`dirname $SCRIPT`

    并根据您的喜好保留未解析的符号链接:

    1
    2
    SCRIPT=`realpath -s $0`
    SCRIPTPATH=`dirname $SCRIPT`


    我在bash中找到完整规范路径的最简单方法是使用cdpwd

    1
    ABSOLUTE_PATH="$(cd"$(dirname"${BASH_SOURCE[0]}")" && pwd)/$(basename"${BASH_SOURCE[0]}")"

    使用${BASH_SOURCE[0]}而不是$0,无论脚本是作为还是source 调用,都会产生相同的行为。


    我今天不得不重新讨论这个问题,发现从内部获取了一个bash脚本的源目录。它还详细阐述了我过去使用过的解决方案。

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

    链接的答案有更多的变体,例如,对于脚本本身是符号链接的情况。


    获取shell脚本的绝对路径

    它在readlink中不使用-f选项,因此它应该在bsd/mac操作系统上工作。

    支架

    • source./脚本(由.点运算符调用时)
    • 绝对路径/路径/目标/脚本
    • 类似于/script的相对路径
    • /路径/dir1/。/dir2/dir3/。/script
    • 从symlink调用时
    • 当symlink嵌套时,例如)foo->dir1/dir2/bar bar->./../doe doe->script
    • 当调用者更改脚本名称时

    我在寻找这个代码不起作用的角落案例。请告诉我。

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    pushd . > /dev/null
    SCRIPT_PATH="${BASH_SOURCE[0]}";
    while([ -h"${SCRIPT_PATH}" ]); do
        cd"`dirname"${SCRIPT_PATH}"`"
        SCRIPT_PATH="$(readlink"`basename"${SCRIPT_PATH}"`")";
    done
    cd"`dirname"${SCRIPT_PATH}"`"> /dev/null
    SCRIPT_PATH="`pwd`";
    popd  > /dev/null
    echo"srcipt=[${SCRIPT_PATH}]"
    echo"pwd   =[`pwd`]"

    已知发行

    脚本必须在某个磁盘上。让它通过网络。如果尝试从管道运行此脚本,它将不起作用

    1
    wget -o /dev/null -O - http://host.domain/dir/script.sh |bash

    从技术上讲,它是未定义的。实际上,没有一种明智的方法可以检测到这一点。(协同进程无法访问父进程的环境。)


    用途:

    1
    SCRIPT_PATH=$(dirname `which $0`)

    which将在shell提示下输入传递的参数时本应执行的可执行文件的完整路径(即$0包含的路径)打印到标准输出。

    dirname从文件名中删除非目录后缀。

    因此,不管是否指定了路径,最终都会得到脚本的完整路径。


    由于在我的Linux系统上未按默认设置安装realpath,因此以下内容适用于我:

    1
    2
    SCRIPT="$(readlink --canonicalize-existing"$0")"
    SCRIPTPATH="$(dirname"$SCRIPT")"

    $SCRIPT将包含脚本的实际文件路径,$SCRIPTPATH将包含脚本所在目录的实际路径。

    在使用之前,请阅读此答案的注释。


    容易阅读?下面是一个备选方案。它忽略符号链接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #!/bin/bash
    currentDir=$(
      cd $(dirname"$0")
      pwd
    )

    echo -n"current"
    pwd
    echo script $currentDir

    自从几年前我发布了上述答案以来,我的实践已经发展到使用这个Linux特定的范例,它可以正确地处理符号链接:

    1
    ORIGIN=$(dirname $(readlink -f $0))


    很晚才回答这个问题,但我使用:

    1
    2
    3
    SCRIPT=$( readlink -m $( type -p $0 ))      # Full path to script
    BASE_DIR=`dirname ${SCRIPT}`                # Directory script is run in
    NAME=`basename ${SCRIPT}`                   # Actual name of script even if linked


    我们已经在Github上放置了自己的产品realpath lib,供免费和无障碍的社区使用。

    无耻的插件,但是有了这个bash库,您可以:

    1
    get_realpath

    此功能是库的核心:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    function get_realpath() {

    if [[ -f"$1" ]]
    then
        # file *must* exist
        if cd"$(echo"${1%/*}")" &>/dev/null
        then
            # file *may* not be local
            # exception is ./file.ext
            # try 'cd .; cd -;' *works!*
            local tmppwd="$PWD"
            cd - &>/dev/null
        else
            # file *must* be local
            local tmppwd="$PWD"
        fi
    else
        # file *cannot* exist
        return 1 # failure
    fi

    # reassemble realpath
    echo"$tmppwd"/"${1##*/}"
    return 0 # success

    }

    它不需要任何外部依赖,只需要bash 4+。还包含到get_dirnameget_filenameget_stemnamevalidate_pathvalidate_realpath的函数。它是免费的,干净的,简单的和良好的记录,所以它也可以用于学习目的,毫无疑问可以得到改进。跨平台尝试。

    更新:经过一些审查和测试,我们已经用同样的结果(不使用dirname,只使用pure bash)替换了上述函数,但效率更高:

    1
    2
    3
    4
    5
    6
    7
    8
    function get_realpath() {

        [[ ! -f"$1" ]] && return 1 # failure : file does not exist.
        [[ -n"$no_symlinks" ]] && local pwdp='pwd -P' || local pwdp='pwd' # do symlinks.
        echo"$( cd"$( echo"${1%/*}" )" 2>/dev/null; $pwdp )"/"${1##*/}" # echo result.
        return 0 # success

    }

    这还包括设置no_symlinks的环境,该环境提供了解析物理系统的符号链接的能力。默认情况下,它保持符号链接不变。


    简单地说:

    1
    BASEDIR=$(readlink -f $0 | xargs dirname)

    不需要花哨的操作员。


    您可以尝试定义以下变量:

    1
    CWD="$(cd -P --"$(dirname --"$0")" && pwd -P)"

    或者可以在bash中尝试以下功能:

    1
    2
    3
    realpath () {
      [[ $1 = /* ]] && echo"$1" || echo"$PWD/${1#./}"
    }

    此函数接受一个参数。如果参数已经有绝对路径,则按原样打印,否则打印$PWD变量+文件名参数(不带./前缀)。

    相关:

    • 带有OS X的bash脚本绝对路径

    • 从内部获取bash脚本的源目录


    Bourne Shell(sh兼容方式:

    1
    SCRIPT_HOME=`dirname $0 | while read a; do cd $a && pwd && break; done`


    接受的解决方案不方便(对我而言)不能"源代码可用":如果你用"EDOCX1"(6),$0就是"EDOCX1"(8)!

    以下函数(对于bash>=3.0)为我提供了正确的路径,但是可以调用脚本(直接或通过source,使用绝对或相对路径):(在"正确路径"中,我是指被调用脚本的完全绝对路径,即使是从另一个路径调用,也可以直接或使用"EDOCX1"〔9〕)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #!/bin/bash
    echo $0 executed

    function bashscriptpath() {
      local _sp=$1
      local ascript="$0"
      local asp="$(dirname $0)"
      #echo"b1 asp '$asp', b1 ascript '$ascript'"
      if [["$asp" =="." &&"$ascript" !="bash" &&"$ascript" !="./.bashrc" ]] ; then asp="${BASH_SOURCE[0]%/*}"
      elif [["$asp" =="." &&"$ascript" =="./.bashrc" ]] ; then asp=$(pwd)
      else
        if [["$ascript" =="bash" ]] ; then
          ascript=${BASH_SOURCE[0]}
          asp="$(dirname $ascript)"
        fi  
        #echo"b2 asp '$asp', b2 ascript '$ascript'"
        if [["${ascript#/}" !="$ascript" ]]; then asp=$asp ;
        elif [["${ascript#../}" !="$ascript" ]]; then
          asp=$(pwd)
          while [["${ascript#../}" !="$ascript" ]]; do
            asp=${asp%/*}
            ascript=${ascript#../}
          done
        elif [["${ascript#*/}" !="$ascript" ]];  then
          if [["$asp" =="." ]] ; then asp=$(pwd) ; else asp="$(pwd)/${asp}"; fi
        fi  
      fi  
      eval $_sp="'$asp'"
    }

    bashscriptpath H
    export H=${H}

    关键是检测"source案例,并使用${BASH_SOURCE[0]}返回实际脚本。


    也许下面问题的公认答案是有帮助的。

    如何在Mac上获取GNU的readlink-f行为?

    假设您只想将$pwd和$0(假设$0不是绝对值)连接后得到的名称规范化,只需沿着abs_dir=${abs_dir//\/.\//\/}等行使用一系列regex替换。

    是的,我知道它看起来很可怕,但它会起作用,而且是纯粹的狂欢。


    再次考虑这个问题:这个线程中引用了一个非常流行的解决方案,它的起源在这里:

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

    由于使用了dirname,我没有使用这个解决方案——它可能会带来跨平台的困难,特别是在出于安全原因需要锁定脚本的情况下。但是作为一个纯粹的bash替代方案,如何使用:

    1
    DIR="$( cd"$( echo"${BASH_SOURCE[0]%/*}" )" && pwd )"

    这是一个选择吗?


    如果我们使用bash,我相信这是最方便的方法,因为它不需要调用任何外部命令:

    1
    2
    THIS_PATH="${BASH_SOURCE[0]}";
    THIS_DIR=$(dirname $THIS_PATH)


    试试这个:

    1
    cd $(dirname $([ -L $0 ] && readlink -f $0 || echo $0))


    我已经成功地使用了以下方法一段时间(但不是在OS X上),据我所见,它只使用了一个内置的shell并处理"source foobar.sh"案例。

    下面的示例代码(匆忙组合)的一个问题是函数使用$pwd,这在函数调用时可能正确,也可能不正确。所以这需要处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #!/bin/bash

    function canonical_path() {
      # Handle relative vs absolute path
      [ ${1:0:1} == '/' ] && x=$1 || x=$PWD/$1
      # Change to dirname of x
      cd ${x%/*}
      # Combine new pwd with basename of x
      echo $(pwd -P)/${x##*/}
      cd $OLDPWD
    }

    echo $(canonical_path"${BASH_SOURCE[0]}")

    type [
    type cd
    type echo
    type pwd


    一班轮

    1
    `dirname $(realpath $0)`


    就为了见鬼,我对一个纯文本、纯bash的脚本进行了一些黑客攻击。我希望我能抓住所有边缘的箱子。

    请注意,我在另一个答案中提到的${var//pat/repl}不起作用,因为您不能使它只替换尽可能短的匹配项,这对于替换/foo/../是一个问题,例如/*/../将带走它之前的所有内容,而不仅仅是一个条目。因为这些模式并不是真正的正则表达式,我不知道如何才能实现。这是我想出的非常复杂的解决方案,享受吧。;)

    顺便问一下,如果您发现任何未处理的边缘情况,请告诉我。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #!/bin/bash

    canonicalize_path() {
      local path="$1"
      OIFS="$IFS"
      IFS=$'/'
      read -a parts < <(echo"$path")
      IFS="$OIFS"

      local i=${#parts[@]}
      local j=0
      local back=0
      local -a rev_canon
      while (($i > 0)); do
        ((i--))
        case"${parts[$i]}" in
         ""|.) ;;
          ..) ((back++));;
          *) if (($back > 0)); then
               ((back--))
             else
               rev_canon[j]="${parts[$i]}"
               ((j++))
             fi;;
        esac
      done
      while (($j > 0)); do
        ((j--))
        echo -n"/${rev_canon[$j]}"
      done
      echo
    }

    canonicalize_path"/.././..////../foo/./bar//foo/bar/.././bar/../foo/bar/./../..//../foo///bar/"

    另一种方法是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    shopt -s extglob

    selfpath=$0
    selfdir=${selfpath%%+([!/])}

    while [[ -L"$selfpath" ]];do
      selfpath=$(readlink"$selfpath")
      if [[ !"$selfpath" =~ ^/ ]];then
        selfpath=${selfdir}${selfpath}
      fi
      selfdir=${selfpath%%+([!/])}
    done

    echo $selfpath $selfdir

    更简单地说,这就是我的工作:

    1
    2
    MY_DIR=`dirname $0`
    source $MY_DIR/_inc_db.sh