Індексація та модифікація масиву параметрів Bash $ @


11

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

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

Мета - DRY : Перший аргумент використовується для однієї речі, а інший для чогось іншого, і я хотів би уникати дублювання коду для нормалізації, $@масиву або для створення для цього окремої функції (хоча в цьому вказуйте, що це, мабуть, найпростіший вихід).

Уточнення: Завданням було змінити значення змінної довжини, $@ щоб полегшити код налагодження. Поточна версія трохи надто хитра для мене, хоча вона працює навіть для химерних доріжок

$'--$`\! *@ \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конструкція.

Тестується з тире 0,5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Побічна примітка: здається, що в базі 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в $fooшляху 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, просто вау. Більше, ніж я просив, у будь-якому випадку. Ви, пане, приголомшливі.
l0b0

У обговоренні POSIX, у першому циклі, де ви нормалізуєте косої риски, навіщо додавати "." за допомогою sprintf потім зніміть його в наступному рядку? Код, здається, працює без нього, але я підозрюю, що ви працюєте з кращим випадком, про який я не знаю.
Алан Де Смет

1
@AlanDeSmet Крайній випадок - це якщо рядок закінчується новим рядком. Заміна команди позбавляє останніх рядків.
перестань бути злим"

6

Чому ви просто не використовуєте $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. і так далі? Це ще більш DRY -er , ніж те , що ви намагаєтеся зробити :)

Більш докладно про співвідношення між $ номером і $ @:

$ @ можна вважати скороченим для "всіх елементів масиву, що містить усі аргументи"

Отже, $ @ є свого роду скороченням $ {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} ...
Peter.O

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.