V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
kgdb00
V2EX  ›  Linux

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

  •  
  •   kgdb00 · 2022-07-19 23:51:37 +08:00 · 1727 次点击
    这是一个创建于 835 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    #!/bin/bash
    
    count=1
    
    counter()
    {
    	count=$((count + 1))
    	echo test
    }
    
    str=$(counter)
    
    echo $count
    
    

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

    6 条回复    2022-07-20 12:52:27 +08:00
    zhlxsh
        1
    zhlxsh  
       2022-07-20 00:33:06 +08:00 via iPhone
    1. $() 的调用方式是开了一个子进程。子进程 count 的值是不会影响父进程的
    2. 在函数里直接覆盖全局变量感觉有些不妥😂不知道别人都是怎么写的
    foam
        2
    foam  
       2022-07-20 01:14:47 +08:00   ❤️ 2
    @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
        3
    wxf666  
       2022-07-20 03:28:03 +08:00   ❤️ 3
    原因 @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
        4
    haoliang  
       2022-07-20 04:19:47 +08:00
    @foam 你应该是忘了加 `--follow-forks` 给 strace, 所以没看到 child process 的系统调用,即便如此也有 clone 呀... 因此 subshell 是个 child process
    zhlxsh
        5
    zhlxsh  
       2022-07-20 09:31:51 +08:00 via iPhone
    结合上面说的调用的时候有问题,可以通过重定向到一个文件,然后读取这个文件,貌似也可以。如:
    counter > /tmp/counter;
    str=$(cat /tmp/counter)
    foam
        6
    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
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2784 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 33ms · UTC 14:41 · PVG 22:41 · LAX 07:41 · JFK 10:41
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.