Экспорт массива в скрипте bash - PullRequest
47 голосов
/ 06 апреля 2011

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

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

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

export myArray=("Hello" "World")

Для несколькихпричины, по которым мне нужно инициализировать мой массив в несколько строк.Есть ли у вас решение?

Ответы [ 9 ]

46 голосов
/ 06 апреля 2011

Переменные массива (пока) не могут быть экспортированы.

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

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

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

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

31 голосов
/ 21 февраля 2014

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.Однако, если вы поставите скрипт в точку, он будет запущен в той же среде;или если вы запускаете подоболочку через ( ), среда также сохраняется (потому что 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 не будет учитывать 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

Обратите внимание, что в функции sharearray нет declare -a, если вы сделаете так, что массив неявно local для функции, что не то, что нужно. 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» - это просто обычный индексированный массив.

8 голосов
/ 30 сентября 2017

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 / #.

1 голос
/ 21 июня 2013

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

0 голосов
/ 14 октября 2016

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

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

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

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

Поскольку 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 $@ $^

0 голосов
/ 29 апреля 2016

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

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

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

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

Два волшебстваиспользуются символы, разделитель полей и разделитель записей (так что несколько массивов могут быть сериализованы в один поток).

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

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

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

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

while unserialise array
do ...
done
0 голосов
/ 15 января 2016

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

_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[@]})
0 голосов
/ 13 октября 2012

Вы (привет!) Можете использовать это, вам не нужно писать файл, для 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

0 голосов
/ 14 августа 2012

Я редактировал другой пост и допустил ошибку. Augh. В любом случае, может быть, это поможет? https://stackoverflow.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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...