Bash 中的 _ 是不是環境變數

紫雲飛發表於2015-09-06

首先,我們想到的會是 export(等價於 declare -x)命令:

$ export | grep 'declare -x _='

沒有找到,那麼結論就是 _ 不是環境變數?當然沒那麼簡單,否則本篇文章就該結束了。別忘了還有 env(或者 printenv)命令:

$ env | grep '_='

_=/usr/bin/env

這下怎麼辦,_ 到底是不是環境變數?誰說的對?然而下面還有更詭異的:

$ bash -c "export | grep 'declare -x _='"

declare -x _="/bin/bash"

$ bash -c ":;export | grep 'declare -x _='"

當用 bash -c 的方式執行且 export 是第一條命令的時候,_ 被認為是環境變數,一旦執行過別的命令,_ 就變成了非環境變數。

為了找到答案,我只好去翻閱 Bash 原始碼,下面直接說出我從原始碼中得出的三點結論:

1.  Bash 啟動的時候

當 Bash 啟動時,如果 Bash 的父程式給 Bash 傳入的環境變數陣列裡有 _,那麼 Bash 一定會繼承這個環境變數。如果父程式沒有傳入 _ 環境變數,那麼 Bash 會自己建立 _ 這個變數,並把它的初始值賦值為父程式傳入的第 0 個引數(argv[0],通常是Bash 的檔名或完整路徑),但不會把 _ 設定為環境變數,僅僅是一個普通變數,相關程式碼在 variables.c 裡的 initialize_shell_variables 函式裡:

/* Set up initial value of $_ */
temp_var = set_if_not ("_", dollar_vars[0]) # 如果用 Bash 程式碼翻譯這句 c 程式碼就是:[[ -z $_ ]] && _=$0

2. 在執行任意簡單命令之後

我們常見的 $_ 的用法就是用它來獲取上一條執行過的命令的最後一個引數,所以顯然在執行完任意一條命令之後,Bash 都得為這個變數重新賦值,不過除了賦值之外,Bash 還做了一件事,就是把 _ 變數標記為非環境變數。在 execute_cmd.c 裡有個 bind_lastarg 函式就是來幹這兩件事的:

static void
bind_lastarg (arg)
char *arg;
{
SHELL_VAR *var;

if (arg == 0)
arg = "";
var = bind_variable ("_", arg, 0); # 這句程式碼是把 _ 的值設定成上次命令的最後一個引數
VUNSETATTR (var, att_exported); # 這句是把 _ 標記為非環境變數
}

3. 執行外部命令之前

在執行外部命令之前,Bash 會專門把 _ 的值設定成這個外部命令的路徑,同時把 _ 標記為環境變數。相關程式碼在 execute_cmd.c 裡的 execute_disk_command 函式中: 

put_command_name_into_env (command);

這個 put_command_name_into_env 函式的實現在 variables.c 裡:

void
put_command_name_into_env (command_name)
char *command_name;
{
update_export_env_inplace ("_=", 2, command_name); # 這句程式碼翻譯成 Bash 程式碼就是:export _=$command,command 就是外部命令的路徑
}

三點結論說完了,然後再回頭看看剛才那些看似詭異的程式碼示例。

為什麼 export 看不到 _ 而 env 能看到?

因為我們執行測試程式碼通常是在互動式的 Shell 下進行的,互動 Shell 會執行 .bashrc 啟動指令碼,所以當我們執行 export 命令的時候,一定已經執行過別的命令了,在執行任意命令之後,Bash 都會把 _ 標記為非環境變數,所以 export 看不到 _,我們可以開啟一個不執行 rc 指令碼的互動 Shell 再看看:

$ bash --norc

$ export | grep 'declare -x _=' # bash 也是個外部命令,所以按照上面第 3 點結論裡說的,現在這個 Shell 的父 Shell 會把 _ 標記為環境變數,

                                             # 同時把它的值設定為 bash 的路徑,現在這個 Shell 繼承了 _ 這個環境變數,所以這裡能看到 _

declare -x _="/bin/bash"

$ export | grep 'declare -x _=' # 當執行完前一條命令後,按照上面第 2 點結論裡說的,當前 Shell 會把 _ 標記為非環境變數,所以這裡 _ 已經不是環境變數了

env 一直能看到,是因為 env 是外部命令,在執行它之前 Bash 總會把 _ 標記為環境變數,同時把 _ 的值設定為 /usr/bin/env。

為什麼連 export _; export  也看不到 _

雖然 export _ 會把 _ 標記為環境變數,但因為 export _ 也是一條命令,按照上面第二點結論說的,當 export _ 執行後,Bash 又把 _ 標記成了非環境變數。

在執行 _=foo env 這條命令時,env 命令裡看到的 _ 環境變數的值是什麼

是 /usr/bin/env,在執行這條命令時,_ 的值先被賦值成了 foo,然後又被改寫成了 /usr/bin/env。

再通過程式碼演示證明一下第 1 點結論

證明一下 Bash 會繼承父程式的 _ 環境變數,以及在父程式沒有 _ 環境變數的時候 Bash 會在啟動時建立這個 _ 變數。

$ env bash -c 'echo $_' # env 繼承了當前 Shell 專門為它設定的 _ 環境變數,然後又傳給了它的子程式 Shell

/usr/bin/env

$ env _=1 bash -c 'echo $_' # env 重新賦值了 _,然後又傳給了 bash

1

$ env -i bash -c 'echo $_' # env 在呼叫 Bash 時沒有傳入任何環境變數,但 Bash 自己初始化了  _ 變數

bash

$ env -i bash -c 'export' # 但 Bash 初始化的 _ 變數不是環境變數,Bash 在啟動時只強制建立三個環境變數 

declare -x OLDPWD
declare -x PWD="/Users/admin"
declare -x SHLVL="1"

manual 裡少說了什麼

看一下 manual 裡講 $_ 的段落省略了哪些資訊:

At shell startup, set to the absolute pathname used to invoke the shell or shell script being executed as passed in the environment or argument list

這裡說的只是如果父程式沒有傳給 Bash _ 環境變數時的表現,如果傳了 _,Bash 不會做這件事。

Subsequently, expands to the last argument to the previous command, after expansion.

沒有說同時會把 _ 標記為非環境變數。

Also set to the full pathname used to invoke each command executed and placed in the environment exported to that command. 

沒明確指出這裡的 command 只指外部命令,雖然 pathname 這個單詞已經隱含了它是個外部命令了。 

總結

總結一下,這個問題的答案就是:_ 在 Bash 剛剛啟動的時候(執行第一條命令之前)可能是環境變數(來自父程式)或者在執行外部命令之前的那一刻是環境變數,在其他時候都是非環境變數。

為什麼

難道你沒有疑問嗎,為什麼要特意的在執行完一條簡單命令後,就把 _ 變成非環境變數,從 Bash 啟動時就讓它一直保持環境變數不就得了,幹嘛切來切去。為此我特地問了 Bash 現任作者,他的猜測(當時不是他維護 Bash)和我預想的一樣:那就是因為沒什麼用,你想想看,如果你執行的是個內部命令,內部命令本來就執行在當前 Shell 裡,即便不是環境變數,它也能訪問到,如果你執行的是個外部命令,_ 本來就會被改寫成環境變數(同時改值),所以當時寫 Bash 的人就寫了那麼一句,沒有什麼特別的考慮。

相關文章