Tại sao (lối ra 1) không thoát khỏi tập lệnh?


48

Tôi có một đoạn script, nó không thoát khi tôi muốn.

Một tập lệnh ví dụ có cùng lỗi là:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Tôi sẽ giả sử để xem đầu ra:

:~$ ./test.sh
1
:~$

Nhưng tôi thực sự thấy:

:~$ ./test.sh
1
2
:~$

Liệu ()lệnh chuỗi bằng cách nào đó tạo ra một phạm vi? Những gì đang exitthoát ra, nếu không phải là kịch bản?


5
Điều này đòi hỏi một câu trả lời duy nhất: subshell
Joshua

Câu trả lời:


87

()chạy các lệnh trong lớp con, do đó, exitbạn đang thoát khỏi lớp con và trở về vỏ cha. Sử dụng dấu ngoặc nhọn {}nếu bạn muốn chạy các lệnh trong trình bao hiện tại.

Từ hướng dẫn sử dụng bash:

(list) list được thực thi trong môi trường subshell. Các phép gán biến và các lệnh dựng sẵn ảnh hưởng đến môi trường của shell không còn hiệu lực sau khi lệnh hoàn thành. Trạng thái trả về là trạng thái thoát của danh sách.

{ danh sách; } danh sách được thực hiện đơn giản trong môi trường shell hiện tại. danh sách phải được chấm dứt bằng một dòng mới hoặc dấu chấm phẩy. Điều này được gọi là một lệnh nhóm. Trạng thái trả về là trạng thái thoát của danh sách. Lưu ý rằng không giống như các siêu ký tự (và), {và} là các từ dành riêng và phải xảy ra khi một từ dành riêng được phép được công nhận. Vì chúng không gây ra ngắt từ, chúng phải được tách ra khỏi danh sách bằng khoảng trắng hoặc metacharacter vỏ khác.

Điều đáng nói là cú pháp shell khá nhất quán và lớp con cũng tham gia vào các ()cấu trúc khác như thay thế lệnh (cũng với `..`cú pháp kiểu cũ ) hoặc thay thế quy trình, do đó, sau đây sẽ không thoát khỏi trình bao hiện tại:

echo $(exit)
cat <(exit)

Mặc dù có thể rõ ràng là các lớp con có liên quan khi các lệnh được đặt rõ ràng bên trong (), nhưng thực tế ít thấy là chúng cũng được sinh ra trong các cấu trúc khác này:

  • lệnh bắt đầu trong nền

    exit &

    không thoát khỏi shell hiện tại bởi vì (sau man bash)

    Nếu một lệnh bị chấm dứt bởi toán tử điều khiển &, shell sẽ thực thi lệnh trong nền trong một lớp con. Shell không chờ lệnh kết thúc và trạng thái trả về là 0.

  • Đường ống dẫn

    exit | echo foo

    vẫn chỉ thoát ra khỏi subshell.

    Tuy nhiên, vỏ khác nhau hành xử khác nhau về vấn đề này. Ví dụ: bashđặt tất cả các thành phần của đường ống vào các khung con riêng biệt (trừ khi bạn sử dụng lastpipetùy chọn trong các lệnh mà không bật điều khiển công việc), nhưng AT & T kshzshchạy phần cuối cùng bên trong lớp vỏ hiện tại (cả hai hành vi đều được POSIX cho phép). Do vậy

    exit | exit | exit

    về cơ bản không có gì trong bash, nhưng thoát khỏi zsh vì cuối cùng exit .

  • coproc exitcũng chạy exittrong một subshell.


5
Ah. Bây giờ để tìm tất cả các nơi, nơi người tiền nhiệm của tôi đã sử dụng sai niềng răng. Cảm ơn vì sự sáng suốt.
Minix

10
Hãy lưu ý cẩn thận trong những khoảng cách trong man page: {}không cú pháp, họ đều được bảo vệ từ và phải được bao quanh bởi không gian, và danh sách phải kết thúc với một terminator lệnh (dấu chấm phẩy, xuống dòng, và)
glenn Jackman

Không quan tâm, điều này có xu hướng thực sự là một quá trình khác hay chỉ là một môi trường riêng biệt trong một ngăn xếp nội bộ? Tôi sử dụng () rất nhiều để cô lập chdirs, và chắc chắn tôi đã rất may mắn khi sử dụng $$, v.v.
Dan Sheppard

5
@DanSheppard Đó là một quá trình khác, nhưng (echo $$)in id shell shell vì $$được mở rộng ngay cả trước khi subshell được tạo. Trong thực tế, quá trình in id của quá trình subshell có thể khó khăn, hãy xem stackoverflow.com/questions/9119885/
Đổi

@jimmij, Làm thế nào nó có thể $$được mở rộng trước khi lớp con được tạo và vẫn $BASHPIDhiển thị giá trị chính xác cho một lớp con?
tự đại diện

13

Thực hiện exittrong một subshell là một cạm bẫy:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

Kịch bản in 42, thoát khỏi lớp con với mã trả về 1và tiếp tục với tập lệnh. Ngay cả việc thay thế cuộc gọi bằng echo $(CALC) || exit 1cũng không giúp ích gì vì mã trả về echolà 0 bất kể mã trả về là gì calc. Và calcđược thực hiện trước echo.

Khó hiểu hơn nữa là cản trở hiệu ứng của exitnó bằng cách gói nó vào phần localdựng sẵn như trong đoạn script sau. Tôi đã vấp phải vấn đề khi tôi viết một hàm để xác minh giá trị đầu vào. Thí dụ:

Tôi muốn tạo một tệp có tên "năm tháng day.log", tức là 20141211.logcho ngày hôm nay. Ngày được nhập bởi người dùng có thể không cung cấp giá trị hợp lý. Do đó, trong chức năng của mình, fnametôi kiểm tra giá trị trả về của dateđể xác minh tính hợp lệ của đầu vào của người dùng:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Có vẻ tốt. Hãy để kịch bản được đặt tên s.sh. Nếu người dùng gọi tập lệnh với ./s.sh "Thu Dec 11 20:45:49 CET 2014", tập tin 20141211.logđược tạo. Tuy nhiên, nếu người dùng gõ ./s.sh "Thu hec 11 20:45:49 CET 2014", thì tập lệnh xuất ra:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

Dòng fname…nói rằng dữ liệu đầu vào xấu đã được phát hiện trong lớp con. Nhưng exit 1ở cuối local …dòng không bao giờ được kích hoạt bởi vì lệnh localluôn luôn quay trở lại 0. Điều này là do localđược thực thi sau $(fname) và do đó ghi đè mã trả về của nó. Và vì điều đó, kịch bản tiếp tục và gọi touchvới một tham số trống. Ví dụ này đơn giản nhưng hành vi của bash có thể khá khó hiểu trong một ứng dụng thực tế. Tôi biết, các lập trình viên thực sự không sử dụng người địa phương.☺

Để làm rõ: Không có local, tập lệnh sẽ hủy bỏ như mong đợi khi nhập ngày không hợp lệ.

Cách khắc phục là chia dòng như

local FNAME
FNAME=$(fname "$1") || exit 1

Hành vi lạ tuân theo tài liệu localtrong trang man của bash: "Trạng thái trả về là 0 trừ khi địa phương được sử dụng bên ngoài hàm, tên không hợp lệ được cung cấp hoặc tên là biến chỉ đọc."

Mặc dù không phải là một lỗi nhưng tôi cảm thấy rằng hành vi của bash là phản trực giác. Tuy nhiên, tôi nhận thức được trình tự thực hiện, localkhông nên che dấu một nhiệm vụ bị hỏng.

Câu trả lời ban đầu của tôi có một số điểm không chính xác. Sau một cuộc thảo luận sâu sắc và sâu sắc với mikeerv (cảm ơn bạn vì điều đó) tôi đã sửa chữa chúng.


@mikeerv: Tôi đã thêm một ví dụ để hiển thị mức độ liên quan.
hermannk

@mikeerv: Vâng, bạn nói đúng. Ngay cả terser. Nhưng cạm bẫy vẫn còn đó.
hermannk

@mikeerv: Xin lỗi, ví dụ của tôi đã bị hỏng. Tôi đã quên bài kiểm tra doit().
hermannk


2

Giải pháp thực tế:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

Việc nhóm lỗi sẽ chỉ thực hiện nếu blatrả về trạng thái lỗi và exitkhông ở trong một nhánh con để toàn bộ tập lệnh dừng lại.


1

Dấu ngoặc bắt đầu một lớp con và lối ra chỉ thoát khỏi lớp con đó.

Bạn có thể đọc $?mã thoát với và thêm mã này vào tập lệnh của mình để thoát tập lệnh nếu thoát khỏi khung con:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
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.