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一つのコマンドとして、すべてのパラメータを実行し、それが失敗した場合、コマンドとそのエラーコードを表示する- -機能を、次のような利点を:
-Iのみ必要をします確認したいコマンドを前に付けて、run1行に
まとめ、スクリプトの簡潔さに影響を与えない-これらのコマンドのいずれかでスクリプトが失敗すると、スクリプトの最後の出力行は、どのコマンドを明確に表示するメッセージになります。終了コードとともに失敗し、デバッグが容易になります

スクリプトの例:

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

@johndodo:固定することができる:ちょうど変化"something"との引数に'"something"'(むしろまたは、、 "'something'"、可能にするsomething(例:変数)が解釈されるように/必要に応じて、最初のレベルで評価)
オリヴィエデュラック

2
間違った答えが結果として64エラーステータスで質問が終了するため、エラーをrun() { $*; … }より正しいものに変更しました。問題は、名前のスペースでコマンド引数を壊したことでしたが、そうしませんでした。run() { "$@"; … }cp$*"$@"
ジョナサンレフラー2014

UnixのStackExchangeの関連質問:unix.stackexchange.com/questions/21930/...
haridsv

last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')少なくともzshと
macOS10.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コマンドを実行するたびに変更されることに注意してください。使用するには、コマンドを変数に保存してください。前のコマンドの戻りステータスも知りたい場合は、両方を1つのコマンドに保存してください。

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(常にではありませんが、多くの場合、コマンドは十分なエラーメッセージを生成するため、追加のコンテキストを提供する必要はありません)。
Gilles'SO-悪であることをやめなさい '

あなたの貢献に感謝します。あなたのソリューションはオーバーヘッドが大きすぎたので、私はカスタム関数(私の投稿を参照)を書くことになりました。
最大

素晴らしいトリック。決定的な+1。セット-eパートとERRトラップがありました。DEBUGパートをくれました。どうもありがとう!
フィリップA.

1
@ JamesThomasMoon1979一般にeval echo "${BASH_COMMAND}"、コマンド置換で任意のコードを実行できます。危ない。のようなコマンドを考えてみましょうcd $(ls -td | head -n 1)—そして今度はと呼ばれるコマンド置換rmか何かを想像してください。
Gilles'SO-悪であることをやめなさい '2014

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 2014年

1
set -o history -o histexpand; echo "!!"bashスクリプトでは、私はまだエラーメッセージが表示されます。!!: event not found(それは引用符と同じです。)
Suzana

2
set -o history -o histexpandスクリプトで->命の恩人!ありがとう!
アルベルトメギア2015

この動作をtime関数で使用されるTIMEFORMAT文字列に組み込む方法はありますか?つまり、export TIMEFORMAT = "*** !: 0は%0lR"を取りました。/ usr / bin / time find -name "* .log" ...これは、エクスポートの実行時に!:0が評価されるため、機能しません:(
Martin

set -o history -ohistexpandの使用についてもっと読む必要があります。私が呼び出すファイルでそれを使用すると、bash印刷され続けます!! 最後の実行コマンドの代わりに。これはどこに文書化されていますか?
ムノ2017年

17

Gillesからの回答を読んだ後、私はvarがトラップでも利用可能かどうか(および目的の値)を確認することにしました-そしてそれはそうです!$BASH_COMMANDEXIT

したがって、次の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コマンドは、それに渡された引数を無視します)


これは絶対に最善の解決策です。「pipefail -euoセット」と私のための魔法のように動作します
Vukasin

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履歴から最後の2つのコマンドラインを head -1返します。最初の行 cut -c8-999だけを返します。コマンドラインだけを返し、PIDとスペースを削除します。


1
コマンド引数とは何か、少し説明していただけますか?それはあなたが何をしたかを理解する助けになる
Sigrist

これは質問に答えるかもしれませんが、この答えが問題の解決にどのように役立つかについての説明を追加することをお勧めします。詳細を知るために良い答えを書く方法を読んでください。
RoshanaPitigala19年

1

最後のコマンド($ _)変数と最後のエラー($?)変数の間には競合状態があります。それらの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のerrono拡張を使用すると、気付くでしょう。

bash拡張のように、bashの「インターン」スクリプトのようなもので可能になるはずですが、私はそのようなものに精通しておらず、互換性もありません。

補正

両方の変数を同時に取得できるとは思いませんでした。コードのスタイルは気に入っていますが、2つのコマンドとして解釈されると思いました。これは間違っていたので、私の答えは次のようになります。

   # 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 2016

1つの例外を除いてうまく機能します。さらにパラメータのエイリアスがある場合は、パラメータが表示されるだけです。誰か結論はありますか?
WGRM 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.