在 bash 或 zsh 中序列化 shell 變數

在 bash 或 zsh 中序列化 shell 變數

有什麼方法可以序列化 shell 變數嗎?假設我有一個變量$VAR,並且我希望能夠將其保存到文件或其他文件中,然後稍後將其讀回以獲取相同的值?

有沒有一種便攜的方法可以做到這一點? (我不這麼認為)

有沒有辦法在 bash 或 zsh 中做到這一點?

答案1

警告:對於任何這些解決方案,您需要意識到您相信資料檔案的完整性是安全的,因為它們將在腳本中作為 shell 程式碼執行。保護它們對於腳本的安全至關重要!

用於序列化一個或多個變數的簡單內聯實現

typeset是的,在 bash 和 zsh 中,您可以使用內建函數和參數以易於檢索的方式序列化變數的內容-p。輸出格式是這樣的,您可以簡單地source輸出來取回您的東西。

 # You have variable(s) $FOO and $BAR already with your stuff
 typeset -p FOO BAR > ./serialized_data.sh

您可以稍後在腳本中或在另一個腳本中像這樣取回內容:

# Load up the serialized data back into the current shell
source serialized_data.sh

這適用於 bash、zsh 和 ksh,包括在不同 shell 之間傳遞資料。 Bash 會將其轉換為其內建declare函數,而 zsh 則使用它來實現此功能,typeset但由於 bash 有一個別名,因此我們可以typeset在此處使用它以實現 ksh 相容性。

使用函數的更複雜的通用實現

上面的實作非常簡單,但是如果您經常呼叫它,您可能會想要給自己一個實用函數以使其更容易。此外,如果您嘗試將上述內容包含在自訂函數中,您將遇到變數作用域問題。這個版本應該可以消除這些問題。

請注意所有這些,為了保持 bash/zsh 交叉相容性,我們將修復這兩種情況typesetdeclare因此程式碼應該在任一或兩個 shell 中運行。這會增加一些體積和混亂,如果您只對一個或另一個外殼執行此操作,則可以消除這些體積和混亂。

為此使用函數(或在其他函數中包含程式碼)的主要問題是,函數typeset產生的程式碼在從函數內部返回腳本時,預設會建立局部變數而不是全域變數。

這可以透過幾種技巧之一來解決。我最初嘗試解決這個問題是透過sed添加-g標誌來解析序列化過程的輸出,以便創建的程式碼在返回時定義一個全域變數。

serialize() {
    typeset -p "$1" | sed -E '0,/^(typeset|declare)/{s/ / -g /}' > "./serialized_$1.sh"
}
deserialize() {
    source "./serialized_$1.sh"
}

請注意,時髦的sed表達式僅匹配第一次出現的“typeset”或“declare”並將其添加-g為第一個參數。有必要只匹配第一次出現,因為,史蒂芬·查澤拉斯在註釋中正確指出,否則它也會匹配序列化字串包含文字換行符後跟單字聲明或排版的情況。

除了修正我最初的解析之外失禮,史蒂芬也建議一種不太脆弱的破解方法,不僅可以解決解析字串的問題,而且可以是一個有用的鉤子,透過使用包裝函數來重新定義重新獲取資料時所採取的操作,從而添加附加功能。不要使用聲明或排版命令玩任何其他遊戲,但在您將此功能作為您自己的另一個功能的一部分包含在內的情況下,或者您無法控制正在寫入的數據以及是否或沒有-g添加標誌。類似的事情也可以使用別名來完成,請參閱吉爾斯的回答進行實施。

為了使結果更加有用,我們可以透過假設參數數組中的每個單字都是變數名稱來迭代傳遞給函數的多個變數。結果變成這樣:

serialize() {
    for var in $@; do
        typeset -p "$var" > "./serialized_$var.sh"
    done
}

deserialize() {
    declare() { builtin declare -g "$@"; }
    typeset() { builtin typeset -g "$@"; }
    for var in $@; do
        source "./serialized_$var.sh"
    done
    unset -f declare typeset
}

無論使用哪種解決方案,用法都將如下所示:

# Load some test data into variables
FOO=(an array or something)
BAR=$(uptime)

# Save it out to our serialized data files
serialize FOO BAR

# For testing purposes unset the variables to we know if it worked
unset FOO BAR

# Load  the data back in from out data files
deserialize FOO BAR

echo "FOO: $FOO\nBAR: $BAR"

答案2

使用重定向、命令替換和參數擴展。需要雙引號來保留空格和特殊字元。尾隨x保存尾隨換行符,否則這些換行符將在命令替換中被刪除。

#!/bin/bash
echo "$var"x > file
unset var
var="$(< file)"
var=${var%x}

答案3

全部序列化 — POSIX

在任何 POSIX shell 中,您可以使用以下命令序列化所有環境變量export -p。這不包括非導出的 shell 變數。輸出被正確引用,以便您可以在同一個 shell 中讀回它並獲得完全相同的變數值。輸出在其他 shell 中可能無法讀取,例如 ksh 使用非 POSIX$'…'語法。

save_environment () {
  export -p >my_environment
}
restore_environment () {
  . ./my_environment
}

序列化部分或全部 — ksh、bash、zsh

Ksh(pdksh/mksh 和 ATT ksh)、bash 和 zsh 提供了更好的工具typeset內建.typeset -p列印所有定義的變數及其值(zsh 省略已用 隱藏的變數值typeset -H)。輸出包含正確的聲明,以便在讀回時導出環境變數(但如果在讀回時變數已經匯出,則不會取消匯出),以便將陣列讀回等。在同一shell 中可讀。您可以傳遞一組變數以在命令列上序列化;如果您不傳遞任何變量,則所有變數都會被序列化。

save_some_variables () {
  typeset -p VAR OTHER_VAR >some_vars
}

在 bash 和 zsh 中,無法從函數中完成恢復,因為typeset函數內的語句僅限於該函數。您需要. ./some_vars在要使用變數值的上下文中運行,注意導出時全域的變數將重新宣告為全域。如果要讀回函數中的值並將其匯出,可以聲明臨時別名或函數。在 zsh 中:

restore_and_make_all_global () {
  alias typeset='typeset -g'
  . ./some_vars
  unalias typeset
}

在 bash 中(使用declare而非typeset):

restore_and_make_all_global () {
  alias declare='declare -g'
  shopt -s expand_aliases
  . ./some_vars
  unalias declare
}

在 ksh 中,typeset在用 定義的函數中宣告局部變量function function_name { … },在用 定義的函數中宣告全域變數function_name () { … }

序列化一些 — POSIX

如果您想要更多控制,可以手動匯出變數的內容。若要將變數的內容精確列印到檔案中,請使用printf內建函數(echo有一些特殊情況,例如echo -n在某些 shell 上並新增換行符):

printf %s "$VAR" >VAR.content

您可以使用 讀回此內容$(cat VAR.content),但命令替換會移除尾隨換行符號。為了避免這種情況,請安排輸出不要以換行符號結尾。

VAR=$(cat VAR.content && echo a)
if [ $? -ne 0 ]; then echo 1>&2 "Error reading back VAR"; exit 2; fi
VAR=${VAR%?}

如果要列印多個變量,可以用單引號引用它們,並將所有嵌入的單引號替換為'\''.這種形式的引用可以讀回任何 Bourne/POSIX 風格的 shell 中。以下程式碼片段適用於任何 POSIX shell。它僅適用於字串變數(以及具有它們的 shell 中的數字變量,儘管它們將作為字串讀回),它不會嘗試處理具有它們的 shell 中的數組變數。

serialize_variables () {
  for __serialize_variables_x do
    eval "printf $__serialize_variables_x=\\'%s\\'\\\\n \"\$${__serialize_variables_x}\"" |
    sed -e "s/'/'\\\\''/g" -e '1 s/=.../=/' -e '$ s/...$//'
  done
}

這是另一種方法,它不分叉子進程,但字串操作較多。

serialize_variables () {
  for __serialize_variables_var do
    eval "__serialize_variables_tail=\${$__serialize_variables_var}"
    while __serialize_variables_quoted="$__serialize_variables_quoted${__serialize_variables_tail%%\'*}"
          [ "${__serialize_variables_tail%%\'*}" != "$__serialize_variables_tail" ]; do
      __serialize_variables_tail="${__serialize_variables_tail#*\'}"
      __serialize_variables_quoted="${__serialize_variables_quoted}'\\''"
    done
    printf "$__serialize_variables_var='%s'\n" "$__serialize_variables_quoted"
  done
}

請注意,在允許只讀變數的 shell 上,如果嘗試讀回只讀變量,將會收到錯誤訊息。

答案4

printf 'VAR=$(cat <<\'$$VAR$$'\n%s\n'$$VAR$$'\n)' "$VAR" >./VAR.file

另一種方法是確保您處理所有'硬引用,如下所示:

sed '"s/'"'/&"&"&/g;H;1h;$!d;g;'"s/.*/VAR='&'/" <<$$VAR$$ >./VAR.file
$VAR
$$VAR$$

或與export

env - "VAR=$VAR" sh -c 'export -p' >./VAR.file 

第一個和第二個選項適用於任何 POSIX shell,假設變數的值不包含字串:

"\n${CURRENT_SHELLS_PID}VAR${CURRENT_SHELLS_PID}\n" 

第三個選項應該適用於任何 POSIX shell,但可能會嘗試定義其他變量,例如_PWD。但事實是,它可能嘗試定義的唯一變數是由 shell 本身設定和維護的 - 因此,即使您export對其中任何一個執行 import 的值 - 例如$PWD- shell 也會簡單地將它們重置為無論如何,立即得到正確的值- 嘗試一下PWD=any_value並親自看看。

而且因為 - 至少對於 GNU 來說- 調試輸出會自動安全引用以重新輸入到 shell,因此無論中的硬引號bash數量如何,這都有效:'"$VAR"

 PS4= VAR=$VAR sh -cx 'VAR=$VAR' 2>./VAR.file

$VAR稍後可以在以下路徑有效的任何腳本中設定為已儲存的值:

. ./VAR.file

相關內容