Повторяете последнюю команду, запущенную в 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
Groovyspaceman
источник
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

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

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