Передача массивов в качестве параметров в bash

188

Как я могу передать массив в качестве параметра функции bash?

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

DevSolar
источник
1
Здесь у вас есть хорошая ссылка и множество примеров.
Артем Баргер
16
Э-э-э ... Три отрицательных ответа на пятилетний вопрос в одну минуту?
DevSolar

Ответы:

220

Вы можете передать несколько массивов в качестве аргументов, используя что-то вроде этого:

takes_ary_as_arg()
{
    declare -a argAry1=("${!1}")
    echo "${argAry1[@]}"

    declare -a argAry2=("${!2}")
    echo "${argAry2[@]}"
}
try_with_local_arys()
{
    # array variables could have local scope
    local descTable=(
        "sli4-iread"
        "sli4-iwrite"
        "sli3-iread"
        "sli3-iwrite"
    )
    local optsTable=(
        "--msix  --iread"
        "--msix  --iwrite"
        "--msi   --iread"
        "--msi   --iwrite"
    )
    takes_ary_as_arg descTable[@] optsTable[@]
}
try_with_local_arys

будет эхом

sli4-iread sli4-iwrite sli3-iread sli3-iwrite  
--msix  --iread --msix  --iwrite --msi   --iread --msi   --iwrite

Редактировать / заметки: (из комментариев ниже)

  • descTableи optsTableпередаются как имена и раскрываются в функции. Таким образом, нет $необходимости при указании в качестве параметров.
  • Обратите внимание, что это по-прежнему работает даже при descTableопределении etc local, поскольку локальные элементы видны для вызываемых ими функций.
  • Значение !in ${!1}расширяет переменную arg 1.
  • declare -a просто делает индексированный массив явным, это не является строго необходимым.
Кен Бертелсон
источник
14
Стоит отметить, что если исходный массив разрежен, массив в принимающей функции не будет иметь такие же индексы.
Приостановлено до дальнейшего уведомления.
13
Это замечательно, но может ли Кен или кто-то объяснить несколько вещей, которые меня озадачивают, почему это работает: 1 - я бы подумал, что descTable и optsTable должны были бы начинаться с префикса $ при передаче в качестве аргументов функции. 2 - Почему в первой строке «берет ...» требуется явное объявление массива? 3 - А что значит! означает в выражении $ {! 1}, и почему [@] там не требуется или даже не разрешен? - Это работает, и все эти детали, кажется, необходимы на основании моих испытаний, но я хотел бы понять, почему!
Ян Хеттих
8
1: descTable и optsTable просто передаются как имена, поэтому $ не существует, они должны быть раскрыты только в вызываемой функции 2: не совсем уверен, но я думаю, что это не обязательно 3: the! используется, потому что параметры, передаваемые в функцию, должны быть расширены дважды: $ 1 расширяется до "descTable [@]", а это должно быть расширено до "$ {descTable [@]}". Синтаксис $ {! 1} делает именно это.
Эльмар Цандер
8
Я не думаю, что часть "объявить -a" необходима. Наличие круглых скобок уже определяет LHS присваивания в виде массива.
Эрик Аронести
3
Этот ответ помог мне решить проблему только сейчас. Тем не менее, я хотел бы отметить, что на моем компьютере (с использованием bash 4.3.42) кавычки "$ {! 1}" и "$ {! 2}" должны быть удалены. Если вы этого не сделаете, значение исходного массива читается как одна строка и присваивается argAry1 [0] и argAry2 [0] соответственно, в основном это означает, что структура массива потеряна.
user.friendly
85

Примечание. Это довольно грубое решение, которое я опубликовал сам, после того как не нашел ответа здесь о переполнении стека. Он допускает передачу только одного массива и является последним элементом списка параметров. На самом деле, он вообще не передает массив, а список его элементов, которые повторно собираются в массив с помощью метода named_function (), но у меня это сработало. Несколько позже Кен опубликовал свое решение, но я оставил свое здесь для «исторической» ссылки.

calling_function()
{
    variable="a"
    array=( "x", "y", "z" )
    called_function "${variable}" "${array[@]}"
}

called_function()
{
    local_variable="${1}"
    shift
    local_array=("${@}")
}

Улучшено TheBonsai, спасибо.

DevSolar
источник
19
Через три года после этого, этот ответ - сохраненный только по историческим причинам - получил два отрицательных отзыва в течение нескольких дней. Как обычно печально на SO, без каких-либо замечаний относительно того, почему люди думают, что это оправдано. Обратите внимание, что этот ответ предшествует всем остальным, и что я принял ответ Кена как лучшее решение. Я прекрасно понимаю, что это далеко не идеально, но в течение четырех месяцев он был лучшим из доступных на SO. Почему это должно быть опущено через два года после того, как оно заняло второе место для идеального решения Кена, мне не под силу.
DevSolar
@geirha: Я бы попросил вас проверить, кто опубликовал вопрос, кто опубликовал этот ответ, и кто, вероятно, принял ответ, который вы называете «плохим». ;-) Вы также можете проверить примечание в вопросе, в котором указано, почему это решение уступает Кену.
DevSolar
2
Я знаю, что вы задали вопрос, вы написали этот ответ и приняли неправильный ответ. Вот почему я так сформулировал. Причина, по которой принятый ответ плохой, заключается в том, что он пытается передать массив по ссылке, чего вам действительно следует избегать. Кроме того, пример объединяет несколько аргументов в одну строку. Если вам действительно нужно передавать массивы по ссылке, то для начала bash - неправильный язык. Даже с новыми переменными nameref в bash 4.3 вы не можете безопасно избежать конфликтов имен (циклическая ссылка).
Гейра
4
Ну, вы можете передать несколько массивов, если вы включите количество элементов каждого массива. called_function "${#array[@]}" "${array[@]}" "${#array2[@]}" "${array2[@]}"и т.д. ... все еще с некоторыми очевидными ограничениями, но на самом деле лучше решить проблему так, как поддерживает язык, а не пытаться заставить язык работать так, как вы привыкли в других языках.
Гейра
1
@geirha: Ну, я думаю, мы должны согласиться, что мы не согласны, и вы должны позволить мне быть судьей, ответ на который лучше всего отвечает на мой вопрос. Лично я все равно предпочитаю передавать массивы по ссылке (независимо от языка, чтобы сохранить копирование данных); тем более, когда альтернатива состоит в том, чтобы наклониться назад и передать размер массива в качестве дополнительного параметра ...
DevSolar
38

Комментируя решение Кена Бертельсона и отвечая Яну Хеттиху:

Как это устроено

takes_ary_as_arg descTable[@] optsTable[@]линия в try_with_local_arys()функции посылает:

  1. Это фактически создает копию descTableи optsTableмассивы , которые доступны для takes_ary_as_argфункции.
  2. takes_ary_as_arg()Функция получает descTable[@]и в optsTable[@]виде строк, что означает $1 == descTable[@]и $2 == optsTable[@].
  3. в начале takes_ary_as_arg()функции используется ${!parameter}синтаксис, который называется косвенной ссылкой или иногда двойной ссылкой , это означает, что вместо использования $1значения 's мы используем значение расширенного значения$1 , например:

    baba=booba
    variable=baba
    echo ${variable} # baba
    echo ${!variable} # booba

    аналогично для $2.

  4. помещение этого argAry1=("${!1}")создает argAry1в виде массива (следующие в скобках =) с развернутым descTable[@], точно так же, как запись argAry1=("${descTable[@]}")непосредственно. declareтам не требуется.

NB. Стоит отметить, что при инициализации массива с помощью этой квадратной формы новый массив инициализируется в соответствии с разделителемIFS или внутренним полем, который по умолчанию представляет собой табуляцию , перевод строки и пробел . в этом случае, поскольку он использовал [@]обозначения, каждый элемент рассматривается сам по себе, как если бы он был в кавычках (вопреки [*]).

Мое бронирование с этим

В BASHлокальной области видимости это текущая функция и каждая дочерняя функция, вызываемая из нее, это означает, что takes_ary_as_arg()функция «видит» их descTable[@]и optsTable[@]массивы, таким образом, она работает (см. Объяснение выше).

В таком случае, почему бы не посмотреть непосредственно на эти переменные? Это так же, как писать там:

argAry1=("${descTable[@]}")

Смотрите выше объяснение, которое просто копирует descTable[@]значения массива в соответствии с текущим IFS.

В итоге

По сути, это ничего не значит - как обычно.

Я также хочу подчеркнуть комментарий Денниса Уильямсона выше: разреженные массивы (массивы без всех ключей определяют - с «дырами» в них) не будут работать должным образом - мы потеряем ключи и «сгущим» массив.

При этом я вижу значение для обобщения, поэтому функции могут получать массивы (или копии), не зная имен:

  • для ~ «копий»: эта техника достаточно хороша, просто нужно знать, что индексы (ключи) ушли.
  • для реальных копий: мы можем использовать eval для ключей, например:

    eval local keys=(\${!$1})

а затем цикл, используя их для создания копии. Примечание: здесь !не используется его предыдущая косвенная / двойная оценка, а скорее в контексте массива он возвращает индексы (ключи) массива.

  • и, конечно, если бы мы передавали descTableи optsTableстроки (без [@]), мы могли бы использовать сам массив (как в ссылке) с eval. для универсальной функции, которая принимает массивы.
Мастер
источник
2
Хорошие объяснения механизма объяснения Кена Бертельсона. На вопрос «В таком случае, почему бы не взглянуть непосредственно на сами эти переменные?», Я отвечу: просто для повторного использования функции. Допустим, мне нужно вызвать функцию с Array1, затем с Array2передачей имен массивов становится удобно.
gfrigon
Отличный ответ, нам нужно больше объяснений, как это!
Эдуард Лопес
22

Основная проблема здесь заключается в том, что разработчик (-и) bash, спроектировавший / реализовавший массивы, действительно испортил собаку. Они решили, что ${array}это всего лишь короткая рука ${array[0]}, что было плохой ошибкой. Особенно, если учесть, что ${array[0]}это не имеет смысла и вычисляет пустую строку, если тип массива является ассоциативным.

Присвоение массива принимает форму, в array=(value1 ... valueN)которой значение имеет синтаксис [subscript]=string, тем самым присваивая значение непосредственно определенному индексу в массиве. Это позволяет создавать два типа массивов: с числовым индексированием и с хэш-индексированием (называемые ассоциативными массивами на языке bash). Это также позволяет создавать разреженные числовые индексы. [subscript]=Сокращение от части является сокращением для численно индексированного массива, начиная с порядкового индекса 0 и увеличивая с каждым новым значением в операторе присваивания.

Поэтому ${array}следует оценивать весь массив, индексы и все. Следует оценить обратное значение оператора присваивания. Любой третий год CS должен знать это. В этом случае этот код будет работать точно так, как вы ожидаете:

declare -A foo bar
foo=${bar}

Затем передача массивов по значению функциям и присвоение одного массива другому будет работать так, как диктует остальная часть синтаксиса оболочки. Но поскольку они не сделали этого правильно, оператор присваивания =не работает для массивов, и массивы нельзя передавать по значению в функции или в подоболочки или выводить вообще ( echo ${array}) без кода, который можно было бы просмотреть через все это.

Итак, если бы все было сделано правильно, то следующий пример показал бы, как полезность массивов в bash может быть существенно лучше:

simple=(first=one second=2 third=3)
echo ${simple}

результирующий вывод должен быть:

(first=one second=2 third=3)

Затем массивы могут использовать оператор присваивания и передаваться по значению функциям и даже другим сценариям оболочки. Легко сохраняется путем вывода в файл и легко загружается из файла в сценарий.

declare -A foo
read foo <file

Увы, мы были подведены превосходной командой разработчиков bash.

Таким образом, для передачи массива в функцию, на самом деле есть только одна опция, а именно использование функции nameref:

function funky() {
    local -n ARR

    ARR=$1
    echo "indexes: ${!ARR[@]}"
    echo "values: ${ARR[@]}"
}

declare -A HASH

HASH=([foo]=bar [zoom]=fast)
funky HASH # notice that I'm just passing the word 'HASH' to the function

приведет к следующему выводу:

indexes: foo zoom
values: bar fast

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

funky "${!array[*]}" "${array[*]}"

а затем написать кучу кода внутри функции, чтобы собрать массив.

tigerand
источник
1
Решение об использовании local -nлучше и актуальнее, чем принятый ответ. Это решение также будет работать для переменной любого типа. Пример, приведенный в этом ответе, можно сократить до local -n ARR=${1}. Однако -nопция local/ declareдоступна только в Bash версии 4.3 и выше.
richardjsimkins
Это хорошо! Небольшая ошибка: если вы передадите переменную с тем же именем, что и локальный аргумент вашей функции (например funky ARR), shell выдаст предупреждение circular name reference, потому что в основном функция попытается это сделать local -n ARR=ARR. Хорошая дискуссия на эту тему.
Джин Павловский
5

В ответе DevSolar есть один момент, которого я не понимаю (возможно, у него есть конкретная причина для этого, но я не могу придумать одно): он устанавливает массив из позиционных параметров поэлементно, итеративно.

Более простой подход был бы

called_function()
{
  ...
  # do everything like shown by DevSolar
  ...

  # now get a copy of the positional parameters
  local_array=("$@")
  ...
}
TheBonsai
источник
1
Причина, по которой я этого не сделал, заключается в том, что я совсем не играл с массивами bash несколько дней назад. Раньше я бы переключился на Perl, если бы он стал сложным, такой возможности у меня нет на моей нынешней работе. Спасибо за подсказку!
DevSolar
3
function aecho {
  set "$1[$2]"
  echo "${!1}"
}

пример

$ foo=(dog cat bird)

$ aecho foo 1
cat
Стивен Пенни
источник
3

Простой способ передать несколько массивов в качестве параметра - использовать разделенную символами строку. Вы можете назвать свой скрипт так:

./myScript.sh "value1;value2;value3" "somethingElse" "value4;value5" "anotherOne"

Затем вы можете извлечь его в свой код следующим образом:

myArray=$1
IFS=';' read -a myArray <<< "$myArray"

myOtherArray=$3
IFS=';' read -a myOtherArray <<< "$myOtherArray"

Таким образом, вы можете фактически передать несколько массивов в качестве параметров, и это не должно быть последними параметрами.

Реми Силия
источник
1

Этот работает даже с пробелами:

format="\t%2s - %s\n"

function doAction
{
  local_array=("$@")
  for (( i = 0 ; i < ${#local_array[@]} ; i++ ))
    do
      printf "${format}" $i "${local_array[$i]}"
  done
  echo -n "Choose: "
  option=""
  read -n1 option
  echo ${local_array[option]}
  return
}

#the call:
doAction "${tools[@]}"
humbleSapiens
источник
2
Интересно, какой смысл здесь. Это просто нормальная передача аргументов. Синтаксис «$ @» предназначен для работы с пробелами: «$ @» эквивалентно «$ 1», «$ 2» ...
Андреас Шпиндлер
Могу ли я передать 2 массива в функцию?
pihentagy
1

С помощью нескольких трюков вы можете передать именованные параметры в функции вместе с массивами.

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

testPassingParams() {

    @var hello
    l=4 @array anArrayWithFourElements
    l=2 @array anotherArrayWithTwo
    @var anotherSingle
    @reference table   # references only work in bash >=4.3
    @params anArrayOfVariedSize

    test "$hello" = "$1" && echo correct
    #
    test "${anArrayWithFourElements[0]}" = "$2" && echo correct
    test "${anArrayWithFourElements[1]}" = "$3" && echo correct
    test "${anArrayWithFourElements[2]}" = "$4" && echo correct
    # etc...
    #
    test "${anotherArrayWithTwo[0]}" = "$6" && echo correct
    test "${anotherArrayWithTwo[1]}" = "$7" && echo correct
    #
    test "$anotherSingle" = "$8" && echo correct
    #
    test "${table[test]}" = "works"
    table[inside]="adding a new value"
    #
    # I'm using * just in this example:
    test "${anArrayOfVariedSize[*]}" = "${*:10}" && echo correct
}

fourElements=( a1 a2 "a3 with spaces" a4 )
twoElements=( b1 b2 )
declare -A assocArray
assocArray[test]="works"

testPassingParams "first" "${fourElements[@]}" "${twoElements[@]}" "single with spaces" assocArray "and more... " "even more..."

test "${assocArray[inside]}" = "adding a new value"

Другими словами, вы можете не только вызывать ваши параметры по их именам (что делает ядро ​​более читабельным), вы также можете передавать массивы (и ссылки на переменные - хотя эта функция работает только в bash 4.3)! Кроме того, все отображаемые переменные находятся в локальной области, как и 1 доллар (и другие).

Код, который делает эту работу довольно легким и работает как в bash 3, так и в bash 4 (это единственные версии, с которыми я его тестировал). Если вас интересуют другие подобные хитрости, которые делают разработку с помощью Bash намного приятнее и проще, вы можете взглянуть на мою среду Bash Infinity Framework , для которой был разработан приведенный ниже код.

Function.AssignParamLocally() {
    local commandWithArgs=( $1 )
    local command="${commandWithArgs[0]}"

    shift

    if [[ "$command" == "trap" || "$command" == "l="* || "$command" == "_type="* ]]
    then
        paramNo+=-1
        return 0
    fi

    if [[ "$command" != "local" ]]
    then
        assignNormalCodeStarted=true
    fi

    local varDeclaration="${commandWithArgs[1]}"
    if [[ $varDeclaration == '-n' ]]
    then
        varDeclaration="${commandWithArgs[2]}"
    fi
    local varName="${varDeclaration%%=*}"

    # var value is only important if making an object later on from it
    local varValue="${varDeclaration#*=}"

    if [[ ! -z $assignVarType ]]
    then
        local previousParamNo=$(expr $paramNo - 1)

        if [[ "$assignVarType" == "array" ]]
        then
            # passing array:
            execute="$assignVarName=( \"\${@:$previousParamNo:$assignArrLength}\" )"
            eval "$execute"
            paramNo+=$(expr $assignArrLength - 1)

            unset assignArrLength
        elif [[ "$assignVarType" == "params" ]]
        then
            execute="$assignVarName=( \"\${@:$previousParamNo}\" )"
            eval "$execute"
        elif [[ "$assignVarType" == "reference" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        elif [[ ! -z "${!previousParamNo}" ]]
        then
            execute="$assignVarName=\"\$$previousParamNo\""
            eval "$execute"
        fi
    fi

    assignVarType="$__capture_type"
    assignVarName="$varName"
    assignArrLength="$__capture_arrLength"
}

Function.CaptureParams() {
    __capture_type="$_type"
    __capture_arrLength="$l"
}

alias @trapAssign='Function.CaptureParams; trap "declare -i \"paramNo+=1\"; Function.AssignParamLocally \"\$BASH_COMMAND\" \"\$@\"; [[ \$assignNormalCodeStarted = true ]] && trap - DEBUG && unset assignVarType && unset assignVarName && unset assignNormalCodeStarted && unset paramNo" DEBUG; '
alias @param='@trapAssign local'
alias @reference='_type=reference @trapAssign local -n'
alias @var='_type=var @param'
alias @params='_type=params @param'
alias @array='_type=array @param'
niieani
источник
1

Просто чтобы добавить к принятому ответу, поскольку я обнаружил, что он не работает хорошо, если содержимое массива примерно так:

RUN_COMMANDS=(
  "command1 param1... paramN"
  "command2 param1... paramN"
)

В этом случае каждый член массива разделяется, поэтому массив, который видит функция, эквивалентен:

RUN_COMMANDS=(
    "command1"
    "param1"
     ...
    "command2"
    ...
)

Чтобы заставить этот случай работать, я нашел способ передать имя переменной в функцию, а затем использовать eval:

function () {
    eval 'COMMANDS=( "${'"$1"'[@]}" )'
    for COMMAND in "${COMMANDS[@]}"; do
        echo $COMMAND
    done
}

function RUN_COMMANDS

Просто мои 2 ©

AlvaroGMJ
источник
1

Как бы ужасно это ни было, вот обходной путь, который работает до тех пор, пока вы не передаете массив явно, а переменную, соответствующую массиву:

function passarray()
{
    eval array_internally=("$(echo '${'$1'[@]}')")
    # access array now via array_internally
    echo "${array_internally[@]}"
    #...
}

array=(0 1 2 3 4 5)
passarray array # echo's (0 1 2 3 4 5) as expected

Я уверен, что кто-то может придумать более ясную реализацию этой идеи, но я нашел, что это лучшее решение, чем передача массива "{array[@]"}и последующий доступ к нему с помощью внутреннего array_inside=("$@"). Это становится сложным, когда есть другие позиционные getoptsпараметры. В этих случаях мне пришлось сначала определить, а затем удалить параметры, не связанные с массивом, используя некоторую комбинацию shiftи удаление элементов массива.

С точки зрения пуристов этот подход, скорее всего, рассматривается как нарушение языка, но, прагматично говоря, этот подход спас меня от многих неприятностей. В смежной теме я также использую, evalчтобы присвоить внутренне построенный массив переменной, названной согласно параметру, который target_varnameя передаю функции:

eval $target_varname=$"(${array_inside[@]})"

Надеюсь, это кому-нибудь поможет.

Блейк Шульце
источник
0

Требование : функция для поиска строки в массиве.
Это небольшое упрощение решения DevSolar в том смысле, что оно использует переданные аргументы, а не копирует их.

myarray=('foobar' 'foxbat')

function isInArray() {
  local item=$1
  shift
  for one in $@; do
    if [ $one = $item ]; then
      return 0   # found
    fi
  done
  return 1       # not found
}

var='foobar'
if isInArray $var ${myarray[@]}; then
  echo "$var found in array"
else
  echo "$var not found in array"
fi 
Andre
источник
0

Мой короткий ответ:

function display_two_array {
    local arr1=$1
    local arr2=$2
    for i in $arr1
    do
       "arrary1: $i"
    done
    
    for i in $arr2
    do
       "arrary2: $i"
    done
}

test_array=(1 2 3 4 5)
test_array2=(7 8 9 10 11)

display_two_array "${test_array[*]}" "${test_array2[*]}"
Следует заметить, что ${test_array[*]}и ${test_array2[*]}должны быть окружены "", иначе у вас ничего не получится.

Томми
источник
Ваш пример неверен, потому что он неполон. Пожалуйста, дайте полный код скрипта.
Деннис В.Р.