我试图编写一个shell脚本,当运行时,将设置一些环境变量,这些变量将在调用者的shell中保持设置。

setenv FOO foo

在csh/tcsh中,或

export FOO=foo

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

我已经知道了

source myscript

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

但问题是:

我希望这个脚本可以从bash或csh中调用。换句话说,我希望任何一个shell的用户都能够运行我的脚本,并更改他们的shell环境。因此,'source'对我来说不适用,因为运行csh的用户不能获取bash脚本的源代码,而运行bash的用户也不能获取csh脚本的源代码。

有没有合理的解决方案,不需要在脚本上编写和维护两个版本?


当前回答

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

我遇到了一个非常相似的问题,我希望能够运行最后一个集测试(而不是所有的测试)。我的第一个计划是编写一个命令来设置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

其他回答

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

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

ln -s setit setit-sh

and

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新闻服务器中也有类似的功能。

这是有效的-这不是我要用的,但它“有效”。让我们创建一个脚本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
%

当你退出这个外壳时,原来的外壳接管:

% 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可用作通用的“设置环境和执行命令”工具。如果有其他参数,你可能想要省略-i,导致:

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

“${@-'-i'}”位表示“如果参数列表包含至少一个参数,则使用原始参数列表;否则,用-i代替不存在的参数'。

我没有看到任何关于如何使用协作流程来解决这个问题的说明。ssh-agent之类的常见模式是让子进程打印父进程可以求值的表达式。

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;

(正如你所看到的,csh和tcsh使用setenv来设置变量。)

您自己的程序也可以做到这一点。

bash$ foo=$(makefoo)

makefoo脚本将简单地计算并打印值,并让调用者对其做任何想做的事情——将其分配给变量是一种常见的用例,但可能不希望将其硬编码到生成值的工具中。

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

下面的代码基于find的捕获输出。-print0转换为bash数组

如果父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

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

我遇到了一个非常相似的问题,我希望能够运行最后一个集测试(而不是所有的测试)。我的第一个计划是编写一个命令来设置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