首页 文章

shell脚本可以设置调用shell的环境变量吗?

提问于
浏览 871
348

我正在尝试编写一个shell脚本,在运行时,将设置一些环境变量,这些变量将保留在调用者的shell中 .

setenv FOO foo

在csh / tcsh中,或

export FOO=foo

在sh / bash中只在脚本执行期间设置它 .

我已经知道了

source myscript

将运行脚本的命令而不是启动新的shell,这可能导致设置“调用者”环境 .

但这是一个问题:

我希望这个脚本可以从bash或csh调用 . 换句话说,我希望任何一个shell的用户都能够运行我的脚本并改变他们的shell环境 . 所以'source'对我来说不起作用,因为运行csh的用户无法获取bash脚本,而运行bash的用户无法获取csh脚本 .

是否有任何合理的解决方案不涉及在脚本上编写和维护两个版本?

21 回答

  • 232

    通过使用gdb和setenv(3)可以实现"kind of",尽管我很难推荐实际执行此操作 . (此外,即最新的ubuntu实际上不会让你这样做而不告诉内核对ptrace更加宽容,同样可能也适用于其他发行版) .

    $ cat setfoo
    #! /bin/bash
    
    gdb /proc/${PPID}/exe ${PPID} <<END >/dev/null
    call setenv("foo", "bar", 0)
    END
    $ echo $foo
    
    $ ./setfoo
    $ echo $foo
    bar
    
  • 223

    你总是可以使用别名

    alias your_env='source ~/scripts/your_env.sh'
    
  • 52

    你应该使用模块,参见http://modules.sourceforge.net/

    编辑:自2012年以来,模块包尚未更新,但仍然适用于基础知识 . 所有新功能,铃声和口哨都发生在今天的lmod(我更喜欢它):https://www.tacc.utexas.edu/research-development/tacc-projects/lmod

  • 44

    另一种选择是使用"Environment Modules"(http://modules.sourceforge.net/) . 不幸的是,这引入了第三种语言 . 您可以使用Tcl语言定义环境,但是有一些方便的命令可用于典型修改(prepend与append vs set) . 您还需要安装环境模块 . 然后,您可以使用 module load *XXX* 命名所需的环境 . 模块命令基本上是Thomas Kammeyer所描述的 eval 机制的奇特别名 . 这里的主要优点是你可以用一种语言维护环境,并依靠"Environment Modules"将它翻译成sh,ksh,bash,csh,tcsh,zsh,python(?!?!!)等 .

  • 24

    我没有看到任何答案记录如何通过合作流程解决这个问题 . 像 ssh-agent 这样的常见模式是让子进程打印一个父进程可以 eval 的表达式 .

    bash$ eval $(shh-agent)
    

    例如, ssh-agent 具有选择Csh或Bourne兼容输出语法的选项 .

    bash$ ssh-agent
    SSH2_AUTH_SOCK=/tmp/ssh-era/ssh2-10690-agent; export SSH2_AUTH_SOCK;
    SSH2_AGENT_PID=10691; export SSH2_AGENT_PID;
    echo Agent pid 10691;
    

    (这会导致代理开始运行,但不允许您实际使用它,除非您现在将此输出复制粘贴到shell提示符 . )比较:

    bash$ ssh-agent -c
    setenv SSH2_AUTH_SOCK /tmp/ssh-era/ssh2-10751-agent;
    setenv SSH2_AGENT_PID 10752;
    echo Agent pid 10752;
    

    (如您所见, cshtcsh 使用 setenv 来设置变量 . )

    你自己的程序也可以做到这一点 .

    bash$ foo=$(makefoo)
    

    你的 makefoo 脚本只是简单地计算和打印它,并让调用者用它做任何他们想做的事情 - 将它分配给变量是一个常见的用例,但可能不是你想要硬编码到产生值 .

  • 12

    这不是我称之为杰出的,但是如果你需要从shell调用脚本,这也有效 . 这不是一个好的解决方案,但对于单个静态环境变量,它运行良好 .

    1.)创建一个条件退出0(成功)或1(不成功)的脚本

    if [[ $foo == "True" ]]; then
        exit 0
    else
        exit 1
    

    2.)创建一个依赖于退出代码的别名 .

    alias='myscript.sh && export MyVariable'
    

    您调用调用脚本的别名,该脚本评估条件,通过'&&'退出零以便在父shell中设置环境变量 .

    这是漂浮物,但它可以在紧要关头使用 .

  • 10

    从技术上讲,这是正确的 - 只有'eval'不会分叉另一个shell . 但是,从您尝试在修改后的环境中运行的应用程序的角度来看,差异是nil:子级继承其父级的环境,因此(已修改的)环境将传递给所有降序进程 .

    事实上,改变的环境变量'坚持' - 只要你在父程序/ shell下运行 .

    如果在父(Perl或shell)退出之后保留环境变量是绝对必要的,则父shell必须执行繁重的操作 . 我在文档中看到的一种方法是当前脚本使用必要的“导出”语言生成可执行文件,然后欺骗父shell执行它 - 始终认识到你需要在前面加上如果您试图将修改后的环境的非易失性版本留在后面,请使用'source'命令 . 充其量只是一个克鲁格 .

    第二种方法是修改启动shell环境的脚本(.bashrc或其他)以包含修改后的参数 . 这可能很危险 - 如果您填写初始化脚本,它可能会在下次尝试启动时使您的shell不可用 . 有很多工具可以修改当前的shell;通过在'启动器'上添加必要的调整,您可以有效地推动这些更改 . 一般不是一个好主意;如果你只需要对于特定应用程序套件的环境更改,您必须返回并在之后将shell启动脚本返回到其原始状态(使用vi或其他) .

    简而言之,没有好的(简单的)方法 . 据推测,这很难确保系统的安全性不会受到不可挽回的损害 .

  • 8

    使用“点空间脚本”调用语法 . 例如,以下是使用脚本完整路径的方法:

    . /path/to/set_env_vars.sh
    

    如果您与脚本位于同一目录中,请执行以下操作:

    . set_env_vars.sh
    

    它们在当前shell下执行脚本而不是加载另一个脚本(如果你做了 ./set_env_vars.sh 会发生这种情况) . 因为它在同一个shell中运行,所以您设置的环境变量在退出时将可用 .

    这与调用 source set_env_vars.sh 是一回事,但输入的时间更短,并且可能在某些 source 没有的地方有用 .

  • 4

    在OS X bash下,您可以执行以下操作:
    创建bash脚本文件以取消设置变量

    #!/bin/bash
    unset http_proxy
    

    使文件可执行

    sudo chmod 744 unsetvar
    

    创建别名

    alias unsetvar='source /your/path/to/the/script/unsetvar'
    

    只要您将包含脚本文件的文件夹附加到路径,就可以使用它 .

  • 3

    除了写入条件取决于$ SHELL / $ TERM设置为什么,没有 . 使用Perl有什么问题?它几乎无处不在(我想不出没有它的单一UNIX变体),它会给你带来麻烦 .

  • 2

    我很多年前就做过这个 . 如果我记得正确,我在每个.bashrc和.cshrc中都包含一个别名,带有参数,将设置环境的相应形式别名化为常用形式 .

    然后,您将在两个shell中的任何一个中获取的脚本都有一个具有该最后一个窗体的命令,该命令在每个shell中都是合适的别名 .

    如果我找到具体的别名,我会发布它们 .

  • 1

    在我的.bash_profile中我有:

    # No Proxy
    function noproxy
    {
        /usr/local/sbin/noproxy  #turn off proxy server
        unset http_proxy HTTP_PROXY https_proxy HTTPs_PROXY
    }
    
    
    # Proxy
    function setproxy
    {
        sh /usr/local/sbin/proxyon  #turn on proxy server 
        http_proxy=http://127.0.0.1:8118/
        HTTP_PROXY=$http_proxy
        https_proxy=$http_proxy
        HTTPS_PROXY=$https_proxy
        export http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
    }
    

    因此,当我想要禁用代理时,函数在登录shell中运行并按预期和需要设置变量 .

  • 1

    简短的回答是否定的,你不能改变父进程的环境,但看起来你想要的是一个具有自定义环境变量和用户选择的shell的环境 .

    那么为什么不是简单的

    #!/usr/bin/env bash
    FOO=foo $SHELL
    

    然后,当您完成环境后,只需 exit .

  • 1

    我使用管道,eval和信号创建了一个解决方案 .

    parent() {
        if [ -z "$G_EVAL_FD" ]; then
                die 1 "Rode primeiro parent_setup no processo pai"
        fi
        if [ $(ppid) = "$$" ]; then
                "$@"
        else
                kill -SIGUSR1 $$
                echo "$@">&$G_EVAL_FD
        fi
    }
    parent_setup() {
        G_EVAL_FD=99
        tempfile=$(mktemp -u)
        mkfifo "$tempfile"
        eval "exec $G_EVAL_FD<>'$tempfile'"
        rm -f "$tempfile"
        trap "read CMD <&$G_EVAL_FD; eval \"\$CMD\"" USR1
    }
    parent_setup #on parent shell context
    ( A=1 ); echo $A # prints nothing
    ( parent A=1 ); echo $A # prints 1
    

    它可能适用于任何命令 .

  • 1

    您无法修改调用者的shell,因为它位于不同的进程上下文中 . 当子进程继承shell的变量时,它们自己继承副本 .

    您可以做的一件事是编写一个脚本,根据tcsh或sh的调用方式发出正确的命令 . 如果您的脚本是“setit”,那么执行:

    ln -s setit setit-sh
    

    ln -s setit setit-csh
    

    现在,无论是直接还是别名,都可以从sh执行此操作

    eval `setit-sh`
    

    或者来自csh

    eval `setit-csh`
    

    setit使用$ 0来确定其输出样式 .

    这与人们如何使用TERM环境变量设置有关 .

    这里的优点是setit只是写在你喜欢的shell中,如:

    #!/bin/bash
    arg0=$0
    arg0=${arg0##*/}
    for nv in \
       NAME1=VALUE1 \
       NAME2=VALUE2
    do
       if [ x$arg0 = xsetit-sh ]; then
          echo 'export '$nv' ;'
       elif [ x$arg0 = xsetit-csh ]; then
          echo 'setenv '${nv%%=*}' '${nv##*=}' ;'
       fi
    done
    

    使用上面给出的符号链接和反引号表达式的eval,这具有期望的结果 .

    要简化csh,tcsh或类似shell的调用:

    alias dosetit 'eval `setit-csh`'
    

    或者sh,bash等:

    alias dosetit='eval `setit-sh`'
    

    关于这一点的一个好处是你只需要将列表保存在一个地方 . 从理论上讲,你甚至可以将列表粘贴在一个文件中,并将 cat nvpairfilename 放在"in"和"do"之间 .

    这几乎是如何进行登录shell终端设置的:脚本将输出要在登录shell中执行的语句 . 别名通常用于简化调用,如“tset vt100” . 正如另一个答案中所提到的,INN UseNet新闻服务器中也有类似的功能 .

  • 1

    您可以指示子进程打印其环境变量(通过调用“env”),然后循环父进程中的打印环境变量并对这些变量调用“export” .

    以下代码基于Capturing output of find . -print0 into a bash array

    如果父shell是bash,则可以使用

    while IFS= read -r -d $'\0' line; do
        export "$line"
    done < <(bash -s <<< 'export VARNAME=something; env -0')
    echo $VARNAME
    

    如果父shell是破折号,则 read 不提供-d标志,代码变得更复杂

    TMPDIR=$(mktemp -d)
    mkfifo $TMPDIR/fifo
    (bash -s << "EOF"
        export VARNAME=something
        while IFS= read -r -d $'\0' line; do
            echo $(printf '%q' "$line")
        done < <(env -0)
    EOF
    ) > $TMPDIR/fifo &
    while read -r line; do export "$(eval echo $line)"; done < $TMPDIR/fifo
    rm -r $TMPDIR
    echo $VARNAME
    
  • 1

    您可以使用不同的bash_profile调用另一个Bash . 此外,您还可以创建特殊的bash_profile,以便在multi-bashprofile环境中使用 .

    请记住,您可以使用bashprofile中的函数,并且该函数将在全局范围内可用 . 例如,"function user { export USER_NAME $1 }"可以在运行时设置变量,例如:user olegchir && env | grep olegchir

  • 1

    您的shell进程具有父级环境的副本,并且无法访问父进程的环境 . 当shell进程终止时,您对其环境所做的任何更改都将丢失 . 获取脚本文件是配置shell环境最常用的方法,您可能只想咬住子弹并为两种shell中的每一种维护一个 .

  • 0

    在bash脚本的顶部添加-l标志,即

    #!/usr/bin/env bash -l
    
    ...
    
    export NAME1="VALUE1"
    export NAME2="VALUE2"
    

    现在, NAME1NAME2 的值已导出到当前环境,但这些更改不是永久性的 . 如果您希望它们是永久性的,则需要将它们添加到 .bashrc 文件或其他init文件中 .

    从手册页:

    -l Make bash act as if it had been invoked as a login shell (see INVOCATION below).
    
  • 0

    我没有看到的另一个解决方法是将变量值写入文件 .

    我遇到了一个非常类似的问题,我想能够运行最后一次设置测试(而不是我所有的测试) . 我的第一个计划是编写一个用于设置env变量TESTCASE的命令,然后使用另一个命令来运行测试 . 不用说我和你一样有同样的问题 .

    但后来我想出了这个简单的黑客攻击:

    第一个命令( testset ):

    #!/bin/bash
    
    if [ $# -eq 1 ]
    then
      echo $1 > ~/.TESTCASE
      echo "TESTCASE has been set to: $1"
    else
      echo "Come again?"
    fi
    

    第二个命令( testrun ):

    #!/bin/bash
    
    TESTCASE=$(cat ~/.TESTCASE)
    drush test-run $TESTCASE
    
  • -9

    这有效 - 它不是't what I'使用,但它'works' . 让我们创建一个脚本 teredo 来设置环境变量 TEREDO_WORMS

    #!/bin/ksh
    export TEREDO_WORMS=ukelele
    exec $SHELL -i
    

    它将由Korn shell解释,导出环境变量,然后用新的交互式shell替换它自己 .

    在运行此脚本之前,我们在环境中将 SHELL 设置为C shell,并且未设置环境变量 TEREDO_WORMS

    % env | grep SHELL
    SHELL=/bin/csh
    % env | grep TEREDO
    %
    

    运行脚本时,您处于新shell,另一个交互式C shell中,但环境变量已设置:

    % teredo
    % env | grep TEREDO
    TEREDO_WORMS=ukelele
    %
    

    当您从此shell退出时,原始shell将接管:

    % exit
    % env | grep TEREDO
    %
    

    环境变量未在原始shell的环境中设置 . 如果使用 exec teredo 来运行命令,则原始交互式shell将由设置环境的Korn shell替换,然后由新的交互式C shell替换:

    % exec teredo
    % env | grep TEREDO
    TEREDO_WORMS=ukelele
    %
    

    如果您键入 exit (或Control-D),那么您的shell将退出,可能会将您从该窗口中退出,或者将您带回到实验开始的前一级shell .

    相同的机制适用于Bash或Korn shell . 您可能会发现退出命令后的提示出现在有趣的地方 .


    请注意评论中的讨论 . 这不是我建议的解决方案,但它确实实现了单个脚本的声明目的,即设置适用于所有shell的环境(接受 -i 选项来创建交互式shell) . 您也可以在选项后添加 "$@" 来中继任何其他参数,这可能会使shell可用作一般的'set environment and execute command'工具 . 如果有其他参数,您可能希望省略 -i ,从而导致:

    #!/bin/ksh
    export TEREDO_WORMS=ukelele
    exec $SHELL "${@-'-i'}"
    

    "${@-'-i'}" 位表示'如果参数列表包含至少一个参数,则使用原始参数列表;否则,将 -i 替换为不存在的参数' .

相关问题