Echo des letzten Befehlslaufs in Bash?

83

Ich versuche, den letzten Befehl in einem Bash-Skript wiederzugeben. Ich habe einen Weg gefunden, dies mit einigen zu tun, history,tail,head,sedder gut funktioniert, wenn Befehle eine bestimmte Zeile in meinem Skript vom Parser-Standpunkt aus darstellen. Unter bestimmten Umständen erhalte ich jedoch nicht die erwartete Ausgabe, beispielsweise wenn der Befehl in eine caseAnweisung eingefügt wird :

Das Skript:

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

Die Ausgabe:

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

[F] Kann mir jemand helfen, einen Weg zu finden, den letzten Ausführungsbefehl wiederzugeben, unabhängig davon, wie / wo dieser Befehl im Bash-Skript aufgerufen wird?

Meine Antwort

Trotz der sehr geschätzten Beiträge meiner SO'-Kollegen habe ich mich dafür entschieden, eine runFunktion zu schreiben, die alle ihre Parameter als einen einzigen Befehl ausführt und den Befehl und seinen Fehlercode anzeigt, wenn er fehlschlägt - mit den folgenden Vorteilen:
-Ich muss nur Stellen Sie den Befehlen, mit rundenen ich prüfen möchte,
voran, wodurch sie in einer Zeile bleiben und die Prägnanz meines Skripts nicht beeinträchtigt wird. Wenn das Skript bei einem dieser Befehle fehlschlägt, ist die letzte Ausgabezeile meines Skripts eine Meldung, die deutlich anzeigt, welcher Befehl angezeigt wird schlägt zusammen mit dem Exit-Code fehl, was das Debuggen erleichtert

Beispielskript:

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

Die Ausgabe:

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

Ich habe verschiedene Befehle mit mehreren Argumenten getestet, Bash-Variablen als Argumente, Argumente in Anführungszeichen ... und die runFunktion hat sie nicht gebrochen. Das einzige Problem, das ich bisher gefunden habe, ist das Ausführen eines Echos, das unterbrochen wird, aber ich habe sowieso nicht vor, meine Echos zu überprüfen.

Max
quelle
+1, geniale Idee! Beachten Sie jedoch, dass run()dies nicht richtig funktioniert, wenn Anführungszeichen verwendet werden. Dies schlägt beispielsweise fehl : run ssh-keygen -t rsa -C [email protected] -f ./id_rsa -N "".
Johndodo
@ Johndodo: Es könnte behoben werden: Ändern Sie einfach die "something"Argumente mit '"something"'(oder besser gesagt, "'something'"um something(z. B. Variablen) auf der ersten Ebene zu interpretieren / zu bewerten, falls erforderlich)
Olivier Dulac
2
Ich habe das Fehlerhafte run() { $*; … }in ein nahezu korrektes geändert, run() { "$@"; … }da die fehlerhafte Antwort dazu führte, dass die Frage cpmit einem 64-Fehlerstatus beendet wurde. Das Problem bestand darin, dass $*die Befehlsargumente an den Leerzeichen in den Namen gebrochen wurden, dies aber "$@"nicht taten.
Jonathan Leffler
Verwandte Frage zu Unix StackExchange: unix.stackexchange.com/questions/21930/…
haridsv
last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')funktionierte besser, zumindest für zsh und macOS 10.11
phil pirozhkov

Antworten:

60

Der Befehlsverlauf ist eine interaktive Funktion. In den Verlauf werden nur vollständige Befehle eingetragen. Beispielsweise wird das caseKonstrukt als Ganzes eingegeben, wenn die Shell das Parsen beendet hat. Weder das Nachschlagen des Verlaufs mit dem historyintegrierten Element (noch das Drucken über die Shell-Erweiterung ( !:p)) führt zu dem, was Sie zu wollen scheinen, nämlich das Aufrufen von Aufrufen einfacher Befehle.

Mit dem DEBUGTrap können Sie einen Befehl unmittelbar vor einer einfachen Befehlsausführung ausführen. In der BASH_COMMANDVariablen ist eine Zeichenfolgenversion des auszuführenden Befehls (mit durch Leerzeichen getrennten Wörtern) verfügbar .

trap 'previous_command=$this_command; this_command=$BASH_COMMAND' DEBUG

echo "last command is $previous_command"

Beachten Sie, dass previous_commandsich dies jedes Mal ändert, wenn Sie einen Befehl ausführen. Speichern Sie ihn daher in einer Variablen, um ihn zu verwenden. Wenn Sie auch den Rückgabestatus des vorherigen Befehls wissen möchten, speichern Sie beide in einem einzigen Befehl.

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

Wenn Sie nur bei fehlgeschlagenen Befehlen abbrechen möchten, verwenden Sie diese Option set -e, um Ihr Skript beim ersten fehlgeschlagenen Befehl zu beenden. Sie können den letzten Befehl aus der EXITTrap anzeigen .

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

Beachten Sie, dass Sie all dies vergessen und verwenden müssen, wenn Sie versuchen, Ihr Skript zu verfolgen, um zu sehen, was es tut set -x.

Gilles 'SO - hör auf böse zu sein'
quelle
1
Ich habe Ihre DEBUG-Falle ausprobiert, aber ich kann sie nicht zum Laufen bringen. Können Sie bitte ein vollständiges Beispiel angeben? -xgibt jeden einzelnen Befehl aus, aber leider bin ich nur daran interessiert, die fehlgeschlagenen Befehle zu sehen (was ich mit meinem Befehl erreichen kann, wenn ich ihn in eine [ ! "$? == "0" ]Anweisung
Max
@ user359650: Behoben. Sie müssen den vorherigen Befehl gespeichert haben, bevor er vom aktuellen Befehl überschrieben wird. Um Ihr Skript abzubrechen, wenn ein Befehl fehlschlägt, verwenden Sie set -e(häufig, aber nicht immer, der Befehl erzeugt eine ausreichend gute Fehlermeldung, sodass Sie keinen weiteren Kontext angeben müssen).
Gilles 'SO - hör auf böse zu sein'
Vielen Dank für Ihren Beitrag. Am Ende habe ich eine benutzerdefinierte Funktion geschrieben (siehe meinen Beitrag), da Ihre Lösung zu viel Aufwand bedeutet.
Max
Erstaunlicher Trick. Definitiv +1. Ich hatte den Set-e-Teil und die ERR-Falle, du hast mir den DEBUG-Teil gegeben. Vielen Dank!
Philippe A.
1
@ JamesThomasMoon1979 Im Allgemeinen eval echo "${BASH_COMMAND}"kann beliebiger Code in Befehlsersetzungen ausgeführt werden. Es ist gefährlich. cd $(ls -td | head -n 1)Stellen Sie sich einen Befehl wie - vor und stellen Sie sich nun die Befehlsersetzung vor, die aufgerufen wird, rmoder so.
Gilles 'SO - hör auf böse zu sein'
167

Bash hat Funktionen für den Zugriff auf den zuletzt ausgeführten Befehl integriert. Aber das ist der letzte ganze Befehl (z. B. der ganze caseBefehl), nicht einzelne einfache Befehle, wie Sie sie ursprünglich angefordert haben.

!:0 = der Name des ausgeführten Befehls.

!:1 = der erste Parameter des vorherigen Befehls

!:* = alle Parameter des vorherigen Befehls

!:-1 = der letzte Parameter des vorherigen Befehls

!! = die vorherige Befehlszeile

etc.

Die einfachste Antwort auf die Frage lautet also:

echo !!

...Alternative:

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

Versuch es selber!

echo this is a test
echo !!

In einem Skript ist die Verlaufserweiterung standardmäßig deaktiviert. Sie müssen sie mit aktivieren

set -o history -o histexpand
groovyspaceman
quelle
7
Der nützlichste Anwendungsfall, den ich gesehen habe, ist das erneute Ausführen des letzten Befehls mit Sudo-Zugriff , dhsudo !!
Travesty3
1
Mit set -o history -o histexpand; echo "!!"in einem Bash-Skript erhalte ich immer noch die Fehlermeldung: !!: event not found(Es ist das gleiche ohne Anführungszeichen.)
Suzana
2
set -o history -o histexpandin Skripten -> Lebensretter! Vielen Dank!
Alberto Megía
Gibt es eine Möglichkeit, dieses Verhalten in die von der Zeitfunktion verwendete Zeichenfolge TIMEFORMAT zu übernehmen? dh export TIMEFORMAT = "*** !: 0 nahm% 0lR"; / usr / bin / time find -name "* .log" ... was nicht funktioniert, da die !: 0 zum Zeitpunkt des Exports ausgewertet wird :(
Martin
Ich muss mehr über die Verwendung von set -o history -o histexpand lesen. Meine Verwendung in einer Datei, die ich bashaufrufe, wird weiter gedruckt !! anstelle des letzten Ausführungsbefehls. Wo ist das dokumentiert?
Muno
17

Nachdem ich die Antwort von Gilles gelesen hatte , entschied ich mich zu prüfen, ob die $BASH_COMMANDVariable auch in einer EXITFalle verfügbar war (und der gewünschte Wert) - und das ist es!

Das folgende Bash-Skript funktioniert also wie erwartet:

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

Die Ausgabe ist

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

barwird nie gedruckt, da set -ebash das Skript beendet, wenn ein Befehl fehlschlägt und der falsche Befehl immer fehlschlägt (per Definition). Das 12345übergeben an falseist nur da, um zu zeigen, dass die Argumente für den fehlgeschlagenen Befehl ebenfalls erfasst werden (der falseBefehl ignoriert alle an ihn übergebenen Argumente).

Hercynium
quelle
Dies ist absolut die beste Lösung. Funktioniert wie ein Zauber für mich mit "set -euo pipefail"
Vukasin
8

Dies konnte ich erreichen, indem ich set -xim Hauptskript (das das Skript jeden ausgeführten Befehl ausdrucken lässt) und ein Wrapper-Skript schrieb, das nur die letzte von erzeugte Ausgabezeile anzeigt set -x.

Dies ist das Hauptskript:

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

Und das ist das Wrapper-Skript:

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

Das Ausführen des Wrapper-Skripts erzeugt dies als Ausgabe:

echo last command
Mark Drago
quelle
3

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

tail -2Gibt die letzten beiden Befehlszeilen aus dem Verlauf head -1zurück. Gibt nur die erste Zeile cut -c8-999zurück. Gibt nur die Befehlszeile zurück und entfernt PID und Leerzeichen.

Guma
quelle
1
Könnten Sie eine kleine Erklärung geben, was die Befehlsargumente sind? Es würde helfen zu verstehen, was Sie getan haben
Sigrist
Während dies die Frage beantworten kann, ist es besser, eine Beschreibung hinzuzufügen, wie diese Antwort zur Lösung des Problems beitragen kann. Bitte lesen Sie Wie schreibe ich eine gute Antwort , um mehr zu erfahren ?
Roshana Pitigala
1

Zwischen den Variablen des letzten Befehls ($ _) und des letzten Fehlers ($?) Gibt es eine Racebedingung. Wenn Sie versuchen, einen von ihnen in einer eigenen Variablen zu speichern, sind beide aufgrund des Befehls set bereits auf neue Werte gestoßen. Tatsächlich hat der letzte Befehl in diesem Fall überhaupt keinen Wert.

Folgendes habe ich getan, um (fast) beide Informationen in eigenen Variablen zu speichern, damit mein Bash-Skript feststellen kann, ob ein Fehler aufgetreten ist UND den Titel mit dem letzten Ausführungsbefehl festgelegt hat:

   # 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

Dieses Skript behält die Informationen bei, wenn ein Fehler aufgetreten ist, und ruft den letzten Ausführungsbefehl ab. Aufgrund der Racecondition kann ich den Istwert nicht speichern. Außerdem kümmern sich die meisten Befehle nicht einmal um Fehlernummern, sondern geben nur etwas anderes als '0' zurück. Sie werden das bemerken, wenn Sie die errono-Erweiterung von bash verwenden.

Es sollte mit so etwas wie einem "internen" Skript für Bash möglich sein, wie in der Bash-Erweiterung, aber ich kenne so etwas nicht und es wäre auch nicht kompatibel.

KORREKTUR

Ich dachte nicht, dass es möglich ist, beide Variablen gleichzeitig abzurufen. Obwohl mir der Stil des Codes gefällt, habe ich angenommen, dass er als zwei Befehle interpretiert wird. Das war falsch, daher lautet meine Antwort:

   # 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

Obwohl mein Beitrag möglicherweise keine positive Bewertung erhält, habe ich mein Problem endlich selbst gelöst. Und dies scheint in Bezug auf den ersten Beitrag angemessen zu sein. :) :)

WGRM
quelle
1
Oh je, Sie müssen sie nur sofort erhalten und dies scheint möglich zu sein: deklarieren Sie RETURNSTATUS = "$ {?}" LASTCOMMAND = "$ {_}";
WGRM
Funktioniert mit einer Ausnahme hervorragend. Wenn ich einen Alias ​​für weitere Parameter habe, werden nur die Parameter angezeigt. Hat jemand irgendwelche Schlussfolgerungen?
WGRM