返回

解决Bash函数在Source和Fork模式下的输出差异

Linux

Bash函数:Source与Fork执行模式下的输出差异

在 Bash 脚本编程中,函数的行为有时会因为执行方式的不同而产生差异,特别是在使用 source (或 .) 命令和直接运行脚本(fork)时。这种差异主要体现在标准输出 (stdout) 上。

问题分析

当 Bash 脚本通过 source 命令执行时,它会在当前 shell 环境中执行。这意味着脚本中定义的任何变量或函数都会成为当前 shell 的一部分。相反,如果直接运行脚本(例如,bash script.sh),Bash 会创建一个新的子 shell 进程来执行该脚本。这个子 shell 拥有自己独立的环境,对父 shell 或其他子 shell 没有影响。

这种机制上的区别是导致函数输出差异的根本原因。具体到当前的问题,当initial-tests.bash 通过source命令被包含时,do_check 函数直接在主Shell环境中执行,echo 的结果能直接体现在期望的输出位置。而当main.bash 直接执行 initial-tests.bash时,相当于fork了一个子Shell进程来执行。do_check函数中的 echo 语句的输出会在子 Shell 进程结束后返回到父Shell,这时输出的位置就和预期不同了,因为子Shell的标准输出在父Shell中会显示出来。

解决方案

以下是一些解决 Bash 函数在不同执行模式下输出差异的方案。

1. 使用返回值代替直接输出

与其让函数直接将结果 echo 到标准输出,不如让函数返回一个值,并在调用函数后处理这个返回值。

原理: 通过返回值,可以将函数的执行结果明确地传递给调用者,避免依赖于标准输出的隐式传递。这样可以更好地控制输出的时机和位置。

操作步骤:

  • 修改 _functions.bash 中的 first_checkdo_check 函数,让它们返回特定值而不是直接 echo
  • initial-tests.bash 中,接收返回值并进行相应的 echo 操作。

代码示例:

_functions.bash 修改后:

#! /bin/bash

echoerr() {
    echo -ne $red
    echo "ERROR: $*" | ts '[%Y-%m-%d %H:%M:%S]' 1>&2
    echo -ne $nc
}

first_check() {
  echoerr "This is an echoerr with echo -e"
  return 1 # 使用返回值表示 false
}

do_check() {
  local x
  if first_check ; then
     return 1 # 使用返回值表示 false
  else
    return 0 # 使用返回值表示 true 或其他含义
  fi
}

initial-tests.bash 修改后:

#! /bin/bash

source ./scripts/_functions.bash

if do_check ; then
    : # 什么都不做,对应原始脚本中的空输出
else
    echo false
fi

main.bash保持不变:

#! /bin/bash

nc='\e[0m'
yellow='\e[1;33m'
green='\e[1;32m'
red='\e[1;31m'
blue='\e[1;34m'

# Fail early in case of argumentation error
echo -e "${blue}- - - Initial Checks - - -${nc}"
source ./scripts/initial-checks-load-config.bash

现在,无论使用 source 还是直接运行脚本,输出结果都将保持一致。

2. 重定向输出

可以将函数的输出重定向到特定的文件符或变量中,从而避免输出到标准输出,然后根据需要在合适的时候打印这些输出。

原理: 通过重定向,可以精确控制输出的目标。可以临时将输出保存,之后再决定如何处理这些输出。

操作步骤:

  • 修改 _functions.bash 中的 do_check 函数,将 echo 输出重定向到一个变量。
  • initial-tests.bash 中,根据需要打印该变量的内容。

代码示例:

_functions.bash 修改后:

#! /bin/bash

echoerr() {
    echo -ne $red
    echo "ERROR: $*" | ts '[%Y-%m-%d %H:%M:%S]' 1>&2
    echo -ne $nc
}

first_check() {
  echoerr "This is an echoerr with echo -e"
  echo false
}

do_check() {
  local x
  x=$(first_check)

  if [[ "$x" = 'false' ]]; then
     # 不做任何输出,返回空字符串
     :
  else
    echo false
  fi
}

initial-tests.bash 修改后:

#! /bin/bash

source ./scripts/_functions.bash

check=$(do_check)
echo "$check"

main.bash 保持不变.

这种方式通过捕获函数的输出并将其存储在变量中,然后由调用者决定如何处理这个输出,从而避免了直接输出到标准输出带来的问题。

3. 使用进程替换

另一种方法是使用进程替换来捕获函数的输出。

原理: 进程替换允许将一个命令的输出作为一个文件来处理。通过这种方式,可以捕获函数的输出并对其进行进一步的处理。

操作步骤:

  • initial-tests.bash 中使用进程替换来捕获 do_check 函数的输出。

代码示例:

_functions.bash保持原始版本:

#! /bin/bash

echoerr() {
    echo -ne $red
    echo "ERROR: $*" | ts '[%Y-%m-%d %H:%M:%S]' 1>&2
    echo -ne $nc
}

first_check() {
  echoerr "This is an echoerr with echo -e"
  echo false
}

do_check() {
  local x
  x=$(first_check)

  if [[ "$x" = 'false' ]]; then
    echo
  else
    echo false
  fi
}

initial-tests.bash 修改后:

#! /bin/bash

source ./scripts/_functions.bash

check=$(do_check)

if [[ -z "$check" ]] ; then
    :  #如果 check 为空,则什么都不做
else
    echo "$check"
fi

main.bash保持不变.

在这个方案中,我们通过检查check变量是否为空字符串,来决定是否打印输出。这使得无论函数通过source还是直接执行,输出行为都保持一致。

总结

Bash 函数在 source 和 fork 模式下的输出差异是由执行环境的不同造成的。通过使用返回值、重定向输出或进程替换,可以有效解决这一问题。在编写 Bash 脚本时,应根据实际情况选择最合适的方案,以确保脚本在各种执行方式下都能产生预期结果。在进行函数设计时,应优先考虑使用返回值传递函数结果,这样可以使代码更清晰、更易于维护,并避免因直接输出导致的各种问题。在涉及到错误输出时,应该总是使用标准错误输出(stderr),而不要混用标准输出和标准错误输出。