Cómo obtener el último argumento para una función / bin / sh

11

¿Cuál es una mejor manera de implementar print_last_arg?

#!/bin/sh

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

print_last_arg foo bar baz
# baz

(Si esto fuera, digamos, en #!/usr/bin/zshlugar de #!/bin/shsaber qué hacer. Mi problema es encontrar una forma razonable de implementar esto #!/bin/sh).

EDITAR: Lo anterior es solo un ejemplo tonto. Mi objetivo no es imprimir el último argumento, sino tener una manera de referirme al último argumento dentro de una función de shell.


EDIT2: Pido disculpas por una pregunta tan poco clara. Espero hacerlo bien esta vez.

Si esto fuera en /bin/zshlugar de /bin/sh, podría escribir algo como esto

#!/bin/zsh

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

La expresión $argv[$#]es un ejemplo de lo que describí en mi primer EDIT como una forma de referirme al último argumento dentro de una función de shell .

Por lo tanto, realmente debería haber escrito mi ejemplo original así:

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

... para dejar en claro que lo que busco es algo menos horrible de poner a la derecha de la tarea.

Sin embargo, tenga en cuenta que en todos los ejemplos, se accede al último argumento de forma no destructiva . IOW, el acceso al último argumento deja los argumentos posicionales en su conjunto sin verse afectados.

kjo
fuente
unix.stackexchange.com/q/145522/117549 señala las diversas posibilidades de #! / bin / sh: ¿puede reducirlo ?
Jeff Schaller
me gusta la edición, pero tal vez también hayas notado que hay una respuesta aquí que ofrece un medio no destructivo de hacer referencia al último argumento ...? no debe ser igual var=$( eval echo \${$#})a eval var=\${$#}- los dos son nada igual.
mikeserv
1
No estoy seguro de que reciba su última nota, pero casi todas las respuestas proporcionadas hasta el momento no son destructivas en el sentido de que conservan los argumentos del script en ejecución. Las soluciones únicas shifty set -- ...basadas pueden ser destructivas a menos que se utilicen en funciones donde también son inofensivas.
jlliagre
@jlliagre, pero siguen siendo destructivos en general, requieren la creación de contextos desechables para que puedan destruir para descubrir. pero ... si obtienes un segundo contexto de todos modos, ¿por qué no solo obtener el que te permite indexar? ¿Hay algo malo con el uso de la herramienta destinada para el trabajo? interpretar expansiones de shell como entrada expandible es el trabajo de eval. y no hay nada significativamente diferente al hacer eval "var=\${$#}"en comparación con, var=${arr[evaled index]}excepto que $#es un valor seguro garantizado. ¿Por qué copiar todo el conjunto y luego destruirlo cuando puedes indexarlo directamente?
mikeserv
1
@mikeserv Un bucle for realizado en la parte principal del shell deja todos los argumentos sin cambios. Estoy de acuerdo en que todos los argumentos en bucle no están optimizados, especialmente si se pasan miles de ellos al shell y también estoy de acuerdo en que acceder directamente al último argumento con el índice adecuado es la mejor respuesta (y no entiendo por qué se rechazó ) pero más allá de eso, no hay nada realmente destructivo y no se crea ningún contexto adicional.
jlliagre

Respuestas:

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

... imprimirá la cadena the last arg isseguida de un <space> , el valor del último argumento y un <newline> final si hay al menos 1 argumento, o bien, para cero argumentos, no imprimirá nada en absoluto.

Si lo hiciste:

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

... entonces asignaría el valor del último argumento a la variable de shell $lastargsi hay al menos 1 argumento, o no haría nada en absoluto. De cualquier manera, lo harías de forma segura, y debería ser portátil incluso para el caparazón de Olde Bourne, creo.

Aquí hay otro que funcionaría de manera similar, aunque sí se requiere la copia de toda la matriz arg dos veces (y requiere una printfen $PATHel shell Bourne) :

if   [ "${1+:}" ]
then set "$#" "$@" "$@"
     shift    "$1"
     printf %s\\n "$1"
     shift
fi
mikeserv
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
terdon
2
Para su información, su primera sugerencia falla bajo el shell bourne heredado con un error de "mala sustitución" si no hay argumentos presentes. Los dos restantes funcionan como se esperaba.
jlliagre
@jillagre - gracias. No estaba tan seguro de eso, pero estaba bastante seguro de los otros dos. solo estaba destinado a ser demostrativo de cómo se podía acceder a los argumentos por referencia en línea. preferiblemente, una función se abriría con algo como de ${1+":"} returninmediato, porque ¿quién quiere que haga algo o arriesgue los efectos secundarios de cualquier tipo si no se solicita nada? Las matemáticas son bonitas para lo mismo: si puede estar seguro de que puede expandir un número entero positivo, puede hacerlo evaltodo el día. $OPTINDes genial por eso
mikeserv
3

Aquí hay una forma simplista:

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

(actualizado según el punto de @ cuonglm de que el original fallaba cuando no se pasaba ningún argumento; esto ahora hace eco de una línea en blanco: cambie ese comportamiento en la elsecláusula si lo desea)

Jeff Schaller
fuente
Esto requería usar / usr / xpg4 / bin / sh en Solaris; avíseme si Solaris #! / bin / sh es un requisito.
Jeff Schaller
Esta pregunta no se trata de un guión específico que estoy tratando de escribir, sino más bien de un tutorial general. Estoy buscando una buena receta para esto. Por supuesto, cuanto más portátil, mejor.
kjo
la matemática $ (()) falla en Solaris / bin / sh; use algo como: shift `expr $ # - 1` si es necesario.
Jeff Schaller
alternativamente para Solaris / bin / sh,echo "$# - 1" | bc
Jeff Schaller
1
eso es exactamente lo que quería escribir, pero falló en el segundo shell que probé - NetBSD, que aparentemente es un shell Almquist que no lo admite :(
Jeff Schaller
3

Dado el ejemplo de la publicación de apertura (argumentos posicionales sin espacios):

print_last_arg foo bar baz

Por defecto IFS=' \t\n', ¿qué tal:

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

Para una expansión más segura de "$*", configure IFS (por @ StéphaneChazelas):

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

Pero lo anterior fallará si sus argumentos posicionales pueden contener espacios. En ese caso, use esto en su lugar:

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

Tenga en cuenta que estas técnicas evitan el uso evaly no tienen efectos secundarios.

Probado en shellcheck.net

AsymLabs
fuente
1
El primero falla si el último argumento contiene espacio.
Cuonglm
1
Tenga en cuenta que el primero tampoco funcionó en el shell pre-POSIX
Cuonglm
@cuonglm Bien visto, su observación correcta se incorpora ahora.
AsymLabs
También supone que el primer carácter de $IFSes un espacio.
Stéphane Chazelas
1
Lo será si no hay $ IFS en el entorno, sin especificar lo contrario. Pero debido a que necesita establecer IFS prácticamente cada vez que usa el operador de división + glob (deje una expansión sin comillas), un enfoque para tratar con IFS es establecerlo siempre que lo necesite. No estaría de más tener IFS=' 'aquí solo para dejar en claro que se está utilizando para la expansión de "$*".
Stéphane Chazelas
3

Aunque esta pregunta tiene poco más de 2 años, pensé en compartir una opción más compacta.

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

Vamos a correrlo

print_last_arg foo bar baz
baz

Expansión de parámetros de shell Bash .

Editar

Aún más conciso: echo "${@: -1}"

(Cuidado con el espacio)

Fuente

Probado en macOS 10.12.6 pero también debería devolver el último argumento sobre la mayoría de los sabores * nix disponibles ...

Duele mucho menos ¯\_(ツ)_/¯

usuario2243670
fuente
Esta debería ser la respuesta aceptada. Aún mejor sería: de echo "${*: -1}"lo shellcheckque no se quejará.
Tom Hale
44
Esto no funcionará con un simple POSIX sh, ${array:n:m:}es una extensión. (la pregunta mencionó explícitamente /bin/sh)
ilkkachu
2

POSIXY:

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

printf '%s\n' "$1"

(Este enfoque también funciona en el viejo shell Bourne)

Con otras herramientas estándar:

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

(Esto no funcionará con el viejo awk, que no tenía ARGV)

Cuonglm
fuente
Donde todavía hay un shell Bourne, awktambién podría ser el viejo awk que no tenía ARGV.
Stéphane Chazelas
@ StéphaneChazelas: De acuerdo. Al menos funcionó con la propia versión de Brian Kernighan. ¿Tienes algún ideal?
Cuonglm
Esto borra los argumentos. ¿Cómo conservarlos?
@BinaryZebra: use el awkenfoque, o use otro forciclo simple como lo hizo otro, o páselos a una función.
Cuonglm
2

Esto debería funcionar con cualquier shell compatible con POSIX y también con el shell Solaris Bourne pre POSIX heredado:

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

y aquí hay una función basada en el mismo enfoque:

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

PD: no me digas que olvidé las llaves alrededor del cuerpo de la función ;-)

jlliagre
fuente
2
Olvidó las llaves alrededor del cuerpo de la función ;-).
@BinaryZebra Te lo advertí ;-) No los olvidé. Las llaves son sorprendentemente opcionales aquí.
jlliagre
1
@jlliagre I, de hecho, fui advertido ;-): P ...... Y: ¡ciertamente!
¿Qué parte de la especificación de sintaxis lo permite?
Tom Hale
@TomHale Las reglas de gramática de shell permiten tanto la falta in ...de un forbucle como las llaves sin rizos alrededor de una ifdeclaración utilizada como cuerpo de la función. Ver for_clausey compound_commanden pubs.opengroup.org/onlinepubs/9699919799 .
jlliagre
2

De "Unix - Preguntas frecuentes"

(1)

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

Si el número de argumentos puede ser cero, entonces $0se asignará el argumento cero (generalmente el nombre del script) $last. Esa es la razón del 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 imprimir una línea vacía cuando no hay argumentos, reemplace echo "$last"for:

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

Se evita un recuento de argumento cero if [ $# -gt 0 ].

Esta no es una copia exacta de lo que está en el enlace en la página, se agregaron algunas mejoras.


fuente
0

Esto debería funcionar en todos los sistemas con perlinstalado (por lo que la mayoría de UNICES):

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

El truco es usar printfpara agregar un \0argumento después de cada argumento de shell y luego perlel -0interruptor que establece su separador de registros en NULL. Luego iteramos sobre la entrada, eliminamos y guardamos \0cada línea terminada en NULL como $l. El ENDbloque (eso es lo que }{es) se ejecutará después de que se haya leído toda la entrada, por lo que imprimirá la última "línea": el último argumento de shell.

terdon
fuente
@mikeserv ah, es cierto, no había probado con una nueva línea en el último argumento. La versión editada debería funcionar para casi cualquier cosa, pero probablemente sea demasiado complicada para una tarea tan simple.
terdon
sí, llamar a un ejecutable externo (si aún no es parte de la lógica) es, para mí, un poco pesado. pero si el punto es pasar ese argumento de que sed, awko perlentonces podría ser una valiosa información de todos modos. aún puede hacer lo mismo con las sed -zversiones actualizadas de GNU.
mikeserv
¿Por qué te votaron mal? esto estuvo bien ... pensé?
mikeserv
0

Aquí hay una versión con recursión. Sin embargo, no tengo idea de cuán compatible con POSIX ...

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

Un concepto simple, sin aritmética, sin bucle, sin evaluación , solo funciones.
Recuerde que el shell Bourne no tenía aritmética (necesaria externa expr). Si desea obtener una aritmética libre, evallibre elección, esta es una opción. Necesitar funciones significa SVR3 o superior (sin sobrescritura de parámetros).
Mire abajo para una versión más robusta con 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 estructura a la llamada printlastse fija , los argumentos se deben establecer en la lista de argumentos de la cáscara $1, $2, etc. (la pila argumento) y la llamada hecho como dado.

Si la lista de argumentos necesita ser cambiada, solo configúrelos:

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

O cree una función ( getlast) más fácil de usar que pueda permitir argumentos genéricos (pero no tan rápido, los argumentos se pasan dos veces).

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

Tenga en cuenta que los argumentos (de getlast, o todos incluidos en $@for lastlast) podrían tener espacios, líneas nuevas, etc. Pero no NUL.

Mejor

Esta versión no se imprime 0cuando la lista de argumentos está vacía, y utiliza el printf más robusto (recurrir a echosi externo printfno está disponible para shells antiguos).

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.

Si el shell Bourne es aún más antiguo y no hay funciones, o si por alguna razón usar eval es la única opción:

Para imprimir el valor:

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

Para establecer el valor en una variable:

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

Si eso debe hacerse en una función, los argumentos deben copiarse en la función:

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.

fuente
es mejor pero dice que no usa un bucle, pero lo hace. Una copia de matriz es un bucle . y con dos funciones usa dos bucles. Además, ¿hay alguna razón por la que iterar toda la matriz debería preferirse a indexarlo eval?
mikeserv
1
¿Ves alguna for loopo while loop?
sí, lo hago
mikeserv
1
Según su estándar, supongo que todo el código tiene bucles, incluso uno echo "Hello"tiene bucles ya que la CPU tiene que leer las ubicaciones de memoria en un bucle. Nuevamente: mi código no tiene bucles agregados.
1
Realmente no me importa tu opinión de bueno o malo, ya se ha demostrado que está equivocado varias veces. Una vez más: mi código no tiene for, whileo untilbucles. ¿Ves alguno for whileo un untilbucle escrito en el código? ¿No ?, entonces solo acepta el hecho, abrázalo y aprende.