Comment obtenir le dernier argument d'une fonction / bin / sh


11

Quelle meilleure façon de mettre en œuvre print_last_arg?

#!/bin/sh

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

print_last_arg foo bar baz
# baz

(Si c'était le cas, par exemple, #!/usr/bin/zshau lieu de #!/bin/shje saurais quoi faire. Mon problème est de trouver un moyen raisonnable de mettre cela en œuvre #!/bin/sh.)

EDIT: Ce qui précède n'est qu'un exemple stupide. Mon objectif n'est pas d' imprimer le dernier argument, mais plutôt d'avoir un moyen de faire référence au dernier argument dans une fonction shell.


EDIT2: Je m'excuse pour une question si peu formulée. J'espère bien faire les choses cette fois.

Si tel était au /bin/zshlieu de /bin/sh, je pourrais écrire quelque chose comme ça

#!/bin/zsh

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

L'expression $argv[$#]est un exemple de ce que j'ai décrit dans mon premier EDIT comme un moyen de faire référence au dernier argument d'une fonction shell .

Par conséquent, j'aurais vraiment dû écrire mon exemple original comme ceci:

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

... pour préciser que ce que je recherche est une chose moins horrible à mettre à droite de la mission.

Notez cependant que dans tous les exemples, le dernier argument est accessible de manière non destructive . IOW, l'accès au dernier argument ne modifie pas les arguments positionnels dans leur ensemble.


unix.stackexchange.com/q/145522/117549 souligne les différentes possibilités de #! / bin / sh - pouvez-vous le restreindre?
Jeff Schaller

j'aime bien la modification, mais vous avez peut-être aussi remarqué qu'il y a une réponse ici qui offre un moyen non destructif de référencer le dernier argument ...? vous ne devriez pas assimiler var=$( eval echo \${$#})à eval var=\${$#}- les deux ne sont rien pareils.
mikeserv

1
Je ne suis pas sûr d'avoir votre dernière note, mais presque toutes les réponses fournies jusqu'à présent ne sont pas destructives dans le sens où elles préservent les arguments du script en cours d'exécution. Seules shiftet set -- ...solutions pourraient être destructrices moins utilisées dans les fonctions où ils sont trop inoffensifs.
jlliagre

@jlliagre - mais ils sont toujours destructeurs dans l'ensemble - ils nécessitent de créer des contextes jetables afin qu'ils puissent détruire pour découvrir. mais ... si vous obtenez un deuxième contexte de toute façon - pourquoi ne pas simplement obtenir celui qui vous permet d'indexer? y a-t-il un problème avec l'utilisation de l'outil prévu pour le travail? interpréter les extensions de shell comme des entrées extensibles est le travail d'eval. et il n'y a rien de très différent à faire eval "var=\${$#}"par rapport à, var=${arr[evaled index]}sauf qu'il $#s'agit d'une valeur sûre garantie. pourquoi copier l'ensemble entier puis le détruire alors que vous pouvez simplement l'indexer directement?
mikeserv

1
@mikeserv Une boucle for effectuée dans la partie principale du shell laisse tous les arguments inchangés. Je suis d'accord que boucler tous les arguments n'est pas optimisé, surtout si des milliers d'entre eux sont passés au shell et je conviens aussi que l'accès direct au dernier argument avec l'index approprié est la meilleure réponse (et je ne comprends pas pourquoi il a été rétrogradé ) mais au-delà, il n'y a rien de vraiment destructeur et aucun contexte supplémentaire créé.
jlliagre

Réponses:


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

... affichera la chaîne the last arg issuivie d'un <espace> , de la valeur du dernier argument et d'un <newline> de fin s'il y a au moins 1 argument, ou bien, pour zéro argument, il n'imprimera rien du tout.

Si vous l'avez fait:

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

... alors soit vous assigneriez la valeur du dernier argument à la variable shell $lastargs'il y a au moins 1 argument, soit vous ne feriez rien du tout. De toute façon, vous le feriez en toute sécurité, et il devrait être portable même pour le shell Olde Bourne, je pense.

Voici un autre qui fonctionnerait de la même manière, bien qu'il nécessite de copier deux fois le tableau d'arg entier (et nécessite un printfin $PATHpour le shell Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi

Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
terdon

2
Pour info, votre première suggestion échoue sous le shell bourne hérité avec une erreur de "mauvaise substitution" si aucun argument n'est présent. Les deux autres fonctionnent comme prévu.
jlliagre

@jillagre - merci. Je n'étais pas si sûr de celui-là - mais j'étais plutôt sûr des deux autres. il était simplement destiné à montrer comment les arguments pouvaient être consultés par référence en ligne. de préférence, une fonction s'ouvrirait avec quelque chose comme ça tout de ${1+":"} returnsuite - car qui veut qu'elle fasse quoi que ce soit ou risque des effets secondaires de quelque nature que ce soit si rien n'était demandé? Les mathématiques sont identiques pour la même chose - si vous pouvez être sûr que vous pouvez développer un entier positif, vous pouvez evaltoute la journée. $OPTINDest super pour ça.
mikeserv

3

Voici une manière simpliste:

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

(mis à jour en fonction du point de @ cuonglm selon lequel l'original a échoué lorsqu'il n'a pas été passé d'arguments; cela fait maintenant écho à une ligne vierge - modifiez ce comportement dans la elseclause si vous le souhaitez)


Cela nécessitait l'utilisation de / usr / xpg4 / bin / sh sur Solaris; faites-moi savoir si Solaris #! / bin / sh est une exigence.
Jeff Schaller

Cette question ne concerne pas un script spécifique que j'essaie d'écrire, mais plutôt un mode d'emploi général. Je cherche une bonne recette pour ça. Bien sûr, plus il est portable, mieux c'est.
kjo

le calcul $ (()) échoue dans Solaris / bin / sh; utilisez quelque chose comme: shift `expr $ # - 1` si c'est nécessaire.
Jeff Schaller

alternativement pour Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller

1
c'est exactement ce que je voulais écrire, mais il a échoué sur le deuxième shell que j'ai essayé - NetBSD, qui est apparemment un shell Almquist qui ne le prend pas en charge :(
Jeff Schaller

3

Étant donné l'exemple du message d'ouverture (arguments positionnels sans espaces):

print_last_arg foo bar baz

Par défaut IFS=' \t\n', que diriez-vous:

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

Pour une extension plus sûre de "$*", réglez IFS (per @ StéphaneChazelas):

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

Mais ce qui précède échouera si vos arguments positionnels peuvent contenir des espaces. Dans ce cas, utilisez plutôt ceci:

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

Notez que ces techniques évitent l'utilisation de evalet n'ont pas d'effets secondaires.

Testé sur shellcheck.net


1
Le premier échoue si le dernier argument contient de l'espace.
cuonglm

1
Notez que le premier ne fonctionnait pas non plus dans le shell pré-POSIX
cuonglm

@cuonglm Bien repéré, votre observation correcte est maintenant intégrée.
AsymLabs

Il suppose également que le premier caractère de $IFSest un espace.
Stéphane Chazelas

1
Ce sera le cas s'il n'y a pas de $ IFS dans l'environnement, autrement non spécifié. Mais comme vous devez définir IFS pratiquement à chaque fois que vous utilisez l'opérateur split + glob (sans expansion), une approche pour gérer IFS consiste à le définir chaque fois que vous en avez besoin. Cela ne nuirait pas au fait d'avoir IFS=' 'ici simplement pour indiquer clairement qu'il est utilisé pour l'expansion de "$*".
Stéphane Chazelas

3

Bien que cette question ait un peu plus de 2 ans, je pensais partager une option un peu plus compacte.

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

Lançons-le

print_last_arg foo bar baz
baz

Expansion des paramètres du shell Bash .

Éditer

Encore plus concis: echo "${@: -1}"

(Attention à l'espace)

La source

Testé sur macOS 10.12.6 mais devrait également renvoyer le dernier argument sur la plupart des versions * nix disponibles ...

Ça fait moins mal ¯\_(ツ)_/¯


Cela devrait être la réponse acceptée. Encore mieux serait: echo "${*: -1}"qui shellcheckne se plaindra pas.
Tom Hale

4
Cela ne fonctionnera pas avec un sh POSIX simple, ${array:n:m:}est une extension. (la question a mentionné explicitement /bin/sh)
ilkkachu

2

POSIX:

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

printf '%s\n' "$1"

(Cette approche fonctionne également dans le vieux shell Bourne)

Avec d'autres outils standard:

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

(Cela ne fonctionnera pas avec l'ancien awk, qui n'avait pas ARGV)


Là où il y a encore un obus Bourne, awkpourrait aussi être l'ancien awk qui n'en avait pas ARGV.
Stéphane Chazelas

@ StéphaneChazelas: D'accord. Au moins, cela a fonctionné avec la propre version de Brian Kernighan. Avez-vous un idéal?
cuonglm

Cela efface les arguments. Comment les garder?.

@BinaryZebra: utilisez l' awkapproche, ou utilisez une autre forboucle simple comme les autres, ou passez-les à une fonction.
cuonglm

2

Cela devrait fonctionner avec n'importe quel shell compatible POSIX et fonctionnera également avec le shell Solaris Bourne antérieur à POSIX:

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

et voici une fonction basée sur la même approche:

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

PS: ne me dites pas que j'ai oublié les accolades autour du corps de la fonction ;-)


2
Vous avez oublié les accolades autour du corps de la fonction ;-).

@BinaryZebra Je vous ai prévenu ;-) Je ne les ai pas oubliés. Les accolades sont étonnamment facultatives ici.
jlliagre

1
@jlliagre J'ai en effet été prévenu ;-): P ...... Et: certainement!

Quelle partie de la spécification de syntaxe le permet?
Tom Hale

@TomHale Les règles de grammaire du shell permettent à la fois un manquant in ...dans une forboucle et un pas d'accolades autour d'une ifinstruction utilisée comme corps de fonction. Voir for_clauseet compound_commanddans pubs.opengroup.org/onlinepubs/9699919799 .
jlliagre

2

De "Unix - Foire aux questions"

(1)

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

Si le nombre d'arguments peut être nul, l'argument zéro $0(généralement le nom du script) sera attribué à $last. C'est la raison du si.

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

Pour éviter d'imprimer une ligne vide en l'absence d'arguments, remplacez le echo "$last"for:

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

Un nombre d'arguments nul est évité par if [ $# -gt 0 ].

Ce n'est pas une copie exacte de ce qui se trouve dans le lien dans la page, certaines améliorations ont été ajoutées.


0

Cela devrait fonctionner sur tous les systèmes avec perlinstallé (donc la plupart des UNICES):

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

L'astuce consiste à printfajouter un \0après chaque argument shell et ensuite perlle -0commutateur qui définit son séparateur d'enregistrement sur NULL. Ensuite, nous itérons sur l'entrée, supprimons le \0et enregistrons chaque ligne terminée par NULL sous $l. Le ENDbloc (c'est ce que }{c'est) sera exécuté une fois que toutes les entrées auront été lues, donc affichera la dernière "ligne": le dernier argument du shell.


@mikeserv ah, c'est vrai, je n'avais testé avec une nouvelle ligne que dans le dernier argument. La version éditée devrait fonctionner à peu près n'importe quoi, mais est probablement trop compliquée pour une tâche aussi simple.
terdon

oui, appeler un exécutable extérieur (si cela ne fait pas déjà partie de la logique) est, pour moi, un peu lourd. mais si le point est de passer cet argument à sed, awkou perlalors il pourrait être des informations précieuses de toute façon. vous pouvez toujours faire la même chose sed -zpour les versions GNU à jour.
mikeserv

pourquoi avez-vous été déçu? c'était bon ... je pensais?
mikeserv

0

Voici une version utilisant la récursivité. Je ne sais pas à quel point c'est conforme à POSIX ...

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

0

Un concept simple, pas d'arithmétique, pas de boucle, pas d' évaluation , juste des fonctions.
N'oubliez pas que le shell Bourne n'avait pas d'arithmétique (nécessaire externe expr). Si vous souhaitez obtenir une arithmétique libre, evallibre choix, c'est une option. Besoin de fonctions signifie SVR3 ou supérieur (pas d'écrasement des paramètres).
Regardez ci-dessous pour une version plus robuste avec 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.

Cette structure d'appel printlastest fixé , les arguments doivent être définis dans la liste des arguments shell $1, $2etc. (la pile d'arguments) et l'appel fait comme indiqué.

Si la liste des arguments doit être modifiée, il suffit de les définir:

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

Ou créez une fonction ( getlast) plus facile à utiliser qui pourrait autoriser des arguments génériques (mais pas aussi rapidement, les arguments sont passés deux fois).

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

Veuillez noter que les arguments (de getlast, ou tous inclus dans $@pour printlast) peuvent avoir des espaces, des retours à la ligne, etc. Mais pas NUL.

Meilleur

Cette version ne s'imprime pas 0lorsque la liste des arguments est vide et utilise le printf plus robuste (revenir à echosi external printfn'est pas disponible pour les anciens shells).

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

Utilisation d'EVAL.

Si le shell Bourne est encore plus ancien et qu'il n'y a pas de fonctions, ou si pour une raison quelconque, utiliser eval est la seule option:

Pour imprimer la valeur:

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

Pour définir la valeur dans une variable:

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

Si cela doit être fait dans une fonction, les arguments doivent être copiés dans la fonction:

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.

c'est mieux mais il dit n'utilise pas de boucle - mais c'est le cas. une copie de tableau est une boucle . et avec deux fonctions, il utilise deux boucles. aussi - y a-t-il une raison pour laquelle itérer le tableau entier devrait être préféré à l'indexer avec eval?
mikeserv

1
Voyez-vous un for loopou while loop?


1
Selon votre norme, je suppose que tout le code a des boucles, même une echo "Hello"a des boucles car le CPU doit lire les emplacements de mémoire dans une boucle. Encore une fois: mon code n'a pas de boucles ajoutées.

1
Je ne me soucie vraiment pas de votre opinion du bien ou du mal, elle a déjà été prouvée à plusieurs reprises. Encore une fois: mon code n'a pas for, whileou des untilboucles. En voyez-vous une for whileou une untilboucle écrite dans le code?. Non ?, alors acceptez simplement le fait, acceptez-le et apprenez.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.