Indización y modificación de la matriz de parámetros de Bash $ @

11

¿Es posible hacer referencia a índices en $@? No puedo encontrar ninguna referencia para usar como la siguiente en ninguna parte de la wiki de GrayCat , y la Guía avanzada de secuencias de comandos y otros asignan esto a una variable diferente antes de modificarla.

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

El objetivo es SECO : el primer argumento se usa para una cosa, y el resto para otra cosa, y me gustaría evitar duplicar el código para normalizar, la $@matriz o crear una función separada para esto (aunque en este momento punto es probablemente la salida más fácil).

Aclaración: El objetivo era modificar los valores de la longitud variable $@ para facilitar la depuración del código . La versión actual es demasiado hacky para mi gusto, aunque funciona incluso para caminos extraños como

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

Actualización : Parece que esto no es posible. El código ahora usa código y duplicación de datos, pero al menos funciona:

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 se dirige a cualquiera que pueda deshacerse de la duplicación de código para colapsar las barras diagonales duplicadas o la duplicación de datos para mantener $1y los otros parámetros, o ambos, mientras mantiene el código de un tamaño razonable y realiza todas las pruebas unitarias:

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
fuente
relacionado: stackoverflow.com/questions/4827690/…
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

Respuestas:

16

POSIX

Para normalizar las barras diagonales en todos los parámetros, usaré el truco del argumento giratorio: $1desactivar, transformar y poner el resultado al final de la lista de parámetros. Si lo hace tantas veces como haya parámetros, ha transformado todos los parámetros y los ha vuelto a ordenar.

Para la segunda parte del código, cambié su lógica para que sea menos confusa: el bucle externo itera sobre los parámetros y el bucle interno itera sobre los componentes de la ruta. for x; do … doneitera sobre los parámetros posicionales, es un modismo conveniente. Utilizo una forma compatible con POSIX de hacer coincidir una cadena con un patrón: la caseconstrucción.

Probado con dash 0.5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Nota al margen: parece haber un error en bash 4.1.5 (no en 3.2): si el patrón del caso es "${common_path%/}"/*, una de las pruebas falla.

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

Si está en bash (o ksh), puede usar matrices; no entiendo por qué parece estar restringiéndose a los parámetros posicionales. Aquí hay una versión que usa una matriz. Tengo que admitir que no es particularmente más claro que la versión POSIX, pero evita la baraja inicial n ^ 2.

Para la parte de normalización de barra, utilizo la construcción de ${foo//PATTERN/REPLACEMENT}construcción ksh93 para reemplazar todas las ocurrencias de PATTERNin $foopor REPLACEMENT. El patrón es +(\/)hacer coincidir una o más barras; bajo bash, shopt -s extglobdebe estar vigente (equivalentemente, comenzar bash con bash -O extglob). La construcción set ${!a[@]}establece los parámetros posicionales en la lista de subíndices de la matriz a. Esto proporciona una forma conveniente de iterar sobre los elementos de la matriz.

Para la segunda parte, utilizo la misma lógica de bucle que la versión POSIX. Esta vez, puedo usarlo [[ … ]]ya que todos los proyectiles apuntados aquí lo admiten.

Probado 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

Lamentablemente, zsh carece de la ${!array[@]}función para ejecutar la versión ksh93 tal cual. Afortunadamente, zsh tiene dos características que hacen que la primera parte sea muy fácil. Puede indexar los parámetros posicionales como si fueran la @matriz, por lo que no es necesario utilizar una matriz intermedia. Y zsh tiene una construcción de iteración de matriz : "${(@)array//PATTERN/REPLACEMENT}"realiza el reemplazo de patrón en cada elemento de matriz a su vez y evalúa la matriz de resultados (de manera confusa, necesita las comillas dobles aunque el resultado sea varias palabras; esta es una generalización de "$@"). La segunda parte es esencialmente sin cambios.

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

Casos de prueba

Mis soluciones se prueban y comentan mínimamente. He cambiado la sintaxis de sus casos de prueba para analizar bajo shells que no tienen $'…'e informar fallas de una manera más 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
}
Gilles 'SO- deja de ser malvado'
fuente
1
+50, simplemente guau. Más de lo que pedí, en cualquier caso. Usted, señor, es asombroso.
l0b0
En la discusión POSIX, en el primer ciclo donde normaliza las barras, ¿por qué agregar "." con el sprintf y luego quitarlo en la siguiente línea? El código parece funcionar sin él, pero sospecho que está manejando un caso límite que desconozco.
Alan De Smet
1
@AlanDeSmet El caso límite es si la cadena termina con una nueva línea. La sustitución de comandos elimina las nuevas líneas finales.
Gilles 'SO- deja de ser malvado'
6

¿Por qué no usas solo $ 1, $ 2 .. $ 9, $ {10}, $ {11} .. y así sucesivamente? Es aún más SECO que lo que estás intentando hacer :)

Más sobre la relación entre $ number y $ @:

$ @ puede considerarse una forma abreviada de "todos los elementos de una matriz que contiene todos los argumentos"

Entonces, $ @ es una especie de taquigrafía de $ {args [@]} (args aquí es una matriz 'virtual' que contiene todos los argumentos, no una variable real, eso sí)

$ 1 es $ {args [1]}, $ 2 es $ {args [2]}, y así sucesivamente.

Cuando presione [9], use una llave: $ {10} es $ {args [10]}, $ {11} es $ {args [11]}, y así sucesivamente.


Use indirectamente un argumento de línea de comando

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

Ejemplo:

argc=$#
for (( argn=1; argn<=argc; argn++)); do
    if [[ ${!argn} == "foo" ]]; then
        echo "Argument $argn of $argc is 'foo'"
    fi
done
pepoluan
fuente
La desventaja obvia de tener que usar $ * number * es que no puede usar una variable de índice como con ${args[$i]}.
intuido
@intuited luego usa indirection; Editaré mi respuesta.
pepoluan
5

El primer argumento se usa para una cosa, y el resto para otra cosa,

Creo que lo que quieres es 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
fuente
1

No sé exactamente por qué no solo usas $ 1 $ 2, etc., pero ... Esto puede satisfacer tus necesidades.

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

salida

  -------- 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 funciona de todo lo que le sigue, para crear $ 1, $ 2 ... etc. Por supuesto, esto anulará los valores originales, así que tenga en cuenta eso.

Peter.O
fuente
ahh ... así que por 'evaluar' quisiste decir referencia indirecta ... $ {! var} construct es más seguro, como lo que escribí en mi respuesta
pepoluan
@pepoluan ... Gracias por alertarme de eso. Es mucho más simple escribir ... (Acabo de volver a la página web a la que me referí, si hubiera leído más, lo habría visto mencionado allí también :( ....
Peter.O
je pero si la indirección ocurre en el lado izquierdo , evaluar es un mal necesario, aunque :)
pepoluan
@peopluan ... bueno, gracias por señalar eso ... y solo como comentario aparte: no entiendo por qué evalalgunos consideran que es evil... (tal vez sea por la ortografía :) ... Si evales "malo", entonces $ {! Var} es igualmente "malo"? ... Para mí es solo una parte del lenguaje, y una parte útil, por cierto ... pero definitivamente prefiero $ {! Var} ...
Peter.O
1

Tenga en cuenta que apoyo espacios en los nombres de archivo.

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

Agregué un caso de prueba para nombres de archivos con espacios y arreglé 2 pruebas que faltaban un /

    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
fuente