如何递归地获取 shell 脚本?

如何递归地获取 shell 脚本?

这个问题,如果重要的话,最好是在 Bash 的上下文中,但我希望有一个跨语言的解决方案。

我遇到的问题是在用另一个脚本“获取”一个脚本的情况下,但以递归方式执行此操作。问题是它似乎无限地执行此操作。

具体案例如下。我创建了一个简单的 bash 脚本来加载同一目录中的其他 bash 脚本。由于在我的项目中所有脚本都位于同一目录中,因此现在可以。

所以“脚本加载器”如下(_source_script.sh 定义以下函数的文件):

_source_script()
{
    # Define a few colors for the possible error messages below.
    RED=$(tput setaf 1)
    NORMAL=$(tput sgr0)

    EXPECTED_DIR="scripts"

    if [ "$#" -ge  "1" ]
    then
        EXPECTED_DIR=$1
    fi

    # Based on: http://stackoverflow.com/a/1371283/3924118
    CURRENT_DIR=${PWD##*/}

    if [ "$CURRENT_DIR" = "$EXPECTED_DIR" ]
    then
        for (( arg = 2; arg <= $#; arg++ ))
        do
            # Based on:
            # - http://askubuntu.com/questions/306851/how-to-import-a-variable-from-a-script
            # - http://unix.stackexchange.com/questions/114300/whats-the-meaning-of-a-dot-before-a-command-in-shell
            # - http://stackoverflow.com/questions/20094271/bash-using-dot-or-source-calling-another-script-what-is-difference

            printf ". ./${!arg}.sh\n"
            # Try to load script ${!arg}.sh
            . ./${!arg}.sh

            # If it was not loaded successfully, exit with status 1.
            if [ $? -ne 0 ]
            then
                printf "${RED}Script '${!arg}.sh' not loaded successfully. Exiting...${NORMAL}\n"
                exit 1
            fi
        done
    else
        printf "No script loaded: $CURRENT_DIR != $EXPECTED_DIR.\n"
        exit 1
    fi
}

要使用这个“脚本加载器”,其他脚本必须首先“获取”它,如下所示:

. ./_source_script.sh

问题是当我尝试通过 function 获取其他脚本来包含它们时_source_script。例如,当我尝试这样做时(在 script 中some_script.sh):

_source_script scripts colors asserts clean_environment

它永远持续运行。

里面colors.sh有:

#!/usr/bin/env bash

# Colors used when printing.
export GREEN=$(tput setaf 2)
export RED=$(tput setaf 1)
export NORMAL=$(tput sgr0)
export YELLOW=$(tput setaf 3)

里面asserts.sh有:

...

. ./_source_script.sh
_source_script scripts colors

里面clean_environment.sh我还有:

. ./_source_script.sh
_source_script scripts colors

根据我的理解,这应该递归运行,直到找到一个不加载任何内容的脚本或找到一个循环,但情况并非如此。

所以,我的解决方案是some_script.sh

_source_script scripts colors 
_source_script scripts asserts
_source_script scripts clean_environment

也就是说,单独运行它们。

那么,为什么我不能在循环中“获取”多个脚本呢?

答案1

不要太仔细地查看代码,一般方法将包括类似于 C 的“标头防护”的内容:

#ifndef HEADER_H
#define HEADER_H

/* The contents of the header file, with typedefs etc. */

#endif

其中HEADER_H是特定于此标头的 C 预处理器宏,仅用于避免再次包含该标头(如果已包含该标头)。我通常创建一个宏,其名称源自头文件本身,例如MCMC_H名为mcmc.h.

在 shell 脚本中,这可能很简单

if [ -z "$script_source_guard" ]; then
script_source_guard=1

# ... do things (define functions etc.)

fi

变量的名称script_source_guard需要特定于此文件。同样,该名称可以任意源自源文件名;名称为 的文件可能有一个名为or或或其他名称myfuncts.shlib的保护变量。MYFUNCTS_MYFUNCTSguard_myfuncts

答案2

这是一个范围问题。按照设计,该_source_script函数实际上是递归调用自身,因为它正在执行. ./${!arg}.sh 并获取调用_source_script.但 shell 脚本中的变量默认是全局的。因此,_source_script同时处于活动状态(在堆栈上)的多个函数化身共享同一个 arg.

详细地:

asserts.shorclean_environment.sh调用时

_source_script scripts colors

函数_source_script正在变得$#= 2。因此for (( arg = 2; arg <= $#; arg++ ))循环运行 while $argis <= 2,所以它最终是 3。当. asserts.sh被调用时,$arg已经是 3,因为assertsis$3

_source_script scripts colors asserts clean_environment

命令在some_script.sh.所以,如果$arg 到 3 in asserts.sh,这最终成为一个空操作——恢复$arg到原来的状态。

但是,当. clean_environment.sh被调用时,$arg是 4,因为clean_environment$4

_source_script scripts colors asserts clean_environment

命令。因此,如果将 in (当调用)$arg设置为 3 时, (in )的顶级化身将失去其在循环中的位置,并返回并再次执行。一次又一次,一次又一次……clean_environment.sh_source_script_source_scriptsome_script.shfor (( arg = 2; arg <= $#; arg++ )). clean_environment.sh

解决方案

当然,解决方案就是简单地 local arg在函数中放置一个声明_source_script


关于脚本的其他注释:

  • 你有这样的情况:

    • 您有一些定义各种内容(常量、函数、别名等)的文件。
    • 您还有其他文件使用上面定义的那些东西,所以它们. (源)包含定义的文件,以及
    • 有些文件同时属于上述两个组;例如, asserts.sh 并且 clean_environment.sh 可能向其调用者提供一些资源,但他们也读取colors.sh以获取其中定义的值。大概有一个层次结构:

      • some_script.sh 来电 colors.sh
      • some_script.sh 来电 asserts.sh
        • asserts.sh 来电 colors.sh
      • some_script.sh 来电 clean_environment.sh
        • clean_environment.sh 来电 colors.sh

      ETC。

  • 一些用户建议使用“标头防护”来防止 shell 命令文件被多次处理。虽然这个建议很好并且与您相关情况 (如上所述),我真的不明白它与你有什么关系问题。

    - 除了你应该使用它_source_script.sh本身

    在上面的层次结构中,我忽略了这样一个事实:每个文件来电_source_script.sh。这意味着该_source_script函数 在运行时被重新定义  这听起来可能非常危险。

  • 你说

    if [ "$#" -ge "1" ]
    then
        EXPECTED_DIR=$1
    fi
    

    你也可以直接说

    if [ "$#" -le  1  ]
    then
        exit 1                                  # or maybe      return 1
    fi
    EXPECTED_DIR=$1
    

    因为如果参数少于两个则无事可做。

  • printf除非您确定需要,否则不要在格式字符串(即第一个参数)中使用变量会更安全 。所以

    printf ". ./${!arg}.sh\n"
    printf "${RED}Script '${!arg}.sh' not loaded successfully. Exiting...${NORMAL}\n"
    printf "No script loaded: $CURRENT_DIR != $EXPECTED_DIR.\n"
    

    应该

    printf ". ./%s.sh\n" "${!arg}"
    printf "%sScript '%s.sh' not loaded successfully. Exiting...%s\n" \
                                                        "$RED" "${!arg}" "$NORMAL"
    printf "No script loaded: %s != %s.\n" "$CURRENT_DIR" "$EXPECTED_DIR"
    
  • 您应该更多地使用引号。

    . ./${!arg}.sh
    

    应该

    . "./${!arg}.sh"
    

    而且,严格来说,要真正做到偏执安全, $?应该是"$?"

  • "$?"但是,为什么不直接说,而不是对 进行明确的测试

    if ! . "./${!arg}.sh"
    then
        printf "%sScript '%s.sh' not loaded successfully. Exiting...%s\n" \
                                                        "$RED" "${!arg}" "$NORMAL"
        exit 1
    fi
    

    请注意,一个.成功的命令将设置$?为文件中最后一个命令的退出状态。因此,请注意您的头文件( .sh定义其他文件要使用的内容的文件)返回准确的退出状态。

答案3

如果您想通过一个父文件来链接采购,没问题。我不会将源语句放在函数中。只需获取父文件,它也应该获取所有子文件。

从内部命令的 bash 手册页source

[It]从文件名中读取并执行命令当前的 shell 环境并返回从 filename 执行的最后一个命令的退出状态。

如果您不能保证当前 shell 环境将持续执行某个函数,您可能需要更仔细地考虑您的配置。

相关内容