Как вернуть массив в bash без использования глобальных переменных?
У меня есть функция, которая создает массив, и я хочу вернуть массив вызывающему объекту:
create_array() {
local my_list=("a", "b", "c")
echo "${my_list[@]}"
}
my_algorithm() {
local result=$(create_array)
}
при этом я получаю только расширенную строку. Как я могу" вернуть " my_list без использования чего-либо глобального?
14 ответов:
что случилось с глобалами?
возврат массивов на самом деле не практично. Есть много подводных камней.
тем не менее, вот один метод, который работает, если это нормально, что переменная имеет то же имя:
$ f () { local a; a=(abc 'def ghi' jkl); declare -p a; } $ g () { local a; eval $(f); declare -p a; } $ f; declare -p a; echo; g; declare -p a declare -a a='([0]="abc" [1]="def ghi" [2]="jkl")' -bash: declare: a: not found declare -a a='([0]="abc" [1]="def ghi" [2]="jkl")' -bash: declare: a: not found
The
declare -p
команды (кроме одной вf()
используются для отображения состояния массива в демонстрационных целях. Вf()
он используется в качестве механизма для возврата массива.Если вам нужен массив, чтобы иметь другое имя, вы можете сделать что-то вроде этого:
$ g () { local b r; r=$(f); r="declare -a b=${r#*=}"; eval "$r"; declare -p a; declare -p b; } $ f; declare -p a; echo; g; declare -p a declare -a a='([0]="abc" [1]="def ghi" [2]="jkl")' -bash: declare: a: not found -bash: declare: a: not found declare -a b='([0]="abc" [1]="def ghi" [2]="jkl")' -bash: declare: a: not found
Bash не может передавать структуры данных в качестве возвращаемых значений. Возвращаемое значение должно быть числовым состоянием выхода между 0-255. Тем не менее, вы можете, конечно, использовать команду или подстановку процесса для передачи команд в оператор eval, если вы так склонны.
Это редко стоит, имхо. Если вы должны передавать структуры данных в Bash, используйте глобальную переменную-вот для чего они предназначены. Если вы не хотите этого делать по какой-то причине, подумайте с точки зрения позиционного параметры.
ваш пример можно легко переписать, чтобы использовать позиционные параметры вместо глобальных переменных:
use_array () { for idx in "$@"; do echo "$idx" done } create_array () { local array=("a" "b" "c") use_array "${array[@]}" }
все это создает определенное количество ненужной сложности, хотя. Функции Bash обычно работают лучше всего, когда вы относитесь к ним больше как к процедурам с побочными эффектами и вызываете их последовательно.
# Gather values and store them in FOO. get_values_for_array () { :; } # Do something with the values in FOO. process_global_array_variable () { :; } # Call your functions. get_values_for_array process_global_array_variable
Если все, что вас беспокоит, это загрязнение вашего глобального пространства имен, вы также можете использовать unset builtin для удаления глобальная переменная после того, как вы закончите с ней. Используя Ваш оригинальный пример, пусть списка my_list быть глобальным (путем удаления local ключевое слово) и добавить
unset my_list
до конца my_algorithm убирать за собой.
используйте метод, разработанный Мэттом Макклюром: http://notes-matthewlmcclure.blogspot.com/2009/12/return-array-from-bash-function-v-2.html
избегая глобальных переменных означает, что вы можете использовать функцию в трубе. Вот пример:
#!/bin/bash makeJunk() { echo 'this is junk' echo '#more junk and "b@d" characters!' echo '!#$^%^&(*)_^&% ^$#@:"<>?/.,\"'"'" } processJunk() { local -a arr=() # read each input and add it to arr while read -r line do arr[${#arr[@]}]='"'"$line"'" is junk'; done; # output the array as a string in the "declare" representation declare -p arr | sed -e 's/^declare -a [^=]*=//' } # processJunk returns the array in a flattened string ready for "declare" # Note that because of the pipe processJunk cannot return anything using # a global variable returned_string=`makeJunk | processJunk` # convert the returned string to an array named returned_array # declare correctly manages spaces and bad characters eval "declare -a returned_array=${returned_string}" for junk in "${returned_array[@]}" do echo "$junk" done
вывод:
"this is junk" is junk "#more junk and "b@d" characters!" is junk "!#$^%^&(*)_^&% ^$#@:"<>?/.,\"'" is junk
вы не были так далеко с вашим оригинальным решением. У вас было несколько проблем, вы использовали запятую в качестве разделителя, и вам не удалось захватить возвращенные элементы в список, попробуйте это:
my_algorithm() { local result=( $(create_array) ) } create_array() { local my_list=("a" "b" "c") echo "${my_list[@]}" }
учитывая комментарии о встроенных пространствах, несколько настроек с помощью
IFS
может решить, что:my_algorithm() { oldIFS="$IFS" IFS=',' local result=( $(create_array) ) IFS="$oldIFS" echo "Should be 'c d': ${result[1]}" } create_array() { IFS=',' local my_list=("a b" "c d" "e f") echo "${my_list[*]}" }
чистый bash, минимальное и надежное решение, основанное на "объявить-p" встроенный - без безумных глобальных переменных
этот подход включает в себя следующие три шага:
- преобразуйте массив с помощью 'declare-p' и сохраните выходные данные в переменной.
myVar="$( declare -p myArray )"
Выход изdeclare -p
оператор может быть использован для воссоздания массива. Например, выводdeclare -p myVar
может выглядеть так:declare -a myVar='([0]="1st field" [1]="2nd field" [2]="3rd field")'
- использовать Эхо встроенный для передачи переменной в функцию или для передачи ее обратно оттуда.
- чтобы сохранить пробелы в полях массива при повторении переменной, IFS временно устанавливается в управляющий символ (например, вертикальная вкладка).
- повторяется только правая часть оператора declare в переменной - это может быть достигнуто путем расширения параметра формы ${parameter#word}. Как для примера выше:
${myVar#*=}
- наконец, воссоздайте массив, где он передается с помощью eval и' declare-a ' builtins.
Пример 1-возвращает массив из функции
#!/bin/bash # Example 1 - return an array from a function function my-fun () { # set up a new array with 3 fields - note the whitespaces in the # 2nd (2 spaces) and 3rd (2 tabs) field local myFunArray=( "1st field" "2nd field" "3rd field" ) # show its contents on stderr (must not be output to stdout!) echo "now in $FUNCNAME () - showing contents of myFunArray" >&2 echo "by the help of the 'declare -p' builtin:" >&2 declare -p myFunArray >&2 # return the array local myVar="$( declare -p myFunArray )" local IFS=$'\v'; echo "${myVar#*=}" # if the function would continue at this point, then IFS should be # restored to its default value: <space><tab><newline> IFS=' '$'\t'$'\n'; } # main # call the function and recreate the array that was originally # set up in the function eval declare -a myMainArray="$( my-fun )" # show the array contents echo "" echo "now in main part of the script - showing contents of myMainArray" echo "by the help of the 'declare -p' builtin:" declare -p myMainArray # end-of-file
вывод примера 1:
now in my-fun () - showing contents of myFunArray by the help of the 'declare -p' builtin: declare -a myFunArray='([0]="1st field" [1]="2nd field" [2]="3rd field")' now in main part of the script - showing contents of myMainArray by the help of the 'declare -p' builtin: declare -a myMainArray='([0]="1st field" [1]="2nd field" [2]="3rd field")'
Пример 2 - передать массив в функцию
#!/bin/bash # Example 2 - pass an array to a function function my-fun () { # recreate the array that was originally set up in the main part of # the script eval declare -a myFunArray="$( echo "" )" # note that myFunArray is local - from the bash(1) man page: when used # in a function, declare makes each name local, as with the local # command, unless the ‘-g’ option is used. # IFS has been changed in the main part of this script - now that we # have recreated the array it's better to restore it to the its (local) # default value: <space><tab><newline> local IFS=' '$'\t'$'\n'; # show contents of the array echo "" echo "now in $FUNCNAME () - showing contents of myFunArray" echo "by the help of the 'declare -p' builtin:" declare -p myFunArray } # main # set up a new array with 3 fields - note the whitespaces in the # 2nd (2 spaces) and 3rd (2 tabs) field myMainArray=( "1st field" "2nd field" "3rd field" ) # show the array contents echo "now in the main part of the script - showing contents of myMainArray" echo "by the help of the 'declare -p' builtin:" declare -p myMainArray # call the function and pass the array to it myVar="$( declare -p myMainArray )" IFS=$'\v'; my-fun $( echo "${myVar#*=}" ) # if the script would continue at this point, then IFS should be restored # to its default value: <space><tab><newline> IFS=' '$'\t'$'\n'; # end-of-file
вывод примера 2:
now in the main part of the script - showing contents of myMainArray by the help of the 'declare -p' builtin: declare -a myMainArray='([0]="1st field" [1]="2nd field" [2]="3rd field")' now in my-fun () - showing contents of myFunArray by the help of the 'declare -p' builtin: declare -a myFunArray='([0]="1st field" [1]="2nd field" [2]="3rd field")'
полезный пример: возвращает массив из функции
function Query() { local _tmp=`echo -n "$*" | mysql 2>> zz.err`; echo -e "$_tmp"; } function StrToArray() { IFS=$'\t'; set ; for item; do echo $item; done; IFS=$oIFS; } sql="SELECT codi, bloc, requisit FROM requisits ORDER BY codi"; qry=$(Query $sql0); IFS=$'\n'; for row in $qry; do r=( $(StrToArray $row) ); echo ${r[0]} - ${r[1]} - ${r[2]}; done
[Примечание: следующее было отклонено как редактирование ответ на причины, которые не имеют никакого смысла для меня (так как редактирование было не предназначен для обращения к автору поста!), поэтому я беру предложение сделать его отдельным ответом.]
более простая реализация адаптация Стива Зобелла техники Мэтта Макклюра использует встроенный bash (начиная с версии >= 4?)
readarray
как предложил Растаматт для создания представления массива, которое может быть преобразовано в массив во время выполнения. (Обратите внимание, что обаreadarray
иmapfile
имя один и тот же код.) Он по-прежнему избегает глобалов (позволяя использовать функцию в трубе) и по-прежнему обрабатывает неприятные символы. Вырежьте следующий блок и вставьте его в терминал bash, чтобы создать/tmp/source.sh
и/tmp/junk1.sh
:FP='/tmp/source.sh' # path to file to be created for `source`ing cat << 'EOF' > "${FP}" # suppress interpretation of variables in heredoc function make_junk { echo 'this is junk' echo '#more junk and "b@d" characters!' echo '!#$^%^&(*)_^&% ^$#@:"<>?/.,\"'"'" } ### Use 'readarray' (aka 'mapfile', bash built-in) to read lines into an array. ### Handles blank lines, whitespace and even nastier characters. function lines_to_array_representation { local -a arr=() readarray -t arr # output array as string using 'declare's representation (minus header) declare -p arr | sed -e 's/^declare -a [^=]*=//' } EOF FP1='/tmp/junk1.sh' # path to script to run cat << 'EOF' > "${FP1}" # suppress interpretation of variables in heredoc #!/usr/bin/env bash source '/tmp/source.sh' # to reuse its functions returned_string="$(make_junk | lines_to_array_representation)" eval "declare -a returned_array=${returned_string}" for elem in "${returned_array[@]}" ; do echo "${elem}" done EOF chmod u+x "${FP1}" # newline here ... just hit Enter ...
выполнить
/tmp/junk1.sh
: вывод должен бытьthis is junk #more junk and "b@d" characters! !#$^%^&(*)_^&% ^$#@:"<>?/.,\"'
Примечание
lines_to_array_representation
также ручки пустой русло. Попробуйте вставить следующий блок в ваш терминал bash:FP2='/tmp/junk2.sh' # path to script to run cat << 'EOF' > "${FP2}" # suppress interpretation of variables in heredoc #!/usr/bin/env bash source '/tmp/source.sh' # to reuse its functions echo '`bash --version` the normal way:' echo '--------------------------------' bash --version echo # newline echo '`bash --version` via `lines_to_array_representation`:' echo '-----------------------------------------------------' bash_version="$(bash --version | lines_to_array_representation)" eval "declare -a returned_array=${bash_version}" for elem in "${returned_array[@]}" ; do echo "${elem}" done EOF chmod u+x "${FP2}" # newline here ... just hit Enter ...
выполнить
/tmp/junk2.sh
: вывод должен быть следующим (в зависимости от )`bash --version` the normal way: -------------------------------- GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu) Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. `bash --version` via `lines_to_array_representation`: ----------------------------------------------------- GNU bash, version 4.3.30(1)-release (x86_64-pc-linux-gnu) Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software; you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
т. е. выходные данные из необработанных и обработанных команд одинаковы.
С Bash версии 4.3 и выше, вы можете использовать nameref чтобы вызывающий абонент мог передать имя массива, а вызываемый абонент мог использовать nameref для заполнения именованного массива,косвенно.
#!/usr/bin/env bash create_array() { local -n arr= # use nameref for indirection arr=(one "two three" four) } use_array() { local my_array create_array my_array # call function to populate the array echo "inside use_array" declare -p my_array # test the array } use_array # call the main function
производит вывод:
inside use_array declare -a my_array=([0]="one" [1]="two three" [2]="four")
Это более элегантный и эффективный подход, так как мы не нуждаемся в замене команд
$()
для захвата стандартного вывода вызываемой функции. Это также помогает, если функция была возврат более одного вывода-мы можем просто использовать столько namerefs, сколько количество выходов.
здесь Bash Manual говорит о nameref:
переменной можно присвоить атрибут nameref с помощью опции-n в объявлении или местные встроенных команд (см. Баш примитивы) для создания nameref, или ссылка на другую переменную. Это позволяет переменные чтобы им манипулировали косвенно. Всякий раз, когда переменная nameref ссылается, назначается, отменяется или имеет измененные атрибуты (другое чем использование или изменение самого атрибута nameref), операция фактически выполняется для переменной, указанной переменной nameref значение. Nameref обычно используется в функциях оболочки для ссылки на a переменная, имя которой передается в качестве аргумента функции. Для например, если имя переменной передается в функцию оболочки, как его первый аргумент, бег
объявить-N ref=$1 внутри функция создает переменную nameref ref чье значение-это имя переменной, переданное в качестве первого аргумента. Ссылки и назначения ref, а также изменения его атрибутов, являются обрабатывается как ссылки, назначения и изменения атрибутов для переменная, имя которой было передано как $1.
Я пробовал различные реализации, и ни один не сохранил массивы, которые имели элементы с пробелами ... потому что все они должны были использовать
echo
.# These implementations only work if no array items contain spaces. use_array() { eval echo '(' \"${\[\@\]}\" ')'; } use_array() { local _array="[@]"; echo '(' "${!_array}" ')'; }
решение
потом я наткнулся ответ Денниса Уильямсона. Я включил его метод в следующие функции, чтобы они могли a) принимать произвольный массив и b) использоваться для передачи, дублирования и добавления массивов.
# Print array definition to use with assignments, for loops, etc. # varname: the name of an array variable. use_array() { local r=$( declare -p ) r=${r#declare\ -a\ *=} # Strip keys so printed definition will be a simple list (like when using # "${array[@]}"). One side effect of having keys in the definition is # that when appending arrays (i.e. `a1+=$( use_array a2 )`), values at # matching indices merge instead of pushing all items onto array. echo ${r//\[[0-9]\]=} } # Same as use_array() but preserves keys. use_array_assoc() { local r=$( declare -p ) echo ${r#declare\ -a\ *=} }
затем другие функции могут возвращать массив с помощью catchable выходные или косвенные аргументы.
# catchable output return_array_by_printing() { local returnme=( "one" "two" "two and a half" ) use_array returnme } eval test1=$( return_array_by_printing ) # indirect argument return_array_to_referenced_variable() { local returnme=( "one" "two" "two and a half" ) eval =$( use_array returnme ) } return_array_to_referenced_variable test2 # Now both test1 and test2 are arrays with three elements
мне нужна была подобная функциональность в последнее время, поэтому ниже приводится сочетание предложений, сделанных RashaMatt и Стив Zobell.
- Эхо каждый элемент массива / списка как отдельная строка внутри функции
- использовать mapfile для чтения всех элементов массива / списка, отраженных функцией.
насколько я могу видеть, строки остаются нетронутыми, а пробелы консервированный.
#!bin/bash function create-array() { local somearray=("aaa" "bbb ccc" "d" "e f g h") for elem in "${somearray[@]}" do echo "${elem}" done } mapfile -t resa <<< "$(create-array)" # quick output check declare -p resa
еще несколько вариаций...
#!/bin/bash function create-array-from-ls() { local somearray=("$(ls -1)") for elem in "${somearray[@]}" do echo "${elem}" done } function create-array-from-args() { local somearray=("$@") for elem in "${somearray[@]}" do echo "${elem}" done } mapfile -t resb <<< "$(create-array-from-ls)" mapfile -t resc <<< "$(create-array-from-args 'xxx' 'yy zz' 't s u' )" sentenceA="create array from this sentence" sentenceB="keep this sentence" mapfile -t resd <<< "$(create-array-from-args ${sentenceA} )" mapfile -t rese <<< "$(create-array-from-args "$sentenceB" )" mapfile -t resf <<< "$(create-array-from-args "$sentenceB" "and" "this words" )" # quick output check declare -p resb declare -p resc declare -p resd declare -p rese declare -p resf
недавно я обнаружил причуду в BASH в том, что функция имеет прямой доступ к переменным, объявленным в функциях выше в стеке вызовов. Я только начал размышлять о том, как использовать эту функцию (она обещает как преимущества, так и опасности), но одно очевидное приложение-это решение духа этой проблемы.
Я также предпочел бы получить возвращаемое значение, а не использовать глобальную переменную при делегировании создания массива. Есть несколько причин для моего предпочтения, среди которых следует избегать возможного нарушения ранее существовавшего значения и не оставлять значение, которое может быть недействительным при последующем доступе. Хотя существуют обходные пути для этих проблем, проще всего, чтобы переменная выходила из области действия, когда код закончен с ней.
мое решение гарантирует, что массив доступен при необходимости и отбрасывается, когда функция возвращает, и оставляет невозмущенной глобальную переменную с тем же именем.
#!/bin/bash myarr=(global array elements) get_an_array() { myarr=( $( date +"%Y %m %d" ) ) } request_array() { declare -a myarr get_an_array "myarr" echo "New contents of local variable myarr:" printf "%s\n" "${myarr[@]}" } echo "Original contents of global variable myarr:" printf "%s\n" "${myarr[@]}" echo request_array echo echo "Confirm the global myarr was not touched:" printf "%s\n" "${myarr[@]}"
когда функция request_array звонки get_an_array, get_an_array смогите сразу установить myarr переменной, которая является локальной для request_array. Так как myarr создано
declare
, Она является локальной для request_array и таким образом выходит за рамки, когда request_array возвращает.хотя это решение не буквально возвращает значение, я предлагаю, чтобы в целом оно удовлетворяло обещаниям истинного возвращаемого значения функции.
Это также можно сделать, просто передав переменную массива в функцию и присвоив значения массива этому var, а затем использовать этот var вне функции. Например.
create_array() { local __resultArgArray= local my_list=("a" "b" "c") eval $__resultArgArray="("${my_list[@]}")" } my_algorithm() { create_array result echo "Total elements in the array: ${#result[@]}" for i in "${result[@]}" do echo $i done } my_algorithm
если исходные данные отформатированы с каждым элементом списка в отдельной строке, то
mapfile
builtin-это простой и элегантный способ чтения списка в массив:$ list=$(ls -1 /usr/local) # one item per line $ mapfile -t arrayVar <<<"$list" # -t trims trailing newlines $ declare -p arrayVar | sed 's#\[#\n[#g' declare -a arrayVar='( [0]="bin" [1]="etc" [2]="games" [3]="include" [4]="lib" [5]="man" [6]="sbin" [7]="share" [8]="src")'
обратите внимание, что с
read
встроенный, вы бы обычно не * использоватьmapfile
в конвейере (или подрешетке), поскольку назначенная переменная массива будет недоступна для последующих операторов (* если управление заданием bash не отключено иshopt -s lastpipe
установить).$ help mapfile mapfile: mapfile [-n count] [-O origin] [-s count] [-t] [-u fd] [-C callback] [-c quantum] [array] Read lines from the standard input into an indexed array variable. Read lines from the standard input into the indexed array variable ARRAY, or from file descriptor FD if the -u option is supplied. The variable MAPFILE is the default ARRAY. Options: -n count Copy at most COUNT lines. If COUNT is 0, all lines are copied. -O origin Begin assigning to ARRAY at index ORIGIN. The default index is 0. -s count Discard the first COUNT lines read. -t Remove a trailing newline from each line read. -u fd Read lines from file descriptor FD instead of the standard input. -C callback Evaluate CALLBACK each time QUANTUM lines are read. -c quantum Specify the number of lines read between each call to CALLBACK. Arguments: ARRAY Array variable name to use for file data. If -C is supplied without -c, the default quantum is 5000. When CALLBACK is evaluated, it is supplied the index of the next array element to be assigned and the line to be assigned to that element as additional arguments. If not supplied with an explicit origin, mapfile will clear ARRAY before assigning to it. Exit Status: Returns success unless an invalid option is given or ARRAY is readonly or not an indexed array.
Вы можете попробовать этот
my_algorithm() { create_array list for element in "${list[@]}" do echo "${element}" done } create_array() { local my_list=("1st one" "2nd two" "3rd three") eval "=()" for element in "${my_list[@]}" do eval "+=(\"${element}\")" done } my_algorithm
выход
1st one 2nd two 3rd three