bash 脚本问题:如果调用一个函数并且把输出保存到一个变量里,那么就会丢失这个函数运行期间对全局变量的修改,这是为什么?

2022-07-19 23:51:37 +08:00
 kgdb00

比如下面这个程序,counter 函数将 count 的值+1 ,但程序最后的输出确还是 1 ,我觉得输出 2 才应该是正常的。

#!/bin/bash

count=1

counter()
{
	count=$((count + 1))
	echo test
}

str=$(counter)

echo $count

如果我调用 counter 的时候没有把输出保存在 str 里,那么程序最后的输出就是 2

1753 次点击
所在节点    Linux
6 条回复
zhlxsh
2022-07-20 00:33:06 +08:00
1. $() 的调用方式是开了一个子进程。子进程 count 的值是不会影响父进程的
2. 在函数里直接覆盖全局变量感觉有些不妥😂不知道别人都是怎么写的
foam
2022-07-20 01:14:47 +08:00
@zhlxsh #1
$() 其实没有 fork 子进程,只是一个 subshell ,可以用 `strace` 看下系统调用,只是 call 了 pipe(),并拷贝了 parent shell 的环境变量。因此 subshell 修改的只是它所在环境的值,无法影响到 parent shell

@OP 如果期望修改 parent shell 的变量,那只能在同一个 shell 环境下去修改。希望返回值的话,可以传个参数进去,然后在函数里使用 printf -v 给该参数赋值

#!/bin/bash

count=1

counter()
{
count=$((count + 1))
printf -v $1 test
}

counter ret

echo $ret

echo $count
wxf666
2022-07-20 03:28:03 +08:00
原因 @zhlxsh #1 和 @foam #2 说了,解决方法 @foam #2 说了,我来扩展下思路


在通过『|』『()』『$()或``』启动的 subshell 中修改变量,只会在 subshell 中生效,不会影响 parent shell:
```bash
declare -i total=0

sum() {
   for i in {1..3}; do
     total+=i
   done
   printf '%4s: %d\n' "$1" "$total"
}

sum '|' | cat
(sum '()')
echo "$(sum '$()')"
echo "外部: $total"
```


结果,三种方式启动的 subshell ,都计算得 total=1+2+3=6 ,但实际都未修改外部的 total:
```
  |: 6
 (): 6
$(): 6
外部: 0
```


若要修改,就要在同一个 shell 环境中。对于『|』,可以尽量用『<<<』『< <()』等代替:
```bash
# seq 3 |
while read -r i; do
   total+=i
done < <(seq 3)
```


如果要捕捉输出,就想办法赋值到某个变量中(如 @foam #2 利用的 printf -v )。但归根结底,还是利用了 bash 的『动态作用域』。

bash 手册说,变量对自身及调用的子函数可见
> variables are visible only to the function and the commands it invokes

函数中使用了某个变量,先在自身找,找不到则在上一层调用者中找,一直到全局作用域
> visible variables and their values are a result of the sequence of function calls that caused execution to reach the current function. The value of a variable that a function sees depends on its value within its caller, if any, whether that caller is the "global" scope or another shell function


1. 所以,简单地,可以直接约定,子函数输出到 out 变量中,调用者直接用 out 变量
```bash
count=1

counter() {
  (( count++ ))
   out='test' # 自身找不到 out ,就在调用者 main 中找到 out 再赋值
}

main() {
   local out
   counter
   echo "count: $count, out: $out"
}

main  # 输出:count: 2, out: test
```


2. 将『要赋值到哪个变量』作为参数 /环境变量,传递给子函数,子函数自己想办法赋值

2.1 使用 @foam #2 说的 printf -v

2.2 使用『引用』
```bash
count=1
global_out=

counter1() {
  # 本函数内,out 是『名为「$1 的值」的变量』的引用(可同名,外部作用域的同名变量暂时被隐藏)
  # 如,out 是 main_out 的引用。对 out 的操作,实际是对 main_out 操作(自身找不到 main_out ,就在 main 找)
   declare -n out=$1; shift
  (( count++ ))
   out='test1'
}

counter2() {
  # 本函数内,out 是『名为「$out 的值」的变量』的引用
  # 右边的 out 是调用者临时扩充的环境变量,如 global_out (自身、main 找不到 global_out ,就在全局作用域找)
   declare -n out=$out
  (( count++ ))
   out='test2'
}

main() {
   local main_out
   counter1 main_out  # 作为参数传递
   out=global_out counter2  # 作为临时环境变量传递(综合觉得这种调用好看)
   echo "count: $count, main_out: $main_out, global_out: $global_out"
}

main  # 输出:count: 3, main_out: test1, global_out: test2
```
haoliang
2022-07-20 04:19:47 +08:00
@foam 你应该是忘了加 `--follow-forks` 给 strace, 所以没看到 child process 的系统调用,即便如此也有 clone 呀... 因此 subshell 是个 child process
zhlxsh
2022-07-20 09:31:51 +08:00
结合上面说的调用的时候有问题,可以通过重定向到一个文件,然后读取这个文件,貌似也可以。如:
counter > /tmp/counter;
str=$(cat /tmp/counter)
foam
2022-07-20 12:52:27 +08:00
@haoliang #4 cc @zhlxsh #1
谢谢指正,之前不知道 clone 也是一个创建子进程的 system call ,学习了。

Clone : Clone, as fork, creates a new process. Unlike fork, these calls allow the child process to share parts of its execution context with the calling process, such as the memory space, the table of file descriptors, and the table of signal handlers

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/867401

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX