Передача масивів як параметрів у bash


188

Як я можу передати масив як параметр функції bash?

Примітка. Не знайшовши відповіді тут, на Stack Overflow, я сам розмістив своє дещо грубе рішення. Він дозволяє передавати лише один масив, і він є останнім елементом списку параметрів. Насправді це зовсім не передача масиву, а перелік його елементів, які заново збираються в масив з назвою_function (), але він працював для мене. Якщо хтось знає кращий спосіб, сміливо додайте його сюди.


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визначеними тощо local, оскільки місцеві жителі видно для функцій, які вони викликають.
  • !В ${!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

Примітка. Це дещо неочищене рішення, яке я опублікував сам, не знайшовши відповіді тут на "Переповнення стека". Він дозволяє передавати лише один масив, і він є останнім елементом списку параметрів. Насправді це зовсім не передача масиву, а перелік його елементів, які заново збираються в масив з назвою_function (), але він працював для мене. Дещо пізніше Кен опублікував своє рішення, але я зберег свою тут для "історичної" довідки.

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

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

Покращено TheBonsai, дякую.


19
Через три роки ця відповідь, яка зберігалася лише з історичних причин, отримала два протоколи за пару днів. Як це не дивно, як правило, на ЗП, без жодної зауваження, чому люди вважають, що це є виправданим. Зауважте, що ця відповідь передувала всім іншим, і що я прийняв відповідь Кена як найкраще рішення. Я прекрасно усвідомлюю, що він ніде не ідеальний, але протягом чотирьох місяців це було найкращим доступним для SO. Чому його слід зняти через два роки після того, як він зайняв друге місце в ідеальному рішенні Кена, не в мене.
DevSolar

@geirha: Я б просив вас перевірити, хто опублікував питання, хто опублікував цю відповідь і хто, ймовірно, прийняв відповідь, яку ви називаєте "поганою". ;-) Ви також можете перевірити Примітку у запитанні, яка вказує, чому це рішення поступається Кену.
DevSolar

2
Я знаю, що ви поставили запитання, ви написали цю відповідь і що ви прийняли погану відповідь. Тому я так і сформулював це. Причина, що прийнята відповідь є поганою, полягає в тому, що вона намагається передавати масив за посиланням, чого слід дійсно уникати. Крім того, приклад об'єднує кілька аргументів в один рядок. Якщо вам дійсно потрібно передавати масиви за посиланням, bash - це неправильна мова для початку. Навіть з новими змінними nameref bash 4.3, ви не можете безпечно уникнути зіткнень імен (кругова посилання).
geirha

4
Ну, ви можете пропустити кілька масивів, якщо включите кількість елементів кожного масиву. called_function "${#array[@]}" "${array[@]}" "${#array2[@]}" "${array2[@]}"і т. д. ... все ще з деякими очевидними обмеженнями, але насправді, краще вирішити проблему способом, який підтримує мова, а не намагатися змусити мову працювати так, як ви звикли до інших мов.
geirha

1
@geirha: Ну, мабуть, нам доведеться погодитися, що ми не згодні, і ти повинен будеш дозволити мені бути суддею, відповідь якого найкраще відповідає на моє питання. Особисто я більше люблю передавати масиви за посиланням у будь-якому випадку (незалежно від мови, щоб зберегти копіювання даних); тим більше, коли альтернативою є
нахил

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значення 'ми використовуємо значення розширеного значення$1 , наприклад:

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

    так само для $2.

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

Примітка: Варто згадати, що ініціалізація масиву за допомогою цієї форми дужок ініціалізує новий масив відповідно до 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}слід оцінювати весь масив, індекси та всі. Він повинен оцінювати до зворотного твердження про призначення. Будь-який спеціаліст третього курсу повинен це знати. У такому випадку цей код буде працювати саме так, як ви його очікуєте:

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

На жаль, нас відпустив інакше чудова команда розвитку башмаків.

Таким чином, для передачі масиву функції дійсно є лише один варіант, а це використовувати функцію 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[*]}"

а потім записати купу коду всередині функції, щоб зібрати масив.


1
Рішення використання local -nє кращим і сучаснішим, ніж прийнята відповідь. Це рішення також буде працювати для змінної будь-якого типу. Приклад, наведений у цій відповіді, може бути скорочений до local -n ARR=${1}. Однак -nопція для local/ declareдоступна лише у версії Bash 4.3 та вище.
richardjsimkins

Це добре! Малий gotcha: якщо ви передасте змінну з тим самим іменем, що і локальний аргумент вашої функції (наприклад funky ARR), оболонка подасть попередження 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=("$@")
  ...
}

1
Моя причина цього не полягає в тому, що я ще не грав з масивами bash до декількох днів тому. Раніше я перейшов би на Perl, якби він став складним - варіант, якого я не маю на своїй нинішній роботі. Дякую за підказку!
DevSolar


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

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'

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 ©


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 

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[*]}слід оточити "", інакше ви не зможете.


Ваш приклад неправильний, оскільки він неповний. Будь ласка, дайте повний код сценарію.
Денніс ВР
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.