为什么在printenv中没有像PS1这样的变量?
从我可以告诉printenv
显示环境变量,但为什么我没有看到其他变量如PS1
自定义shell提示符?
究竟什么是printenv
输出,为什么它不接收PS1
? 有没有比printenv
更全面的输出命令?
那是因为PS1
通常不会导出。
环境变量用于设置子进程的执行环境; 因为PS1
在交互式shell中才真正具有重要性,所以通常没有任何点导出它 – 它只是一个普通的shell变量 。
如果你启动一个交互式子shell ,那么它将从shell的资源文件中读取并设置它的PS1
,例如~/.bashrc
如果export PS1
,则会在printenv
输出中看到它。 或者,您可以使用bash内置set
来查看纯shell变量,如此处所述如何列出所有变量名称及其当前值?
有没有比
printenv
更全面的输出命令?
printenv
只打印环境变量 ,这可能被认为是一个优势。 但是如果你想打印shell变量,也可以使用echo "$x"
(或printf '%s\n' "$x"
,这是更强大的 )而不是printenv x
。
steeldriver对这些问题的解释是有用和正确的,但我在这里以另一种方式提出这个主题。
printenv
是一个外部命令 – 没有内置到你的shell中,而是一个独立于shell的程序。 它显示了自己的环境变量,它是从用于运行它的shellinheritance的环境变量。 但是, shell不会将所有变量传递到其子进程的环境中 。 相反,他们保持区分哪些变量是环境变量而哪些变量不是。 (那些不经常调用shell变量 。)
壳变量
要查看其工作原理,请尝试这些命令,这些命令包含在(
)
因此它们彼此独立地运行。 单独地,当您在没有 (
)
情况下运行它们时,这些命令中的每一个都是相同的,但是您在先前命令中创建的变量仍将存在于以后的命令中。 在子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本身从其父进程inheritance环境变量。 这些变量最初存在于shell的环境中,如果您选择以这种方式考虑,它们会自动导出 – 或保持导出状态。 这打印foo
(记住, VAR=val cmd
运行cmd
,其环境中VAR
设置为val
):
x=foo bash -c 'printenv x'
子进程中设置的变量不会影响父进程,即使它们已导出。 这打印foo
(不是bar
):
(x=foo; bash -c 'export x=bar'; echo "$x")
子shell
子shell也是子进程2 ; 这也打印foo
:
(x=foo; (export x=bar); echo "$x")
这应该更清楚为什么我在(
)
包含大部分这些命令以在子shell中运行它们。
但是,子壳是特殊的。 与其他子进程(如运行printenv
或bash
等外部命令时创建的子进程)不同, 子shellinheritance其大部分父shell的状态 。 特别是, 子壳inheritance甚至未导出的变量 。 就像(x=foo; echo "$x")
打印foo
, (x=foo; (echo "$x"))
。
未导出的变量仍未在子shell中导出 – 除非您将其导出 – 因此,正如(x=foo; printenv x)
打印任何内容一样, (x=foo; (printenv x))
。
子shell是一种特殊的子进程,它是一个shell。 并非所有shell的子进程都是子shell。 通过运行bash
创建的shell不是子shell ,也不会inheritance未导出的变量。 因此,此命令打印一个空行(因为即使用空参数调用, echo
也会打印一个换行符):
(x=foo; bash -c 'echo "$x"')
为什么PS1
不是环境变量(通常不应该是一个)
最后,至于为什么像PS1
这样的提示变量是shell变量而不是环境变量,原因是:
- 它们只需要在shell中,而不是其他程序。
- 它们是为每个交互式shell设置的,非交互式shell根本不需要它们。 也就是说,它们不需要inheritance。
- 尝试将
PS1
传递给新shell通常会失败,因为shell通常会重置PS1
。
第3点值得更多解释,但如果你从未尝试将PS1
作为环境变量,那么你可能并不需要知道细节。
当Bash以非交互方式开始时,它会取消PS1
。
当一个非交互式Bash shell启动时,它总是3个 unset PS1
。 这会打印一个空白行(不是foo
):
PS1=foo bash -c 'echo "$PS1"'
要validation它是否实际未设置,而不是仅设置为空,您可以运行此命令,打印未unset
:
PS1=foo bash -c 'if [[ -v PS1 ]]; then echo set; else echo unset; fi'
要validation这是否与其他启动行为无关,您可以尝试在-c
之前传递--login
, – --norc
或--posix
任意组合,或者将BASH_ENV
设置为某个脚本的路径(例如, BASH_ENV=~/.bashrc PS1=foo bash ...
),如果你通过了--posix
ENV
。 在任何情况下,非交互式Bash shell都无法取消设置PS1
。
这意味着如果您导出PS1
并运行一个本身运行交互式shell的非交互式shell,它将不会设置您最初设置的PS1
值。 出于这个原因 – 还因为除了Bash之外的其他shell(如Ksh)并不都表现出相同的方式,并且为Bash编写PS1
的方式并不总是适用于那些shell – 我建议不要尝试使PS1
成为环境变量。 只需编辑~/.bashrc
即可设置所需的提示。
当Bash以交互方式启动时,它通常会设置或重置PS1
。
相反,如果您取消设置 PS1
并运行交互式Bash shell,即使您通过传递--norc
阻止它从启动脚本运行命令,它仍会自动将 PS1
设置为默认值。 运行env -u PS1 bash --norc
为您提供了一个交互式Bash shell, PS1
设置为\s-\v\$
。 由于Bash将\s
扩展为shell的名称,将\v
扩展为版本号,因此在Ubuntu 16.04 LTS上显示bash-4.3$
作为提示。 请注意,将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是否是交互式的常用方法。
这就是为什么非交互式Bash shell 4自动取消设置 PS1
非常重要的原因。 如第6.3.2节, 这壳牌是否互动? Bash参考手册中说:
[S] tartup脚本可以检查变量
PS1
; 它在非交互式shell中未设置,并在交互式shell中设置。
要了解其工作原理,请参阅其中的示例。 或者查看Ubuntu中的实际用途。 默认情况下,Ubuntu中的/etc/profile
包括:
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
检查交互性的不同方法的细微之处:
为了实现相同的目标, ~/.bashrc
在创建帐户时复制到用户的主目录中(因此~/.bashrc
可能类似),具有:
# If not running interactively, don't do anything case $- in *i*) ;; *) return;; esac
这是检查shell是否是交互式的另一种常见方法:查看通过扩展 特殊参数获得的文本(通过写$-
)是否包含字母i
。 通常这具有完全相同的效果。 但是,假设您没有修改上面显示的代码,默认情况下出现在Ubuntu中的Bash启动脚本中,并且:
- 您导出
PS1
作为环境变量, 和 - 它已设置,但是为空值, 和
- 你启动一个交互式Bash shell …
然后/etc/profile
(如果它是登录shell)或/etc/bash.bashrc
将不会运行它们通常为交互式shell运行的命令。 ~/.bashrc
仍然会。
如果你想通过PS1
检查shell是否是交互式的,即使PS1
设置为空也能得到正确的答案,你可以使用[[ -v PS1 ]]
或[ -v PS1 ]
/ test -v PS1
代替。 但请注意, [[
关键字和[
和test
shell内置函数]的-v
测试特别适用于Bash。 并非所有其他Bourne风格的shell都接受它们。 所以你不应该在~/.profile
和/etc/profile
这样的脚本中使用它们,这些脚本可能在其他shell中运行(或者以图形方式登录时由显示管理器运行),除非你在脚本中有其他东西来检查shell是什么运行并且只在该shell为Bash时执行特定于Bash的命令(例如,通过检查$BASH_VERSION
)。
笔记
1 本文详细介绍了子壳。 3.2.4.3分组 Bash参考手册的命令解释了(
)
语法。
2请注意,在某些情况下,即使使用(
)
语法,命令也会在子shell中运行。 例如,当您将命令分隔为|
在一个管道中 ,Bash在子shell中运行它们中的每一个(除非设置了lastpipe
shell选项 )。
3除了子壳 。 可以说这甚至不是一个例外,因为在我们讨论这个问题时,子壳不会按照通常的意义“启动”。 (它们实际上没有重要的初始化行为。)请注意,当您在Bash shell中运行bash
或不带参数)时,会创建一个shell的子进程,但它不是子shell。
4请注意,并非所有炮弹 – 甚至不是所有Bourne风格的炮弹 -都是这样的。 但Bash确实如此,Bash代码(包括启动脚本中的代码)依赖它是很常见的。