第 21 章. 子 shell

运行 shell 脚本会启动一个新的进程,即一个 子 shell

子 shell 是命令处理器的一个独立实例 -- 即在控制台或 xterm 窗口中为您提供提示符的 shell。正如您的命令在命令行提示符下被解释一样,脚本也以 批处理 方式处理一系列命令。每个正在运行的 shell 脚本实际上都是 shell 的一个子进程(子进程)。

一个 shell 脚本本身可以启动子进程。这些 子 shell 允许脚本进行并行处理,实际上是同时执行多个子任务。

#!/bin/bash
# subshell-test.sh

(
# Inside parentheses, and therefore a subshell . . .
while [ 1 ]   # Endless loop.
do
  echo "Subshell running . . ."
done
)

#  Script will run forever,
#+ or at least until terminated by a Ctl-C.

exit $?  # End of script (but will never get here).



Now, run the script:
sh subshell-test.sh

And, while the script is running, from a different xterm:
ps -ef | grep subshell-test.sh

UID       PID   PPID  C STIME TTY      TIME     CMD
500       2698  2502  0 14:26 pts/4    00:00:00 sh subshell-test.sh
500       2699  2698 21 14:26 pts/4    00:00:24 sh subshell-test.sh

          ^^^^

Analysis:
PID 2698, the script, launched PID 2699, the subshell.

Note: The "UID ..." line would be filtered out by the "grep" command,
but is shown here for illustrative purposes.

通常,脚本中的一个 外部命令fork 出一个子进程,[1] 而 Bash 内建命令 则不会。因此,内建命令的执行速度比其等效的外部命令更快,并且使用的系统资源更少。

括号内的命令列表

( 命令 1; 命令 2; 命令 3; ... )

嵌入在以下之间的命令列表括号会作为子 shell 运行。

子 shell 中的变量在子 shell 的代码块外部是不可见的。它们无法被 父进程,即启动子 shell 的 shell 访问。实际上,这些变量是 局部子进程 的变量。

示例 21-1. 子 shell 中的变量作用域

#!/bin/bash
# subshell.sh

echo

echo "We are outside the subshell."
echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
# Bash, version 3, adds the new         $BASH_SUBSHELL variable.
echo; echo

outer_variable=Outer
global_variable=
#  Define global variable for "storage" of
#+ value of subshell variable.

(
echo "We are inside the subshell."
echo "Subshell level INSIDE subshell = $BASH_SUBSHELL"
inner_variable=Inner

echo "From inside subshell, \"inner_variable\" = $inner_variable"
echo "From inside subshell, \"outer\" = $outer_variable"

global_variable="$inner_variable"   #  Will this allow "exporting"
                                    #+ a subshell variable?
)

echo; echo
echo "We are outside the subshell."
echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
echo

if [ -z "$inner_variable" ]
then
  echo "inner_variable undefined in main body of shell"
else
  echo "inner_variable defined in main body of shell"
fi

echo "From main body of shell, \"inner_variable\" = $inner_variable"
#  $inner_variable will show as blank (uninitialized)
#+ because variables defined in a subshell are "local variables".
#  Is there a remedy for this?
echo "global_variable = "$global_variable""  # Why doesn't this work?

echo

# =======================================================================

# Additionally ...

echo "-----------------"; echo

var=41                                                 # Global variable.

( let "var+=1"; echo "\$var INSIDE subshell = $var" )  # 42

echo "\$var OUTSIDE subshell = $var"                   # 41
#  Variable operations inside a subshell, even to a GLOBAL variable
#+ do not affect the value of the variable outside the subshell!


exit 0

#  Question:
#  --------
#  Once having exited a subshell,
#+ is there any way to reenter that very same subshell
#+ to modify or access the subshell variables?

另请参阅 $BASHPID示例 34-2

Note

虽然 $BASH_SUBSHELL 内部变量指示子 shell 的嵌套级别,但 $SHLVL 变量在子 shell 中没有变化

echo " \$BASH_SUBSHELL outside subshell       = $BASH_SUBSHELL"           # 0
  ( echo " \$BASH_SUBSHELL inside subshell        = $BASH_SUBSHELL" )     # 1
  ( ( echo " \$BASH_SUBSHELL inside nested subshell = $BASH_SUBSHELL" ) ) # 2
# ^ ^                           *** nested ***                        ^ ^

echo

echo " \$SHLVL outside subshell = $SHLVL"       # 3
( echo " \$SHLVL inside subshell  = $SHLVL" )   # 3 (No change!)

在子 shell 中进行的目录更改不会传递到父 shell。

示例 21-2. 列出用户配置文件

#!/bin/bash
# allprofs.sh: Print all user profiles.

# This script written by Heiner Steven, and modified by the document author.

FILE=.bashrc  #  File containing user profile,
              #+ was ".profile" in original script.

for home in `awk -F: '{print $6}' /etc/passwd`
do
  [ -d "$home" ] || continue    # If no home directory, go to next.
  [ -r "$home" ] || continue    # If not readable, go to next.
  (cd $home; [ -e $FILE ] && less $FILE)
done

#  When script terminates, there is no need to 'cd' back to original directory,
#+ because 'cd $home' takes place in a subshell.

exit 0

子 shell 可以用于为命令组设置一个 "专用环境"

COMMAND1
COMMAND2
COMMAND3
(
  IFS=:
  PATH=/bin
  unset TERMINFO
  set -C
  shift 5
  COMMAND4
  COMMAND5
  exit 3 # Only exits the subshell!
)
# The parent shell has not been affected, and the environment is preserved.
COMMAND6
COMMAND7
如此处所见,exit 命令仅终止它正在运行的子 shell,而不是父 shell 或脚本。

这种 "专用环境" 的一个应用是测试变量是否已定义。

if (set -u; : $variable) 2> /dev/null
then
  echo "Variable is set."
fi     #  Variable has been set in current script,
       #+ or is an an internal Bash variable,
       #+ or is present in environment (has been exported).

# Could also be written [[ ${variable-x} != x || ${variable-y} != y ]]
# or                    [[ ${variable-x} != x$variable ]]
# or                    [[ ${variable+x} = x ]]
# or                    [[ ${variable-x} != x ]]

另一个应用是检查锁文件

if (set -C; : > lock_file) 2> /dev/null
then
  :   # lock_file didn't exist: no user running the script
else
  echo "Another user is already running that script."
exit 65
fi

#  Code snippet by St�phane Chazelas,
#+ with modifications by Paulo Marcel Coelho Aragao.

+

进程可以在不同的子 shell 中并行执行。这允许将复杂的任务分解为并发处理的子组件。

示例 21-3. 在子 shell 中运行并行进程

	(cat list1 list2 list3 | sort | uniq > list123) &
	(cat list4 list5 list6 | sort | uniq > list456) &
	# Merges and sorts both sets of lists simultaneously.
	# Running in background ensures parallel execution.
	#
	# Same effect as
	#   cat list1 list2 list3 | sort | uniq > list123 &
	#   cat list4 list5 list6 | sort | uniq > list456 &
	
	wait   # Don't execute the next command until subshells finish.
	
	diff list123 list456

将 I/O 重定向到子 shell 使用 "|" 管道操作符,例如ls -al | (命令).

Note

花括号 之间的代码块不会启动子 shell。

{ 命令 1; 命令 2; 命令 3; . . . 命令 N; }

var1=23
echo "$var1"   # 23

{ var1=76; }
echo "$var1"   # 76

注释

[1]

使用 exec 调用的外部命令不会(通常)fork 出子进程/子 shell。