bash 脚本问题:如果调用一个函数并且把输出保存到一个变量里,那么就会丢失这个函数运行期间对全局变量的修改,这是为什么? - V2EX
Distributions
Ubuntu
Fedora
CentOS
中文资源站
网易开源镜像站
kgdb00
V2EX    Linux

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

  •  
  •   kgdb00 Jul 19, 2022 2192 views
    This topic created in 1398 days ago, the information mentioned may be changed or developed.

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

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

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

    6 replies    2022-07-20 12:52:27 +08:00
    zhlxsh
        1
    zhlxsh  
       Jul 20, 2022 via iPhone
    1. $() 的调用方式是开了一个子进程。子进程 count 的值是不会影响父进程的
    2. 在函数里直接覆盖全局变量感觉有些不妥不知道别人都是怎么写的
    foam
        2
    foam  
       Jul 20, 2022   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  
       Jul 20, 2022   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  
       Jul 20, 2022
    @foam 你应该是忘了加 `--follow-forks` 给 strace, 所以没看到 child process 的系统调用,即便如此也有 clone 呀... 因此 subshell 是个 child process
    zhlxsh
        5
    zhlxsh  
       Jul 20, 2022 via iPhone
    结合上面说的调用的时候有问题,可以通过重定向到一个文件,然后读取这个文件,貌似也可以。如:
    counter > /tmp/counter;
    str=$(cat /tmp/counter)
    foam
        6
    foam  
       Jul 20, 2022
    @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
    About     Help     Advertise     Blog     API     FAQ     Solana     2998 Online   Highest 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 39ms UTC 12:43 PVG 20:43 LAX 05:43 JFK 08:43
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86