从shell脚本导入函数

xwbd5t1u  于 2023-05-18  发布在  Shell
关注(0)|答案(6)|浏览(153)

我有一个shell脚本,我想用shUnit测试。脚本(和所有函数)都在一个文件中,因为它使安装更加容易。
script.sh示例

#!/bin/sh

foo () { ... }
bar () { ... }

code

我想编写第二个文件(不需要分发和安装)来测试script.sh中定义的函数
类似于run_tests.sh

#!/bin/sh

. script.sh

# Unit tests

现在问题出在.(或Bash中的source)。它不仅解析函数定义,还执行脚本中的代码。
因为没有论据的剧本没什么不好的我可以

. script.sh > /dev/null 2>&1

但我想知道是否有更好的方法来实现我的目标。

编辑

在源脚本调用exit的情况下,我建议的解决方法不起作用,因此我必须捕获出口

#!/bin/sh

trap run_tests ERR EXIT

run_tests() {
   ...
}

. script.sh

run_tests函数被调用,但一旦重定向源命令的输出,脚本中的函数就不会被解析,并且在陷阱处理程序中也不可用
这是可行的,但我得到了script.sh的输出:

#!/bin/sh
trap run_tests ERR EXIT
run_tests() {
   function_defined_in_script_sh
}
. script.sh

这不打印输出,但我得到一个错误,即函数未定义:

#!/bin/sh
trap run_tests ERR EXIT
run_tests() {
   function_defined_in_script_sh
}
. script.sh | grep OUTPUT_THAT_DOES_NOT_EXISTS

这不会打印输出,并且根本不调用run_tests陷阱处理程序:

#!/bin/sh
trap run_tests ERR EXIT
run_tests() {
   function_defined_in_script_sh
}
. script.sh > /dev/null
rxztt3cl

rxztt3cl1#

根据bash manpage的“Shell Builtin Commands”一节,.也就是source接受一个可选的参数列表,这些参数被传递给脚本源。你可以用它来引入一个什么都不做的选项。例如,script.sh可以是:

#!/bin/sh

foo() {
    echo foo $1
}

main() {
    foo 1
    foo 2
}

if [ "${1}" != "--source-only" ]; then
    main "${@}"
fi

unit.sh可以是:

#!/bin/bash

. ./script.sh --source-only

foo 3

然后script.sh将正常运行,unit.sh将访问script.sh的所有函数,但不会调用main()代码。
请注意,source的额外参数不在POSIX中,所以/bin/sh可能无法处理它-因此unit.sh的开头有#!/bin/bash

hfwmuf9z

hfwmuf9z2#

这是从Python中学习到的,但这个概念在bash或任何其他shell中都能很好地工作。
我们的想法是把脚本的主代码部分转换成一个函数。然后,在脚本的最后,我们放了一个'if'语句,它只会在执行脚本时调用该函数,而不是在源代码时调用该函数。然后,我们从'runtests'脚本中显式调用script()函数,该脚本包含了'script'脚本的来源,因此包含了它的所有函数。
这依赖于这样一个事实,即如果我们源脚本,bash维护的环境变量$0,它是正在执行的脚本的名称,将是调用(父)脚本的名称(在本例中是runtests),而不是源脚本的名称。
(我将script.sh重命名为script,因为.sh是多余的,让我困惑。:—)
下面是两个剧本。一些笔记...

  • $@计算为作为单个字符串传递给函数或脚本的所有参数。如果我们使用$*,那么所有的参数都将连接到一个字符串中。
  • RUNNING="$(basename $0)"是必需的,因为$0总是至少包含当前目录前缀,就像./script中一样。
  • 测试if [[ "$RUNNING" == "script" ]]...。只有当script直接从命令行运行时,script才会调用script()函数。
    脚本
#!/bin/bash

foo ()    { echo "foo()"; }

bar ()    { echo "bar()"; }

script () {
  ARG1=$1
  ARG2=$2
  #
  echo "Running '$RUNNING'..."
  echo "script() - all args:  $@"
  echo "script() -     ARG1:  $ARG1"
  echo "script() -     ARG2:  $ARG2"
  #
  foo
  bar
}

RUNNING="$(basename $0)"

if [[ "$RUNNING" == "script" ]]
then
  script "$@"
fi

运行测试

#!/bin/bash

source script 

# execute 'script' function in sourced file 'script'
script arg1 arg2 arg3
x6492ojm

x6492ojm3#

如果您使用Bash,可以使用BASH_SOURCE数组来实现与@andrewdotn方法类似的解决方案(但不需要额外的标志或依赖于脚本名称)。
script.sh:

#!/bin/bash

foo () { ... }
bar () { ... }

main() {
    code
}

if [[ "${#BASH_SOURCE[@]}" -eq 1 ]]; then
    main "$@"
fi

run_tests.sh:

#!/bin/bash

. script.sh

# Unit tests
ycggw6v2

ycggw6v24#

现在问题出在.(或Bash中的source)上。它不仅解析函数定义,还执行脚本中的代码。
我喜欢让它看起来比这里的其他答案更像Python。我做了一点额外的,这样我就可以结束与if [ "$__name__" = "__main__" ]; then线。也可以在这里看到我的回答:What is the bash equivalent to Python's if __name__ == '__main__' ?,这里我描述了:

if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    # This script is being run.
    __name__="__main__"
else
    # This script is being sourced.
    __name__="__source__"
fi

# Code entry point. Only run `main` if this script is being **run**, NOT
# sourced (imported).
# - See my answer: https://stackoverflow.com/a/70662116/4561887
if [ "$__name__" = "__main__" ]; then
    main "$@"
fi

让我们更进一步,我将展示一个完整的库示例和多种导入它的方法:

详细示例:如何在Bash中编写、导入、使用和测试库?

这是我多年来编写和使用Bash库的一个非常漂亮的、几乎类似Python的风格。Bash是一种漂亮的“胶水”类型的语言,它允许您轻松地将多种语言的可执行文件绑定在一起。考虑到Bash已经存在了多久,我不知道为什么下面的风格没有更受欢迎,但也许它以前没有被想到或使用过这种方式。所以,开始吧。我想你会发现它真的很有用。
您还可以看到我在eRCaGuy_hello_world仓库中的hello_world_best.sh文件中用于所有bash脚本的一般起点。
您可以在floating_point_math.sh中看到一个完整的库示例。

library_basic_example.sh

#!/usr/bin/env bash

RETURN_CODE_SUCCESS=0
RETURN_CODE_ERROR=1

# Add your library functions here. Ex:

my_func1() {
    echo "100.1"
}

my_func2() {
    echo "200"
}

my_func3() {
    echo "hello world"
}

# Note: make "private" functions begin with an underscore `_`, like in Python,
# so that users know they are not intended for use outside this library.

# Assert that the two input argument strings are equal, and exit if they are not
_assert_eq() {
    if [ "$1" != "$2" ]; then
        echo "Error: assertion failed. Arguments not equal!"
        echo "  arg1 = $1; arg2 = $2"
        echo "Exiting."
        exit $RETURN_CODE_ERROR
    fi
}

# Run some unit tests of the functions found herein
_test() {
    printf "%s\n\n" "Running tests."

    printf "%s\n" "Running 'my_func1'"
    result="$(my_func1)"
    printf "%s\n\n" "result = $result"
    _assert_eq "$result" "100.1"

    printf "%s\n" "Running 'my_func2'"
    result="$(my_func2)"
    printf "%s\n\n" "result = $result"
    _assert_eq "$result" "200"

    printf "%s\n" "Running 'my_func3'"
    result="$(my_func3)"
    printf "%s\n\n" "result = $result"
    _assert_eq "$result" "hello world"

    echo "All tests passed!"
}

main() {
    _test
}

# Determine if the script is being sourced or executed (run).
# See:
# 1. "eRCaGuy_hello_world/bash/if__name__==__main___check_if_sourced_or_executed_best.sh"
# 1. My answer: https://stackoverflow.com/a/70662116/4561887
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    # This script is being run.
    __name__="__main__"
else
    # This script is being sourced.
    __name__="__source__"
fi

# Code entry point. Only run `main` if this script is being **run**, NOT
# sourced (imported).
# - See my answer: https://stackoverflow.com/a/70662116/4561887
if [ "$__name__" = "__main__" ]; then
    main "$@"
fi

运行库以运行其单元测试

现在,使文件可执行。运行它将运行其内部单元测试:

# make it executable
chmod +x library_basic_example.sh

# run it
./library_basic_example.sh

运行命令和输出示例:

eRCaGuy_hello_world$ bash/library_basic_example.sh 
Running tests.

Running 'my_func1'
result = 100.1

Running 'my_func2'
result = 200

Running 'my_func3'
result = hello world

All tests passed!

导入(源)库

要导入一个Bash库,您可以使用source.(更好)命令来“源”它。在这里阅读更多关于我的答案:source ( . ) vs export (and also some file lock [ flock ] stuff at the end)

1.使用手动设置导入路径

您可以直接在bash终端中执行此操作,也可以在自己的bash脚本中执行此操作。现在就在你自己的终端里试试吧!:

source "path/to/library_basic_example.sh"

# Or (better, since it's Posix-compliant)
. "path/to/library_basic_example.sh"

一旦你源(导入)了Bash库,你就可以直接调用它的函数。下面是一个完整的运行和输出示例,显示一旦我获取(导入)了这个Bash库,我就可以在我的终端中执行my_func1my_func2等函数调用!:

eRCaGuy_hello_world$ . bash/library_basic_example.sh
eRCaGuy_hello_world$ my_func1
100.1
eRCaGuy_hello_world$ my_func2
200
eRCaGuy_hello_world$ my_func3
hello world
eRCaGuy_hello_world$ _test
Running tests.

Running 'my_func1'
result = 100.1

Running 'my_func2'
result = 200

Running 'my_func3'
result = hello world

All tests passed!
2.使用BASHLIBS环境变量,使导入Bash库更容易

你可以从 * 任意路径 * 获取bash库。但是,环境变量,例如BASHLIBS,使它更容易。将此添加到~/.bashrc文件的底部:

if [ -d "$HOME/libs_bash/libraries" ] ; then
    export BASHLIBS="$HOME/libs_bash/libraries"
fi

现在,您可以将您的Bash库符号链接到该目录中,如下所示:

# symlink my `library_basic_example.sh` file into the `~/libs_bash/libraries`
# dir
mkdir -p ~/libs_bash/libraries
cd path/to/dir_where_my_library_file_of_interest_is_stored
# make the symlink
ln -si "$(pwd)/library_basic_example.sh" ~/libs_bash/libraries/

现在,library_basic_example.sh文件的符号链接存储在~/libs_bash/libraries/中,并且BASHLIBS环境变量已经设置并且export绑定到我的环境中,我可以将我的库导入到任何Bash终端或我在终端中运行的脚本中,如下所示:

. "$BASHLIBS/library_basic_example.sh"
3.【我最常用的技巧!]使用相对导入路径

这真的很漂亮很有力量。看这个!
假设您有以下目录布局:

dir1/
    my_lib_1.sh

    dir2/
        my_lib_2.sh
        my_script.sh
    
        dir3/
            my_lib_3.sh

上面的表示可以很容易地存在于一个大型程序或工具链中,你已经建立了一个分散的可运行脚本和库,特别是当你的各种bash脚本之间共享(采购/导入)代码时,无论它们位于哪里。
假设你有这些限制/要求:
1.上面的整个目录结构都存储在一个GitHub仓库中。
1.你需要任何人能够git clone这个仓库,只是有所有的脚本和导入和东西 * 神奇的工作 * 每个人!
1.这意味着在将bash脚本相互导入时需要使用 relative imports
1.例如,您将运行my_script.sh
1.您必须能够从任何地方调用my_script.sh *,这意味着:当你调用这个脚本运行时,你应该能够被cd艾德到你整个文件系统中的 any 目录。

  1. my_script.sh必须使用相对导入来导入my_lib_1.shmy_lib_2.shmy_lib_3.sh
    基本上,我们要找到正在运行的脚本的路径 *,然后使用该路径作为脚本周围其他脚本的相对起点!
    阅读我的完整答案以了解更多细节:How to obtain the full file path, full directory, and base filename of any script being run OR sourced...even when the called script is called from within another bash function or script, or when nested sourcing is being used!
    完整示例:

my_script.sh:

#!/usr/bin/env bash

# Get the path to the directory this script is in.
FULL_PATH_TO_SCRIPT="$(realpath "${BASH_SOURCE[-1]}")"
SCRIPT_DIRECTORY="$(dirname "$FULL_PATH_TO_SCRIPT")"

# Now, source the 3 Bash libraries of interests, using relative paths to this
# script!
. "$SCRIPT_DIRECTORY/../my_lib_1.sh"
. "$SCRIPT_DIRECTORY/my_lib_2.sh"
. "$SCRIPT_DIRECTORY/dir3/my_lib_3.sh"

# Now you've sourced (imported) all 3 of those scripts!

就是这样!如果你知道命令的话超级简单!
Python能做到吗?不,至少不是天生的。在这方面,Bash比Python容易得多!Python在import时不直接使用文件系统路径。是much more complicated and convoluted than that但是,我正在开发一个import_helper.py Python模块,以使这种类型的事情在Python中也很容易。我很快也会出版的。

更进一步

1.在我的eRCaGuy_hello_world存储库中查看我的完整和真正有用的Bash浮点库:floating_point_math.sh
1.请参阅我的自述文件中关于Bash库安装和使用的一些替代说明:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/tree/master/bash/libraries

参见

  1. Testing a bash shell script-这个答案提到了this assert.sh Bash repo,它看起来对更健壮的Bash单元测试非常有用!
  2. Bash and Test-Driven Development
  3. Unit testing Bash scripts
    注意:我迁移了这个答案from here,在那里我不小心创建了一个重复的Q&A。
unguejic

unguejic5#

如果您正在使用Bash,另一种解决方案可能是:

#!/bin/bash

foo () { ... }
bar () { ... }

[[ "${FUNCNAME[0]}" == "source" ]] && return
code
lo8azlld

lo8azlld6#

我设计了这个。假设shell库文件是以下文件,名为aLib.sh:

funcs=("a" "b" "c")                   # File's functions' names
for((i=0;i<${#funcs[@]};i++));        # Avoid function collision with existing
do
        declare -f "${funcs[$i]}" >/dev/null
        [ $? -eq 0 ] && echo "!!ATTENTION!! ${funcs[$i]} is already sourced"
done

function a(){
        echo function a
}
function b(){
        echo function b
}
function c(){
        echo function c
}

if [ "$1" == "--source-specific" ];      # Source only specific given as arg
then    
        for((i=0;i<${#funcs[@]};i++));
        do      
                for((j=2;j<=$#;j++));
                do      
                        anArg=$(eval 'echo ${'$j'}')
                        test "${funcs[$i]}" == "$anArg" && continue 2
                done    
                        unset ${funcs[$i]}
        done
fi
unset i j funcs

在开始时,它检查并警告检测到任何函数名称冲突。最后,bash已经获得了所有函数的来源,因此它释放了它们的内存,只保留选中的函数。
可以这样使用:

user@pc:~$ source aLib.sh --source-specific a c
 user@pc:~$ a; b; c
 function a
 bash: b: command not found
 function c

~

相关问题