Bash

如何確定變數賦值的返回狀態?

  • September 20, 2020

我在腳本中看到了這樣的構造:

if somevar="$(somecommand 2>/dev/null)"; then
...
fi

這是在某處記錄的嗎?如何確定變數的返回狀態以及它與命令替換有何關係?(例如,我會得到相同的結果if echo "$(somecommand 2>/dev/null)"; then嗎?)

它記錄在(針對 POSIX) 開放組基本規範的第 2.9.1 節簡單命令中。那裡有一堵文字牆;我請您注意最後一段:

如果有命令名稱,則應按照命令搜尋和執行中的說明繼續執行。如果沒有命令名稱,但該命令包含命令替換,則該命令應以執行的最後一個命令替換的退出狀態完成。否則,命令將以零退出狀態完成。

所以,例如,

  Command                                         Exit Status
$ FOO=BAR                                   0 (but see also the note from icarus, below)
$ FOO=$(bar)                                Exit status from "bar"
$ FOO=$(bar)$(quux)                         Exit status from "quux"
$ FOO=$(bar) baz                            Exit status from "baz"
$ foo $(bar)                                Exit status from "foo"

這也是 bash 的工作方式。但另請參閱最後的“不那麼簡單”部分。

phk,在他的問題中,作業就像具有退出狀態的命令,除非有命令替換?, 建議

…看起來好像賦值本身算作命令…退出值為零,但在賦值右側之前應用(例如,命令替換呼叫…)

這不是一個可怕的看待它的方式。確定簡單命令(不包含;&|或)返回狀態的粗略方案是:&&``||

  • 從左到右掃描該行,直到到達末尾或命令字(通常是程序名稱)。
  • 如果您看到變數賦值,則該行的返回狀態可能為 0。
  • 如果您看到命令替換——即$(…)——從該命令中獲取退出狀態。
  • 如果您到達實際命令(不是在命令替換中),請從該命令中獲取退出狀態。
  • 該行的返回狀態是您遇到的最後一個數字。

命令替換作為命令的參數,例如,foo $(bar)不計算在內;您從中獲得退出狀態foo。套用phk的符號,這裡的行為是

 temporary_variable  = EXECUTE( "bar" )
 overall_exit_status = EXECUTE( "foo", temporary_variable )

但這有點過於簡單化了。整體退貨狀況從

A=$( *cmd 1* ) B=$( *cmd 2* ) C=$( *cmd 3* ) D=$( *cmd 4* ) E=mc 2

是 的退出狀態。分配後發生的分配不會將整體退出狀態設置為 0。 *cmd4*``E=``D=icarus他對 phk 問題的回答中提出了一個重要的觀點:變數可以設置為只讀。POSIX 標準第 2.9.1 節的倒數第三段說,

如果任何變數賦值嘗試為在目前 shell 環境中設置了只讀屬性的變數賦值(無論賦值是否在該環境中進行),將發生變數賦值錯誤。有關這些錯誤的後果,請參閱Shell 錯誤的後果。

所以如果你說

readonly A
C=Garfield A=Felix T=Tigger

Garfield返回狀態為 1。字元串,Felix和/或Tigger 是否被命令替換替換並不重要- 但請參閱下面的註釋。

第 2.8.1 節 Shell 錯誤的後果有另一組文本和一個表格,並以

在表中顯示的所有要求互動式 shell 不退出的情況下,shell 不應對發生錯誤的命令執行任何進一步的處理。

一些細節是有道理的;有些不:

  • 正如最後一句似乎指定的那樣,賦值A=有時會*中止命令行。*在上面的範例中,C設置為Garfield,但未T設置(當然,也不是 A)。
  • 同樣, 執行 但不執行。**但是,**在我的 bash 版本(包括 4.1.X 和 4.3.X)中,它確實執行. (順便說一句,這進一步彈劾了 phk 的解釋,即賦值的退出值適用於賦值的右側。)C=$(*cmd1*) A=$(*cmd2*) T=$(*cmd3*)``*cmd1*``*cmd3*

*cmd2*

但這裡有一個驚喜:

在我的 bash 版本中,

只讀A
C=*某事*A=*某事*T=*某事* *cmd 0*

確實執行。尤其,*cmd0*

C=$( *cmd 1* ) A=$( *cmd 2* ) T=$( *cmd 3* ) *cmd 0*

執行 and ,但不執行. (請注意,這與沒有命令時的行為相反。)並且它設置(以及)在. 我想知道這是否是 bash 中的錯誤。 *cmd1*``*cmd3*``*cmd2*``T``C``*cmd0*


沒那麼簡單:

該答案的第一段是指“簡單命令”。  規範說,

“簡單命令”是一系列可選的變數分配和重定向,以任何順序,可選地後跟單詞和重定向,由控制運算符終止。

這些語句類似於我的第一個範例塊中的語句:

$ FOO=BAR
$ FOO=$(bar)
$ FOO=$(bar) baz
$ foo $(bar)

其中前三個包括變數賦值,最後三個包括命令替換。

但是一些變數賦值並不是那麼簡單。  bash(1)說,

賦值語句也可以作為**alias, declare, typeset, export,readonlylocal**內置命令(聲明命令)的參數出現。

對於exportPOSIX 規範說,

退出狀態

0所有名稱操作數均已成功導出。

>0無法導出至少一個名稱,或者-p指定了選項並發生錯誤。

POSIX 不支持local,但是bash(1)說,

**local不在函式內時使用是錯誤的。返回狀態為 0,除非local**在函式外部使用、提供了無效名稱name是只讀變數。

在兩行之間閱讀,我們可以看到聲明命令像

export FOO=$(bar)

local FOO=$(bar)

更像

foo $(bar)

只要他們忽略退出狀態bar 並根據主命令(、、或)為您提供export退出local狀態foo。所以我們有奇怪的像

  Command                                           Exit Status
$ FOO=$(bar)                                    Exit status from "bar"
                                                 (unless FOO is readonly)
$ export FOO=$(bar)                             0 (unless FOO is readonly,
                                                 or other error from “export”)
$ local FOO=$(bar)                              0 (unless FOO is readonly,
                                                 statement is not in a function,
                                                 or other error from “local”)

我們可以用它來證明

$ export FRIDAY=$(date -d tomorrow)
$ echo "FRIDAY   = $FRIDAY, status = $?"
FRIDAY   = Fri, May 04, 2018  8:58:30 PM, status = 0
$ export SATURDAY=$(date -d "day after tomorrow")
date: invalid date ‘day after tomorrow’
$ echo "SATURDAY = $SATURDAY, status = $?"
SATURDAY = , status = 0

myfunc() {
   local x=$(echo "Foo"; true);  echo "x = $x -> $?"
   local y=$(echo "Bar"; false); echo "y = $y -> $?"
   echo -n "BUT! "
   local z; z=$(echo "Baz"; false); echo "z = $z -> $?"
}

$ myfunc
x = Foo -> 0
y = Bar -> 0
BUT! z = Baz -> 1

幸運的是, ShellCheck擷取了錯誤並引發了SC2155,這表明

export foo="$(mycmd)"

應該改為

foo=$(mycmd)
export foo

local foo="$(mycmd)"

應該改為

local foo
foo=$(mycmd)

信用和參考

我得到了連接命令替換的想法 $(bar)$(quux)——來自 Gilles 對 How can I get bash to exit on backtick failure 以類似於 pipefail 的方式的回答?,其中包含與此問題相關的大量資訊。

引用自:https://unix.stackexchange.com/questions/270828