Подтвердить что ты не робот

Есть ли способ избежать позиционных аргументов в bash?

Мне нужно написать функцию в bash. Функция будет принимать около 7 аргументов. Я знаю, что могу вызвать такую ​​функцию:

Чтобы вызвать функцию с параметрами:

function_name $arg1 $arg2

И я могу ссылаться на мои параметры, подобные этому внутри функции:

function_name () {
   echo "Parameter #1 is $1"
}

Мой вопрос в том, есть ли лучший способ обратиться к параметрам внутри функции? Могу ли я избежать $1, $2, $3,.... и просто использовать $arg1, $arg2,...?

Есть ли подходящий метод для этого или мне нужно повторно назначить эти параметры некоторым другим переменным внутри функции? Например:.

function_name () {
   $ARG1=$1
   echo "Parameter #1 is $ARG1"
}

Любой пример будет высоко оценен.

4b9b3361

Ответ 1

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

copy() {
    local from=${1}
    local to=${2}

    # ...
}

Другим решением может быть синтаксический анализ в стиле getopt.

copy() {
    local arg from to
    while getopts 'f:t:' arg
    do
        case ${arg} in
            f) from=${OPTARG};;
            t) to=${OPTARG};;
            *) return 1 # illegal option
        esac
    done
}

copy -f /tmp/a -t /tmp/b

К сожалению, bash не может обрабатывать длинные параметры, которые были бы более читабельными, то есть:

copy --from /tmp/a --to /tmp/b

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

copy() {
    local from to

    while [[ ${1} ]]; do
        case "${1}" in
            --from)
                from=${2}
                shift
                ;;
            --to)
                to=${2}
                shift
                ;;
            *)
                echo "Unknown parameter: ${1}" >&2
                return 1
        esac

        if ! shift; then
            echo 'Missing parameter argument.' >&2
            return 1
        fi
    done
}

copy --from /tmp/a --to /tmp/b

Также смотрите: с помощью getopts в bash shell script, чтобы получить длинные и короткие параметры командной строки


Вы также можете лениться и просто передавать "переменные" в качестве аргументов функции, т.е.:

copy() {
    local "${@}"

    # ...
}

copy from=/tmp/a to=/tmp/b

и вы будете иметь ${from} и ${to} в функции как локальные переменные.

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

copy() {
    local from to    # reset first
    local "${@}"

    # ...
}

чтобы гарантировать, что ${from} и ${to} будут отменены, если они не пройдены.


И если что-то очень плохое вас интересует, вы также можете назначить аргументы в качестве глобальных переменных при вызове функции, то есть:

from=/tmp/a to=/tmp/b copy

Тогда вы можете просто использовать ${from} и ${to} в функции copy(). Просто отметьте, что вы должны всегда передавать все параметры. В противном случае случайная переменная может протекать в функции.

from= to=/tmp/b copy   # safe
to=/tmp/b copy         # unsafe: ${from} may be declared elsewhere

Если у вас bash 4.1 (я думаю), вы также можете попробовать использовать ассоциативные массивы. Это позволит вам передавать именованные аргументы, но это будет уродливо. Что-то вроде:

args=( [from]=/tmp/a [to]=/tmp/b )
copy args

И затем в copy() вам нужно захватить массив.

Ответ 2

Вы всегда можете передавать вещи через среду:

#!/bin/sh
foo() {
  echo arg1 = $arg1
  echo arg2 = $arg2
}

arg1=banana arg2=apple foo

Ответ 3

Функции оболочки имеют полный доступ к любой переменной, доступной в их области вызова, за исключением тех имен переменных, которые используются как локальные переменные внутри самой функции. Кроме того, любая нелокальная переменная, установленная внутри функции, доступна снаружи после вызова функции. Рассмотрим следующий пример:

A=aaa
B=bbb

echo "A=$A B=$B C=$C"

example() {
    echo "example(): A=$A B=$B C=$C"

    A=AAA
    local B=BBB
    C=CCC

    echo "example(): A=$A B=$B C=$C"
}

example

echo "A=$A B=$B C=$C"

Этот фрагмент имеет следующий вывод:

A=aaa B=bbb C=
example(): A=aaa B=bbb C=
example(): A=AAA B=BBB C=CCC
A=AAA B=bbb C=CCC

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

Самый распространенный способ справиться с этим - использовать локальные переменные для аргументов и любую временную переменную внутри функции:

example() {
   local A="$1" B="$2" C="$3" TMP="/tmp"

   ...
}

Это позволяет избежать загрязнения пространства имен оболочки с помощью локальных переменных.

Ответ 4

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

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

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\" \"\[email protected]\"; [[ \$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'

Ответ 5

Аргументы передаются в функции как набор отдельных элементов, поэтому у них нет имен как таковых, а только позиций. это позволяет некоторые интересные возможности, как показано ниже, но это означает, что вы застряли с 1 долларом. $2 и т.д. Относительно того, следует ли сопоставлять их с лучшими именами, вопрос сводится к тому, насколько велика функция, и насколько яснее будет читать код. если его комплекс, то отображение значимых имен ($ BatchID, $FirstName, $SourceFilePath) является хорошей идеей. для простых вещей, хотя, это, вероятно, не нужно. Я certianly не стал бы беспокоиться, если вы используете имена типа $arg1.

теперь, если вы просто хотите отследить параметры, вы можете перебирать их:

for $arg in "[email protected]"
do
  echo "$arg"
done

просто забавный факт; если вы не обрабатываете список, вы, вероятно, заинтересованы в более полезном

Ответ 6

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

parseOptions()
{
  args=("[email protected]")
  for opt in "${args[@]}"; do
    if [[ ! "${opt}" =~ .*=.* ]]; then
      echo "badly formatted option \"${opt}\" should be: option=value, stopping..."
      return 1
    fi
    local var="${opt%%=*}"
    local value="${opt#*=}"
    export ${var}="${value}"
  done
  return 0
}

Ответ 7

Я лично надеялся увидеть какой-то синтаксис вроде

func(a b){
    echo $a
    echo $b
}

Но так как это не вещь, и я вижу довольно много ссылок на глобальные переменные (не обойтись без оговорок и конфликтов имен), я поделюсь своим подходом.

Используя функцию copy из Михал ответ:

copy(){
    cp $from $to
}
from=/tmp/a
to=/tmp/b
copy

Это плохо, потому что from и to - такие широкие слова, что любое количество функций могло бы использовать это. Вы могли бы быстро конкретизировать конфликт имен или "утечку" на ваших руках.

letter(){
    echo "From: $from"
    echo "To:   $to"
    echo
    echo "$1"
}

to=Emily
letter "Hello Emily, you're fired for missing two days of work."

# Result:
#   From: /tmp/a
#   To:   Emily

#   Hello Emily, you're fired for missing two days of work.

Итак, мой подход заключается в том, чтобы "импровизировать" их. Я называю переменную после функции и удаляю ее после выполнения этой функции. Конечно, я использую его только для дополнительных значений, которые имеют значения по умолчанию. В противном случае я просто использую позиционные аргументы.

copy(){
    if [[ $copy_from ]] && [[ $copy_to ]]; then
        cp $copy_from $copy_to
        unset copy_from copy_to
    fi
}
copy_from=/tmp/a
copy_to=/tmp/b
copy # Copies /tmp/a to /tmp/b
copy # Does nothing, as it ought to
letter "Emily, you're 'not' re-hired for the 'not' bribe ;)"
# From: (no /tmp/a here!)
# To:

# Emily, you're 'not' re-hired for the 'not' bribe ;)

Я бы сделал ужасного босса...


На практике мои имена функций более сложны, чем "копия" или "письмо".

Самый последний пример моей памяти - get_input(), который имеет gi_no_sort и gi_prompt.

  • gi_no_sort - значение true/false, которое определяет, отсортированы ли предложения по завершению или нет. По умолчанию true
  • gi_prompt - это строка, которая... ну, это понятно. По умолчанию "".

Фактические аргументы, которые принимает функция, являются источником вышеупомянутых "предложений о завершении" для приглашения ввода, и поскольку указанный список выбран из [email protected] в функции, "именованные аргументы" являются необязательными [1 ] и нет очевидного способа отличить строку, предназначенную как завершение, и логическое/приглашение-сообщение, или действительно что-то разделенное пробелами в bash, если на то пошло [2]; вышеприведенное решение в конечном итоге спасло меня от многих неприятностей.

заметки:

  • Так что жестко закодированные shift и $1, $2 и т.д. не могут быть и речи.

  • например. "0 Enter a command: {1..9} $(ls)" значение 0, "Enter a command:" и набор 1 2 3 4 5 6 7 8 9 <directory contents>? Или "0", "Enter", "a" и "command:" часть этого набора также? Bash будет считать последнее, нравится вам это или нет.

Ответ 8

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

function test() {
    echo $a
}

a='hello world' test
#prove variable didnt leak
echo $a .

введите описание изображения здесь

Это не просто функция функций, вы можете использовать эту функцию в ней script и вызвать a='hello world' test.sh, и она будет работать точно так же


В качестве дополнительного немного удовольствия вы можете комбинировать этот метод с позиционными аргументами (например, вы делаете script, а некоторые пользователи могут не знать имена переменных).
Черт возьми, почему бы не позволить ему иметь дефолты для этих аргументов? Ну, конечно, легко peasy!

function test2() {
    [[ -n "$1" ]] && local a="$1"; [[ -z "$a" ]] && local a='hi'
    [[ -n "$2" ]] && local b="$2"; [[ -z "$b" ]] && local b='bye'
    echo $a $b
}

#see the defaults
test2

#use positional as usual
test2 '' there
#use named parameter
a=well test2
#mix it up
b=one test2 nice

#prove variables didnt leak
echo $a $b .

введите описание изображения здесь

Обратите внимание, что если test был его собственным script, вам не нужно использовать ключевое слово local.