我可以让 cd 成为函数的本地吗?

Mas*_*son 27 bash shell-script shell-builtin

是否可以制作一个类似的功能

function doStuffAt {
    cd $1
    # do stuff
}
Run Code Online (Sandbox Code Playgroud)

但是让它如此调用该函数实际上并没有改变我的密码,它只是在函数的持续时间内改变它?我知道我可以保存密码并在最后设置它,但我希望有一种方法可以让它在本地发生而不必担心。

ter*_*don 47

是的。只需让函数在( )子 shell 中运行它的命令而不是{ }组命令:

doStuffAt() (
        cd -- "$1" || exit # the subshell if cd failed.
        # do stuff
)
Run Code Online (Sandbox Code Playgroud)

括号 ( ( )) 打开一个新的子 shell,它将继承其父级的环境。子shell将在运行它的命令完成后立即退出,将您返回到父shell并且cd只会影响子shell,因此您的PWD将保持不变。

请注意,子shell 还会复制所有shell 变量,因此您无法通过全局变量将信息从子shell 函数传递回主脚本。

有关子外壳的更多信息,请查看man bash

(列表)

list 在子 shell 环境中执行(请参阅下面的命令执行环境)。影响 shell 环境的变量赋值和内置命令在命令完成后不再有效。返回状态是列表的退出状态。

相比于:

{ 列表; }

list 只是在当前 shell 环境中执行。列表必须以换行符或分号结束。这称为组命令。返回状态是列表的退出状态。请注意,与元字符 ( and ) 不同,{ 和 } 是保留字,必须出现在允许识别保留字的地方。由于它们不会导致断字,因此它们必须通过空格或其他 shell 元字符与列表分开。

  • 只有 `(...)` 打开子shell,`{...}` 不会。它们的共同点是它们是复合命令。我们关于该主题的参考问题是 https://unix.stackexchange.com/questions/306111/what-is-the-difference-between-the-bash-operators-vs-vs-vs/306141#306141 (9认同)
  • +1,尽管我认为你的原始版本比当前版本更清晰。使用 `( ... )` *代替* `{ ... ; 恕我直言,函数体的 }` 非常微妙,它的影响很可能会被忽略;而*添加* `( ... )` 可以非常清楚地表明发生了什么。 (2认同)

Gil*_*il' 18

这取决于。

您可以将该函数放在子shell 中。(另请参阅括号是否真的将命令放入子shell 中?子shell 中发生的事情会保留在子shell 中。函数对变量、当前目录、重定向、陷阱等所做的更改不会影响调用代码。子shell 从其父shell 继承了所有这些属性,但没有向另一个方向转移。exit在子shell中只退出子shell,而不是调用进程。您可以将一段代码放在一个子shell 中,方法是将其包裹在括号中(括号前后的换行符甚至空格是可选的):

(
  set -e # to exit the subshell as soon as an error happens
  cd -- "$1"
  do stuff # in $1
)
do more stuff # in whatever directory was current before the '('
Run Code Online (Sandbox Code Playgroud)

如果要在子shell 中运行整个函数,可以使用括号代替大括号来包装函数代码。

doStuffAt () (
    set -e
    cd -- "$1"
    # do stuff
)
Run Code Online (Sandbox Code Playgroud)

使用 Korn 风格的函数定义语法,您需要:

function doStuffAt { (
    set -e
    cd -- "$1"
    # do stuff
) }
Run Code Online (Sandbox Code Playgroud)

子shell的缺点是没有什么可以逃脱它。如果您需要更改当前目录但随后更新某些变量,则不能使用子shell 执行此操作。从子shell中检索信息只有两种简单的方法。像任何其他命令一样,子shell 具有退出状态,但这是一个介于 0 和 255 之间的整数,因此它不会传达太多信息。您可以使用命令替换来产生一些输出:命令替换是一个子shell,其标准输出(减去尾随换行符)被收集到一个字符串中。这使您可以输出一个字符串。

data=$(
  set -e
  cd -- "$1"
  do stuff # in $1
)
# Now you're still in the original directory, and you have some data in $data
Run Code Online (Sandbox Code Playgroud)

您可以将当前目录保存到一个变量中,并在以后恢复它。

set -e
old_cwd="$PWD"
cd -- "$1"
…
cd "$old_cwd"
Run Code Online (Sandbox Code Playgroud)

然而,这不是很可靠。如果代码在两个cd命令之间存在,它将位于错误的目录中。如果同时移动旧目录,第二个cd将不会返回到正确的位置。可能位于您无权更改的目录中(因为脚本的权限低于其调用者的权限),在这种情况下,第二个cd将失败。因此,除非您处于不会发生这种情况的受控环境中(例如,cd进出由脚本创建的临时目录),否则您不应该这样做。

如果您需要临时更改目录并以某种方式(例如设置变量)影响 shell 环境,则需要小心地将脚本拆分为影响 shell 环境的部分和更改当前目录的部分。shell 继承了早期 unix 系统的限制,无法返回到以前的目录。现代 unix 系统可以(您可以“保存”当前目录的文件描述符,并fchdir()在异常处理程序中返回它),但此功能没有 shell 接口。


FeR*_*eRD 11

当“临时”进入一个目录做一些工作时——IOW,当你想以某种方式改变目录的范围时——通过使用和利用目录堆栈是有意义的。这是构建脚本之类的常用技术。pushdpopd

假设您正在构建一堆插件。

for plugindir in plugin1 plugin2 plugin2 plugin4; do
  pushd -- "$plugindir"
  make
  popd
done
Run Code Online (Sandbox Code Playgroud)

  • `pushd`/`popd` 很棒,但它们确实与标准输出的路径相呼应。如果这会使输出混乱,请确保将每个重定向到 `/dev/null`。 (3认同)

piz*_*ect 7

[这个答案假设bash ; 它不适用于其他外壳]

在大多数实际情况下,使用子shell 既简单又完美,除非您的函数必须修改主脚本中的变量。

对于这种情况,您可以RETURN在从函数返回时使用陷阱更改回旧的当前目录。该RETURN陷阱不管你的函数是如何退出调用,以及其他功能,默认情况下不继承。

doStuffAt(){
    cd -- "$1" || return
    local opwd=$OLDPWD
    trap 'cd "$opwd"' RETURN
    #
    # do stuff
    shift; "$@"
    # change some variable
    wasAt+=("$PWD")
}


% doStuffAt /
% pwd
/home/user
% doStuffAt /usr
% echo ${wasAt[@]}
/ /usr
Run Code Online (Sandbox Code Playgroud)

但是当您尝试改回旧目录时,旧目录可能已被重命名,并且您将无法通过其路径执行此操作。在Linux 上,您可以/dev/fd在 shell 中模拟打开文件描述符.然后调用fchdir它的更安全的方法:

doStuffAt() {
    local fd
    command exec {fd}< . || return
    trap 'cd -P "/dev/fd/$fd"; exec {fd}<&-' RETURN; exec {fd}<.
    cd -- "$1" || return

    # do stuff
    shift; "$@"
    # change some variable
    wasAt+=("$PWD")
}
Run Code Online (Sandbox Code Playgroud)

这里{fd}<.与局部变量一起使用,而不是固定的 fd,以便函数可重入。

请注意,cd -P /dev/fd/X即使无法访问其解析路径中的前导目录,它也会成功。简单的例子:

t=$(mktemp -d); mkdir -p $t/b/c; exec 7<$t/b/c; chmod -rwx $t
cd -P /dev/fd/7  # this will succeed
pwd
cd $(pwd)        # this will fail
Run Code Online (Sandbox Code Playgroud)

当然,旧的 cwd 可以使其自身无法访问,而不是导致它的目录(通过删除其x模式);但在那种情况下,fchdir(2)也无济于事。

此外,仍然存在删除旧 cwd 的情况。在这种情况下,陷阱可以更改为cd "/dev/fd/$fd"; cd -P . 2>/dev/null-P主要用于美容目的)。但目前尚不清楚改回空的已删除目录有何用处。