Indexação e modificação da matriz de parâmetros Bash $ @

11

É possível consultar os índices em $@? Não consigo encontrar nenhuma referência para usar como a seguir em qualquer lugar no wiki do GrayCat , e o Advanced Scripting Guide e outros atribuem isso a uma variável diferente antes de modificá-la.

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

O objetivo é SECO : O primeiro argumento é usado para uma coisa, e o restante para outra, e eu gostaria de evitar duplicar o código para normalizar, a $@matriz ou criar uma função separada para isso (embora neste momento provavelmente é a saída mais fácil).

Esclarecimento: O objetivo era modificar os valores do comprimento variável $@ para facilitar a depuração do código . A versão atual é um pouco hacky para o meu gosto, embora funcione mesmo para caminhos bizarros como

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

Atualização : parece que isso não é possível. O código agora usa duplicação de código e dados, mas pelo 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"
}

O Bounty é direcionado a qualquer pessoa que possa se livrar da duplicação de código para recolher barras duplicadas ou duplicação de dados para manter $1e os outros parâmetros, ou ambos, mantendo o código em um tamanho razoável e sucedendo em todos os testes de unidade:

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
fonte
relacionado: stackoverflow.com/questions/4827690/…
Ciro Santilli é o nome de usuário mais popular na Web

Respostas:

16

POSIX

Para normalizar as barras em todos os parâmetros, usarei o truque do argumento rotativo: $1desative, transforme-o e coloque o resultado no final da lista de parâmetros. Se você fizer isso quantas vezes houver parâmetros, você transformou todos os parâmetros e os colocou novamente em ordem.

Na segunda parte do código, mudei sua lógica para menos confusa: o loop externo itera sobre os parâmetros e o loop interno itera sobre os componentes do caminho. for x; do … doneitera sobre os parâmetros posicionais, é um idioma conveniente. Eu uso uma maneira compatível com POSIX de combinar uma string com um padrão: a caseconstrução.

Testado com o traço 0.5.5.1, pdksh 5.2.14, bash 3.2.39, bash 4.1.5, ksh 93s +, zsh 4.3.10.

Nota: parece haver um bug em bash 4.1.5 (e não em 3.2): se o padrão caso é "${common_path%/}"/*, um dos testes falhar.

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 você está no bash (ou ksh), pode usar matrizes - não entendo por que você parece estar se restringindo aos parâmetros posicionais. Aqui está uma versão que usa uma matriz. Eu tenho que admitir que não é particularmente mais claro que a versão POSIX, mas evita o embaralhamento inicial n ^ 2.

Para a parte de normalização da barra, eu uso a construção de ${foo//PATTERN/REPLACEMENT}construção ksh93 para substituir todas as ocorrências de PATTERNin $foopor REPLACEMENT. O padrão é +(\/)corresponder a uma ou mais barras; em bash, shopt -s extglobdeve estar em vigor (equivalentemente, inicie o bash com bash -O extglob). A construção set ${!a[@]}define os parâmetros posicionais para a lista de subscritos da matriz a. Isso fornece uma maneira conveniente de iterar sobre os elementos da matriz.

Para a segunda parte, tenho a mesma lógica de loop que a versão POSIX. Desta vez, eu posso usar, [[ … ]]já que todas as conchas direcionadas aqui o suportam.

Testado com o 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

Infelizmente, o zsh não possui o ${!array[@]}recurso para executar a versão ksh93 como está. Felizmente, o zsh tem dois recursos que facilitam a primeira parte. Você pode indexar os parâmetros posicionais como se fossem a @matriz, portanto, não há necessidade de usar uma matriz intermediária. E o zsh tem uma construção de iteração de matriz : "${(@)array//PATTERN/REPLACEMENT}"executa a substituição de padrão em cada elemento da matriz e avalia a matriz de resultados (confusamente, você precisa de aspas duplas, mesmo que o resultado seja várias palavras; isso é uma generalização de "$@"). A segunda parte é essencialmente inalterada.

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 teste

Minhas soluções são minimamente testadas e comentadas. Alterei a sintaxe dos seus casos de teste para analisar em shells que não possuem $'…'e reportar falhas de uma maneira mais 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- parar de ser mau'
fonte
1
+50, apenas uau. Mais do que eu pedi, pelo menos. Você, senhor, é incrível.
precisa saber é o seguinte
Na discussão do POSIX, no primeiro ciclo em que você normaliza barras, por que acrescentar "." com o sprintf então tira-o na próxima linha? O código parece funcionar sem ele, mas desconfio que você esteja lidando com um caso que não conheço.
Alan De Smet
1
@AlanDeSmet O caso da borda é se a sequência termina com uma nova linha. A substituição de comandos retira as novas linhas à direita.
Gilles 'SO- stop be evil' (
6

Por que você não usa apenas $ 1, $ 2 .. $ 9, $ {10}, $ {11} ... e assim por diante? É ainda mais SECO do que você está tentando fazer :)

Mais sobre a relação entre $ number e $ @:

$ @ pode ser considerado uma abreviação para "todos os elementos de uma matriz que contêm todos os argumentos"

Portanto, $ @ é uma forma abreviada de $ {args [@]} (args aqui é uma matriz 'virtual' contendo todos os argumentos - e não uma variável real, veja bem)

$ 1 é $ {args [1]}, $ 2 é $ {args [2]} e assim por diante.

Quando você clicar em [9], use uma chave: $ {10} é $ {args [10]}, $ {11} é $ {args [11]} e assim por diante.


Indiretamente, use um argumento de linha de comando

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

Exemplo:

argc=$#
for (( argn=1; argn<=argc; argn++)); do
    if [[ ${!argn} == "foo" ]]; then
        echo "Argument $argn of $argc is 'foo'"
    fi
done
pepoluan
fonte
A desvantagem óbvia de ter que usar $ * number * é que você não pode usar uma variável de índice como com ${args[$i]}.
intuited
@ intuited use indireção; Vou editar minha resposta.
precisa saber é o seguinte
5

O primeiro argumento é usado para uma coisa, e o restante para outra coisa,

Eu acho que o que você quer é 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
fonte
1

Eu não sei bem por que você não usa apenas $ 1 $ 2, etc. mas ... Isso pode atender às suas necessidades.

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

resultado

  -------- 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 qualquer coisa que se segue, para criar $ 1, $ 2 .. etc. Isso obviamente substituirá os valores originais, portanto, esteja ciente disso.

Peter.O
fonte
ahh ... então por 'eval' você quis dizer referência indireta ... $ {! var} construção é mais seguro, como o que escrevi na minha resposta
pepoluan
@pepoluan ... Obrigado por me alertar sobre isso. É muito mais simples de escrever ... (Acabei de voltar para a página em que me referi, se tivesse lido mais, também teria mencionado aqui :( ....
Peter.O
heh mas se o engano acontece na esquerda lado, eval é um mal necessário, tho' :)
pepoluan
@peopluan ... ok, obrigado por apontar isso ... e apenas como um aparte: não entendo por que evalé considerado, por alguns, como evil... (talvez seja por causa da ortografia :) ... Se evalfor "ruim", então $ {! Var} é igualmente "ruim"? ... Para mim, isso é apenas parte da linguagem, e uma parte útil, pelo que .. mas eu definitivamente prefiro $ ... {var!}
Peter.O
1

Nota: Suporte espaços em nomes de arquivos.

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

Adicionei um caso de teste para nomes de arquivos com espaços e corrigi 2 testes que estavam faltando um líder /

    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
fonte