Indeksowanie i modyfikowanie tablicy parametrów Bash $ @

11

Czy można odwoływać się do indeksów w $@? Nie mogę znaleźć żadnego odniesienia do użycia, takiego jak nigdzie indziej w wiki GrayCata , a Advanced Scripting Guide i inni przypisują to do innej zmiennej przed jej zmodyfikowaniem.

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

Celem jest OSUSZANIE : Pierwszy argument jest używany do jednej rzeczy, a reszta do czegoś innego, i chciałbym uniknąć duplikowania albo kodu do normalizacji, $@tablicy, albo do stworzenia oddzielnej funkcji do tego (chociaż w tym przypadku zaznacz, że jest to prawdopodobnie najłatwiejsze wyjście).

Wyjaśnienie: celem było zmodyfikowanie wartości zmiennej długości, $@ aby ułatwić debugowanie kodu . Obecna wersja jest dla mnie trochę zbyt zepsuta, chociaż działa nawet na dziwne ścieżki

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

Aktualizacja : Wygląda na to, że nie jest to możliwe. Kod używa teraz zarówno duplikacji kodu, jak i danych, ale przynajmniej działa:

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 trafia do każdego, kto może pozbyć się duplikacji kodu, aby zwinąć duplikaty ukośników lub duplikaty danych do przechowywania, $1a także inne parametry, lub oba, jednocześnie zachowując odpowiedni rozmiar kodu i pomyślnie przechodząc wszystkie testy jednostkowe:

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
l0b0
źródło
powiązane: stackoverflow.com/questions/4827690/…
Ciro Santilli 30 病毒 审查 六四 事件 法轮功

Odpowiedzi:

16

POSIX

Aby znormalizować ukośniki we wszystkich parametrach, użyję obrotowej sztuczki argumentów: odsuń $1, przekształć ją i umieść wynik na końcu listy parametrów. Jeśli robisz to tyle razy, ile jest parametrów, przekształcasz wszystkie parametry i odzyskujesz je w kolejności.

W drugiej części kodu zmieniłem twoją logikę, aby była mniej myląca: zewnętrzna pętla iteruje parametry, a wewnętrzna pętla iteruje komponenty ścieżki. for x; do … doneiteruje parametry pozycyjne, jest to wygodny idiom. Używam zgodnego z POSIX sposobu dopasowania łańcucha do wzorca: casekonstrukcja.

Testowane z myślnikiem 0.5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Uwaga dodatkowa: wydaje się, że w wersji 4.1.5 bash występuje błąd (nie w wersji 3.2): jeśli wzór jest taki "${common_path%/}"/*, jeden z testów kończy się niepowodzeniem.

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

Jeśli jesteś w bash (lub ksh), możesz użyć tablic - nie rozumiem, dlaczego wydajesz się ograniczać do parametrów pozycyjnych. Oto wersja wykorzystująca tablicę. Muszę przyznać, że nie jest to szczególnie wyraźne niż wersja POSIX, ale pozwala uniknąć początkowego przetasowania n ^ 2.

W części normalizującej ukośnik używam konstruktu ${foo//PATTERN/REPLACEMENT}konstrukcyjnego ksh93, aby zastąpić wszystkie wystąpienia PATTERNin $fooprzez REPLACEMENT. Wzór ma +(\/)pasować do jednego lub więcej ukośników; pod bash, shopt -s extglobmusi obowiązywać (równoważnie, zacznij bash z bash -O extglob). Konstrukt set ${!a[@]}ustawia parametry pozycyjne na listę indeksów tablicy a. Zapewnia to wygodny sposób na iterację elementów tablicy.

W drugiej części mam taką samą logikę pętli jak wersja POSIX. Tym razem mogę użyć, [[ … ]]ponieważ wszystkie ukierunkowane tu pociski obsługują to.

Testowane z 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

Niestety, zsh nie ma opcji, ${!array[@]}aby wykonać wersję ksh93 taką, jaka jest. Na szczęście Zsh ma dwie funkcje, dzięki którym pierwsza część jest bardzo prosta. Możesz indeksować parametry pozycyjne tak, jakby były @tablicą, więc nie ma potrzeby używania tablicy pośredniej. A zsh ma konstrukcję iteracji tablicy : "${(@)array//PATTERN/REPLACEMENT}"wykonuje kolejno zamianę wzoru na każdym elemencie tablicy i ocenia tablicę wyników (myląco potrzebujesz podwójnych cudzysłowów, mimo że wynik jest wieloma słowami; jest to uogólnienie "$@"). Druga część jest zasadniczo niezmieniona.

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"
}

Przypadki testowe

Moje rozwiązania są minimalnie testowane i komentowane. Zmieniłem składnię twoich przypadków testowych, aby parsować pod powłokami, które nie mają, $'…'i zgłaszać awarie w wygodniejszy sposób.

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
}
Gilles „SO- przestań być zły”
źródło
1
+50, po prostu wow. W każdym razie więcej niż prosiłem. Pan jest niesamowity.
l0b0
W dyskusji na temat POSIX, w pierwszej pętli, w której normalizujesz ukośniki, dlaczego dołączasz „.” z sprintfem, a następnie usunąć go w następnym wierszu? Wygląda na to, że kod działa bez niego, ale podejrzewam, że zajmujesz się przypadkiem, którego nie jestem świadomy.
Alan De Smet
1
@AlanDeSmet Przypadek krawędzi ma miejsce, gdy łańcuch kończy się nową linią. Podstawianie poleceń usuwa końcowe znaki nowej linii.
Gilles „SO- przestań być zły”
6

Dlaczego nie użyjesz 1 $, 2 $ .. 9 $, {10}, {11} .. i tak dalej? Jest jeszcze bardziej SUCHY niż to, co próbujesz zrobić :)

Więcej na temat relacji między liczbą $ a $ @:

$ @ można traktować jako skrót dla „wszystkich elementów tablicy zawierającej wszystkie argumenty”

$ @ Jest więc skrótem $ {args [@]} (args tutaj to tablica „wirtualna” zawierająca wszystkie argumenty - nie jest to prawdziwa zmienna, pamiętajcie)

1 $ to $ {args [1]}, 2 $ to $ {args [2]} i tak dalej.

Kiedy klikniesz [9], użyj nawiasu klamrowego: $ {10} to $ {args [10]}, $ {11} to $ {args [11]} i tak dalej.


Pośrednio użyj argumentu wiersza poleceń

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

Przykład:

argc=$#
for (( argn=1; argn<=argc; argn++)); do
    if [[ ${!argn} == "foo" ]]; then
        echo "Argument $argn of $argc is 'foo'"
    fi
done
pepoluan
źródło
Oczywistym minusem konieczności używania $ * number * jest to, że nie można użyć zmiennej indeksu jak w przypadku ${args[$i]}.
intuicyjnie
@ zainicjowano, a następnie użyj pośredniego; Zmienię swoją odpowiedź.
pepoluan
5

Pierwszy argument służy do jednej rzeczy, a reszta do czegoś innego,

Myślę, że chcesz 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
forcefsck
źródło
1

Nie do końca wiem, dlaczego nie używasz 1 USD 2 USD itp., Ale .. Może to odpowiadać Twoim potrzebom.

$ 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"

wynik

  -------- 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 działa na wszystko, co następuje po nim, aby utworzyć 1 $, 2 $ .. itd. To oczywiście zastąpi oryginalne wartości, więc pamiętaj o tym.

Peter.O
źródło
ahh ... więc przez 'eval' miałeś na myśli odniesienie pośrednie ... Konstrukcja $ {! var} jest bezpieczniejsza, tak jak napisałem w mojej odpowiedzi
pepoluan
@pepoluan ... Dzięki za powiadomienie mnie o tym. O wiele prostsze jest pisanie ... (Właśnie wróciłem do strony, o której mówiłem, gdybym czytał dalej, widziałbym, że też o tym wspominał :( ....
Peter.O
heh ale jeśli pośrednictwo zdarza się po lewej stronie, eval jest złem koniecznym, choć ':)
pepoluan
@peopluan ... okej, dzięki za wskazanie tego ... i tak na marginesie: nie rozumiem, dlaczego evalniektórzy uważają go za evil... (może to z powodu pisowni :) ... Jeśli eval„zły”, to czy $ {! Var} jest równie „zły”? ... Dla mnie jest to tylko część języka i przydatna część, ale zdecydowanie wolę $ {!
Var
1

Uwaga: Obsługuję spacje w nazwach plików.

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}}"
}

Dodałem przypadek testowy dla nazw plików ze spacjami i naprawiłem 2 testy, w których brakowało wiodącego /

    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
}
John Kearney
źródło