Phát ra lệnh cuối cùng chạy trong Bash?


84

Tôi đang cố gắng lặp lại lệnh chạy cuối cùng bên trong một tập lệnh bash. Tôi đã tìm thấy một cách để làm điều đó với một số history,tail,head,sedhoạt động tốt khi các lệnh đại diện cho một dòng cụ thể trong tập lệnh của tôi từ quan điểm phân tích cú pháp. Tuy nhiên trong một số trường hợp, tôi không nhận được kết quả mong đợi, ví dụ: khi lệnh được chèn vào bên trong một casecâu lệnh:

Kịch bản:

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

Đầu ra:

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] Ai đó có thể giúp tôi tìm cách lặp lại lệnh chạy cuối cùng bất kể lệnh này được gọi như thế nào / ở đâu trong tập lệnh bash?

Câu trả lời của tôi

Bất chấp những đóng góp được đánh giá cao từ những người bạn của tôi, tôi đã chọn viết một runhàm - chạy tất cả các tham số của nó dưới dạng một lệnh duy nhất và hiển thị lệnh và mã lỗi của nó khi nó không thành công - với những lợi ích sau:
-Tôi chỉ cần thêm trước các lệnh tôi muốn kiểm tra runđể giữ chúng trên một dòng và không ảnh hưởng đến tính ngắn gọn của tập lệnh của tôi -
Bất cứ khi nào tập lệnh bị lỗi ở một trong các lệnh này, dòng đầu ra cuối cùng của tập lệnh của tôi là một thông báo hiển thị rõ ràng lệnh nào không thành công cùng với mã thoát của nó, giúp gỡ lỗi dễ dàng hơn

Tập lệnh mẫu:

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

Đầu ra:

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

Tôi đã thử nghiệm các lệnh khác nhau với nhiều đối số, biến bash làm đối số, đối số được trích dẫn ... và runhàm không phá vỡ chúng. Vấn đề duy nhất tôi tìm thấy cho đến nay là chạy một tiếng vọng bị vỡ nhưng tôi không có kế hoạch kiểm tra tiếng vọng của mình.


+1, ý tưởng tuyệt vời! Lưu ý tuy nhiên đó run()không hoạt động đúng khi có dấu ngoặc kép được sử dụng, ví dụ này không thành công: run ssh-keygen -t rsa -C info@example.org -f ./id_rsa -N "".
johndodo

@johndodo: nó có thể được cố định: chỉ cần thay đổi "something"trong lập luận với '"something"'(hoặc, đúng hơn, "'something'"để cho phép something(ví dụ: các biến) để được giải thích / đánh giá ở cấp độ đầu tiên, nếu cần thiết)
Olivier Dulac

2
Tôi đã thay đổi câu sai run() { $*; … }thành một câu gần đúng hơn run() { "$@"; … }vì câu trả lời sai cuối cùng dẫn đến câu hỏi cpthoát ra với trạng thái lỗi 64 , trong đó vấn đề là đã $*phá vỡ các đối số lệnh tại các khoảng trắng trong tên, nhưng "$@"sẽ không làm như vậy.
Jonathan Leffler

Câu hỏi liên quan về Unix StackExchange: unix.stackexchange.com/questions/21930/…
haridsv

last=$(history | tail -n1 | sed 's/^[[:space:]][0-9]*[[:space:]]*//g')làm việc tốt hơn, ít nhất là cho zsh và MacOS 10.11
phil pirozhkov

Câu trả lời:


60

Lịch sử lệnh là một tính năng tương tác. Chỉ các lệnh hoàn chỉnh mới được nhập vào lịch sử. Ví dụ, casecấu trúc được nhập như một tổng thể, khi trình bao hoàn tất quá trình phân tích cú pháp của nó. Việc tra cứu lịch sử với historycài đặt sẵn (cũng như in nó thông qua mở rộng shell ( !:p)) sẽ không làm những gì bạn có vẻ muốn, đó là in các lệnh đơn giản.

Cái DEBUGbẫy cho phép bạn thực hiện một lệnh ngay trước khi thực hiện bất kỳ lệnh đơn giản nào. Một phiên bản chuỗi của lệnh để thực thi (với các từ được phân tách bằng dấu cách) có sẵn trong BASH_COMMANDbiến.

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

Lưu ý rằng điều đó previous_commandsẽ thay đổi mỗi khi bạn chạy một lệnh, vì vậy hãy lưu nó vào một biến để sử dụng nó. Nếu bạn cũng muốn biết trạng thái trả về của lệnh trước đó, hãy lưu cả hai trong một lệnh duy nhất.

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

Hơn nữa, nếu bạn chỉ muốn hủy bỏ một lệnh không thành công, hãy sử dụng set -eđể thoát tập lệnh của bạn ở lệnh không thành công đầu tiên. Bạn có thể hiển thị lệnh cuối cùng từ EXITbẫy .

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

Lưu ý rằng nếu bạn đang cố gắng theo dõi tập lệnh của mình để xem nó đang làm gì, hãy quên tất cả điều này đi và sử dụng set -x.


1
Tôi đã thử bẫy GỬI của bạn nhưng tôi không thể làm cho nó hoạt động, bạn có thể cung cấp ví dụ đầy đủ được không? -xxuất ra mọi lệnh nhưng tiếc là tôi chỉ quan tâm đến các lệnh bị lỗi (mà tôi có thể đạt được bằng lệnh của mình nếu tôi đặt nó bên trong một [ ! "$? == "0" ]câu lệnh.
Tối đa

@ user359650: Đã sửa. Bạn cần phải lưu lệnh trước đó trước khi nó bị ghi đè bởi lệnh hiện tại. Để hủy bỏ tập lệnh của bạn nếu một lệnh không thành công, hãy sử dụng set -e(thường xuyên, nhưng không phải lúc nào, lệnh sẽ tạo ra một thông báo lỗi đủ tốt để bạn không cần cung cấp thêm ngữ cảnh).
Gilles 'SO- đừng xấu xa nữa'

cảm ơn sự đóng góp của bạn. Tôi đã kết thúc việc viết một hàm tùy chỉnh (xem bài đăng của tôi) vì giải pháp của bạn có quá nhiều chi phí.
Tối đa

Bí quyết tuyệt vời. Dứt khoát +1. Tôi đã có phần cài đặt -e và bẫy ERR, bạn đã cho tôi phần GỬI. Cảm ơn rất nhiều!
Philippe A.

1
@ JamesThomasMoon1979 Nói chung eval echo "${BASH_COMMAND}"có thể thực thi mã tùy ý trong các lệnh thay thế. Nguy hiểm. Hãy xem xét một lệnh như cd $(ls -td | head -n 1)- và bây giờ hãy tưởng tượng lệnh thay thế được gọi là rmhoặc một cái gì đó.
Gilles 'SO- đừng ác nữa'

170

Bash đã tích hợp sẵn các tính năng để truy cập lệnh cuối cùng được thực thi. Nhưng đó là toàn bộ lệnh cuối cùng (ví dụ: toàn bộ caselệnh), không phải các lệnh đơn giản riêng lẻ như bạn yêu cầu ban đầu.

!:0 = tên của lệnh được thực thi.

!:1 = tham số đầu tiên của lệnh trước đó

!:* = tất cả các tham số của lệnh trước đó

!:-1 = tham số cuối cùng của lệnh trước

!! = dòng lệnh trước đó

Vân vân.

Vì vậy, câu trả lời đơn giản nhất cho câu hỏi trên thực tế là:

echo !!

... cách khác:

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

Hãy thử nó cho mình!

echo this is a test
echo !!

Trong một tập lệnh, tính năng mở rộng lịch sử bị tắt theo mặc định, bạn cần bật tính năng này với

set -o history -o histexpand

7
Các trường hợp sử dụng hữu ích nhất mà tôi đã nhìn thấy là dành cho tái chạy lệnh cuối cùng với truy cập sudo , tứcsudo !!
Travesty3

1
Với set -o history -o histexpand; echo "!!"một kịch bản bash tôi vẫn nhận được thông báo lỗi: !!: event not found(Đó là cùng không có dấu ngoặc kép.)
Suzana

2
set -o history -o histexpandtrong script -> phao cứu sinh! cảm ơn!
Alberto Megía

Có cách nào để đưa hành vi này vào chuỗi TIMEFORMAT được sử dụng bởi hàm thời gian không? tức là export TIMEFORMAT = "*** !: 0 đã% 0lR"; / usr / bin / time find -name "* .log" ... không hoạt động vì!: 0 được đánh giá tại thời điểm bạn chạy xuất :(
Martin

Tôi cần đọc thêm về việc sử dụng set -o history -o histexpand. Việc tôi sử dụng nó trong một tệp mà tôi kêu gọi bashvẫn tiếp tục in !! thay cho lệnh chạy cuối cùng. Tài liệu này ở đâu?
Muno

17

Sau khi đọc câu trả lời từ Gilles , tôi quyết định xem liệu $BASH_COMMANDvar cũng có sẵn (và giá trị mong muốn) trong một EXITcái bẫy hay không - và đúng như vậy!

Vì vậy, tập lệnh bash sau hoạt động như mong đợi:

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

Đầu ra là

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

barkhông bao giờ được in vì set -ekhiến bash thoát khỏi tập lệnh khi một lệnh bị lỗi và lệnh sai luôn không thành công (theo định nghĩa). Truyền 12345đến falsechỉ ở đó để cho thấy rằng các đối số của lệnh bị lỗi cũng được ghi lại ( falselệnh bỏ qua bất kỳ đối số nào được chuyển đến nó)


Đây hoàn toàn là giải pháp tốt nhất. Công trình như một nét duyên dáng cho tôi với "bộ pipefail -euo"
Vukašin

8

Tôi đã có thể đạt được điều này bằng cách sử dụng set -xtrong tập lệnh chính (làm cho tập lệnh in ra mọi lệnh được thực thi) và viết một tập lệnh trình bao bọc chỉ hiển thị dòng đầu ra cuối cùng được tạo bởi set -x.

Đây là kịch bản chính:

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

Và đây là tập lệnh trình bao bọc:

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

Chạy tập lệnh trình bao bọc sẽ tạo ra kết quả như sau:

echo last command

3

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

tail -2trả về hai dòng lệnh cuối cùng từ lịch sử head -1trả về dòng đầu tiên cut -c8-999chỉ trả về dòng lệnh, loại bỏ PID và dấu cách.


1
Bạn có thể giải thích một chút về các đối số lệnh là gì không? Nó sẽ giúp bạn hiểu những gì bạn đã làm
Sigrist

Mặc dù điều này có thể trả lời câu hỏi, nhưng tốt hơn là thêm một số mô tả về cách câu trả lời này có thể giúp giải quyết vấn đề. Vui lòng đọc Làm thế nào để tôi viết một câu trả lời tốt để biết thêm.
Roshana Pitigala

1

Có một điều kiện chạy đua giữa các biến lệnh cuối cùng ($ _) và lỗi cuối cùng ($?). Nếu bạn cố gắng lưu trữ một trong số chúng trong một biến riêng, cả hai đều gặp phải các giá trị mới do lệnh set. Trên thực tế, lệnh cuối cùng không có bất kỳ giá trị nào trong trường hợp này.

Đây là những gì tôi đã làm để lưu trữ (gần như) cả hai thông tin trong các biến riêng, vì vậy tập lệnh bash của tôi có thể xác định xem có bất kỳ lỗi nào không VÀ đặt tiêu đề bằng lệnh chạy cuối cùng:

   # 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

Tập lệnh này sẽ giữ lại thông tin, nếu có lỗi xảy ra và sẽ nhận được lệnh chạy cuối cùng. Vì điều kiện cuộc đua, tôi không thể lưu trữ giá trị thực tế. Bên cạnh đó, hầu hết các lệnh thực sự thậm chí không quan tâm đến số lỗi, chúng chỉ trả về một cái gì đó khác với '0'. Bạn sẽ nhận thấy rằng, nếu bạn sử dụng mở rộng thời gian của bash.

Nó có thể có một cái gì đó giống như một kịch bản "thực tập" cho bash, giống như trong bash mở rộng, nhưng tôi không quen thuộc với một cái gì đó như vậy và nó cũng sẽ không tương thích.

ĐIỀU CHỈNH

Tôi không nghĩ rằng có thể truy xuất cả hai biến cùng một lúc. Mặc dù tôi thích phong cách của mã, tôi cho rằng nó sẽ được hiểu là hai lệnh. Điều này đã sai, vì vậy câu trả lời của tôi được chia thành:

   # 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

Mặc dù bài đăng của tôi có thể không nhận được bất kỳ đánh giá tích cực nào, nhưng cuối cùng thì tôi đã tự giải quyết được vấn đề của mình. Và điều này có vẻ thích hợp liên quan đến bài viết phức tạp. :)


1
Ôi trời, bạn chỉ cần nhận được chúng ngay lập tức và CÓ THỂ LÀM được điều này: khai báo RETURNSTATUS = "$ {?}" LASTCOMMAND = "$ {_}";
WGRM

Hoạt động tuyệt vời với một ngoại lệ. Nếu tôi có bí danh cho các tham số khác, nó chỉ hiển thị các tham số. Có ai kết luận không?
WGRM
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.