Индексирование и изменение массива параметров Bash $ @


11

Можно ли ссылаться на индексы в $@? Я не могу найти какую-либо ссылку для использования, как, например, в вики-сайте GrayCat , и Руководство по расширенному написанию сценариев и другие присваивают его другой переменной, прежде чем изменять ее.

$ echo ${@[0]}
-bash: ${@[0]}: bad substitution

Цель - СУХОЙ : первый аргумент используется для одной цели , а остальные - для чего-то другого, и я бы хотел избежать дублирования либо кода для нормализации, $@массива, либо создания для этого отдельной функции (хотя на этом дык это наверное самый простой выход).

Пояснение: цель состояла в том, чтобы изменить значения переменной длины, $@ чтобы облегчить отладку кода . Текущая версия мне не нравится, хотя она работает даже для странных путей, таких как

$'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'

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

path_common()
{
    # Get the deepest common path.
    local common_path="$(echo -n "${1:-}x" | tr -s '/')"
    common_path="${common_path%x}"
    shift # $1 is obviously part of $1
    local path

    while [ -n "${1+defined}" ]
    do
        path="$(echo -n "${1}x" | tr -s '/')"
        path="${path%x}"
        if [[ "${path%/}/" = "${common_path%/}/"* ]]
        then
            shift
        else
            new_common_path="${common_path%/*}"
            [ "$new_common_path" = "$common_path" ] && return 1 # Dead end
            common_path="$new_common_path"
        fi
    done
    printf %s "$common_path"
}

Bounty идет к любому , кто может избавиться от дублирования кода коллапса дубликатов слэшей или дублирования данных , чтобы держать $1и другие параметры, или оба, сохраняя при этом коде разумного размера и успеха всех модульных тестов:

test "$(path_common /a/b/c/d /a/b/e/f; echo x)" = /a/bx
test "$(path_common /long/names/foo /long/names/bar; echo x)" = /long/namesx
test "$(path_common / /a/b/c; echo x)" = /x
test "$(path_common a/b/c/d a/b/e/f ; echo x)" = a/bx
test "$(path_common ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
test "$(path_common $'\n/\n/\n' $'\n/\n'; echo x)" = $'\n/\n'x
test "$(path_common --/-- --; echo x)" = '--x'
test "$(path_common '' ''; echo x)" = x
test "$(path_common /foo/bar ''; echo x)" = x
test "$(path_common /foo /fo; echo x)" = x
test "$(path_common $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n' $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'; echo x)" = $'--$`\! *@ \a\b\e\E\f\r\t\v\\\"\' \n'x
test "$(path_common /foo/bar //foo//bar//baz; echo x)" = /foo/barx
test "$(path_common foo foo; echo x)" = foox
test "$(path_common /fo /foo; echo x)" = x

Ответы:


16

POSIX

Чтобы нормализовать косые черты во всех параметрах, я буду использовать трюк с вращающимся аргументом: сдвинуть $1, преобразовать его и поместить результат в конец списка параметров. Если вы делаете это столько раз, сколько есть параметров, вы преобразуете все параметры и возвращаете их в порядок.

Для второй части кода я изменил вашу логику, чтобы она была менее запутанной: внешний цикл перебирает параметры, а внутренний цикл перебирает компоненты пути. for x; do … doneперебирает позиционные параметры, это удобная идиома Я использую POSIX-совместимый способ сопоставления строки с шаблоном: caseконструкцией.

Протестировано с dash 0.5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Примечание: в bash 4.1.5, по-видимому, есть ошибка (не в 3.2): если шаблон случая "${common_path%/}"/*один из тестов не пройден.

posix_path_common () {
  for tmp; do
    tmp=$(printf %s. "$1" | tr -s "/")
    set -- "$@" "${tmp%.}"
    shift
  done
  common_path=$1; shift
  for tmp; do
    while case ${tmp%/}/ in "${common_path%/}/"*) false;; esac; do
      new_common_path=${common_path%/*}
      if [ "$new_common_path" = "$common_path" ]; then return 1; fi
      common_path=$new_common_path
    done
  done
  printf %s "$common_path"
}

баш, кш

Если вы используете bash (или ksh), вы можете использовать массивы - я не понимаю, почему вы, похоже, ограничиваетесь позиционными параметрами. Вот версия, которая использует массив. Я должен признать, что он не особенно понятен, чем версия POSIX, но он избегает начального перемешивания n ^ 2.

Для части нормализации слэша я использую конструкцию ksh93, ${foo//PATTERN/REPLACEMENT}чтобы заменить все вхождения PATTERNв $fooby REPLACEMENT. Шаблон +(\/)должен соответствовать одному или нескольким слешам; под bash, shopt -s extglobдолжен быть в силе (эквивалентно, начать bash с bash -O extglob). Конструкция set ${!a[@]}устанавливает позиционные параметры в список индексов массива a. Это обеспечивает удобный способ перебора элементов массива.

Для второй части я использую ту же логику цикла, что и версия POSIX. На этот раз я могу использовать, [[ … ]]так как все оболочки, предназначенные здесь, поддерживают это.

Протестировано с bash 3.2.39, bash 4.1.5, ksh 93s +.

array_path_common () {
  typeset a i tmp common_path new_common_path
  a=("$@")
  set ${!a[@]}
  for i; do
    a[$i]=${a[$i]//+(\/)//}
  done
  common_path=${a[$1]}; shift
  for tmp; do
    tmp=${a[$tmp]}
    while [[ "${tmp%/}/" != "${common_path%/}/"* ]]; do
      new_common_path="${common_path%/*}"
      if [[ $new_common_path = $common_path ]]; then return 1; fi
      common_path="$new_common_path"
    done
  done
  printf %s "$common_path"
}

ЗШ

К сожалению, в zsh отсутствует ${!array[@]}возможность запуска версии ksh93 как есть. К счастью, у zsh есть две особенности, которые делают первую часть легкой. Вы можете индексировать позиционные параметры, как если бы они были @массивом, поэтому нет необходимости использовать промежуточный массив. Кроме того, zsh имеет конструкцию итерации массива : "${(@)array//PATTERN/REPLACEMENT}"выполняет замену шаблона для каждого элемента массива по очереди и вычисляет массив результатов (что приводит к путанице в двойных кавычках, даже если результат состоит из нескольких слов; это обобщение "$@"). Вторая часть практически не изменилась.

zsh_path_common () {
  setopt local_options extended_glob
  local tmp common_path new_common_path
  set -- "${(@)@//\/##//}"
  common_path=$1; shift
  for tmp; do
    while [[ "${tmp%/}/" != "${common_path%/}/"* ]]; do
      new_common_path="${common_path%/*}"
      if [[ $new_common_path = $common_path ]]; then return 1; fi
      common_path="$new_common_path"
    done
  done
  printf %s "$common_path"
}

Контрольные примеры

Мои решения минимально проверены и прокомментированы. Я изменил синтаксис ваших тестовых случаев, чтобы анализировать их в оболочках, в которых их нет, $'…'и сообщать о сбоях более удобным способом.

do_test () {
  if test "$@"; then echo 0; else echo $? "$@"; failed=$(($failed+1)); fi
}

run_tests () {
  function_to_test=$1; shift
  failed=0
  do_test "$($function_to_test /a/b/c/d /a/b/e/f; echo x)" = /a/bx
  do_test "$($function_to_test /long/names/foo /long/names/bar; echo x)" = /long/namesx
  do_test "$($function_to_test / /a/b/c; echo x)" = /x
  do_test "$($function_to_test a/b/c/d a/b/e/f ; echo x)" = a/bx
  do_test "$($function_to_test ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
  do_test "$($function_to_test '
/
/
' '
/
'; echo x)" = '
/
'x
  do_test "$($function_to_test --/-- --; echo x)" = '--x'
  do_test "$($function_to_test '' ''; echo x)" = x
  do_test "$($function_to_test /foo/bar ''; echo x)" = x
  do_test "$($function_to_test /foo /fo; echo x)" = x
  do_test "$($function_to_test '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
' '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'; echo x)" = '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'x
  do_test "$($function_to_test /foo/bar //foo//bar//baz; echo x)" = /foo/barx
  do_test "$($function_to_test foo foo; echo x)" = foox
  do_test "$($function_to_test /fo /foo; echo x)" = x
  if [ $failed -ne 0 ]; then echo $failed failures; return 1; fi
}

1
+50, просто вау. Во всяком случае, больше, чем я просил. Вы, сэр, потрясающие.
10

В обсуждении POSIX, в первом цикле, где вы нормализуете косые черты, зачем добавлять «.» с помощью sprintf, затем удалите его в следующей строке? Код, кажется, работает без него, но я подозреваю, что вы обрабатываете крайний случай, о котором я не знаю.
Алан Де Смет

1
@AlanDeSmet Крайний случай, если строка заканчивается новой строкой. Подстановка команд удаляет завершающие символы новой строки.
Жиль "ТАК - перестань быть злым"

6

Почему бы вам просто не использовать $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. и так далее? Это еще более DRY -er , чем то , что вы пытаетесь сделать :)

Подробнее об отношении между $ number и $ @:

$ @ можно рассматривать как сокращение для «всех элементов массива, содержащих все аргументы»

Таким образом, $ @ - это своего рода сокращение от $ {args [@]} (здесь args - это «виртуальный» массив, содержащий все аргументы - не реальная переменная, заметьте)

$ 1 - это $ {args [1]}, $ 2 - это $ {args [2]} и так далее.

Когда вы нажмете [9], используйте фигурную скобку: $ {10} равно $ {args [10]}, $ {11} равно $ {args [11]} и так далее.


Косвенно использовать аргумент командной строки

argnum=3  # You want to get the 3rd arg
do-something ${!argnum}  # Do something with the 3rd arg

Пример:

argc=$#
for (( argn=1; argn<=argc; argn++)); do
    if [[ ${!argn} == "foo" ]]; then
        echo "Argument $argn of $argc is 'foo'"
    fi
done

Очевидным недостатком использования $ * number * является то, что вы не можете использовать индексную переменную, как с ${args[$i]}.
интуитивно

@intuited, тогда используйте косвенное обращение; Я отредактирую свой ответ.
pepoluan

5

Первый аргумент используется для одного, а остальные для чего-то другого,

Я думаю, что вы хотите shift

$ set one two three four five
$ echo $@
one two three four five
$ echo $1
one
$ foo=$1
$ echo $foo
one
$ shift
$ echo $@
two three four five
$ shift 2
$ echo $@
four five
$ echo $1
four

1

Я не совсем понимаю, почему вы не просто используете $ 1 $ 2 и т. Д., Но ... Это может удовлетворить ваши потребности.

$ script "ed    it" "cat/dog"  33.2  \D  

  echo "-------- Either use 'indirect reference'"
  for ((i=1;i<=${#@};i++)) ;do
    #  eval echo \"\$$i\" ..works, but as *pepoluan* 
    #    has pointed out: echo "${!i}" ..is better.
    echo "${!i}"
  done
  echo "-------- OR use an array"
  array=("$@")
  for ((i=0;i<${#array[@]};i++)) ;do
    echo "${array[$i]}" 
  done
  echo "-------- OR use 'set'"
  set  "$@"
  echo "$1"
  echo "$2"
  echo "$3"
  echo "$4"

вывод

  -------- Either use 'indirect reference'
  ed    it
  cat/dog
  33.2
  D
  -------- OR use an array
  ed    it
  cat/dog
  33.2
  D
  -------- OR use 'set'
  ed    it
  cat/dog
  33.2
  D

set Произведения всего, что следует за ним, для создания $ 1, $ 2 .. и т. д. Это, конечно, переопределит исходные значения, так что просто помните об этом.


ааа ... так что под 'eval' вы имели в виду косвенную ссылку ... Конструкция $ {! var} безопаснее, чем то, что я написал в своем ответе
pepoluan

@pepoluan ... Спасибо, что предупредили меня об этом. Гораздо проще написать ... (Я только что вернулся на страницу, на которую я ссылался, если бы я читал дальше, я бы увидел, что там тоже упоминается :( ....
Peter.O

хе. но если перенаправление происходит на левой стороне, eval - неизбежное зло, хотя ':)
pepoluan

@peopluan ... хорошо, спасибо за то, что указал на это ... и просто в качестве отступления: я не понимаю, почему evalнекоторые считают это evil... (может быть, это из-за написания :) ... Если eval«плохо», то равно ли $ {! Var} «плохо»? ... Для меня это всего лишь часть языка и полезная часть, но я определенно предпочитаю $ {!
Var

1

Примечание. Я поддерживаю пробелы в именах файлов.

function SplitFilePath {
    IFS=$'/' eval "${1}"=\( \${2} \)
}
function JoinFilePath {
    IFS=$'/' eval echo -n \"\${*}\"
    [ $# -eq 1 -a "${1}" = "" ] && echo -n "/"
}
function path_common {
    set -- "${@//\/\///}"       ## Replace all '//' with '/'
    local -a Path1
    local -i Cnt=0
    SplitFilePath Path1 "${1}"
    IFS=$'/' eval set -- \${2} 
    for CName in "${Path1[@]}" ; do
        [ "${CName}" != "${1}" ] && break;
        shift && (( Cnt++ ))
    done
    JoinFilePath "${Path1[@]:0:${Cnt}}"
}

Я добавил тестовый пример для имен файлов с пробелами и исправил 2 теста, в которых отсутствовал ведущий /

    do_test () {

  if test "${@}"; then echo 0; else echo $? "$@"; failed=$(($failed+1)); fi
}

run_tests () {
  function_to_test=$1; shift
  failed=0
  do_test "$($function_to_test /a/b/c/d /a/b/e/f; echo x)" = /a/bx
  do_test "$($function_to_test /long/names/foo /long/names/bar; echo x)" = /long/namesx
  do_test "$($function_to_test / /a/b/c; echo x)" = /x      
  do_test "$($function_to_test a/b/c/d a/b/e/f ; echo x)" = a/bx
  do_test "$($function_to_test ./a/b/c/d ./a/b/e/f; echo x)" = ./a/bx
  do_test "$($function_to_test '
/
/
' '
/
'; echo x)" = '
/
'x
  do_test "$($function_to_test --/-- --; echo x)" = '--x'
  do_test "$($function_to_test '' ''; echo x)" = x
  do_test "$($function_to_test /foo/bar ''; echo x)" = x
  do_test "$($function_to_test /foo /fo; echo x)" = /x      ## Changed from x
  do_test "$($function_to_test '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
' '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'; echo x)" = '--$`\! *@ \a\b\e\E\f\r\t\v\\\"'\'' 
'x
  do_test "$($function_to_test /foo/bar //foo//bar//baz; echo x)" = /foo/barx
  do_test "$($function_to_test foo foo; echo x)" = foox
  do_test "$($function_to_test /fo /foo; echo x)" = /x          ## Changed from x
  do_test "$($function_to_test "/fo d/fo" "/fo d/foo"; echo x)" = "/fo dx"

  if [ $failed -ne 0 ]; then echo $failed failures; return 1; fi
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.