Повторюється останній запуск команди в 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'ers, я зупинив свій вибір на написанні 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

@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(часто, але не завжди, команда видасть достатньо гарне повідомлення про помилку, тому вам не потрібно надавати подальший контекст).
Жиль 'ТАК - перестань бути злим'

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

Дивовижний фокус. Остаточний +1. У мене була частина -e та пастка ERR, ви дали мені частину DEBUG. Дуже дякую!
Філіп А.

1
@ JamesThomasMoon1979 Загалом eval echo "${BASH_COMMAND}"міг виконувати довільний код під час заміни команд. Це небезпечно. Розгляньте команду типу cd $(ls -td | head -n 1)- і тепер уявіть, як викликається підміна команди rmабо щось інше.
Жиль 'ТАК - перестань бути злим'

170

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

7
Найбільш корисний варіант використання, який я бачив, - це повторний запуск останньої команди з доступом 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 обчислюється під час запуску експорту :(
Мартін,

Мені потрібно прочитати більше про використання набору -o history -o histexpand. Моє використання його у файлі, до якого я телефоную, bashпродовжує друкувати !! замість останньої команди запуску. Де це задокументовано?
Муно,

17

Прочитавши відповідь від Жиля , я вирішив перевірити, чи $BASH_COMMANDдоступний var (і бажане значення) у файлі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 зі сценарію, коли команда виходить з ладу, а помилкова команда завжди виходить з ладу (за визначенням). 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
О, дорогий, ви просто повинні їх отримати одразу, і Здається, це можливо: оголосіть RETURNSTATUS = "$ {?}" LASTCOMMAND = "$ {_}";
WGRM

Чудово працює за одним винятком. Якщо у мене є псевдонім для подальших параметрів, він просто відображає параметри. У кого-небудь якісь висновки?
WGRM
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.