簡體   English   中英

在 bash 腳本中導出數組

[英]Exporting an array in bash script

我無法將數組從 bash 腳本導出到另一個 bash 腳本,如下所示:

export myArray[0]="Hello"
export myArray[1]="World"

當我這樣寫時,沒有問題:

export myArray=("Hello" "World")

出於多種原因,我需要將數組初始化為多行。 你有什么解決辦法嗎?

數組變量可能(尚未)導出。

來自 ubuntu 10.04 下 bash 版本 4.1.5 的聯機幫助頁。

Chet Ramey(截至 2011 年的當前 bash 維護者)的以下聲明可能是關於此“錯誤”的最官方文檔:

將數組變量編碼到環境中並沒有真正的好方法。

http://www.mail-archive.com/bug-bash@gnu.org/msg01774.html

TL;DR:直接支持可導出數組,包括 bash-4.3,但您可以(有效)通過以下兩種方式之一導出數組:

  • 對子腳本調用方式的簡單修改
  • 使用導出的函數來存儲數組初始化,對子腳本進行簡單的修改

或者,您可以等到 bash-4.3 發布(截至 2014 年 2 月處於開發/RC 狀態,請參閱更新日志中的 ARRAY_EXPORT)。 更新:此功能在 4.3 中啟用。 如果在構建時定義ARRAY_EXPORT ,構建將失敗。 作者已聲明不計划完成此功能。


首先要了解的是 bash 環境(更恰當地說是命令執行環境)不同於 POSIX 環境的概念。 POSIX 環境是一組無類型的name=value對,可以通過各種方式從進程傳遞給它的子進程(實際上是IPC 的一種有限形式)。

bash 執行環境實際上是它的超集,具有類型化變量、只讀和可導出標志、數組、函數等。 這部分解釋了為什么set (bash builtin) 和envprintenv不同。

當您調用另一個 bash shell 時,您正在啟動一個新進程,您會丟失一些 bash 狀態。 但是,如果您點源腳本,則該腳本將在相同的環境中運行; 或者,如果您通過( )運行子 shell,則環境也會被保留(因為 bash 分叉,保留其完整狀態,而不是使用進程環境重新初始化)。


@lesmana 的答案中引用的限制是因為 POSIX 環境只是name=value對,沒有額外的含義,因此沒有商定的方法來編碼或格式化類型變量,請參閱下面有關函數的有趣 bash 怪癖 ,以及即將發生的變化bash-4.3 (已放棄提議的數組功能)。

有幾種簡單的方法可以使用declare -p (內置)將一些 bash 環境輸出為一組一個或多個declare語句,這些語句可用於重建“名稱”的類型和值。 這是基本的序列化,但其他一些答案暗示的復雜性要低得多。 declare -p保留數組索引、稀疏數組和麻煩值的引用。 對於數組的簡單序列化,您可以逐行轉read -a myarray ,並使用read -a myarray來恢復它(適用於連續的 0 索引數組,因為read -a自動分配索引)。

這些方法不需要對您將數組傳遞到的腳本進行任何修改。

declare -p array1 array2 > .bash_arrays       # serialise to an intermediate file
bash -c ". .bash_arrays; . otherscript.sh"    # source both in the same environment

上述bash -c "..."形式的變體有時在 crontab 中(錯誤地)用於設置變量。

替代方案包括:

declare -p array1 array2 > .bash_arrays       # serialise to an intermediate file
BASH_ENV=.bash_arrays otherscript.sh          # non-interactive startup script

或者,作為單線:

BASH_ENV=<(declare -p array1 array2) otherscript.sh

最后一個使用進程替換declare命令的輸出作為 rc 腳本傳遞。 (此方法僅適用於 bash-4.0 或更高版本:早期版本無條件地fstat() rc 文件並使用返回的大小一次read()文件;FIFO 返回大小為 0,因此不能作為希望。)

非交互式 shell (即 shell 腳本)中,由BASH_ENV變量指向的文件是自動獲取的 您必須確保正確調用 bash,可能使用 shebang 顯式調用“bash”,而不是#!/bin/sh因為 bash 在歷史/POSIX 模式下將不支持BASH_ENV

如果您的所有數組名稱碰巧都有一個公共前綴,您可以使用declare -p ${!myprefix*}來擴展它們的列表,而不是枚舉它們。

您可能不應該嘗試使用這種方法導出和重新導入整個bash 環境,一些特殊的 bash 變量和數組是只讀的,並且在修改特殊變量時可能會有其他副作用。

(您也可以通過將數組定義序列化為可導出變量並使用eval來做一些稍微令人不快的事情,但我們不鼓勵使用eval ...

$ array=([1]=a [10]="b c")
$ export scalar_array=$(declare -p array)
$ bash # start a new shell
$ eval $scalar_array
$ declare -p array
declare -a array='([1]="a" [10]="b c")'

)


如上所述,有一個有趣的怪癖:通過環境導出函數的特殊支持:

function myfoo() {
    echo foo
}

使用export -fset +a啟用此行為,將在(進程)環境中導致此行為,可通過printenv看到:

myfoo=() { echo foo
}

變量是functionname (或functioname()以實現向后兼容性),其值為() { functionbody } 當后續的 bash 進程啟動時,它將從每個這樣的環境變量中重新創建一個函數。 如果您查看 bash-4.2 源文件variables.c您會看到以() {開頭的變量是經過特殊處理的。 (雖然禁止使用此語法與declare -f一起創建函數。)更新:shellshock”安全問題與此功能有關,當代系統可能會禁用從環境中自動導入函數作為緩解措施。

但是,如果您繼續閱讀,您會看到一個#if 0 (或#if ARRAY_EXPORT )保護代碼,該代碼檢查以([並以)結尾的變量,以及一條說明“數組變量可能尚未導出”的注釋。 好消息是在當前的開發版本 bash-4.3rc2 中啟用了導出索引數組(非關聯) 功能。 如上所述,此功能不太可能啟用。

我們可以使用它來創建一個函數來恢復所需的任何數組數據:

% function sharearray() {
    array1=(a b c d)
}

% export -f sharearray 

% bash -c 'sharearray; echo ${array1[*]}'

因此,與之前的方法類似,使用以下命令調用子腳本:

bash -c "sharearray; . otherscript.sh"

或者,您可以通過在某個適當的點添加來有條件地調用子腳本中的sharearray函數:

[ "`type -t sharearray`" = "function" ] && sharearray

請注意,在sharearray函數中沒有declare -a ,如果您這樣做,該數組對函數來說是隱式本地的,這不是我們想要的。 bash-4.2 支持declare -g顯式地將變量設為全局變量,因此可以使用 ( declare -ga )。 (由於關聯數組需要declare -A ,因此在 bash-4.2 之前,您將無法將此方法用於關聯數組。)GNU parallel文檔對此方法有一些有用的變化,請參閱手冊頁中--env的討論.


您所提出的問題也表明您可能在export本身時遇到問題。 您可以在創建或修改名稱后導出名稱。 “exportable”是變量的標志或屬性,為方便起見,您還可以在單​​個語句中設置和導出。 bash-4.2 export只需要一個名稱,支持簡單(標量)變量或函數名稱。

即使您(將來)可以導出數組,也可能不支持導出選定的索引(切片)(盡管由於數組是稀疏的,因此沒有理由不允許)。 雖然 bash 也支持語法declare -a name[0] ,下標被忽略,“name”只是一個普通的索引數組。

天啊。 我不知道為什么其他答案讓這變得如此復雜。 Bash 幾乎內置了對此的支持。

在導出腳本中:

myArray=( '  foo"bar  ' $'\n''\nbaz)' )  # an array with two nasty elements

myArray="${myArray[@]@Q}" ./importing_script.sh

(注意,雙引號對於正確處理數組元素中的空格是必要的。)

進入importing_script.shmyArray環境變量的值包含以下 26 個字節:

'  foo"bar  ' $'\n\\nbaz)'

然后以下內容將重組數組:

eval "myArray=( ${myArray} )"

警告! 如果您不信任myArray環境變量的來源,請不要這樣eval 這個技巧展示了“Little Bobby Tables”漏洞。 想象一下,如果有人將myArray的值設置為) ; rm -rf / # ) ; rm -rf / # .

基於@ mr.spuratic使用BASH_ENV ,在這里我隧道$@通過script -f -c

script -c <command> <logfile>可用於在另一個 pty(和進程組)內運行命令,但它不能將任何結構化參數傳遞給<command>

相反<command>是一個簡單的字符串,作為system庫調用的參數。

我需要隧道$@外的bash為$@的慶典援引腳本。

由於declare -p不能接受@ ,因此我在這里使用了魔法 bash 變量_ (帶有一個虛擬的第一個數組值,因為它會被 bash 覆蓋)。 這讓我免於踐踏任何重要變量:

概念證明: BASH_ENV=<( declare -a _=("" "$@") && declare -p _ ) bash -c 'set -- "${_[@]:1}" && echo "$@"'

“但是,”你說,“你正在向 bash 傳遞參數——確實我是,但這些是一個簡單的已知字符字符串。這里是script使用

SHELL=/bin/bash BASH_ENV=<( declare -a _=("" "$@") && declare -p _ && echo 'set -- "${_[@]:1}"') script -f -c 'echo "$@"' /tmp/logfile

這給了我這個包裝函數in_pty

in_pty() { SHELL=/bin/bash BASH_ENV=<( declare -a _=("" "$@") && declare -p _ && echo 'set -- "${_[@]:1}"') script -f -c 'echo "$@"' /tmp/logfile }

或者這個無函數包裝器作為 Makefile 的可組合字符串:

in_pty=bash -c 'SHELL=/bin/bash BASH_ENV=<( declare -a _=("" "$$@") && declare -p _ && echo '"'"'set -- "$${_[@]:1}"'"'"') script -qfc '"'"'"$$@"'"'"' /tmp/logfile' --

...

$(in_pty) test --verbose $@ $^

環境只是鍵值對的集合,它們都是字符串。 適用於任何類型數組的適當解決方案可以

  • 將每個元素保存在不同的變量中(例如 MY_ARRAY_0=myArray[0])。 由於動態變量名稱而變得復雜。
  • 將數組保存在文件系統中(聲明 -p myArray >file)。
  • 將所有數組元素序列化為單個字符串。

這些在其他帖子中都有介紹。 如果您知道您的值從不包含某個字符(例如| )並且您的鍵是連續整數,您可以簡單地將數組保存為一個分隔列表:

export MY_ARRAY=$(IFS='|'; echo "${myArray[*]}")

並在子進程中恢復:

IFS='|'; myArray=($MY_ARRAY); unset IFS

我正在編輯另一個帖子並犯了一個錯誤。 啊。 無論如何,也許這可能會有所幫助? https://stackoverflow.com/a/11944320/1594168

請注意,由於 shell 的數組格式在 bash 或任何其他 shell 方面沒有記錄,因此很難以獨立於平台的方式返回 shell 數組。 您必須檢查版本,並制作一個簡單的腳本,將所有 shell 數組連接到一個其他進程可以解析的文件中。

但是,如果您知道要帶回家的陣列的名稱,那么有一種方法,雖然有點臟。

可以說我有

MyAry[42]="whatever-stuff";
MyAry[55]="foo";
MyAry[99]="bar";

所以我想把它帶回家

name_of_child=MyAry
take_me_home="`declare -p ${name_of_child}`";
export take_me_home="${take_me_home/#declare -a ${name_of_child}=/}"

通過從子進程中檢查,我們可以看到它被導出

echo ""|awk '{print "from awk =["ENVIRON["take_me_home"]"]";  }'

結果 :

from awk =['([42]="whatever-stuff" [55]="foo" [99]="bar")']

如果我們絕對必須,請使用 env var 轉儲它。

env > some_tmp_file

然后

在運行另一個腳本之前,

# This is the magic that does it all
source some_tmp_file

正如 lesmana 報道的那樣,你不能導出數組。 所以你必須在通過環境之前將它們序列化。 這種序列化在其他只適合字符串的地方也很有用(su -c 'string', ssh host 'string')。 執行此操作的最短代碼方法是濫用“getopt”

# preserve_array(arguments). return in _RET a string that can be expanded
# later to recreate positional arguments. They can be restored with:
#   eval set -- "$_RET"
preserve_array() {
    _RET=$(getopt --shell sh --options "" -- -- "$@") && _RET=${_RET# --}
}

# restore_array(name, payload)
restore_array() {
   local name="$1" payload="$2"
   eval set -- "$payload"
   eval "unset $name && $name=("\$@")"
}

像這樣使用它:

foo=("1: &&& - *" "2: two" "3: %# abc" )
preserve_array "${foo[@]}"
foo_stuffed=${_RET}
restore_array newfoo "$foo_stuffed"
for elem in "${newfoo[@]}"; do echo "$elem"; done

## output:
# 1: &&& - *
# 2: two
# 3: %# abc

這不解決未設置/稀疏數組。 您也許可以減少 restore_array 中的 2 個“eval”調用。

你(嗨!)可以使用這個,不需要寫文件,對於 ubuntu 12.04,bash 4.2.24

此外,您的多行數組可以導出。

貓 >>exportArray.sh

function FUNCarrayRestore() {
    local l_arrayName=$1
    local l_exportedArrayName=${l_arrayName}_exportedArray

    # if set, recover its value to array
    if eval '[[ -n ${'$l_exportedArrayName'+dummy} ]]'; then
        eval $l_arrayName'='`eval 'echo $'$l_exportedArrayName` #do not put export here!
    fi
}
export -f FUNCarrayRestore

function FUNCarrayFakeExport() {
    local l_arrayName=$1
    local l_exportedArrayName=${l_arrayName}_exportedArray

    # prepare to be shown with export -p
    eval 'export '$l_arrayName
    # collect exportable array in string mode
    local l_export=`export -p \
        |grep "^declare -ax $l_arrayName=" \
        |sed 's"^declare -ax '$l_arrayName'"export '$l_exportedArrayName'"'`
    # creates exportable non array variable (at child shell)
    eval "$l_export"
}
export -f FUNCarrayFakeExport

在終端 bash 上測試此示例(適用於 bash 4.2.24):

source exportArray.sh
list=(a b c)
FUNCarrayFakeExport list
bash
echo ${list[@]} #empty :(
FUNCarrayRestore list
echo ${list[@]} #profit! :D

我可以在這里改進它

PS。:如果有人清除/改進/makeItRunFaster我想知道/看到,謝謝! :D

對於沒有空格的值的數組,我一直在使用一組簡單的函數來遍歷每個數組元素並連接數組:

_arrayToStr(){
    array=($@)

    arrayString=""
    for (( i=0; i<${#array[@]}; i++ )); do
        if [[ $i == 0 ]]; then
            arrayString="\"${array[i]}\""
        else
            arrayString="${arrayString} \"${array[i]}\""
        fi
    done

    export arrayString="(${arrayString})"
}

_strToArray(){
    str=$1

    array=${str//\"/}
    array=(${array//[()]/""})

    export array=${array[@]}
}

第一個函數通過添加左括號和右括號並轉義所有雙引號將數組轉換為字符串。 第二個函數將去除引號和括號並將它們放入一個虛擬數組中。

為了導出數組,您需要傳入原始數組的所有元素:

array=(foo bar)
_arrayToStr ${array[@]}

至此,數組已經導出為值$arrayString。 要在目標文件中導入數組,請重命名數組並進行相反的轉換:

_strToArray "$arrayName"
newArray=(${array[@]})

非常感謝@stéphane-chazelas,他指出了我之前嘗試的所有問題,現在這似乎可以將數組序列化為標准輸出或變量。

此技術不會對輸入進行 shell 解析(與declare -a / declare -p ),因此可以安全地防止在序列化文本中惡意插入元字符。

注意:換行符不會被轉義,因為read刪除了\\<newlines>字符對,所以-d ...必須傳遞給 read,然后保留未轉義的換行符。

所有這些都在反unserialise功能中進行管理。

使用了兩個魔法字符,字段分隔符和記錄分隔符(以便多個數組可以序列化到同一個流)。

這些字符可以定義為FSRS但都不能定義為newline因為read刪除了轉義的換行符。

轉義字符必須是\\反斜杠,因為這是read用來避免將字符識別為IFS字符的內容。

serialise會將"$@" serialise_to化為 stdout, serialise_to將序列化為$1命名的變量

serialise() {
  set -- "${@//\\/\\\\}" # \
  set -- "${@//${FS:-;}/\\${FS:-;}}" # ; - our field separator
  set -- "${@//${RS:-:}/\\${RS:-:}}" # ; - our record separator
  local IFS="${FS:-;}"
  printf ${SERIALIZE_TARGET:+-v"$SERIALIZE_TARGET"} "%s" "$*${RS:-:}"
}
serialise_to() {
  SERIALIZE_TARGET="$1" serialise "${@:2}"
}
unserialise() {
  local IFS="${FS:-;}"
  if test -n "$2"
  then read -d "${RS:-:}" -a "$1" <<<"${*:2}"
  else read -d "${RS:-:}" -a "$1"
  fi
}

並反序列化:

unserialise data # read from stdin

或者

unserialise data "$serialised_data" # from args

例如

$ serialise "Now is the time" "For all good men" "To drink \$drink" "At the \`party\`" $'Party\tParty\tParty'
Now is the time;For all good men;To drink $drink;At the `party`;Party   Party   Party:

(沒有尾隨換行符)

再讀一遍:

$ serialise_to s "Now is the time" "For all good men" "To drink \$drink" "At the \`party\`" $'Party\tParty\tParty'
$ unserialise array "$s"
$ echo "${array[@]/#/$'\n'}"

Now is the time 
For all good men 
To drink $drink 
At the `party` 
Party   Party   Party

或者

unserialise array # read from stdin

Bash 的read尊重轉義字符\\ (除非您傳遞 -r 標志)以刪除字符的特殊含義,例如用於輸入字段分隔或行分隔。

如果你想序列化一個數組而不是單純的參數列表,那么只需將你的數組作為參數列表傳遞:

serialise_array "${my_array[@]}"

您可以像read一樣在循環中使用反unserialise ,因為它只是一個包裝讀取 - 但請記住,流不是換行符分隔的:

while unserialise array
do ...
done

我為此編寫了自己的函數並使用IFS改進了方法:

特點

  • 不調用$(...) ,因此不會產生另一個 bash shell 進程
  • 序列化? | 字符轉換為?00?01序列並返回,因此可以在具有這些字符的數組上使用
  • 處理序列化/反序列化之間的行返回字符作為其他字符
  • cygwin bash 3.2.48 Linux bash 4.3.48Linux bash 4.3.48
function tkl_declare_global()
{
  eval "$1=\"\$2\"" # right argument does NOT evaluate
}

function tkl_declare_global_array()
{
  local IFS=$' \t\r\n' # just in case, workaround for the bug in the "[@]:i" expression under the bash version lower than 4.1
  eval "$1=(\"\${@:2}\")"
}

function tkl_serialize_array()
{
  local __array_var="$1"
  local __out_var="$2"

  [[ -z "$__array_var" ]] && return 1
  [[ -z "$__out_var" ]] && return 2

  local __array_var_size
  eval declare "__array_var_size=\${#$__array_var[@]}"

  (( ! __array_var_size )) && { tkl_declare_global $__out_var ''; return 0; }

  local __escaped_array_str=''

  local __index
  local __value
  for (( __index=0; __index < __array_var_size; __index++ )); do
    eval declare "__value=\"\${$__array_var[__index]}\""
    __value="${__value//\?/?00}"
    __value="${__value//|/?01}"
    __escaped_array_str="$__escaped_array_str${__escaped_array_str:+|}$__value"
  done

  tkl_declare_global $__out_var "$__escaped_array_str"

  return 0
}

function tkl_deserialize_array()
{
  local __serialized_array="$1"
  local __out_var="$2"

  [[ -z "$__out_var" ]] && return 1
  (( ! ${#__serialized_array} )) && { tkl_declare_global $__out_var ''; return 0; }

  local IFS='|'
  local __deserialized_array=($__serialized_array)

  tkl_declare_global_array $__out_var

  local __index=0
  local __value
  for __value in "${__deserialized_array[@]}"; do
    __value="${__value//\?01/|}"
    __value="${__value//\?00/?}"
    tkl_declare_global $__out_var[__index] "$__value"
    (( __index++ ))
  done

  return 0
}

例子:

a=($'1 \n 2' "3\"4'" 5 '|' '?')
tkl_serialize_array a b
tkl_deserialize_array "$b" c

雖然這個問題/答案已經很老了,但這篇文章似乎是搜索"bash serialize array"時的熱門話題

而且,雖然最初的問題與序列化/反序列化數組不太相關,但答案似乎確實朝着這個方向發展。

所以……我提供了我的解決方案:

優點
  • 所有核心 Bash 概念
  • 沒有評估
  • 沒有子命令
缺點
  • 函數將變量名作為參數(與實際值相比)
  • 序列化要求至少有一個存在於數組中的字符

serialize_array.sh

#!/usr/bin/env bash

##
# serialize_array
# Serializes a bash array to a string, with a configurable seperator.
#
# $1 = source varname ( contains array to be serialized )
# $2 = target varname ( will contian the serialized string )
# $3 = seperator ( optional, defaults to $'\x01' )
#
# example:
#
#    my_arry=( one "two three" four )
#    serialize_array my_array my_string '|'
#    declare -p my_string
#
# result:
#
#    declare -- my_string="one|two three|four"
#
function serialize_array() {
    declare -n _array=${1} # _array => local
    local IFS
    IFS="${3:-$'\x01'}"
    printf -v ${2} "%s" "${_array[*]}"
}

##
# deserialize_array
# Deserializes a string into a bash array, with a configurable seperator.
#
# $1 = source varname ( contains string to be deserialized )
# $2 = target varname ( will contain the deserialized array )
# $3 = seperator ( optional, defaults to $'\x01' )
#
# example:
#
#    my_string="one|two three|four"
#    deserialize_array my_string my_array '|'
#    declare -p my_array
#
# result:
#
#    declare -a my_array=([0]="one" [1]="two three" [2]="four")
#
function deserialize_array() {
    declare -n _array=${2} #_array => local
    local IFS
    IFS="${3:-$'\x01'}"
    _array=( ${!1} )
}

我認為您可以通過這種方式嘗試(通過在導出獲取腳本):

導出 myArray=(Hello World)

. 你的腳本文件

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM