Powtarzając ostatnie polecenie uruchomione w Bash?

84

Próbuję powtórzyć ostatnie polecenie uruchomione w skrypcie bash. Znalazłem sposób, aby to zrobić z niektórymi, history,tail,head,sedktóre działają dobrze, gdy polecenia reprezentują określoną linię w moim skrypcie z punktu widzenia parsera. Jednak w pewnych okolicznościach nie otrzymuję oczekiwanego wyniku, na przykład gdy polecenie jest wstawione do caseinstrukcji:

Scenariusz:

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

Wyjście:

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

[Q] Czy ktoś może mi pomóc znaleźć sposób na powtórzenie ostatniego polecenia uruchomienia, niezależnie od tego, jak / gdzie to polecenie jest wywoływane w skrypcie bash?

Moja odpowiedź

Pomimo bardzo cenionego wkładu ze strony moich kolegów SO'ów, zdecydowałem się napisać runfunkcję - która uruchamia wszystkie swoje parametry jako jedno polecenie i wyświetla polecenie oraz kod błędu, gdy się nie powiedzie - z następującymi korzyściami:
- Muszę tylko dołącz polecenia, które chcę sprawdzić, runco utrzymuje je w jednym wierszu i nie wpływa na zwięzłość mojego skryptu
-Gdy skrypt zawiedzie jedno z tych poleceń, ostatnim wierszem wyjściowym mojego skryptu jest komunikat, który wyraźnie wyświetla, które polecenie kończy się niepowodzeniem wraz z kodem zakończenia, co ułatwia debugowanie

Przykładowy skrypt:

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

Wyjście:

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

Przetestowałem różne polecenia z wieloma argumentami, zmiennymi bash jako argumentami, argumentami w cudzysłowach ... i runfunkcja ich nie złamała. Jedynym problemem, jaki do tej pory znalazłem, jest uruchomienie echa, które się psuje, ale i tak nie planuję sprawdzać swoich ech.

Maks
źródło
+1, genialny pomysł! Zauważ jednak, że run()nie działa prawidłowo, gdy używane są cytaty, na przykład ten nie powiedzie się: run ssh-keygen -t rsa -C [email protected] -f ./id_rsa -N "".
johndodo
@johndodo: można to naprawić: po prostu zmień "something"argumenty za pomocą '"something"'(lub raczej, "'something'"aby umożliwić something(np. zmienne) interpretację / ocenę na pierwszym poziomie, jeśli to konieczne)
Olivier Dulac
2
Zmieniłem błędne run() { $*; … }na bardziej prawie poprawne, run() { "$@"; … }ponieważ błędna odpowiedź zakończyła się cpwynikiem pytania o status błędu 64 , gdzie problem polegał na tym, że $*złamał argumenty poleceń w spacjach w nazwach, ale "$@"tego nie zrobił.
Jonathan Leffler,
Powiązane pytanie na temat Unix StackExchange: unix.stackexchange.com/questions/21930/ ...
haridsv
last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')działał lepiej, przynajmniej dla zsh i macOS 10.11
phil pirozhkov

Odpowiedzi:

60

Historia poleceń jest funkcją interaktywną. Do historii wprowadzane są tylko kompletne polecenia. Na przykład casekonstrukcja jest wprowadzana jako całość, gdy powłoka zakończy jej analizowanie. Ani przeglądanie historii za pomocą funkcji historywbudowanej (ani drukowanie jej przez rozwinięcie powłoki ( !:p)) nie robi tego, co wydaje się pożądane, czyli drukowania wywołań prostych poleceń.

DEBUGPułapka pozwala wykonywać prawo dowodzenia przed każdym prostym wykonaniem polecenia. W BASH_COMMANDzmiennej dostępna jest łańcuchowa wersja polecenia do wykonania (ze słowami oddzielonymi spacjami) .

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

Zauważ, że previous_commandzmieni się to za każdym razem, gdy uruchomisz polecenie, więc zapisz je w zmiennej, aby z niej skorzystać. Jeśli chcesz również poznać stan powrotu poprzedniego polecenia, zapisz oba w jednym poleceniu.

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

Ponadto, jeśli chcesz przerwać tylko przy nieudanym poleceniu, użyj, set -eaby zakończyć skrypt przy pierwszym nieudanym poleceniu. Możesz wyświetlić ostatnie polecenie z EXITpułapki .

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

Zauważ, że jeśli próbujesz prześledzić swój skrypt, aby zobaczyć, co robi, zapomnij o tym i użyj set -x.

Gilles 'SO- przestań być zły'
źródło
1
Wypróbowałem twoją pułapkę DEBUG, ale nie mogę jej sprawić, czy możesz podać pełny przykład? -xwyświetla każde pojedyncze polecenie, ale niestety jestem zainteresowany tylko wyświetlaniem poleceń, które zawodzą (co mogę osiągnąć za pomocą mojego polecenia, jeśli umieszczę je w [ ! "$? == "0" ]instrukcji.
Max
@ user359650: Naprawiono. Musisz zapisać poprzednie polecenie, zanim zostanie nadpisane przez bieżące polecenie. Aby przerwać skrypt, jeśli polecenie nie powiedzie się, użyj set -e(często, ale nie zawsze, polecenie wyświetli wystarczająco dobry komunikat o błędzie, więc nie musisz podawać dalszego kontekstu).
SO- Gilles 'SO- przestań być zły'
dziękuję za twój wkład. Skończyło się na napisaniu funkcji niestandardowej (patrz mój post), ponieważ Twoje rozwiązanie było zbyt dużym obciążeniem.
Maksymalnie
Niesamowita sztuczka. Ostateczne +1. Miałem set -e część i ERR trap, dałeś mi część DEBUG. Wielkie dzięki!
Philippe A.
1
@ JamesThomasMoon1979 Ogólnie eval echo "${BASH_COMMAND}"może wykonać dowolny kod podczas podstawiania poleceń. To jest niebezpieczne. Rozważ takie polecenie cd $(ls -td | head -n 1)- a teraz wyobraź sobie, że zostało wywołane podstawienie polecenia, rmczy coś w tym stylu.
SO- Gilles 'SO- przestań być zły'
170

Bash ma wbudowane funkcje dostępu do ostatniego wykonanego polecenia. Ale to jest ostatnia cała komenda (np. Cała casekomenda), a nie pojedyncze proste komendy, jak pierwotnie żądałeś.

!:0 = nazwa wykonanego polecenia.

!:1 = pierwszy parametr poprzedniego polecenia

!:* = wszystkie parametry poprzedniego polecenia

!:-1 = ostatni parametr poprzedniego polecenia

!! = poprzednia linia poleceń

itp.

Tak więc najprostsza odpowiedź na to pytanie brzmi tak:

echo !!

...alternatywnie:

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

Spróbuj sam!

echo this is a test
echo !!

W skrypcie rozwijanie historii jest domyślnie wyłączone, należy je włączyć za pomocą

set -o history -o histexpand
groovyspaceman
źródło
7
Najbardziej przydatnym przypadkiem użycia, jaki widziałem, jest ponowne uruchomienie ostatniego polecenia z dostępem do sudo , tj.sudo !!
Travesty3
1
W set -o history -o histexpand; echo "!!"skrypcie bash nadal otrzymuję komunikat o błędzie: !!: event not found(To to samo bez cudzysłowów.)
Suzana
2
set -o history -o histexpandw skryptach -> ratownik! dzięki!
Alberto Megía
Czy istnieje sposób, aby uzyskać to zachowanie w ciągu TIMEFORMAT używanym przez funkcję czasu? tj. eksport TIMEFORMAT = "***!: 0 zajęło% 0lR"; / usr / bin / time find -name "* .log" ... co nie działa, ponieważ!: 0 jest sprawdzane w momencie uruchamiania eksportu :(
Martin
Muszę przeczytać więcej na temat korzystania z set -o history -o histexpand. Moje użycie tego w pliku, do którego dzwonię, bashciągle się drukuje !! zamiast ostatniego polecenia uruchomienia. Gdzie to jest udokumentowane?
Muno
17

Po przeczytaniu odpowiedzi od Gillesa postanowiłem sprawdzić, czy $BASH_COMMANDvar jest również dostępny (i pożądana wartość) w EXITpułapce - i tak jest!

Tak więc następujący skrypt bash działa zgodnie z oczekiwaniami:

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

Wynik jest

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

barnigdy nie jest drukowane, ponieważ set -epowoduje, że bash kończy działanie skryptu, gdy polecenie się nie powiedzie, a polecenie false zawsze zawiedzie (z definicji). 12345Przekazane falsejest tylko tam, aby pokazać, że argumenty do nieudanego polecenia są ujęte jako dobrze (the falsekomenda ignoruje wszelkie argumenty przekazywane do niej)

Hercynium
źródło
To absolutnie najlepsze rozwiązanie. Działa jak urok dla mnie z „set -euo pipefail”
Vukasin
8

Udało mi się to osiągnąć, używając set -xw głównym skrypcie (który sprawia, że ​​skrypt wyświetla każde wykonywane polecenie) i pisząc skrypt opakowujący, który pokazuje tylko ostatnią linię danych wyjściowych wygenerowanych przez set -x.

Oto główny skrypt:

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

A to jest skrypt opakowania:

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

Uruchomienie skryptu opakowującego daje wynik:

echo last command
Mark Drago
źródło
3

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

tail -2zwraca ostatnie dwa wiersze poleceń z historii head -1zwraca tylko pierwszy wiersz cut -c8-999zwraca tylko wiersz poleceń, usuwając PID i spacje.

Guma
źródło
1
Czy mógłbyś trochę wyjaśnić, jakie są argumenty poleceń? Pomogłoby to zrozumieć, co zrobiłeś
Sigrist
Chociaż może to odpowiedzieć na pytanie, lepiej dodać opis, w jaki sposób ta odpowiedź może pomóc w rozwiązaniu problemu. Przeczytaj artykuł Jak napisać dobrą odpowiedź, aby dowiedzieć się więcej.
Roshana Pitigala
1

Między ostatnią komendą ($ _) a ostatnim błędem ($?) Występuje warunek wyścigu. Jeśli spróbujesz zapisać jedną z nich we własnej zmiennej, obie napotkały nowe wartości już z powodu polecenia set. Właściwie ostatnie polecenie w tym przypadku nie ma żadnej wartości.

Oto, co zrobiłem, aby zapisać (prawie) obie informacje we własnych zmiennych, aby mój skrypt bash mógł określić, czy wystąpił błąd ORAZ ustawienie tytułu za pomocą polecenia ostatniego uruchomienia:

   # 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

Ten skrypt zachowa informacje, jeśli wystąpi błąd i uzyska ostatnie polecenie uruchomienia. Ze względu na stan wyścigu nie mogę zapisać rzeczywistej wartości. Poza tym większość poleceń w rzeczywistości nawet nie dba o numery błędów, po prostu zwracają coś innego niż „0”. Zauważysz to, jeśli użyjesz rozszerzenia bash errono.

Powinno to być możliwe z czymś w rodzaju skryptu „stażysta” dla basha, na przykład w rozszerzeniu basha, ale nie znam czegoś takiego i nie byłoby to również kompatybilne.

KOREKTA

Nie sądziłem, że można było pobrać obie zmienne jednocześnie. Chociaż podoba mi się styl kodu, założyłem, że zostanie zinterpretowany jako dwa polecenia. To było złe, więc moja odpowiedź sprowadza się do:

   # 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

Chociaż mój post może nie otrzymać żadnej pozytywnej oceny, w końcu sam rozwiązałem problem. I wydaje się to właściwe w odniesieniu do pierwszego postu. :)

WGRM
źródło
1
Ojej, po prostu musisz je otrzymać od razu i WYDAJE SIĘ, że jest to możliwe: deklaruj RETURNSTATUS = "$ {?}" LASTCOMMAND = "$ {_}";
WGRM,
Działa świetnie z jednym wyjątkiem. Jeśli mam alias dla dalszych parametrów, wyświetla tylko parametry. Czy ktoś ma jakieś wnioski?
WGRM,