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

AJJ*_*AJJ 8 command-line bash environment-variables ps1

据我所知printenv显示环境变量,但为什么我看不到其他变量,例如PS1自定义 shell 提示?

究竟是什么printenv输出,为什么不回升PS1?是否有更全面的输出命令比printenv?

ste*_*ver 7

那是因为PS1通常不导出。

环境变量用于设置子进程的执行环境;因为PS1只有在交互式 shell 中才真正有意义,所以通常没有任何意义导出它 - 它只是一个普通的shell 变量

如果您启动一个交互式子shell,那么它将PS1从 shell 的资源文件中读取并设置它,例如~/.bashrc

如果你export PS1那么你会在printenv输出中看到它。或者,您可以使用 bash 内置命令查看普通 shell 变量,set如此处所述如何列出所有变量名称及其当前值?


Eli*_*gan 5

是否有更全面的输出命令比printenv?

printenv只打印环境变量,这可能被认为是一个优势。但是,如果你想打印shell变量为好,使用echo "$x"(或者printf '%s\n' "$x",这是更强大的)代替printenv x

steeldriver对这些问题的解释是有用且正确的,但我在这里以另一种方式呈现该主题。

printenv是一个外部命令——不是内置在你的 shell 中,而是一个独立于你的 shell 的程序。它显示了自己的环境变量,这些变量是从您用来运行它的 shell 继承的。但是,shell 不会将所有变量传递到其子进程的环境中。相反,它们区分哪些变量是环境变量,哪些不是。(那些不是通常称为shell variables。)


壳变量

要了解这是如何工作的,请尝试这些命令,它们被包含在内,( )因此它们彼此独立1。单独地,当您在没有 的情况下运行这些命令时,这些命令中的每一个的工作方式都是相同的( ),但是您在之前的命令中创建的变量将仍然存在于以后的命令中。在子shell中运行命令可以防止这种情况。

创建新变量,然后运行外部命令,不会将变量传递到命令的环境中。除非在您已经拥有环境变量的异常情况下x,此命令不会产生任何输出:

(x=foo; printenv x)
Run Code Online (Sandbox Code Playgroud)

但是,该变量在 shell 中分配的。此命令输出foo

(x=foo; echo "$x")
Run Code Online (Sandbox Code Playgroud)

shell 支持将变量传递到命令环境中而不影响当前 shell 环境的语法。这输出foo

x=foo printenv x
Run Code Online (Sandbox Code Playgroud)

(当然,这也适用于子shell——(x=foo printenv x)但我没有展示它,( )因为当您使用该语法时,没有为当前shell设置任何内容,因此无需使用子shell来防止后续命令被做作的。)

这打印foo,然后打印bar

(x=bar; x=foo printenv x; echo "$x")
Run Code Online (Sandbox Code Playgroud)

出口

导出变量时,它会自动传递到从同一 shell 运行的所有后续外部命令的环境中。export命令执行此操作。你可以使用它,你定义变量之前,您需要定义后,或者你甚至可以定义变量export命令本身。所有这些打印foo

(x=foo; export x; printenv x)
Run Code Online (Sandbox Code Playgroud)
(export x; x=foo; printenv x)
Run Code Online (Sandbox Code Playgroud)
(export x=foo; printenv x)
Run Code Online (Sandbox Code Playgroud)

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

(x=foo; export x; unset x; x=bar; printenv x)
Run Code Online (Sandbox Code Playgroud)

但是在导出变量后更改变量的值确实会影响导出的值。这打印foo,然后bar

(export x=foo; printenv x; x=bar; printenv x)
Run Code Online (Sandbox Code Playgroud)

与其他进程一样,您的 shell 本身从其父进程继承环境变量。此类变量最初存在于您的 shell 环境中,并且它们会自动导出——或者保持导出状态,如果您选择这样考虑的话。这会打印foo(请记住,在其环境中使用set toVAR=val cmd运行):cmdVARval

x=foo bash -c 'printenv x'
Run Code Online (Sandbox Code Playgroud)

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

(x=foo; bash -c 'export x=bar'; echo "$x")
Run Code Online (Sandbox Code Playgroud)

子壳

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

(x=foo; (export x=bar); echo "$x")
Run Code Online (Sandbox Code Playgroud)

这应该更清楚为什么我将这些命令中的大部分包含( )在子shell中运行它们。

但是,子shell 是特殊的。与其他子进程不同,例如在运行外部命令时创建的子进程,例如printenvbash子shell 继承了其父shell 的大部分状态。特别是,子shell 甚至会继承未导出的变量。就像(x=foo; echo "$x")印刷品一样foo,也是如此(x=foo; (echo "$x"))

未导出的变量仍然不会在子shell 中导出——除非你导出它——所以,就像(x=foo; printenv x)什么都不打印一样,(x=foo; (printenv x)).

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

(x=foo; bash -c 'echo "$x"')
Run Code Online (Sandbox Code Playgroud)

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

最后,至于为什么像PS1shell这样的提示变量是shell变量而不是环境变量,原因如下:

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

第 3 点值得更多解释,但如果您从不尝试创建PS1环境变量,那么您可能真的不需要知道细节。

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

当非交互式 Bash shell 启动时,它总是3 unsets PS1。这将打印一个空行(不是foo):

PS1=foo bash -c 'echo "$PS1"'
Run Code Online (Sandbox Code Playgroud)

要验证它实际上是未设置的,而不仅仅是设置而是空的,您可以运行它,它会打印unset

PS1=foo bash -c 'if [[ -v PS1 ]]; then echo set; else echo unset; fi'
Run Code Online (Sandbox Code Playgroud)

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

这意味着如果您导出PS1并运行一个本身运行交互式 shell的非交互式 shell,它不会设置为PS1您最初设置的值。出于这个原因——也因为除了 Bash(如 Ksh)之外的其他 shell 的行为方式并不完全相同,而且你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 取消设置——它将保持该值......直到启动脚本像 global /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 是否是交互式的常用方法。

这就是为什么它是对非交互式击炮弹如此重要的4取消设置 PS1自动。如第6.3.2节,这是 Shell Interactive 吗?Bash 参考手册说:

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

要了解这是如何工作的,请参阅那里的示例。或者查看 Ubuntu 中的实际用途。默认情况下,/etc/profile在 Ubuntu 中包括:

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
Run Code Online (Sandbox Code Playgroud)

/etc/bash.bashrc,当 shell 是非交互式的时,它什么都不做,它有:

# If not running interactively, don't do anything
[ -z "$PS1" ] && return
Run Code Online (Sandbox Code Playgroud)

检查交互性的不同方法的微妙之处:

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

# If not running interactively, don't do anything
case $- in
    *i*) ;;
      *) return;;
esac
Run Code Online (Sandbox Code Playgroud)

这是另一个常见的方法来检查,如果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代替。但是请注意,[[关键字以及-v[testshell 内置函数的测试是 Bash 特有的。并非所有其他 Bourne 风格的 shell 都接受它们。所以,你应该不会像脚本中使用它们~/.profile,并/etc/profile可能在其他shell执行(或者通过显示管理器,当你登录图形),除非你有别的东西在检查什么shell运行脚本,只执行的Bash特有的命令当该 shell 是 Bash 时(例如,通过检查$BASH_VERSION)。


笔记

1 本文详细解释了子shell。Bash 参考手册的3.2.4.3 Grouping Commands解释了( )语法。

2请注意,在某些情况下,即使( )不使用语法,命令也会在子 shell 中运行。例如,当您在管道中使用分隔命令|时,Bash 在子shell 中运行每个命令(除非设置了lastpipe shell 选项)。

3子壳除外。可以说这甚至不是一个例外,因为子shell不会按照我们谈论它时的通常意义上的意思“启动”。(它们实际上并没有显着的初始化行为。)请注意,当您bash在 Bash shell 中运行-- 带或不带参数时,会创建一个是 shell 的子进程,但它不是子 shell。

4请注意,并非所有shell——甚至不是所有Bourne 风格的shell——都具有这种行为。但是 Bash 确实如此,而且 Bash 代码(包括启动脚本中的代码)依赖它是很常见的。