leafee98-blog/content/posts/为单一命令设置代理环境变量的终端函数.md

5.9 KiB

title date lastmod tags categories weight show_comments draft
为单一命令设置环境变量的终端函数 2021-04-19T14:10:41+08:00 2022-11-17T16:36:00+08:00
linux
shell
bash
tech
50 true false

实际应用

需求

  • 一个命令行函数
  • 将其参数作为命令运行
  • 为运行的命令设置环境变量, 同时不影响原本的 shell 环境变量

结果

function proxyenv {
        (
                proxy_dest="http://localhost:8888"

                proxy=${proxy_dest}
                http_proxy=${proxy_dest}
                https_proxy=${proxy_dest}

                PROXY=${proxy_dest}
                HTTP_PROXY=${proxy_dest}
                HTTPS_PROXY=${proxy_dest}

                export proxy http_proxy https_proxy PROXY HTTP_RPOXY HTTPS_PROXY
                "$@"
        )
}

Update: 注意最后一条命令现在是 "$@" 而不是以前的 eval "$@",这样才能够避免括号产生额外的意义。

在 Bash 下使用 help eval 可以看到 eval 的作用,其会将所有作为参数的字符串合并为一个,然后直接传递给 shell 运行,所以这些字符串会再一次经过变量代换、命令行代换、引号移除等展开过程。

$ help eval
eval: eval [arg ...]
    Execute arguments as a shell command.
    
    Combine ARGs into a single string, use the result as input to the shell,
    and execute the resulting commands.
    
    Exit Status:
    Returns exit status of command or success if command is null.

针对 sudo 命令

在使用 sudo 命令时,它的默认行为是不保留任何当前 shell 下的环境变量,因此即便使用 proxyenv sudo wget 命令,也无法在 wget 中使用这些环境变量。一个临时的解决方案是使用 sudo--preserve-env 参数,像下面这样

$ proxyenv sudo -E wget http://example.org

如果希望简化每次需要敲的命令,可以在 /etc/sudoer 中添加以下内容,如此便可以每次默认保留这几个环境变量,proxyenv sudo wget 也会保留这些环境变量。

Defaults env_keep += "PROXY HTTP_PROXY HTTPS_PROXY proxy http_proxy https_proxy"

分析

使用圆括号创建 subshell 来隔离原来的变量环境, 在内部创建变量或 export 不会影响外部 shell 的变量.

使用 eval "$@" 将传入的参数作为命令执行. 由于 $@ 的特性会将每一个参数展开为一个使用双引号的字符串, 以及 bash 会自动将双引号包裹的字符串作为一整个字符串并处理掉双引号, 所以不必担心在在调用时使用了双引号而导致变量的位置或者数量产生意料之外的行为(见参考 2).

相关知识

subshell 和 export

使用括号包裹的命令会被置于一个 subshell 中运行, 即为这些命令单独开一个子进程并同时应运行 shell 来执行这些命令, 子进程中的 shell 称为 subshell. subshell 中绝大多数的环境变量等均与原 shell 相同, 即便有些没有被 export 的变量也能够在 subshell 中获取(见参考 3).

在 shell 中的变量如果不经过 export, 那么这些变量仅在当前的 shell 以及其 subshell 中可以获取, 在其子进程中则无法获取.

对于 subshell 和普通子进程的环境变量的区别可以见如下:

v="hello shell"

( echo $v )
bash -c 'echo $v'

export v

( echo $v )
bash -c 'echo $v'

上面的输出为:

hello shell

hello shell
hello shell

可以看出, 尚未被 export 的变量可以在 subshell 中获取, 但是子进程(这里为新开的 bash)对于这个变量一无所知, 所以第二行输出一个空行; 而已经 export 的变量可以 subshell 和子进程中获取.

特殊变量 * 和 @

*

Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, it expands to a single word with the value of each parameter separated by the first character of the IFS special variable. That is, "$*" is equivalent to "$1c$2c...", where c is the first character of the value of the IFS variable. If IFS is unset, the parameters are separated by spaces. If IFS is null, the parameters are joined without intervening separators.

@

Expands to the positional parameters, starting from one. When the expansion occurs within double quotes, each parameter expands to a separate word. That is, "$@" is equivalent to "$1" "$2" ... If the double-quoted expansion occurs within a word, the expansion of the first parameter is joined with the beginning part of the original word, and the expansion of the last parameter is joined with the last part of the original word. When there are no positional parameters, "$@" and $@ expand to nothing (i.e., they are removed).

$* 变量会被展开为一个字符串, 不同的参数之间会使用 IFS 变量的第一个字符作为分隔符, 通常情况下是空格, 所以通常的效果就是一个使用空格分隔多个单词的字符串.

$@ 变量会被展开为多个字符串, 不同的参数会成为各自独立的字符串, 若变量展开在一个字符串中, 那么展开后的第一个字符串会加入到原字符串的结尾, 最后一个字符串会加入到原字符串的开头, 比如 "start$@end" 会成为 "start$1" "$2" ... "$n-1" "$nend"

参考

  1. What is the best way to write a wrapper function that runs commands and logs their exit code
  2. Bash script - variable content as a command to run
  3. Do parentheses really put the command in a subshell?
  4. sudoer(5) - Linux manual page