据我所知,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
运行):cmd
VAR
val
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变量而不是环境变量,原因是:
- 它们仅在 shell 中需要,而其他程序则不需要。
- 它们是为每个交互式 shell 设置的,非交互式 shell 根本不需要它们。也就是说,它们不需要被继承。
- 尝试传递
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
--posix
PS1
这意味着如果你导出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/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
/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 启动脚本中),并且:
- 你将其导出
PS1
为环境变量,和 - 它已设置,但空的价值,和
- 您启动一个交互式 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 代码(包括启动脚本中的代码)非常普遍地依赖它。