【问题标题】:How best to include other scripts?如何最好地包含其他脚本?
【发布时间】:2010-09-16 13:51:37
【问题描述】:

通常包含脚本的方式是使用“源”

例如:

main.sh:

#!/bin/bash

source incl.sh

echo "The main script"

包括.sh:

echo "The included script"

执行“./main.sh”的输出为:

The included script
The main script

...现在,如果您尝试从另一个位置执行该 shell 脚本,它无法找到包含,除非它在您的路径中。

什么是确保您的脚本可以找到包含脚本的好方法,尤其是在脚本需要可移植的情况下?

【问题讨论】:

  • 你的问题非常好,内容丰富,我的问题在你问之前就得到了回答你的问题!干得好!
  • @GabrielStaples 节省了一些滚动,我猜?妈
  • @NateT,当然!
  • 相关/也回答了这个问题,顺便说一句:这是我关于如何获取正在运行的脚本的SCRIPT_DIRECTORY 路径的答案,因此您可以使用它来导入 (via source or .)与当前脚本相关的其他脚本:How can I get the source directory of a Bash script from within the script itself?。 (我的答案在列表的最后,拥有这个链接也可以为你节省一些滚动时间:))。我的答案可以被认为是这里接受的答案的变体。

标签: bash


【解决方案1】:

1。最整洁

我几乎探索了每一个建议,这是最适合我的建议:

script_root=$(dirname $(readlink -f $0))

即使脚本被符号链接到$PATH 目录,它也可以工作。

在此处查看实际操作:https://github.com/pendashteh/hcagent/blob/master/bin/hcagent

2。最酷的

# Copyright https://stackoverflow.com/a/13222994/257479
script_root=$(ls -l /proc/$$/fd | grep "255 ->" | sed -e 's/^.\+-> //')

这实际上来自该页面上的另一个答案,但我也将其添加到我的答案中!

3。最可靠的

或者,在极少数情况下这些方法不起作用,这里是防弹方法:

# Copyright http://stackoverflow.com/a/7400673/257479
myreadlink() { [ ! -h "$1" ] && echo "$1" || (local link="$(expr "$(command ls -ld -- "$1")" : '.*-> \(.*\)$')"; cd $(dirname $1); myreadlink "$link" | sed "s|^\([^/].*\)\$|$(dirname $1)/\1|"); }
whereis() { echo $1 | sed "s|^\([^/].*/.*\)|$(pwd)/\1|;s|^\([^/]*\)$|$(which -- $1)|;s|^$|$1|"; } 
whereis_realpath() { local SCRIPT_PATH=$(whereis $1); myreadlink ${SCRIPT_PATH} | sed "s|^\([^/].*\)\$|$(dirname ${SCRIPT_PATH})/\1|"; } 

script_root=$(dirname $(whereis_realpath "$0"))

您可以在taskrunner 中看到它的实际效果,来源:https://github.com/pendashteh/taskrunner/blob/master/bin/taskrunner

希望这对那里的人有所帮助:)

另外,如果对您不起作用,请留下评论并提及您的操作系统和模拟器。谢谢!

【讨论】:

    【解决方案2】:

    我在这里看到的大多数答案似乎都过于复杂了。这种方法对我来说一直很可靠:

    FULLPATH=$(readlink -f $0)
    INCPATH=${FULLPATH%/*}
    

    INCPATH 将保存脚本的完整路径,不包括脚本文件名,无论脚本如何调用(通过 $PATH、相对或绝对)。

    之后,只需执行此操作即可将文件包含在同一目录中:

    . $INCPATH/file_to_include.sh
    

    参考:TecPorto / Location independent includes

    【讨论】:

      【解决方案3】:

      我知道我迟到了,但是无论您如何启动脚本并专门使用内置插件,这都应该有效:

      DIR="${BASH_SOURCE%/*}"
      if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi
      . "$DIR/incl.sh"
      . "$DIR/main.sh"
      

      .(点)命令是source的别名,$PWD是工作目录的路径,BASH_SOURCE是一个数组变量,其成员是源文件名,${string%substring}去除最短匹配$string 后面的 $substring

      【讨论】:

      • 这是线程中唯一对我有用的答案
      • @sacii 我可以知道什么时候需要if [[ ! -d "$DIR" ]]; then DIR="$PWD"; fi 行吗?如果将命令粘贴到 bash 提示符以运行,我会发现需要它。但是,如果在脚本文件上下文中运行,我看不出需要它...
      • 还值得注意的是,它在多个sources 中按预期工作(我的意思是,如果你source 一个脚本在另一个目录中sources 另一个脚本等等,它仍然作品)。
      • 不应该是${BASH_SOURCE[0]},因为您只想要最新的调用吗?此外,使用DIR=$(dirname ${BASH_SOURCE[0]}) 可以让您摆脱 if 条件
      • 如果您从带有$ bash script.sh 的脚本目录调用脚本,其中$BASH_SOURCE 中不包含前导点并且DIR 解析为script.sh$(dirname $BASH_SOURCE) 正确解析为.
      【解决方案4】:

      根据man hier,脚本包含的合适位置是/usr/local/lib/

      /usr/local/lib

      与本地安装程序相关的文件。

      我个人更喜欢 /usr/local/lib/bash/includes 包含。 有bash-helper lib 用于以这种方式包含库:

      #!/bin/bash
      
      . /usr/local/lib/bash/includes/bash-helpers.sh
      
      include api-client || exit 1                   # include shared functions
      include mysql-status/query-builder || exit 1   # include script functions
      
      # include script functions with status message
      include mysql-status/process-checker; status 'process-checker' $? || exit 1
      include mysql-status/nonexists; status 'nonexists' $? || exit 1
      

      【讨论】:

        【解决方案5】:

        这个问题的答案组合提供了最可靠的解决方案。

        它在生产级脚本中为我们工作,对依赖项和目录结构有很好的支持:

        #!/bin/bash
        
        # 当前脚本的完整路径
        THIS=`readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0`
        
        # 当前脚本所在目录
        DIR=`目录名“${THIS}”`
        
        # 'Dot' 表示'来源',即'include':
        . "$DIR/compile.sh"

        该方法支持所有这些:

        • 空格在路径中
        • 链接(来自readlink
        • ${BASH_SOURCE[0]}$0 更健壮

        【讨论】:

        • THIS=readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0 如果您的阅读链接是 BusyBox v1.01
        • @AlexxRoche 谢谢!这适用于所有 Linux 吗?
        • 我希望如此。似乎适用于 Debian Sid 3.16 和 QNAP 的 armv5tel 3.4.6 Linux。
        • 我放在这一行:DIR=$(dirname $(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null||echo $0)) # https://stackoverflow.com/a/34208365/
        【解决方案6】:

        个人将所有库放在lib 文件夹中,并使用import 函数加载它们。

        文件夹结构

        script.sh 内容

        # Imports '.sh' files from 'lib' directory
        function import()
        {
          local file="./lib/$1.sh"
          local error="\e[31mError: \e[0mCannot find \e[1m$1\e[0m library at: \e[2m$file\e[0m"
          if [ -f "$file" ]; then
             source "$file"
            if [ -z $IMPORTED ]; then
              echo -e $error
              exit 1
            fi
          else
            echo -e $error
            exit 1
          fi
        }
        

        请注意,此导入函数应位于脚本的开头,然后您可以像这样轻松导入库:

        import "utils"
        import "requirements"
        

        在每个库的顶部添加一行(即 utils.sh):

        IMPORTED="$BASH_SOURCE"
        

        现在您可以从script.sh 访问utils.shrequirements.sh 中的函数

        TODO:编写一个链接器来构建单个 sh 文件

        【讨论】:

        • 这是否也解决了在它所在的目录之外执行脚本的问题?
        • @AaronH。不,这是一种在大型项目中包含依赖项的结构化方式。
        【解决方案7】:

        我认为最好的方法是使用 Chris Boran 的方式,但是你应该这样计算 MY_DIR:

        #!/bin/sh
        MY_DIR=$(dirname $(readlink -f $0))
        $MY_DIR/other_script.sh
        

        引用 readlink 的手册页:

        readlink - display value of a symbolic link
        
        ...
        
          -f, --canonicalize
                canonicalize  by following every symlink in every component of the given 
                name recursively; all but the last component must exist
        

        我从未遇到过没有正确计算MY_DIR 的用例。如果您通过 $PATH 中的符号链接访问您的脚本,它可以工作。

        【讨论】:

        • 不错且简单的解决方案,并且在我能想到的尽可能多的脚本调用变体中为我工作。谢谢。
        • 除了缺少引号的问题外,是否有任何实际用例需要解决符号链接而不是直接使用$0
        • @l0b0: 想象你的脚本是/home/you/script.sh 你可以cd /home 并从那里运行你的脚本./you/script.sh 在这种情况下dirname $0 将返回./you 并且包括其他脚本将失败
        • 很好的建议,但是,我需要执行以下操作才能读取我在 `MY_DIR=$(dirname $(readlink -f $0)); source $MY_DIR/incl.sh 中的变量
        【解决方案8】:

        Shell Script Loader 是我的解决方案。

        它提供了一个名为 include() 的函数,该函数可以在许多脚本中多次调用以引用单个脚本,但只会加载一次脚本。该函数可以接受完整路径或部分路径(在搜索路径中搜索脚本)。还提供了一个名为 load() 的类似函数,它将无条件加载脚本。

        它适用于 bashkshpd kshzsh,其中每个脚本都经过优化;以及其他与原始 sh 通用兼容的 shell,如 ashdashheirloom sh 等,通过自动优化的通用脚本它的功能取决于外壳可以提供的功能。

        [转发示例]

        start.sh

        这是一个可选的启动脚本。将启动方法放在这里只是为了方便,可以放在主脚本中。如果要编译脚本,也不需要此脚本。

        #!/bin/sh
        
        # load loader.sh
        . loader.sh
        
        # include directories to search path
        loader_addpath /usr/lib/sh deps source
        
        # load main script
        load main.sh
        

        ma​​in.sh

        include a.sh
        include b.sh
        
        echo '---- main.sh ----'
        
        # remove loader from shellspace since
        # we no longer need it
        loader_finish
        
        # main procedures go from here
        
        # ...
        

        a.sh

        include main.sh
        include a.sh
        include b.sh
        
        echo '---- a.sh ----'
        

        b.sh

        include main.sh
        include a.sh
        include b.sh
        
        echo '---- b.sh ----'
        

        输出:

        ---- b.sh ----
        ---- a.sh ----
        ---- main.sh ----
        

        最好的是基于它的脚本也可以用可用的编译器编译成单个脚本。

        这是一个使用它的项目:http://sourceforge.net/p/playshell/code/ci/master/tree/。它可以在编译或不编译脚本的情况下可移植地运行。也可以编译生成单个脚本,这在安装过程中很有帮助。

        我还为任何可能想简要了解实现脚本如何工作的保守派创建了一个更简单的原型:https://sourceforge.net/p/loader/code/ci/base/tree/loader-include-prototype.bash。它很小,如果他们的代码打算在 Bash 4.0 或更高版本上运行,任何人都可以在他们的主脚本中包含代码,而且它也不使用eval

        【讨论】:

        • 12 KB 的 Bash 脚本包含 100 多行 evaled 代码来加载依赖项。 哎哟
        • 底部附近的三个 eval 块之一始终运行。所以无论是否需要,它肯定是使用eval
        • 那个 eval 调用是安全的,如果你有 Bash 4.0+,它就不会被使用。我明白了,你是那些认为eval 纯粹是邪恶的老脚本编写者之一,并且不知道如何充分利用它。
        • 我不知道您所说的“未使用”是什么意思,但 它已运行。 经过几年的 shell 脚本作为我工作的一部分,是的,我我比以往任何时候都更加确信eval 是邪恶的。
        • 两种可移植的、简单的标记文件的方法立即浮现在脑海中:通过 inode 编号引用它们,或者将 NUL 分隔的路径放入文件中。
        【解决方案9】:

        即使脚本是来源的,这也有效:

        source "$( dirname "${BASH_SOURCE[0]}" )/incl.sh"
        

        【讨论】:

        • 您能解释一下“[0]”的用途吗?
        • @Ray BASH_SOURCE 是一个路径数组,对应一个调用栈。第一个元素对应于堆栈中最新的脚本,即当前正在执行的脚本。实际上,作为变量调用的$BASH_SOURCE 默认会展开到它的第一个元素,所以这里不需要[0]。详情请见this link
        【解决方案10】:

        这应该可以可靠地工作:

        source_relative() {
         local dir="${BASH_SOURCE%/*}"
         [[ -z "$dir" ]] && dir="$PWD"
         source "$dir/$1"
        }
        
        source_relative incl.sh
        

        【讨论】:

          【解决方案11】:

          当然,每个人都有自己的想法,但我认为下面的块非常可靠。我相信这涉及到查找目录的“最佳”方式,以及调用另一个 bash 脚本的“最佳”方式:

          scriptdir=`dirname "$BASH_SOURCE"`
          source $scriptdir/incl.sh
          
          echo "The main script"
          

          所以这可能是包含其他脚本的“最佳”方式。这是基于tells a bash script where it is stored 的另一个“最佳”答案

          【讨论】:

            【解决方案12】:

            我倾向于让我的脚本都相互关联。 这样我就可以使用目录名:

            #!/bin/sh
            
            my_dir="$(dirname "$0")"
            
            "$my_dir/other_script.sh"
            

            【讨论】:

            • 如果脚本是通过 $PATH 执行的,这将不起作用。那么which $0 会很有用
            • 没有可靠的方法来确定一个shell脚本的位置,见mywiki.wooledge.org/BashFAQ/028
            • @Philipp,该条目的作者是正确的,它很复杂,并且有陷阱。但是它遗漏了一些关键点,首先,作者假设了很多关于您将使用 bash 脚本做什么的事情。我也不希望 python 脚本在没有它的依赖项的情况下运行。 Bash 是一种胶水语言,它可以让你快速完成原本很难做的事情。当您需要构建系统工作时,实用主义(以及关于脚本无法找到依赖项的一个很好的警告)获胜。
            • 刚刚了解了BASH_SOURCE数组,以及该数组中的第一个元素如何始终指向当前源。
            • 如果脚本位于不同的位置,这将不起作用。例如/home/me/main.sh 调用 /home/me/test/inc.sh,因为 dirname 将返回 /home/me。使用 BASH_SOURCE 的 sacii 答案是更好的解决方案 stackoverflow.com/a/12694189/1000011
            【解决方案13】:

            我们只需要找出存放我们的incl.sh和main.sh的文件夹即可;只需更改您的 main.sh 即可:

            main.sh

            #!/bin/bash
            
            SCRIPT_NAME=$(basename $0)
            SCRIPT_DIR="$(echo $0| sed "s/$SCRIPT_NAME//g")"
            source $SCRIPT_DIR/incl.sh
            
            echo "The main script"
            

            【讨论】:

            • 引用不正确,echo 的不必要使用,以及sedg 选项的错误使用。 -1.
            • 无论如何,让这个脚本工作:SCRIPT_DIR=$(echo "$0" | sed "s/${SCRIPT_NAME}//") 然后source "${SCRIPT_DIR}incl.sh"
            【解决方案14】:

            我把我所有的启动脚本都放在了一个 .bashrc.d 目录中。 这是 /etc/profile.d 等地方的常用技术。

            while read file; do source "${file}"; done <<HERE
            $(find ${HOME}/.bashrc.d -type f)
            HERE
            

            使用通配符解决的问题...

            for file in ${HOME}/.bashrc.d/*.sh; do source ${file};done
            

            ...您可能有一个“太长”的文件列表。 类似的方法......

            find ${HOME}/.bashrc.d -type f | while read file; do source ${file}; done
            

            ...运行,但不会根据需要更改环境。

            【讨论】:

              【解决方案15】:

              使用 source 或 $0 不会为您提供脚本的真实路径。您可以使用脚本的进程 ID 来检索其真实路径

              ls -l       /proc/$$/fd           | 
              grep        "255 ->"            |
              sed -e      's/^.\+-> //'
              

              我正在使用这个脚本,它一直对我很有帮助:)

              【讨论】:

                【解决方案16】:

                你也可以使用:

                PWD=$(pwd)
                source "$PWD/inc.sh"
                

                【讨论】:

                • 您假设您位于脚本所在的同一目录中。如果您在其他地方,它将无法正常工作。
                【解决方案17】:
                SRC=$(cd $(dirname "$0"); pwd)
                source "${SRC}/incl.sh"
                

                【讨论】:

                • 当 dirname "$0" 应该完成同样的事情时,我怀疑你对 "cd ..." 投了反对票...
                • 即使从当前目录执行脚本,此代码也会返回绝对路径。单独的 $(dirname "$0") 将只返回 "."
                • 并且./incl.sh 解析为与cd + pwd 相同的路径。那么改目录有什么好处呢?
                • 有时你需要脚本的绝对路径,例如当您必须来回更改目录时。
                • 感谢您添加此内容。仅 dirname 的所有用法就让我很痒 :) 几年前我从导师那里学到了这一点,但是 DIR="$(cd "$(dirname $0)" && pwd)" 的形式略有不同
                【解决方案18】:

                Steve 的回复绝对是正确的技术,但应该重构它,以便您的 installpath 变量位于一个单独的环境脚本中,所有此类声明都在其中进行。

                然后所有脚本都来自该脚本并且应该更改安装路径,您只需在一个位置更改它。让事情变得更加,呃,面向未来。上帝我讨厌这个词! (-:

                顺便说一句,在以示例中所示的方式使用变量时,您应该真正使用 ${installpath} 来引用变量:

                . ${installpath}/incl.sh
                

                如果大括号被省略,一些 shell 会尝试扩展变量“installpath/incl.sh”!

                【讨论】:

                  【解决方案19】:

                  替代:

                  scriptPath=$(dirname $0)
                  

                  是:

                  scriptPath=${0%/*}
                  

                  .. 优点是不依赖 dirname,它不是内置命令(在模拟器中并不总是可用)

                  【讨论】:

                  • basePath=$(dirname $0) 在获取包含脚本文件时给了我空白值。
                  【解决方案20】:

                  您需要指定其他脚本的位置,没有其他办法。我建议在脚本顶部使用可配置变量:

                  #!/bin/bash
                  installpath=/where/your/scripts/are
                  
                  . $installpath/incl.sh
                  
                  echo "The main script"
                  

                  或者,您可以坚持让用户维护一个环境变量,指示您的程序主目录在哪里,例如 PROG_HOME 或类似的东西。这可以通过在 /etc/profile.d/ 中创建包含该信息的脚本来自动为用户提供,每次用户登录时都会获取该信息。

                  【讨论】:

                  • 我很欣赏对特异性的渴望,但我不明白为什么需要完整路径,除非包含脚本是另一个包的一部分。我没有看到从特定相对路径(即脚本正在执行的相同目录)加载与特定完整路径的安全差异。为什么说没有办法呢?
                  • 因为你的脚本正在执行的目录不一定是你想要包含在你的脚本中的脚本所在的位置。您想在安装脚本的位置加载脚本,并且没有可靠的方法来判断它在运行时的位置。不使用固定位置也是包含错误(即黑客提供的)脚本并运行它的好方法。
                  • 使用运行脚本位置的相对路径在运行时是可靠的。并且可以使用 $(cd "$(dirname $0)" && pwd)" 可靠地找到 $0 的绝对规范路径,它也可以用于规范化库目录:"$(cd "$(cd "$(dirname $0 )" && pwd)/../lib" && pwd)"
                  【解决方案21】:

                  我建议您创建一个 setenv 脚本,其唯一目的是为系统中的各种组件提供位置。

                  然后所有其他脚本都将获取此脚本,以便所有位置在所有使用 setenv 脚本的脚本中都是通用的。

                  这在运行 cronjobs 时非常有用。运行 cron 时您会获得一个最小的环境,但如果您让所有 cron 脚本首先包含 setenv 脚本,那么您就可以控制和同步您希望 cronjobs 在其中执行的环境。

                  我们在构建猴子上使用了这种技术,用于在大约 2,000 kSLOC 的项目中进行持续集成。

                  【讨论】:

                    【解决方案22】:

                    如果在同一目录下可以使用dirname $0:

                    #!/bin/bash
                    
                    source $(dirname $0)/incl.sh
                    
                    echo "The main script"
                    

                    【讨论】:

                    • 两个陷阱:1) $0./t.sh 并且 dirname 返回 .; 2) 在cd bin 之后返回的. 不正确。 $BASH_SOURCE 也好不到哪里去。
                    • 另一个陷阱:尝试在目录名中使用空格。
                    • source "$(dirname $0)/incl.sh" 适用于这些情况
                    猜你喜欢
                    • 2011-09-21
                    • 1970-01-01
                    • 1970-01-01
                    • 2010-10-13
                    • 1970-01-01
                    • 1970-01-01
                    • 2012-10-16
                    • 2013-12-07
                    相关资源
                    最近更新 更多