为什么 printenv 中没有像 $PS1 这样的变量?

为什么 printenv 中没有像 $PS1 这样的变量?

据我所知,printenv它显示了环境变量,但为什么我看不到其他变量(例如PS1用于自定义 shell 提示符的变量)?

输出到底是什么?printenv为什么没有拾取PS1?是否有比 更全面的输出命令printenv

答案1

这是因为PS1它通常不被导出。

环境变量用于设置子进程的执行环境;由于PS1它只在交互式 shell 中才有意义,所以通常没有必要导出它——它只是一个简单的shell 变量

如果你开始与孩子互动然后它将PS1从 shell 的资源文件中读取并设置它,例如~/.bashrc

如果你export PS1这样做,你将在输出中看到它printenv。或者,你可以使用 bash 内置命令查看普通的 shell 变量,set如下所述如何列出所有变量名称及其当前值?

答案2

有没有比 更全面的输出命令printenv

printenv仅打印环境变量,这可能被认为是一个优点。但是,如果您还想打印 shell 变量,请使用echo "$x"(或printf '%s\n' "$x",这更加稳健) 代替printenv x

steeldriver 的解释这些问题都是有用且正确的,但我在这里以另一种方式来介绍这个主题。

printenv是外部命令——不是内置你的 shell,而是一个与你的 shell 不同的程序。它显示它自己的环境变量,这些变量是从你用来运行它的 shell 继承的。但是,shell 不会将所有变量都传递到其子流程' 环境。相反,它们保持了对哪些变量是环境变量哪些不是。(那些不是的通常被称为shell 变量


Shell 变量

要了解其工作原理,请尝试这些命令,它们被包含在内,( )因此它们可以独立运行1彼此。单独运行时,每个命令的工作方式都相同没有,但( )您在较早的命令中创建的变量仍会存在于后续的命令中。在子 shell 中运行命令可避免这种情况。

创建新变量,然后运行外部命令,不会将该变量传递到命令的环境中。除非在已经有一个环境变量的特殊情况下x,否则此命令不会产生任何输出:

(x=foo; printenv x)

变量不过,在 shell 中已分配。此命令输出foo

(x=foo; echo "$x")

shell 支持将变量传递到命令环境中的语法没有影响当前 shell 的环境。输出如下foo

x=foo printenv x

(当然,这也可以在子 shell 中使用(x=foo printenv x)——但是我没有展示它,( )因为当你使用该语法时,当前 shell 不会进行任何设置,因此不需要使用子 shell 来防止后续命令受到影响。)

这将打印foo,然后打印bar

(x=bar; x=foo printenv x; echo "$x")

出口

当您导出一个变量时,它会自动传递到从同一 shell 运行的所有后续外部命令的环境中。export命令可以做到这一点。您可以在定义变量之前使用它,也可以在定义变量之后使用它,甚至可以定义变量命令export本身。所有这些打印foo

(x=foo; export x; printenv x)
(export x; x=foo; printenv x)
(export x=foo; printenv x)

没有unexport命令。尽管你可以在设置变量之前导出它,但取消设置变量也会取消导出它,也就是说,这不会打印任何内容,而不是打印bar

(x=foo; export x; unset x; x=bar; printenv x)

改变导出变量后的值影响导出的值。这将打印foo,然后bar

(export x=foo; printenv x; x=bar; printenv x)

与其他进程一样,shell 本身会从其父进程继承环境变量。这些变量最初存在于 shell 的环境中,并会自动导出——或者保持如果您选择这样想,那么可以将其导出。这将打印foo(请记住,在其环境中使用set toVAR=val cmd运行):cmdVARval

x=foo bash -c 'printenv x'

子进程中设置的变量不会影响父进程,即使它们被导出。这将打印foo(不是bar):

(x=foo; bash -c 'export x=bar'; echo "$x")

子壳层

子 shell 也是一个子进程2;这还会打印foo

(x=foo; (export x=bar); echo "$x")

这应该更清楚为什么我将大多数这些命令括起来以便( )在子 shell 中运行它们。

不过,子 shell 比较特殊。与其他子进程(例如运行printenv或等外部命令时创建的子进程)不同bash子 shell 继承了其父 shell 的大部分状态。 尤其,子 shell 甚至可以继承未导出的变量. 就像(x=foo; echo "$x")印刷品一样foo, 也是如此(x=foo; (echo "$x"))

未导出的变量仍然不会在子 shell 中导出 - 除非您导出它 - 因此,就像(x=foo; printenv x)不打印任何内容一样,也不会打印任何内容(x=foo; (printenv x))

子 shell 是一种特殊的子进程,它是一种 shell。并非所有作为 shell 的子进程都是子 shell。运行创建的shellbash不是子 shell并且它不继承未导出的变量。因此此命令会打印一个空白行(因为echo即使使用空参数调用也会打印换行符):

(x=foo; bash -c 'echo "$x"')

为什么PS1不是环境变量(通常不应该是环境变量)

最后,至于为什么像提示变量PS1是shell变量而不是环境变量,原因是:

  1. 它们仅在 shell 中需要,而其他程序则不需要。
  2. 它们是为每个交互式 shell 设置的,非交互式 shell 根本不需要它们。也就是说,它们不需要被继承。
  3. 尝试传递PS1到新的 shell 通常会失败,因为shell通常会重置PS1

第 3 点值得进一步解释,但如果你从未尝试创建PS1环境变量,那么你可能真的不需要了解详情。

当 Bash 以非交互方式启动时,它会取消设置PS1

当非交互式 Bash shell 启动时,它总是3 取消设置 PS1. 这将打印一个空白行(不是foo):

PS1=foo bash -c 'echo "$PS1"'

为了验证它实际上是否未设置,而不是仅设置而且为空,您可以运行此命令,它会打印unset

PS1=foo bash -c 'if [[ -v PS1 ]]; then echo set; else echo unset; fi'

要验证这是否独立于其他启动行为,您可以尝试将--login--norc或的任意组合传递到--posix之前-c,或 将 设置BASH_ENV为某个脚本的路径(例如),或者如果您传递了。在任何情况下,非交互式 Bash shell 都不会无法取消设置。BASH_ENV=~/.bashrc PS1=foo bash ...ENV--posixPS1

这意味着如果你导出PS1并运行一个非交互式 shell,本身运行交互式 shell,它不会设置PS1您最初设置的值。出于这个原因——也因为除了 Bash 之外的其他 shell(如 Ksh)并非都以相同的方式运行,并且您PS1为 Bash 编写的方式并不总是适用于这些 shell——我建议不要尝试创建PS1环境变量。只需编辑~/.bashrc以设置您想要的任何提示即可。

当 Bash 交互启动时,它经常设置或重置PS1

相反,如果你取消设置 PS1并运行交互式 Bash shell,即使你通过传递阻止它运行启动脚本中的命令--norc,它仍会自动 PS1设置为默认值。运行后,env -u PS1 bash --norc您将获得一个交互式 Bash shell,并将PS1设置为\s-\v\$。由于 Bash 会扩展\s为 shell 的名称和\v版本号,因此bash-4.3$在 Ubuntu 16.04 LTS 上显示为提示符。请注意,将PS1的值设置为空的字符串与取消设置不同。如下所述,运行PS1= bash会为您提供具有奇怪启动行为的交互式 shell。在实际使用中,应避免PS1在将其设置为空字符串时导出,除非您理解并想要该行为。

但是,如果您设置PS1并运行交互式 Bash shell——并且它不会被中间非交互式 shell 取消设置——它将保留该值……直到启动脚本(如全局/etc/profile(对于登录 shell)或/etc/bash.bashrc,或您的每个用户~/.profile~/.bash_login~/.bash_profile(全部用于登录 shell)或~/.bashrc重置它)。

即使您编辑这些文件以防止它们设置PS1(在/etc/profile和 的情况下/etc/bash.bashrc,我建议不要这样做,因为它们会影响所有用户),您也不能真正依赖这一点。如上所述,从非交互式 shell 启动的交互式 shell 不会有PS1,除非您在非交互式 shell 中重置并重新导出它。此外,在这样做之前您应该三思而后行,因为 shell 代码(包括您可能已定义的 shell 函数)通常会检查以PS1确定它正在运行的 shell 是交互式的还是非交互式的。

检查PS1是确定当前 shell 是否是交互式的常用方法。

这就是为什么它对于非交互式 Bash shell 如此重要4取消设置 PS1自动。作为部分6.3.2 这个 Shell 是交互式的吗?Bash 参考手册说:

[S] 启动脚本可能会检查变量PS1;它在非交互式 shell 中未设置,而在交互式 shell 中设置。

要了解其工作原理,请参见那里的示例。或者查看 Ubuntu 中的实际用途。默认情况下,/etc/profileUbuntu 包括:

if [ "$PS1" ]; then
  if [ "$BASH" ] && [ "$BASH" != "/bin/sh" ]; then
    # The file bash.bashrc already sets the default PS1.
    # PS1='\h:\w\$ '
    if [ -f /etc/bash.bashrc ]; then
      . /etc/bash.bashrc
    fi
  else
    if [ "`id -u`" -eq 0 ]; then
      PS1='# '
    else
      PS1='$ '
    fi
  fi
fi

/etc/bash.bashrc当 shell 为非交互式时,它不应该执行任何操作,但它具有:

# If not running interactively, don't do anything
[ -z "$PS1" ] && return

检查交互性的不同方法的细微差别:

为了实现同样的目标,/etc/skel/.bashrc在创建用户帐户时,它会被复制到用户的主目录中(因此您的帐户~/.bashrc可能类似),它具有:

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac

这是检查 shell 是否为交互式的另一种常用方法:查看通过扩展特殊参数 -(写成$-)包含字母i。通常这具有完全相同的效果。但是,假设您没有修改上面显示的代码(默认情况下该代码出现在 Ubuntu 的 Bash 启动脚本中),并且:

  1. 你将其导出PS1为环境变量,
  2. 它已设置,但空的价值,
  3. 您启动一个交互式 Bash shell...

然后/etc/profile(如果它是登录shell)或者/etc/bash.bashrc将不会运行它们通常为交互式shell运行的命令。~/.bashrc仍然会。

如果您想通过使用来检查 shell 是否是交互式的PS1,并且即使PS1设置了但为空也能获得正确的答案,则可以使用[[ -v PS1 ]][ -v PS1 ]/test -v PS1代替。但请注意,[[关键字以及和shell 内置命令-v的测试是 Bash 特有的。并非所有其他 Bourne 风格的 shell 都接受它们。因此您应该[test不是~/.profile在可能在其他 shell 中运行的脚本(或图形登录时由显示管理器运行的脚本)中使用它们/etc/profile,除非脚本中有其他内容来检查哪个 shell 正在运行,并且仅在该 shell 为 Bash 时执行特定于 Bash 的命令(例如,通过检查$BASH_VERSION)。


笔记

1 本文详细解释子壳层。3.2.4.3 分组命令Bash 参考手册解释了该( )语法。

2请注意情况即使( )没有使用语法,命令也会在子 shell 中运行。例如,当你有|管道中用分隔符分隔的命令,Bash 在子 shell 中运行它们(除非lastpipe shell 选项已设置)。

3除了子壳层。可以说这甚至不是一个例外,因为子 shell 不会像我们通常所说的那样“启动”。(它们实际上没有重要的初始化行为。)请注意,当您bash在 Bash shell 中运行 -- 带或不带参数 -- 时,会创建一个子进程,它是一个 shell,但它是不是一个子壳。

4请注意,并非所有的 shell——甚至不是所有的伯恩风格的贝壳-- 这样做。但是 Bash 确实如此,并且 Bash 代码(包括启动脚本中的代码)非常普遍地依赖它。

相关内容