Экспорт массива в скрипт bash

Я не могу экспортировать массив из сценария bash в другой сценарий bash, например так:

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

Когда я пишу так, нет проблем:

export myArray=("Hello" "World")

По нескольким причинам мне нужно инициализировать мой массив в несколько строк. У вас есть какое-нибудь решение?

13 ответов

Решение

Переменные массива нельзя (пока) экспортировать.

С man-страницы bash версии 4.1.5 под Ubuntu 10.04.

Следующее утверждение от Chet Ramey (текущий сопровождающий bash по состоянию на 2011 год), вероятно, является самой официальной документацией об этой "ошибке":

На самом деле не существует хорошего способа кодирования переменной массива в среду.

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

TL;DR: экспортируемые массивы напрямую не поддерживаются вплоть до bash-4.3, но вы можете (эффективно) экспортировать массивы одним из двух способов:

  • простая модификация способа вызова дочерних скриптов
  • используйте экспортированную функцию для хранения инициализации массива с простой модификацией дочерних скриптов

Или вы можете подождать, пока не выйдет bash-4.3 (в состоянии разработки /RC по состоянию на февраль 2014 года, см. ARRAY_EXPORT в журнале изменений). Обновление: эта функция не включена в 4.3. Если вы определите ARRAY_EXPORT при сборке сборка не удастся. Автор заявил, что не планируется завершать эту функцию.


Первое, что нужно понять, это то, что среда bash (точнее среда выполнения команд) отличается от концепции среды POSIX. Среда POSIX представляет собой набор нетипизированных name=value пары, и могут быть переданы от процесса к его дочерним элементам различными способами(фактически ограниченная форма IPC).

Среда выполнения bash, по сути, является ее расширенным набором с типизированными переменными, доступными только для чтения и экспортируемыми флагами, массивами, функциями и многим другим. Это частично объясняет, почему вывод set (встроенный Bash) иenvили жеprintenvотличаются.

Когда вы вызываете другую оболочку bash, вы запускаете новый процесс, вы теряете некоторое состояние bash. Тем не менее, если вы поставите скрипт в точку, он будет запущен в той же среде; или если вы запускаете subshell через( )среда также сохраняется (поскольку bash-вилки сохраняют свое полное состояние, а не реинициализируются с использованием среды процесса).


Ограничение, на которое ссылается ответ @lesmana, возникает из-за того, что среда POSIX простоname=valueпары без дополнительного значения, так что нет согласованного способа кодирования или форматирования типизированных переменных, см. ниже интересную причуду bash, касающуюся функций, и предстоящее изменение в bash-4.3(предложенная функция массива отменена).

Есть несколько простых способов сделать это, используя declare -p(встроенный) для вывода части среды bash в виде набора из одного или несколькихdeclareоператоры, которые могут быть использованы, восстанавливают тип и значение "имени". Это базовая сериализация, но с меньшей сложностью, которую подразумевают другие ответы. declare -p сохраняет индексы массивов, разреженные массивы и цитирование проблемных значений. Для простой сериализации массива вы можете просто выводить значения построчно и использовать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 "..."Форма иногда (неправильно) используется в crontabs для установки переменных.

Альтернативы включают в себя:

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, и поэтому не будет работать так, как ожидается.)

В неинтерактивной оболочке(то есть сценарии оболочки) файл, на который указываетBASH_ENVпеременная автоматически получена. Вы должны убедиться, что bash вызывается правильно, возможно, используя shebang для явного вызова bash, а не#!/bin/shкак баш не почтитBASH_ENVкогда в историческом /POSIX режиме.

Если все имена ваших массивов имеют общий префикс, вы можете использовать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 -fили жеset +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

Заметки нет declare -a в sharearray Если вы сделаете это, массив будет неявно локальным по отношению к функции, а это не то, что нужно. 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]нижний индекс игнорируется, а "имя" - это просто обычный индексированный массив.

Jeez. Я не знаю, почему другие ответы сделали это настолько сложным. Bash имеет почти встроенную поддержку для этого.

В скрипте экспорта:

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

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

(Обратите внимание, что двойные кавычки необходимы для правильной обработки пробелов в элементах массива.)

При входе в importing_script.sh, значение myArray Переменная окружения содержит эти 26 байтов:

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

Затем следующее будет воссоздавать массив:

eval "myArray=( ${myArray} )"

ВНИМАНИЕ! Не делайте eval как это, если вы не можете доверять источнику myArray переменная окружения. Этот трюк демонстрирует уязвимость "Little Bobby Tables". Представьте себе, если кто-то должен был установить значение myArray в ) ; rm -rf / #,

Среда это просто набор пар ключ-значение, оба из которых являются строки символов. Правильное решение, которое работает для любого типа массива, может либо

  • Сохраните каждый элемент в отдельной переменной (например, MY_ARRAY_0=myArray[0]). Усложняется из-за имен динамических переменных.
  • Сохраните массив в файловой системе (объявите -p myArray> file).
  • Сериализуйте все элементы массива в одну строку.

Об этом рассказывается в других сообщениях. Если вы знаете, что ваши значения никогда не содержат определенного символа (например,|), а ваши ключи представляют собой последовательные целые числа, вы можете просто сохранить массив как список с разделителями:

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

И восстановим в дочернем процессе:

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

На основании @mr.spuratic использования BASH_ENVвот я туннель $@ через script -f -c

script -c <command> <logfile> может использоваться для запуска команды внутри другого pty (и группы процессов), но она не может передавать структурированные аргументы <command>,

Вместо <command> простая строка, чтобы быть аргументом system библиотечный звонок.

Мне нужно туннель $@ внешнего удара в $@ Баш, вызванный скриптом.

Как declare -p не может взять @здесь я использую переменную magic 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 }

или эта оболочка без функции в качестве компоноваемой строки для Makefiles:

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

...

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

Как сообщила Лесмана, вы не можете экспортировать массивы. Таким образом, вы должны сериализовать их, прежде чем проходить через среду. Эта сериализация полезна и в других местах, где подходит только строка (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

Это не относится к неустановленным / разреженным массивам. Возможно, вы сможете уменьшить 2 'eval' вызовов в restore_array.

Я редактировал другой пост и допустил ошибку. Augh. В любом случае, может быть, это поможет? https://stackru.com/a/11944320/1594168

Обратите внимание, что поскольку формат массива оболочки недокументирован на стороне bash или любой другой оболочки, очень трудно вернуть массив оболочки независимым от платформы способом. Вам нужно будет проверить версию, а также создать простой сценарий, объединяющий все массивы оболочки в файл, в который могут преобразовываться другие процессы.

Однако, если вы знаете имя массива, который хотите вернуть домой, то есть способ, хотя и немного грязный.

Допустим, у меня есть

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

Хотя этот вопрос / ответы довольно старые, этот пост кажется самым популярным при поиске "bash serialize array"

И хотя исходный вопрос не был полностью связан с сериализацией / десериализацией массивов, похоже, что ответы на него перешли в этом направлении.

Итак, с этим ... предлагаю свое решение:

Плюсы
  • Все основные концепции Bash
  • Нет Evals
  • Нет подкоманд
Минусы
  • Функции принимают имена переменных в качестве аргументов (по сравнению с фактическими значениями)
  • Сериализация требует наличия хотя бы одного символа, которого нет в массиве.

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} )
}

Большое спасибо @stéphane-chazelas, который указал на все проблемы с моими предыдущими попытками, теперь кажется, что это работает для сериализации массива в stdout или в переменную.

Этот метод не выполняет синтаксический анализ ввода (в отличие от declare -a/declare -p) и так безопасен от злонамеренной вставки метасимволов в сериализованный текст.

Примечание: переводы строки не экранированы, потому что read удаляет \<newlines> пара символов, так -d ... вместо этого должны быть переданы для чтения, и затем сохраненные новые строки сохраняются.

Все это управляется в unserialise функция.

Используются два магических символа: разделитель полей и разделитель записей (чтобы несколько массивов можно было сериализовать в один поток).

Эти символы могут быть определены как FS а также RS но ни один из них не может быть определен как newline символ, потому что экранированный символ новой строки удаляется read,

Экранирующий персонаж должен быть \ обратная косая черта, так как это то, что используется read чтобы персонаж не был признан IFS персонаж.

serialise будет сериализовать "$@" вытащить, serialise_to будет сериализовать в Varable названный в $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[@]}"

Ты можешь использовать unserialise в цикле, как вы бы read потому что это просто обернутое чтение - но помните, что поток не разделен символом новой строки:

while unserialise array
do ...
done

Я написал для этого свои собственные функции и улучшил метод с помощью IFS:

Особенности:

  • Не звонит $(...) и поэтому не порождает другой процесс оболочки bash
  • Сериализует ? а также | персонажей в ?00 а также ?01 последовательности и обратно, поэтому их можно использовать над массивом с этими символами
  • Обрабатывает символы возврата строки между сериализацией / десериализацией как другие символы
  • Проверено в cygwin bash 3.2.48 а также Linux 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

Вы (привет!) можете использовать это, вам не нужно писать файл, для Ubuntu 12.04, Bash 4.2.24

Кроме того, ваш массив из нескольких строк может быть экспортирован.

cat >> 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[@]})

Я думаю, вы можете попробовать это так (получив свой скрипт после экспорта):

экспорт myArray=(Привет, мир)

. yourScript.sh

Другие вопросы по тегам