Facendo eco all'ultimo comando eseguito in Bash?


84

Sto cercando di ripetere l'ultimo comando eseguito all'interno di uno script bash. Ho trovato un modo per farlo con alcuni history,tail,head,sedche funzionano bene quando i comandi rappresentano una riga specifica nel mio script dal punto di vista del parser. Tuttavia in alcune circostanze non ottengo l'output atteso, ad esempio quando il comando viene inserito all'interno di caseun'istruzione:

Il copione:

#!/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

Il risultato:

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

[D] Qualcuno può aiutarmi a trovare un modo per eseguire l'eco dell'ultimo comando eseguito indipendentemente da come / dove viene chiamato questo comando all'interno dello script bash?

La mia risposta

Nonostante i contributi molto apprezzati dai miei colleghi SO, ho optato per la scrittura di una runfunzione - che esegue tutti i suoi parametri come un unico comando e visualizza il comando e il suo codice di errore quando fallisce - con i seguenti vantaggi:
-Devo solo anteponi i comandi che voglio controllare con runche li mantiene su una riga e non influisce sulla concisione del mio script
-Quando lo script fallisce su uno di questi comandi, l'ultima riga di output del mio script è un messaggio che mostra chiaramente quale comando fallisce insieme al suo codice di uscita, il che rende più facile il debug

Script di esempio:

#!/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

Il risultato:

$ ./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

Ho testato vari comandi con più argomenti, variabili bash come argomenti, argomenti tra virgolette ... e la runfunzione non li ha interrotti. L'unico problema che ho riscontrato finora è stato quello di eseguire un eco che si interrompe ma non ho comunque intenzione di controllare i miei echi.


+1, idea geniale! Si noti comunque che run()non funziona correttamente quando si utilizzano le virgolette, per esempio questo non riesce: run ssh-keygen -t rsa -C info@example.org -f ./id_rsa -N "".
johndodo

@johndodo: potrebbe essere risolto: basta cambiare "something"argomento con '"something"'(o, meglio, "'something'"per consentire something(es: variabili) di essere interpretato / valutato al primo livello, se necessario)
Olivier Dulac

2
Ho cambiato l'errore run() { $*; … }in uno più quasi corretto run() { "$@"; … }perché la risposta errata ha finito per restituire la domanda cpesce con uno stato di errore 64 , dove il problema era che ha $*interrotto gli argomenti del comando negli spazi nei nomi, ma "$@"non lo avrebbe fatto.
Jonathan Leffler,

Domanda correlata su Unix StackExchange: unix.stackexchange.com/questions/21930/…
haridsv

last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')ha funzionato meglio, almeno per zsh e macOS 10.11
phil pirozhkov

Risposte:


60

La cronologia dei comandi è una funzionalità interattiva. Nella cronologia vengono inseriti solo i comandi completi. Ad esempio, il casecostrutto viene immesso nel suo insieme, quando la shell ha terminato di analizzarlo. Né la ricerca della cronologia con il historybuilt-in (né la stampa tramite l'espansione della shell ( !:p)) fa quello che sembra desiderare, ovvero stampare invocazioni di semplici comandi.

La DEBUGtrap ti consente di eseguire un comando subito prima dell'esecuzione di qualsiasi comando semplice. Nella BASH_COMMANDvariabile è disponibile una versione in stringa del comando da eseguire (con parole separate da spazi) .

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

Nota che previous_commandcambierà ogni volta che esegui un comando, quindi salvalo in una variabile per usarlo. Se vuoi conoscere anche lo stato di ritorno del comando precedente, salva entrambi in un unico comando.

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

Inoltre, se si desidera interrompere solo un comando fallito, utilizzare set -eper far uscire lo script al primo comando fallito. È possibile visualizzare l'ultimo comando dal EXITtrap .

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

Nota che se stai cercando di tracciare il tuo script per vedere cosa sta facendo, dimentica tutto questo e usa set -x.


1
Ho provato la tua trappola DEBUG ma non riesco a farla funzionare, puoi fornire un esempio completo per favore? -xemette ogni singolo comando ma sfortunatamente mi interessa solo vedere i comandi che falliscono (cosa che posso ottenere con il mio comando se lo metto all'interno di [ ! "$? == "0" ]un'istruzione.
Max

@ user359650: risolto. È necessario aver salvato il comando precedente prima che venga sovrascritto dal comando corrente. Per interrompere lo script se un comando fallisce, usa set -e(spesso, ma non sempre, il comando produrrà un messaggio di errore sufficientemente buono in modo che tu non abbia bisogno di fornire ulteriore contesto).
Gilles "SO- smettila di essere malvagio"

grazie per il tuo contributo. Ho finito per scrivere una funzione personalizzata (vedi il mio post) poiché la tua soluzione era troppo sovraccarico.
Max

Trucco incredibile. +1 definitivo. Avevo la parte set -e e ERR trap, tu mi hai dato la parte DEBUG. Molte grazie!
Philippe A.

1
@ JamesThomasMoon1979 In generale eval echo "${BASH_COMMAND}"potrebbe eseguire codice arbitrario nelle sostituzioni di comandi. È pericoloso. Considera un comando come cd $(ls -td | head -n 1)- e ora immagina la sostituzione del comando chiamata rmo qualcosa del genere.
Gilles 'SO- smettila di essere malvagio'

173

Bash ha funzionalità integrate per accedere all'ultimo comando eseguito. Ma questo è l'ultimo comando completo (ad esempio l'intero casecomando), non singoli comandi semplici come quelli originariamente richiesti.

!:0 = il nome del comando eseguito.

!:1 = il primo parametro del comando precedente

!:* = tutti i parametri del comando precedente

!:-1 = il parametro finale del comando precedente

!! = la riga di comando precedente

eccetera.

Quindi, la risposta più semplice alla domanda è, infatti:

echo !!

... in alternativa:

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

Prova tu stesso!

echo this is a test
echo !!

In uno script, l'espansione della cronologia è disattivata per impostazione predefinita, è necessario abilitarla con

set -o history -o histexpand

8
Il caso d'uso più utile che ho visto è quello di rieseguire l'ultimo comando con accesso sudo , ovverosudo !!
Travesty3

1
Con set -o history -o histexpand; echo "!!"uno script bash ricevo ancora il messaggio di errore: !!: event not found(È lo stesso senza virgolette.)
Suzana

2
set -o history -o histexpandnegli script -> salvavita! Grazie!
Alberto Megía

C'è un modo per ottenere questo comportamento nella stringa TIMEFORMAT utilizzata dalla funzione time? cioè export TIMEFORMAT = "***!: 0 ha preso% 0lR"; / usr / bin / time find -name "* .log" ... che non funziona perché!: 0 viene valutato al momento dell'esportazione :(
Martin

Ho bisogno di leggere di più sull'uso di set -o history -o histexpand. Il mio utilizzo in un file che richiamo bashcontinua a stampare !! invece dell'ultimo comando di esecuzione. Dove è documentato?
Muno

17

Dopo aver letto la risposta di Gilles , ho deciso di vedere se la $BASH_COMMANDvar era disponibile (e il valore desiderato) anche in una EXITtrappola - e lo è!

Quindi, il seguente script bash funziona come previsto:

#!/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"

L'output è

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

barnon viene mai stampato perché set -efa sì che bash esca dallo script quando un comando fallisce e il comando falso fallisce sempre (per definizione). Il 12345passato a falseè lì solo per mostrare che vengono catturati anche gli argomenti del comando fallito (il falsecomando ignora qualsiasi argomento passato ad esso)


Questa è in assoluto la soluzione migliore. Funziona come un incantesimo per me con "set -euo pipefail"
Vukasin

8

Sono stato in grado di ottenere ciò utilizzando set -xnello script principale (che fa stampare allo script ogni comando eseguito) e scrivendo uno script wrapper che mostra solo l'ultima riga di output generata da set -x.

Questo è lo script principale:

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

E questo è lo script wrapper:

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

L'esecuzione dello script wrapper produce questo come output:

echo last command

3

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

tail -2restituisce le ultime due righe di comando dalla cronologia head -1restituisce solo la prima riga cut -c8-999restituisce solo la riga di comando, rimuovendo PID e spazi.


1
Potresti fare una piccola spiegazione su quali sono gli argomenti dei comandi? Aiuterebbe a capire cosa hai fatto
Sigrist

Sebbene questo possa rispondere alla domanda, è meglio aggiungere qualche descrizione su come questa risposta può aiutare a risolvere il problema. Si prega di leggere Come scrivo una buona risposta per saperne di più.
Roshana Pitigala

1

Esiste una condizione di gara tra le variabili dell'ultimo comando ($ _) e dell'ultimo errore ($?). Se provi a memorizzarne uno in una propria variabile, entrambi hanno riscontrato nuovi valori già a causa del comando set. In realtà, l'ultimo comando non ha alcun valore in questo caso.

Ecco cosa ho fatto per memorizzare (quasi) entrambe le informazioni nelle proprie variabili, in modo che il mio script bash possa determinare se si è verificato un errore E impostare il titolo con l'ultimo comando eseguito:

   # 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

Questo script manterrà le informazioni, se si è verificato un errore, e otterrà l'ultimo comando di esecuzione. A causa delle condizioni di gara non posso memorizzare il valore effettivo. Inoltre, la maggior parte dei comandi in realtà non si preoccupa nemmeno dei numeri di errore, restituiscono solo qualcosa di diverso da "0". Lo noterai, se usi l'estensione errono di bash.

Dovrebbe essere possibile con qualcosa come uno script "intern" per bash, come nell'estensione bash, ma non ho familiarità con qualcosa del genere e non sarebbe compatibile.

CORREZIONE

Non pensavo che fosse possibile recuperare entrambe le variabili contemporaneamente. Sebbene mi piaccia lo stile del codice, ho pensato che sarebbe stato interpretato come due comandi. Questo era sbagliato, quindi la mia risposta si divide in:

   # 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

Sebbene il mio post potrebbe non ottenere alcuna valutazione positiva, ho risolto il mio problema da solo, finalmente. E questo sembra appropriato per quanto riguarda il post iniziale. :)


1
Oh caro, devi solo riceverli subito e SEMBRA ESSERE possibile: declare RETURNSTATUS = "$ {?}" LASTCOMMAND = "$ {_}";
WGRM

Funziona alla grande con un'eccezione. Se ho un alias per ulteriori parametri, visualizza solo i parametri. Qualcuno ha delle conclusioni?
WGRM
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.