Próbuję powtórzyć ostatnie polecenie uruchomione w skrypcie bash. Znalazłem sposób, aby to zrobić z niektórymi, history,tail,head,sed
któ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 case
instrukcji:
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ć run
funkcję - 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ć, run
co 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 run
funkcja 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.
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 ""
."something"
argumenty za pomocą'"something"'
(lub raczej,"'something'"
aby umożliwićsomething
(np. zmienne) interpretację / ocenę na pierwszym poziomie, jeśli to konieczne)run() { $*; … }
na bardziej prawie poprawne,run() { "$@"; … }
ponieważ błędna odpowiedź zakończyła sięcp
wynikiem pytania o status błędu 64 , gdzie problem polegał na tym, że$*
złamał argumenty poleceń w spacjach w nazwach, ale"$@"
tego nie zrobił.last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')
działał lepiej, przynajmniej dla zsh i macOS 10.11Odpowiedzi:
Historia poleceń jest funkcją interaktywną. Do historii wprowadzane są tylko kompletne polecenia. Na przykład
case
konstrukcja jest wprowadzana jako całość, gdy powłoka zakończy jej analizowanie. Ani przeglądanie historii za pomocą funkcjihistory
wbudowanej (ani drukowanie jej przez rozwinięcie powłoki (!:p
)) nie robi tego, co wydaje się pożądane, czyli drukowania wywołań prostych poleceń.DEBUG
Pułapka pozwala wykonywać prawo dowodzenia przed każdym prostym wykonaniem polecenia. WBASH_COMMAND
zmiennej 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_command
zmieni 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 -e
aby zakończyć skrypt przy pierwszym nieudanym poleceniu. Możesz wyświetlić ostatnie polecenie zEXIT
puł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
.źródło
-x
wyś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.set -e
(często, ale nie zawsze, polecenie wyświetli wystarczająco dobry komunikat o błędzie, więc nie musisz podawać dalszego kontekstu).eval echo "${BASH_COMMAND}"
może wykonać dowolny kod podczas podstawiania poleceń. To jest niebezpieczne. Rozważ takie poleceniecd $(ls -td | head -n 1)
- a teraz wyobraź sobie, że zostało wywołane podstawienie polecenia,rm
czy coś w tym stylu.Bash ma wbudowane funkcje dostępu do ostatniego wykonanego polecenia. Ale to jest ostatnia cała komenda (np. Cała
case
komenda), 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
źródło
sudo !!
set -o history -o histexpand; echo "!!"
skrypcie bash nadal otrzymuję komunikat o błędzie:!!: event not found
(To to samo bez cudzysłowów.)set -o history -o histexpand
w skryptach -> ratownik! dzięki!bash
ciągle się drukuje !! zamiast ostatniego polecenia uruchomienia. Gdzie to jest udokumentowane?Po przeczytaniu odpowiedzi od Gillesa postanowiłem sprawdzić, czy
$BASH_COMMAND
var jest również dostępny (i pożądana wartość) wEXIT
puł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]
bar
nigdy nie jest drukowane, ponieważset -e
powoduje, że bash kończy działanie skryptu, gdy polecenie się nie powiedzie, a polecenie false zawsze zawiedzie (z definicji).12345
Przekazanefalse
jest tylko tam, aby pokazać, że argumenty do nieudanego polecenia są ujęte jako dobrze (thefalse
komenda ignoruje wszelkie argumenty przekazywane do niej)źródło
Udało mi się to osiągnąć, używając
set -x
w 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 przezset -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
źródło
history | tail -2 | head -1 | cut -c8-999
tail -2
zwraca ostatnie dwa wiersze poleceń z historiihead -1
zwraca tylko pierwszy wierszcut -c8-999
zwraca tylko wiersz poleceń, usuwając PID i spacje.źródło
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. :)
źródło