¿Haciéndose eco del último comando ejecutado en Bash?

84

Estoy tratando de hacer eco del último comando ejecutado dentro de un script bash. Encontré una manera de hacerlo con algunos history,tail,head,sedque funcionan bien cuando los comandos representan una línea específica en mi script desde el punto de vista del analizador. Sin embargo, en algunas circunstancias no obtengo el resultado esperado, por ejemplo, cuando el comando se inserta dentro de una casedeclaración:

La secuencia de comandos:

#!/bin/bash
set -o history
date
last=$(echo `history |tail -n2 |head -n1` | sed 's/[0-9]* //')
echo "last command is [$last]"

case "1" in
  "1")
  date
  last=$(echo `history |tail -n2 |head -n1` | sed 's/[0-9]* //')
  echo "last command is [$last]"
  ;;
esac

La salida:

Tue May 24 12:36:04 CEST 2011
last command is [date]
Tue May 24 12:36:04 CEST 2011
last command is [echo "last command is [$last]"]

[P] ¿Alguien puede ayudarme a encontrar una manera de repetir el último comando de ejecución independientemente de cómo / dónde se llame este comando dentro del script bash?

Mi respuesta

A pesar de las contribuciones muy apreciadas de mis compañeros SO'ers, opté por escribir una runfunción, que ejecuta todos sus parámetros como un solo comando y muestra el comando y su código de error cuando falla, con los siguientes beneficios:
-Sólo necesito anteponer los comandos que quiero verificar con lo runque los mantiene en una línea y no afecta la concisión de mi script
-Siempre que el script falla en uno de estos comandos, la última línea de salida de mi script es un mensaje que muestra claramente qué comando falla junto con su código de salida, lo que facilita la depuración

Script de ejemplo:

#!/bin/bash
die() { echo >&2 -e "\nERROR: $@\n"; exit 1; }
run() { "$@"; code=$?; [ $code -ne 0 ] && die "command [$*] failed with error code $code"; }

case "1" in
  "1")
  run ls /opt
  run ls /wrong-dir
  ;;
esac

La salida:

$ ./test.sh
apacheds  google  iptables
ls: cannot access /wrong-dir: No such file or directory

ERROR: command [ls /wrong-dir] failed with error code 2

Probé varios comandos con múltiples argumentos, variables bash como argumentos, argumentos entre comillas ... y la runfunción no los rompió. El único problema que encontré hasta ahora es ejecutar un eco que se rompe, pero no planeo comprobar mis ecos de todos modos.

Max
fuente
+1, idea brillante! Nota sin embargo, que run()no funciona correctamente cuando se utilizan comillas, por ejemplo, esto no funciona: run ssh-keygen -t rsa -C [email protected] -f ./id_rsa -N "".
johndodo
@johndodo: podría arreglarse: simplemente cambie los "something"argumentos con '"something"'(o, más bien, "'something'"para permitir something(por ejemplo: variables) que se interpreten / evalúen en el primer nivel, si es necesario)
Olivier Dulac
2
Cambié lo erróneo run() { $*; … }a uno más casi correcto run() { "$@"; … }porque la respuesta errónea terminó produciendo cpsalidas de preguntas con un estado de error 64 , donde el problema era que $*rompían los argumentos del comando en los espacios en los nombres, pero "$@"no lo haría.
Jonathan Leffler
Pregunta relacionada en Unix StackExchange: unix.stackexchange.com/questions/21930/…
haridsv
last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')funcionó mejor, al menos para zsh y macOS 10.11
phil pirozhkov

Respuestas:

60

El historial de comandos es una función interactiva. Solo los comandos completos se ingresan en el historial. Por ejemplo, la caseconstrucción se ingresa como un todo, cuando el shell ha terminado de analizarlo. Ni buscar el historial con el historyincorporado (ni imprimirlo a través de la expansión de shell ( !:p)) hace lo que parece querer, que es imprimir invocaciones de comandos simples.

La DEBUGtrampa le permite ejecutar un comando justo antes de ejecutar cualquier comando simple. Una versión de cadena del comando para ejecutar (con palabras separadas por espacios) está disponible en la BASH_COMMANDvariable.

trap 'previous_command=$this_command; this_command=$BASH_COMMAND' DEBUG
…
echo "last command is $previous_command"

Tenga en cuenta que previous_commandcambiará cada vez que ejecute un comando, así que guárdelo en una variable para poder usarlo. Si también desea conocer el estado de retorno del comando anterior, guarde ambos en un solo comando.

cmd=$previous_command ret=$?
if [ $ret -ne 0 ]; then echo "$cmd failed with error code $ret"; fi

Además, si solo desea abortar en un comando fallido, use set -epara hacer que su script salga en el primer comando fallido. Puede mostrar el último comando de la EXITtrampa .

set -e
trap 'echo "exit $? due to $previous_command"' EXIT

Tenga en cuenta que si está tratando de rastrear su secuencia de comandos para ver qué está haciendo, olvide todo esto y use set -x.

Gilles 'SO- deja de ser malvado'
fuente
1
He probado su trampa DEBUG pero no puedo hacer que funcione, ¿puede proporcionar un ejemplo completo por favor? -xgenera todos los comandos, pero desafortunadamente solo estoy interesado en ver los comandos que fallan (lo que puedo lograr con mi comando si lo coloco dentro de una [ ! "$? == "0" ]declaración.
Max
@ user359650: arreglado. Debe haber guardado el comando anterior antes de que el comando actual lo sobrescriba. Para abortar su secuencia de comandos si un comando falla, use set -e(a menudo, pero no siempre, el comando producirá un mensaje de error suficientemente bueno para que no necesite proporcionar más contexto).
Gilles 'SO- deja de ser malvado'
gracias por tu aporte. Terminé escribiendo una función personalizada (vea mi publicación) ya que su solución era demasiado general.
Máximo
Truco asombroso. Definitivo +1. Tenía la parte set -e y la trampa ERR, me diste la parte DEBUG. ¡Muchas gracias!
Philippe A.
1
@ JamesThomasMoon1979 En general, eval echo "${BASH_COMMAND}"podría ejecutar código arbitrario en sustituciones de comandos. Es peligroso. Considere un comando como cd $(ls -td | head -n 1)- y ahora imagine la sustitución de comando llamada rmo algo así.
Gilles 'SO- deja de ser malvado'
170

Bash tiene funciones integradas para acceder al último comando ejecutado. Pero ese es el último comando completo (por ejemplo, el casecomando completo ), no comandos simples individuales como solicitó originalmente.

!:0 = el nombre del comando ejecutado.

!:1 = el primer parámetro del comando anterior

!:* = todos los parámetros del comando anterior

!:-1 = el parámetro final del comando anterior

!! = la línea de comando anterior

etc.

Entonces, la respuesta más simple a la pregunta es, de hecho:

echo !!

...alternativamente:

echo "Last command run was ["!:0"] with arguments ["!:*"]"

¡Inténtalo tú mismo!

echo this is a test
echo !!

En un script, la expansión del historial está desactivada de manera predeterminada, debe habilitarla con

set -o history -o histexpand
groovyspaceman
fuente
7
El caso de uso más útil que he visto es volver a ejecutar el último comando con acceso sudo , es decirsudo !!
Travesty3
1
Con set -o history -o histexpand; echo "!!"un script de bash todavía recibo el mensaje de error: !!: event not found(Es lo mismo sin las comillas).
Suzana
2
set -o history -o histexpanden guiones -> salvavidas! ¡Gracias!
Alberto Megía
¿Hay alguna forma de incluir este comportamiento en la cadena TIMEFORMAT utilizada por la función de tiempo? es decir, exportar TIMEFORMAT = "***!: 0 tomó% 0lR"; / usr / bin / time find -name "* .log" ... que no funciona porque!: 0 se evalúa en el momento de ejecutar la exportación :(
Martin
Necesito leer más sobre el uso de set -o history -o histexpand. ¡¡Mi uso en un archivo que llamo bashsigue imprimiendo !! en lugar del último comando de ejecución. ¿Dónde está documentado esto?
Muno
17

Después de leer la respuesta de Gilles , decidí ver si la $BASH_COMMANDvar también estaba disponible (y el valor deseado) en una EXITtrampa, ¡y lo está!

Entonces, el siguiente script bash funciona como se esperaba:

#!/bin/bash

exit_trap () {
  local lc="$BASH_COMMAND" rc=$?
  echo "Command [$lc] exited with code [$rc]"
}

trap exit_trap EXIT
set -e

echo "foo"
false 12345
echo "bar"

La salida es

foo
Command [false 12345] exited with code [1]

barnunca se imprime porque set -ehace que bash salga del script cuando falla un comando y el comando falso siempre falla (por definición). El 12345paso a falseestá ahí para mostrar que los argumentos del comando fallido también se capturan (el falsecomando ignora cualquier argumento que se le pase)

Hercynium
fuente
Ésta es absolutamente la mejor solución. Funciona como un encanto para mí con "set -euo pipefail"
Vukasin
8

Pude lograr esto usando set -xen el script principal (que hace que el script imprima cada comando que se ejecuta) y escribiendo un script contenedor que solo muestra la última línea de salida generada por set -x.

Este es el guión principal:

#!/bin/bash
set -x
echo some command here
echo last command

Y este es el script de envoltura:

#!/bin/sh
./test.sh 2>&1 | grep '^\+' | tail -n 1 | sed -e 's/^\+ //'

La ejecución del script de envoltura produce esto como salida:

echo last command
Mark Drago
fuente
3

history | tail -2 | head -1 | cut -c8-999

tail -2devuelve las dos últimas líneas de comando del historial head -1devuelve solo la primera línea cut -c8-999devuelve solo la línea de comando, eliminando PID y espacios.

Guma
fuente
1
¿Podría hacer una pequeña explicación de cuáles son los argumentos de los comandos? Ayudaría a entender lo que hiciste
Sigrist
Si bien esto puede responder a la pregunta, es mejor agregar una descripción sobre cómo esta respuesta puede ayudar a resolver el problema. Por favor, lea Cómo escribo una buena respuesta para saber más.
Roshana Pitigala
1

Existe una condición de carrera entre las variables de último comando ($ _) y último error ($?). Si intenta almacenar uno de ellos en una variable propia, ambos ya encontraron nuevos valores debido al comando set. En realidad, el último comando no tiene ningún valor en este caso.

Esto es lo que hice para almacenar (casi) ambas informaciones en variables propias, por lo que mi script bash puede determinar si hubo algún error Y establecer el título con el último comando de ejecución:

   # This construct is needed, because of a racecondition when trying to obtain
   # both of last command and error. With this the information of last error is
   # implied by the corresponding case while command is retrieved.

   if   [[ "${?}" == 0 && "${_}" != "" ]] ; then
    # Last command MUST be retrieved first.
      LASTCOMMAND="${_}" ;
      RETURNSTATUS='✓' ;
   elif [[ "${?}" == 0 && "${_}" == "" ]] ; then
      LASTCOMMAND='unknown' ;
      RETURNSTATUS='✓' ;
   elif [[ "${?}" != 0 && "${_}" != "" ]] ; then
    # Last command MUST be retrieved first.
      LASTCOMMAND="${_}" ;
      RETURNSTATUS='✗' ;
      # Fixme: "$?" not changing state until command executed.
   elif [[ "${?}" != 0 && "${_}" == "" ]] ; then
      LASTCOMMAND='unknown' ;
      RETURNSTATUS='✗' ;
      # Fixme: "$?" not changing state until command executed.
   fi

Este script retendrá la información, si ocurrió un error y obtendrá el último comando de ejecución. Debido a la condición de carrera, no puedo almacenar el valor real. Además, la mayoría de los comandos ni siquiera se preocupan por los números de error, solo devuelven algo diferente de '0'. Notarás que, si usas la extensión errono de bash.

Debería ser posible con algo así como un script "interno" para bash, como en la extensión de bash, pero no estoy familiarizado con algo así y tampoco sería compatible.

CORRECCIÓN

No pensé que fuera posible recuperar ambas variables al mismo tiempo. Aunque me gusta el estilo del código, asumí que se interpretaría como dos comandos. Esto estaba mal, por lo que mi respuesta se divide en:

   # Because of a racecondition, both MUST be retrieved at the same time.
   declare RETURNSTATUS="${?}" LASTCOMMAND="${_}" ;

   if [[ "${RETURNSTATUS}" == 0 ]] ; then
      declare RETURNSYMBOL='✓' ;
   else
      declare RETURNSYMBOL='✗' ;
   fi

Aunque es posible que mi publicación no obtenga una calificación positiva, finalmente resolví mi problema yo mismo. Y esto parece apropiado con respecto a la publicación inicial. :)

WGRM
fuente
1
Dios mío, solo tienes que recibirlos de una vez y esto PARECE SER posible: declara RETURNSTATUS = "$ {?}" LASTCOMMAND = "$ {_}";
WGRM
Funciona muy bien con una excepción. Si tengo un alias para más parámetros, solo muestra los parámetros. ¿Alguien alguna conclusión?
WGRM