Повторяете последнюю команду, запущенную в Bash?


84

Я пытаюсь повторить последнюю команду, запущенную внутри сценария bash. Я нашел способ сделать это с некоторыми, history,tail,head,sedкоторые отлично работают, когда команды представляют определенную строку в моем сценарии с точки зрения парсера. Однако при некоторых обстоятельствах я не получаю ожидаемого результата, например, когда команда вставлена ​​внутри caseоператора:

Сценарий:

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

Выход:

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] Может ли кто-нибудь помочь мне найти способ повторить последнюю команду запуска независимо от того, как / где эта команда вызывается в сценарии bash?

Мой ответ

Несмотря на очень ценный вклад от моих товарищей по SO, я решил написать runфункцию, которая запускает все свои параметры как одну команду и отображает команду и ее код ошибки в случае сбоя со следующими преимуществами:
-Мне нужно только добавьте команды, которые я хочу проверить, с помощью runкоторых они хранятся в одной строке и не влияют на краткость моего скрипта -
всякий раз, когда скрипт не выполняет одну из этих команд, последняя строка вывода моего скрипта представляет собой сообщение, которое четко показывает, какая команда дает сбой вместе с кодом выхода, что упрощает отладку

Пример сценария:

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

Выход:

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

Я тестировал различные команды с несколькими аргументами, переменными bash в качестве аргументов, аргументами в кавычках ... и runфункция их не сломала. Единственная проблема, которую я обнаружил до сих пор, - это запуск эха, которое прерывается, но я все равно не планирую проверять свои эхо.


+1, блестящая идея! Однако здесь следует отметить , что run()не работает должным образом, когда используются кавычки, например , это не удается: run ssh-keygen -t rsa -C info@example.org -f ./id_rsa -N "".
— johndodo 01

@johndodo: это можно исправить: просто измените "something"аргументы с помощью '"something"'(или, скорее, "'something'"чтобы разрешить something(например: переменные) интерпретировать / оценивать на первом уровне, если необходимо)
— Оливье Дюлак,

2
Я изменил ошибочный run() { $*; … }на более близкий к правильному, run() { "$@"; … }потому что ошибочный ответ привел к тому, что вопрос завершился cpсо статусом ошибки 64 , где проблема заключалась в том, $*что аргументы команды сломались в пробелах в именах, но "$@"не сделали этого.
— Джонатан Леффлер,

Связанный вопрос по Unix StackExchange: unix.stackexchange.com/questions/21930/…
— haridsv

last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')работал лучше, по крайней мере, для zsh и macOS 10.11
— Фил Пирожков

Ответы:


60

История команд - это интерактивная функция. В историю заносятся только полные команды. Например, caseконструкция вводится как единое целое, когда оболочка завершает ее анализ. Ни поиск истории с помощью historyвстроенной программы (ни ее печать с помощью расширения оболочки ( !:p)) не делает того, что вам кажется, а именно печати вызовов простых команд.

DEBUGЛовушка позволяет выполнить команду прямо перед любым простым выполнением команды. В BASH_COMMANDпеременной доступна строковая версия команды для выполнения (со словами, разделенными пробелами) .

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

Обратите внимание, что это previous_commandбудет меняться каждый раз, когда вы запускаете команду, поэтому сохраните ее в переменной, чтобы использовать. Если вы хотите узнать статус возврата предыдущей команды, сохраните их в одной команде.

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

Более того, если вы хотите прервать выполнение только невыполненных команд, используйте, set -eчтобы завершить выполнение сценария при первой неудачной команде. Вы можете отобразить последнюю команду из EXITловушки .

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

Обратите внимание: если вы пытаетесь отследить свой сценарий, чтобы увидеть, что он делает, забудьте обо всем этом и используйте set -x.


1
Я пробовал вашу ловушку DEBUG, но не могу заставить ее работать. Не могли бы вы предоставить полный пример? -xвыводит каждую команду, но, к сожалению, меня интересуют только те команды, которые терпят неудачу (чего я могу добиться с помощью своей команды, если помещу ее в [ ! "$? == "0" ]оператор).
— Макс,

@ user359650: Исправлено. Вам необходимо сохранить предыдущую команду, прежде чем она будет перезаписана текущей командой. Чтобы прервать выполнение сценария в случае сбоя команды, используйте set -e(часто, но не всегда, команда выдаст достаточно хорошее сообщение об ошибке, поэтому вам не нужно предоставлять дополнительный контекст).
— Жиль 'SO- перестань быть злом'

спасибо за ваш вклад. В итоге я написал настраиваемую функцию (см. Мой пост), поскольку ваше решение было слишком накладным.
— Макс

Удивительный трюк. Окончательный +1. У меня была часть set -e и ловушка ERR, вы дали мне часть DEBUG. Большое спасибо!
— Philippe A.

1
@ JamesThomasMoon1979 В общем, eval echo "${BASH_COMMAND}"может выполнять произвольный код в подстановках команд. Это опасно. Рассмотрим команду вроде cd $(ls -td | head -n 1)- а теперь представьте, что вызывается подстановка команды rmили что-то в этом роде.
— Жиль 'SO- перестань быть злом'

173

Bash имеет встроенные функции для доступа к последней выполненной команде. Но это последняя вся команда (например, вся caseкоманда), а не отдельные простые команды, как вы изначально запрашивали.

!:0 = имя выполненной команды.

!:1 = первый параметр предыдущей команды

!:* = все параметры предыдущей команды

!:-1 = последний параметр предыдущей команды

!! = предыдущая командная строка

и т.п.

Итак, самый простой ответ на вопрос:

echo !!

... альтернативно:

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

Попробуй сам!

echo this is a test
echo !!

В скрипте раскрытие истории отключено по умолчанию, вам нужно включить его с помощью

set -o history -o histexpand

8
Самый полезный вариант использования, который я видел, - это повторный запуск последней команды с доступом sudo , то естьsudo !!
— Travesty3

1
В set -o history -o histexpand; echo "!!"сценарии bash я все еще получаю сообщение об ошибке: !!: event not found( То же самое, без кавычек.)
— Сюзана

2
set -o history -o histexpandв скриптах -> спасатель! благодаря!
— Альберто Мегиа

Есть ли способ передать это поведение в строку TIMEFORMAT, используемую функцией времени? т.е. экспорт TIMEFORMAT = "***!: 0 занял% 0lR"; / usr / bin / time find -name "* .log" ... который не работает, потому что!: 0 вычисляется во время выполнения экспорта :(
— Мартин,

Мне нужно узнать больше об использовании set -o history -o histexpand. Я использую его в файле, который я вызываю, bashпродолжает печатать !! вместо последней команды запуска. Где это задокументировано?
— Muno

17

После прочтения ответа от Жиля , я решил посмотреть , если $BASH_COMMANDпеременная была также доступна (и требуемое значение) в EXITловушку - и это!

Итак, следующий сценарий bash работает должным образом:

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

На выходе

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

barникогда не печатается, потому что set -eзаставляет bash выйти из сценария, когда команда терпит неудачу, а команда false всегда терпит неудачу (по определению). 12345Передаются falseтолько там , чтобы показать , что аргументы несостоявшихся команд захватываются , а также ( falseкоманда игнорирует все аргументы , переданные ему)


Это абсолютно лучшее решение. Работает как шарм для меня с «множества -euo pipefail»
— Vukašin

8

Я смог добиться этого, используя set -xв основном скрипте (который заставляет скрипт распечатывать каждую выполняемую команду) и писать скрипт-оболочку, который просто показывает последнюю строку вывода, сгенерированную set -x.

Это основной сценарий:

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

А это сценарий-обертка:

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

Запуск сценария-оболочки дает следующий результат:

echo last command

3

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

tail -2возвращает две последние командные строки из истории head -1возвращает только первую строку, cut -c8-999возвращает только командную строку, удаляя PID и пробелы.


1
Не могли бы вы немного пояснить, каковы аргументы команд? Это поможет понять, что вы сделали
— Sigrist

Хотя это может ответить на вопрос, лучше добавить некоторое описание того, как этот ответ может помочь решить проблему. Пожалуйста, прочтите Как мне написать хороший ответ, чтобы узнать больше.
— Рошана Питигала

1

Между переменной последней команды ($ _) и последней ошибки ($?) Существует условие гонки. Если вы попытаетесь сохранить одно из них в собственной переменной, оба уже встретили новые значения из-за команды set. Собственно, последняя команда в данном случае вообще не имеет значения.

Вот что я сделал, чтобы сохранить (почти) обе информации в собственных переменных, поэтому мой сценарий bash может определить, была ли ошибка, И установить заголовок с помощью последней команды запуска:

   # 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

Этот сценарий сохранит информацию, если произошла ошибка, и получит последнюю команду запуска. Из-за состояния гонки я не могу сохранить фактическое значение. Кроме того, большинству команд даже не нужны номера ошибок, они просто возвращают что-то отличное от «0». Вы заметите это, если используете расширение bash.

Это должно быть возможно с чем-то вроде "внутреннего" сценария для bash, например, в расширении bash, но я не знаком с чем-то подобным, и это также не будет совместимо.

ИСПРАВЛЕНИЕ

Я не думал, что можно получить обе переменные одновременно. Хотя мне нравится стиль кода, я предполагал, что он будет интерпретирован как две команды. Это было неправильно, поэтому мой ответ сводится к следующему:

   # 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

Хотя мой пост мог и не получить положительной оценки, я, наконец, решил свою проблему сам. И это кажется уместным в отношении исходного поста. :)


1
О боже, вы просто должны получить их сразу, и это, КАЖЕТСЯ, возможно: declare RETURNSTATUS = "$ {?}" LASTCOMMAND = "$ {_}";
— WGRM

Отлично работает за одним исключением. Если у меня есть псевдоним для дополнительных параметров, он просто отображает параметры. Кому-нибудь какие выводы?
— WGRM
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.