Indicizzazione e modifica dell'array di parametri Bash $ @


11

È possibile fare riferimento agli indici in $@? Non riesco a trovare alcun riferimento da usare come il seguente da nessuna parte nel wiki di GrayCat , e la Guida agli script avanzati e altri assegnano questo a una variabile diversa prima di modificarlo.

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

L'obiettivo è DRY : il primo argomento viene utilizzato per una cosa e il resto per qualcos'altro, e vorrei evitare di duplicare il codice per normalizzare, l' $@array o per creare una funzione separata per questo (anche se a questo punto è probabilmente la via più semplice).

Chiarimento: lo scopo era modificare i valori della lunghezza variabile $@ per facilitare il debug del codice . La versione attuale è un po 'troppo confusa per i miei gusti, anche se funziona anche per percorsi bizzarri come

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

Aggiornamento : sembra che questo non sia possibile. Il codice ora utilizza sia la duplicazione del codice che dei dati, ma almeno funziona:

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 va a chiunque possa sbarazzarsi della duplicazione del codice per comprimere barre duplicate o duplicazione dei dati da conservare $1e gli altri parametri, o entrambi, mantenendo il codice di dimensioni ragionevoli e superando tutti i test unitari:

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

Risposte:


16

POSIX

Per normalizzare le barre in tutti i parametri, userò il trucco dell'argomento rotante: spostare $1, trasformare e mettere il risultato alla fine dell'elenco dei parametri. Se lo fai tutte le volte che ci sono parametri, hai trasformato tutti i parametri e li hai riportati in ordine.

Per la seconda parte del codice, ho modificato la tua logica per renderla meno confusa: il ciclo esterno scorre i parametri e il ciclo interno scorre i componenti del percorso. for x; do … donescorre i parametri posizionali, è un linguaggio conveniente. Uso un modo conforme a POSIX per abbinare una stringa a uno schema: il casecostrutto.

Testato con trattino 0.5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Nota a margine: sembra esserci un bug in bash 4.1.5 (non in 3.2): se lo schema del caso è "${common_path%/}"/*, uno dei test fallisce.

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

Se sei in bash (o ksh), puoi usare le matrici - non capisco perché sembri limitarti ai parametri posizionali. Ecco una versione che utilizza un array. Devo ammettere che non è particolarmente più chiaro della versione POSIX, ma evita lo shuffle iniziale n ^ 2.

Per la parte di normalizzazione della barra, utilizzo il costrutto ksh93 ${foo//PATTERN/REPLACEMENT}per sostituire tutte le occorrenze di PATTERNin $foodi REPLACEMENT. Lo schema è +(\/)di abbinare una o più barre; sotto bash, shopt -s extglobdeve essere attivo (equivalentemente, inizia bash con bash -O extglob). Il costrutto set ${!a[@]}imposta i parametri posizionali sull'elenco dei pedici dell'array a. Ciò fornisce un modo conveniente per scorrere gli elementi dell'array.

Per la seconda parte, ho la stessa logica di loop della versione POSIX. Questa volta, posso usarlo [[ … ]]poiché tutte le shell targetizzate qui lo supportano.

Testato con 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

Purtroppo, a zsh manca la ${!array[@]}funzionalità per eseguire la versione ksh93 così com'è. Fortunatamente, zsh ha due caratteristiche che rendono la prima parte un gioco da ragazzi. È possibile indicizzare i parametri posizionali come se fossero l' @array, quindi non è necessario utilizzare un array intermedio. E zsh ha un costrutto di iterazione di array : "${(@)array//PATTERN/REPLACEMENT}"esegue la sostituzione del pattern su ogni elemento dell'array a sua volta e valuta l'array dei risultati (confusamente, sono necessarie le doppie virgolette anche se il risultato è più parole; questa è una generalizzazione di "$@"). La seconda parte è sostanzialmente invariata.

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

Casi test

Le mie soluzioni sono minimamente testate e commentate. Ho modificato la sintassi dei casi di test per analizzare sotto shell che non hanno $'…'e segnalare gli errori in un modo più conveniente.

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, solo wow. Più di quanto chiedessi, in ogni caso. Signore, siete fantastici.
l0b0,

Nella discussione POSIX, nel primo ciclo in cui si normalizzano le barre, perché aggiungere "." con lo sprintf quindi toglierlo nella riga successiva? Il codice sembra funzionare senza di esso, ma sospetto che tu stia gestendo un caso limite di cui non sono a conoscenza.
Alan De Smet,

1
@AlanDeSmet Il caso limite è se la stringa termina con una nuova riga. La sostituzione del comando elimina le nuove righe finali.
Gilles 'SO- smetti di essere malvagio' l'

6

Perché non usi solo $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. e così via? È ancora più ASCIUTTO di quello che stai tentando di fare :)

Maggiori informazioni sulla relazione tra $ numero e $ @:

$ @ può essere considerato una scorciatoia per "tutti gli elementi di un array contenente tutti gli argomenti"

Quindi, $ @ è una specie di scorciatoia di $ {args [@]} (args qui è un array 'virtuale' contenente tutti gli argomenti - non una variabile reale, intendiamoci)

$ 1 è $ {args [1]}, $ 2 è $ {args [2]} e così via.

Quando hai premuto [9], usa una parentesi graffa: $ {10} è $ {args [10]}, $ {11} è $ {args [11]} e così via.


Utilizzare indirettamente un argomento della riga di comando

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

Esempio:

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

L'ovvio svantaggio di dover usare $ * numero * è che non puoi usare una variabile indice come con ${args[$i]}.
intuito il

@intuited usa quindi il riferimento indiretto; Modificherò la mia risposta.
pepoluan

5

Il primo argomento è usato per una cosa e il resto per qualcos'altro,

Penso che quello che vuoi sia 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

Non so bene perché non usi solo $ 1 $ 2, ecc. Ma .. Questo potrebbe soddisfare le tue esigenze.

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

produzione

  -------- 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 funziona di tutto ciò che lo segue, per creare $ 1, $ 2 .. ecc. Questo ovviamente sovrascriverà i valori originali, quindi basta essere consapevoli di ciò.


ahh ... quindi per "eval" intendevi un riferimento indiretto ... $ {! var} il costrutto è più sicuro, come quello che ho scritto nella mia risposta
pepoluan

@pepoluan ... Grazie per avermi avvisato. Scrivere è molto più semplice ... (Sono appena tornato alla pagina web a cui mi
riferivo

eh. ma se l'indirizzamento indiretto si verifica sul lato sinistro , eval è un male necessario, anche se :)
pepoluan

@peopluan ... ok, grazie per averlo sottolineato ... e solo a parte: non capisco perché eval, da alcuni, sia considerato evil... (forse è a causa dell'ortografia :) ... Se evalè "cattivo", allora $ {! Var} è ugualmente "cattivo"? ... Per me è solo una parte della lingua, e una parte utile, in questo .. ma preferisco decisamente $ {! Var} ...
Peter.O

1

Nota I supporti spazi nei nomi dei file.

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

Ho aggiunto un caso di test per nomi di file con spazi e risolti 2 test a cui mancava un vantaggio /

    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
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.