Como obter o último argumento para uma função / bin / sh

11

Qual é a melhor maneira de implementar print_last_arg?

#!/bin/sh

print_last_arg () {
    eval "echo \${$#}"  # this hurts
}

print_last_arg foo bar baz
# baz

(Se assim fosse, digamos, em #!/usr/bin/zshvez de #!/bin/shsaber o que fazer. Meu problema é encontrar uma maneira razoável de implementar isso #!/bin/sh.)

EDIT: O acima é apenas um exemplo bobo. Meu objetivo não é imprimir o último argumento, mas sim ter uma maneira de me referir ao último argumento dentro de uma função shell.


EDIT2: Peço desculpas por uma pergunta tão pouco clara. Espero acertar desta vez.

Se isso fosse /bin/zsh, em vez de /bin/sh, eu poderia escrever algo como isto

#!/bin/zsh

print_last_arg () {
    local last_arg=$argv[$#]
    echo $last_arg
}

A expressão $argv[$#]é um exemplo do que descrevi na minha primeira edição como uma maneira de se referir ao último argumento dentro de uma função shell .

Portanto, eu realmente deveria ter escrito meu exemplo original assim:

print_last_arg () {
    local last_arg=$(eval "echo \${$#}")   # but this hurts even more
    echo $last_arg
}

... para deixar claro que o que estou procurando é uma coisa menos terrível para colocar à direita da tarefa.

Observe, no entanto, que em todos os exemplos, o último argumento é acessado de maneira não destrutiva . IOW, o acesso ao último argumento não afeta os argumentos posicionais como um todo.

kjo
fonte
unix.stackexchange.com/q/145522/117549 mostra as várias possibilidades de #! / bin / sh - você pode restringir alguma?
Jeff Schaller
Eu gosto da edição, mas talvez você também tenha notado que há uma resposta aqui que oferece um meio não destrutivo de referenciar o último argumento ...? você não deve se equiparar var=$( eval echo \${$#})a eval var=\${$#}- os dois não são iguais.
mikeserv
1
Não tenho certeza se recebo sua última observação, mas quase todas as respostas fornecidas até agora não são destrutivas, no sentido em que preservam os argumentos do script em execução. Soluções únicas shifte set -- ...baseadas podem ser destrutivas, a menos que sejam usadas em funções onde também são inofensivas.
Jlliagre
@ jlliagre - mas eles ainda são destrutivos em geral - eles exigem a criação de contextos descartáveis ​​para que eles possam destruir para descobrir. mas ... se você tiver um segundo contexto de qualquer maneira - por que não apenas o que permite indexar? há algo de errado em usar a ferramenta destinada ao trabalho? interpretar expansões de shell como entrada expansível é o trabalho de eval. e não há nada significativamente diferente em fazer eval "var=\${$#}"quando comparado, var=${arr[evaled index]}exceto que esse $#é um valor seguro garantido. por que copiar todo o conjunto e destruí-lo quando você pode indexá-lo diretamente?
precisa saber é o seguinte
1
@mikeserv Um loop for feito na parte principal do shell está deixando todos os argumentos inalterados. Eu concordo que o loop de todos os argumentos seja muito otimizado, especialmente se milhares deles forem passados ​​para o shell e também concordo que o acesso direto ao último argumento com o índice adequado é a melhor resposta (e não entendo por que o voto foi negativo) ) mas, além disso, não há nada realmente destrutivo e nenhum contexto extra criado.
Jlliagre

Respostas:

1
eval printf %s${1+"'\n' \"the last arg is \${$#"\}\"}

... imprimirá a sequência the last arg isseguida de um <space> , o valor do último argumento e um <newline> à direita, se houver pelo menos 1 argumento, ou, para zero argumentos, não imprimirá nada.

Se você fez:

eval ${1+"lastarg=\${$#"\}}

... então você atribuiria o valor do último argumento à variável shell $lastargse houver pelo menos 1 argumento ou não faria nada. De qualquer forma, você faria isso com segurança, e deve ser portátil até para você, olde Bourne, eu acho.

Aqui está mais um que funcionam de forma semelhante, embora ele requer copiar a matriz arg toda duas vezes (e requer um printfno $PATHpara o shell Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi
mikeserv
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
terdon
2
Para sua informação, sua primeira sugestão falhará sob o shell herdado de bourne com um erro de "má substituição" se nenhum argumento estiver presente. Ambos os restantes funcionam como esperado.
Jlliagre
@jillagre - obrigado. eu não tinha tanta certeza sobre o top - mas tinha certeza dos outros dois. era apenas para demonstrar como os argumentos podem ser acessados ​​por referência em linha. de preferência, uma função abriria com algo como ${1+":"} returnimediatamente - porque quem quer que ela faça alguma coisa ou arrisque efeitos colaterais de qualquer tipo se não for necessária? A matemática é a mesma coisa - se você tiver certeza de que pode expandir um número inteiro positivo, pode o evaldia inteiro. $OPTINDé ótimo para isso.
mikeserv
3

Aqui está uma maneira simplista:

print_last_arg () {
  if [ "$#" -gt 0 ]
  then
    s=$(( $# - 1 ))
  else
    s=0
  fi
  shift "$s"
  echo "$1"
}

(atualizado com base no argumento de @ cuonglm de que o original falhou quando não passou argumentos; isso agora ecoa uma linha em branco - altere esse comportamento na elsecláusula, se desejado)

Jeff Schaller
fonte
Isso é necessário usando / usr / xpg4 / bin / sh no Solaris; informe-me se o Solaris #! / bin / sh for um requisito.
Jeff Schaller
Esta pergunta não é sobre um script específico que estou tentando escrever, mas um tutorial geral. Estou procurando uma boa receita para isso. Obviamente, quanto mais portátil, melhor.
KJo
a matemática $ (()) falha no Solaris / bin / sh; use algo como: shift `expr $ # - 1` se for necessário.
Jeff Schaller
alternativamente para Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller
1
isso é exatamente o que eu queria escrever, mas falhou no segundo shell que tentei - NetBSD, que aparentemente é um shell almquist que não o suporta :(
Jeff Schaller
3

Dado o exemplo do post de abertura (argumentos posicionais sem espaços):

print_last_arg foo bar baz

Para o padrão IFS=' \t\n', que tal:

args="$*" && printf '%s\n' "${args##* }"

Para uma expansão mais segura de "$*", defina IFS (por @ StéphaneChazelas):

( IFS=' ' args="$*" && printf '%s\n' "${args##* }" )

Mas o acima falhará se seus argumentos posicionais puderem conter espaços. Nesse caso, use isso:

for a in "$@"; do : ; done && printf '%s\n' "$a"

Observe que essas técnicas evitam o uso evale não têm efeitos colaterais.

Testado em shellcheck.net

AsymLabs
fonte
1
O primeiro falha se o último argumento contiver espaço.
cuonglm
1
Note que o primeiro também não funcionou no pré-POSIX shell
cuonglm
@cuonglm Bem visto, sua observação correta está incorporada agora.
AsymLabs
Ele também assume que o primeiro caractere de $IFSé um espaço.
Stéphane Chazelas
1
Será se não houver $ IFS no ambiente, caso contrário não especificado. Mas como você precisa configurar o IFS virtualmente sempre que usar o operador split + glob (deixe uma expansão sem citação), uma abordagem para lidar com o IFS é configurá-lo sempre que você precisar. Não prejudicaria ter IFS=' 'aqui apenas para deixar claro que está sendo usado para a expansão de "$*".
Stéphane Chazelas
3

Embora essa pergunta tenha pouco mais de 2 anos, pensei em compartilhar uma opção de compactação.

print_last_arg () {
    echo "${@:${#@}:${#@}}"
}

Vamos correr

print_last_arg foo bar baz
baz

Expansão de parâmetros do shell Bash .

Editar

Ainda mais conciso: echo "${@: -1}"

(Cuidado com o espaço)

Fonte

Testado no macOS 10.12.6, mas também deve retornar o último argumento na maioria dos sabores * nix disponíveis ...

Dói muito menos ¯\_(ツ)_/¯

user2243670
fonte
Essa deve ser a resposta aceita. Melhor ainda: o echo "${*: -1}"que shellchecknão vai reclamar.
Tom Hale
4
Isso não funcionará com um POSIX sh simples, ${array:n:m:}é uma extensão. (a questão foi explicitamente mencionado /bin/sh)
ilkkachu
2

POSIXly:

while [ "$#" -gt 1 ]; do
  shift
done

printf '%s\n' "$1"

(Essa abordagem também funciona no shell Bourne antigo)

Com outras ferramentas padrão:

awk 'BEGIN{print ARGV[ARGC-1]}' "$@"

(Isso não funcionará com o antigo awk, que não tinha ARGV)

cuonglm
fonte
Onde ainda existe uma concha Bourne, awktambém pode ser o velho awk que não tinha ARGV.
Stéphane Chazelas
@ StéphaneChazelas: Concordo. Pelo menos, funcionou com a própria versão de Brian Kernighan. Você tem algum ideal?
cuonglm
Isso apaga os argumentos. Como mantê-los?
@BinaryZebra: Use a awkabordagem, ou use outro forloop simples como outro, ou passe-os para uma função.
cuonglm
2

Isso deve funcionar com qualquer shell compatível com POSIX e também com o shell Solaris Bourne anterior ao POSIX:

do=;for do do :;done;printf "%s\n" "$do"

e aqui está uma função baseada na mesma abordagem:

print_last_arg()
  if [ "$*" ]; then
    for do do :;done
    printf "%s\n" "$do"
  else
    echo
  fi

PS: não me diga que eu esqueci o aparelho em volta do corpo da função ;-)

jlliagre
fonte
2
Você esqueceu as chaves em volta do corpo da função ;-).
@BinaryZebra eu avisei ;-) Eu não os esqueci. Os aparelhos são surpreendentemente opcionais aqui.
Jlliagre
1
@ jlliagre Eu, de fato, fui avisado ;-): P ...... E: certamente!
Qual parte da especificação de sintaxe permite isso?
Tom Hale
@TomHale As regras gramaticais do shell permitem a falta in ...de um forloop e a ausência de chaves em torno de uma ifinstrução usada como corpo da função. Veja for_clausee compound_commandem pubs.opengroup.org/onlinepubs/9699919799 .
Jlliagre
2

Do "Unix - Perguntas freqüentes"

(1)

unset last
if    [ $# -gt 0 ]
then  eval last=\${$#}
fi
echo  "$last"

Se o número de argumentos puder ser zero, o argumento zero $0(geralmente o nome do script) será atribuído $last. Essa é a razão do if.

2)

unset last
for   last
do    :
done
echo  "$last"

(3)

for     i
do
        third_last=$second_last
        second_last=$last
        last=$i
done
echo    "$last"

Para evitar a impressão de uma linha vazia quando não houver argumentos, substitua echo "$last"por:

${last+false} || echo "${last}"

Uma contagem de argumento zero é evitada por if [ $# -gt 0 ].

Esta não é uma cópia exata do conteúdo do link na página; algumas melhorias foram adicionadas.


fonte
0

Isso deve funcionar em todos os sistemas com o perlinstalado (portanto, a maioria dos UNICES):

print_last_arg () {
    printf '%s\0' "$@" | perl -0ne 's/\0//;$l=$_;}{print "$l\n"'
}

O truque é usar printfpara adicionar um \0argumento depois de cada shell e, em seguida perl, o -0switch que define seu separador de registros como NULL. Em seguida, iteramos sobre a entrada, removemos \0e salvamos cada linha terminada em NULL como $l. O ENDbloco (que é o que }{é) será executado depois que todas as entradas tiverem sido lidas e, portanto, imprimirá a última "linha": o último argumento do shell.

terdon
fonte
@ MikeServ ah, é verdade, eu não tinha testado com uma nova linha, mas o último argumento. A versão editada deve funcionar para praticamente qualquer coisa, mas provavelmente é complicada demais para uma tarefa tão simples.
terdon
Sim, chamar um executável externo (se ainda não faz parte da lógica) é, para mim, um pouco pesado. mas se o ponto é para passar esse argumento para sed, awkou perlentão ele poderia ser informação valiosa de qualquer maneira. ainda assim, você pode fazer o mesmo com sed -zversões atualizadas do GNU.
mikeserv
por que você foi derrotado? isso foi bom ... pensei?
precisa saber é
0

Aqui está uma versão usando recursão. Não faço ideia de como isso é compatível com POSIX ...

print_last_arg()
{
    if [ $# -gt 1 ] ; then
        shift
        echo $( print_last_arg "$@" )
    else
        echo "$1"
    fi
}
brm
fonte
0

Um conceito simples, sem aritmética, nenhum laço, nenhuma eval , apenas funções.
Lembre-se de que o shell Bourne não possuía aritmética (necessário externo expr). Se você deseja obter uma aritmética livre, evallivre escolha, esta é uma opção. Necessitar de funções significa SVR3 ou superior (sem substituição de parâmetros).
Veja abaixo uma versão mais robusta com printf.

printlast(){
    shift "$1"
    echo "$1"
}

printlast "$#" "$@"          ### you may use ${1+"$@"} here to allow
                             ### a Bourne empty list of arguments,
                             ### but wait for a correct solution below.

Esta estrutura de chamada printlasté fixa , os argumentos precisam ser definidas na lista de argumentos shell $1, $2etc. (a pilha de argumento) e o chamado feito como dado.

Se a lista de argumentos precisar ser alterada, apenas defina-os:

set -- o1e t2o t3r f4u
printlast "$#" "$@"

Ou crie uma função mais fácil de usar ( getlast) que possa permitir argumentos genéricos (mas não tão rápido, os argumentos são passados ​​duas vezes).

getlast(){ printlast "$#" "$@"; }
getlast o1e t2o t3r f4u

Observe que os argumentos (de getlast, ou todos incluídos no $@printlast) podem ter espaços, novas linhas, etc. Mas não NUL.

Melhor

Esta versão não é impressa 0quando a lista de argumentos está vazia e usa o printf mais robusto (volte para echose externo printfnão estiver disponível para shells antigos).

printlast(){ shift  "$1"; printf '%s' "$1"; }
getlast  ()  if     [ $# -gt 0 ]
             then   printlast "$#" "$@"
                    echo    ### optional if a trailing newline is wanted.
             fi
### The {} braces were removed on purpose.

getlast 1 2 3 4    # will print 4

Usando EVAL.

Se o shell Bourne for ainda mais antigo e não houver funções, ou se, por algum motivo, usar eval for a única opção:

Para imprimir o valor:

if    [ $# -gt 0 ]
then  eval printf "'1%s\n'" \"\$\{$#\}\"
fi

Para definir o valor em uma variável:

if    [ $# -gt 0 ]
then  eval 'last_arg='\"\$\{$#\}\"
fi

Se isso precisar ser feito em uma função, os argumentos deverão ser copiados para a função:

print_last_arg () {
    local last_arg                ### local only works on more modern shells.
    eval 'last_arg='\"\$\{$#\}\"
    echo "last_arg3=$last_arg"
}

print_last_arg "$@"               ### Shell arguments are sent to the function.

fonte
é melhor, mas diz que não usa um loop - mas usa. uma cópia de matriz é um loop . e com duas funções, ele usa dois loops. também - há alguma razão para iterar toda a matriz, em vez de indexá-la eval?
precisa saber é
1
Você vê algum for loopou while loop?
sim eu faço
mikeserv
1
Pelo seu padrão, acho que todo o código possui loops, até um echo "Hello"tem loops, pois a CPU precisa ler os locais de memória em um loop. Novamente: meu código não possui loops adicionados.
1
Eu realmente não me importo com a sua opinião de bom ou ruim, já foi provado errado várias vezes. Novamente: meu código não possui for, whileou untilloops. Você vê algum for whileou untilloop escrito no código ?. Não ?, aceite o fato, aceite-o e aprenda.