ทำไม (ทางออก 1) ไม่ออกจากสคริปต์


47

ฉันมีสคริปต์ที่ไม่ได้ออกเมื่อฉันต้องการมัน

ตัวอย่างสคริปต์ที่มีข้อผิดพลาดเดียวกันคือ:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

ฉันคิดว่าจะเห็นผลลัพธ์:

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

แต่ฉันเห็นจริง:

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

ที่ไม่()สั่งการผูกมัดใดสร้างขอบเขตหรือไม่? เกิดอะไรขึ้นexitถ้าไม่ใช่สคริปต์?


5
สิ่งนี้เรียกร้องคำตอบเดียว: subshell
Joshua

เกี่ยวข้อง: unix.stackexchange.com/q/247187/135943
Wildcard

คำตอบ:


87

()เรียกใช้คำสั่งใน subshell ดังนั้นโดยexitคุณออกจาก subshell และกลับไปที่ parent shell ใช้เครื่องหมายปีกกา{}หากคุณต้องการเรียกใช้คำสั่งในเชลล์ปัจจุบัน

จากทุบตีคู่มือ:

(รายการ)รายการถูกดำเนินการในสภาพแวดล้อม subshell การกำหนดตัวแปรและคำสั่ง builtin ที่ส่งผลกระทบต่อสภาพแวดล้อมของเชลล์จะไม่มีผลบังคับใช้หลังจากคำสั่งเสร็จสิ้น สถานะการส่งคืนคือสถานะการออกของรายการ

{รายการ; } list ถูกเรียกใช้งานโดยง่ายในสภาวะแวดล้อมเชลล์ปัจจุบัน รายการจะต้องถูกยกเลิกด้วยการขึ้นบรรทัดใหม่หรืออัฒภาค สิ่งนี้เรียกว่าคำสั่งกลุ่ม สถานะการส่งคืนคือสถานะการออกของรายการ โปรดทราบว่าแตกต่างจาก metacharacters (และ), {และ} เป็นคำสงวนและต้องเกิดขึ้นเมื่อคำที่สงวนไว้ได้รับอนุญาตให้จดจำ เนื่องจากไม่ทำให้เกิดการแบ่งคำจึงต้องแยกออกจากรายการด้วยช่องว่างหรือเชลล์ตัวอักขระอื่น

เป็นมูลค่าการกล่าวขวัญว่าไวยากรณ์เปลือกค่อนข้างสอดคล้องและ subshell มีส่วนร่วมในการ()สร้างอื่น ๆเช่นการทดแทนคำสั่ง (เช่นเดียวกับ`..`ไวยากรณ์แบบเก่า) หรือทดแทนกระบวนการดังนั้นต่อไปนี้จะไม่ออกจากเปลือกปัจจุบันทั้ง:

echo $(exit)
cat <(exit)

ในขณะที่มันอาจจะเห็นได้ชัดว่า subshells มีส่วนร่วมเมื่อคำสั่งถูกวางไว้อย่างชัดเจนภายใน()ความจริงที่มองเห็นได้น้อยกว่าก็คือพวกเขาจะเกิดในโครงสร้างอื่น ๆ เหล่านี้:

  • คำสั่งเริ่มต้นในพื้นหลัง

    exit &

    ไม่ออกจากเชลล์ปัจจุบันเพราะ (หลังman bash)

    หากคำสั่งถูกยกเลิกโดยผู้ควบคุมเครื่อง & เปลือกจะดำเนินการคำสั่งในพื้นหลังเป็นเชลล์ย่อย เชลล์ไม่รอให้คำสั่งเสร็จสิ้นและสถานะการส่งคืนคือ 0

  • ท่อ

    exit | echo foo

    ยังคงออกจาก subshell เท่านั้น

    อย่างไรก็ตามกระสุนที่แตกต่างกันมีพฤติกรรมแตกต่างกัน ตัวอย่างเช่นbashทำให้ส่วนประกอบทั้งหมดของไปป์ไลน์ลงใน subshells แยกกัน (เว้นแต่คุณจะใช้lastpipeตัวเลือกในการเรียกใช้ที่ไม่ได้เปิดใช้งานการควบคุมงาน) แต่ AT&T kshและzshเรียกใช้ส่วนสุดท้ายภายในเชลล์ปัจจุบัน (POSIX ทั้งสองพฤติกรรม) ดังนั้น

    exit | exit | exit

    ไม่โดยทั่วไปไม่มีอะไรในทุบตี แต่ออกจาก zsh เพราะสุดท้าย exit

  • coproc exitยังทำงานexitใน subshell


5
อา ตอนนี้เพื่อหาสถานที่ทั้งหมดที่บรรพบุรุษของฉันใช้เครื่องมือจัดฟันผิด ขอบคุณสำหรับความเข้าใจ
Minix

10
โปรดระวังการเว้นวรรคในหน้า man: {และ}ไม่ใช่ไวยากรณ์คำเหล่านี้สงวนไว้และต้องล้อมรอบด้วยช่องว่างและรายการจะต้องลงท้ายด้วยตัวยกเลิกคำสั่ง (semicolon, newline, ampersand)
glenn jackman

ไม่น่าสนใจสิ่งนี้มีแนวโน้มที่จะเป็นกระบวนการจริงหรือเป็นเพียงแค่สภาพแวดล้อมที่แยกจากกันในกองซ้อนภายในหรือไม่? ฉันใช้ () มากสำหรับการแยก chdirs และต้องโชคดีกับการใช้ $$ เป็นต้นหากครั้งก่อน
Dan Sheppard

5
@DanSheppard มันเป็นกระบวนการอื่น แต่(echo $$)พิมพ์ parent shell id เพราะ$$ถูกขยายแม้กระทั่งก่อนที่จะสร้างเชลล์ย่อย ในความเป็นจริงการพิมพ์รหัสกระบวนการ subshell อาจเป็นเรื่องยุ่งยากดูstackoverflow.com/questions/9119885/ …
jimmij

@jimmij จะ$$ขยายได้อย่างไรก่อนที่จะสร้าง subshell และยัง$BASHPIDแสดงค่าที่ถูกต้องสำหรับ subshell
สัญลักษณ์ตัวแทน

13

การดำเนินการexitใน subshell เป็นหนึ่งหลุมพราง:

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

สคริปต์พิมพ์ 42 ออกจากsubshellด้วยโค้ดส่งคืน1และดำเนินการต่อกับสคริปต์ แม้เปลี่ยนการโทรโดยecho $(CALC) || exit 1ไม่ได้ช่วยเพราะการกลับมาของรหัสechoเป็น 0 calcโดยไม่คำนึงถึงรหัสการกลับมาของ และจะดำเนินการไปก่อนcalcecho

ยิ่งทำให้งงงวยก็คือการขัดขวางผลกระทบของการexitห่อไว้ในlocalbuiltin เช่นในสคริปต์ต่อไปนี้ ฉันสะดุดปัญหาเมื่อฉันเขียนฟังก์ชั่นเพื่อตรวจสอบค่าอินพุต ตัวอย่าง:

ฉันต้องการสร้างไฟล์ชื่อ "year month day.log" เช่น20141211.logสำหรับวันนี้ วันที่จะถูกป้อนโดยผู้ใช้ที่อาจไม่สามารถให้ค่าที่สมเหตุสมผล ดังนั้นในฟังก์ชั่นของfnameฉันฉันจะตรวจสอบค่าตอบแทนของdateเพื่อตรวจสอบความถูกต้องของอินพุตของผู้ใช้:

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

ดูดี. s.shขอสคริปต์ที่จะตั้งชื่อ หากผู้ใช้เรียกสคริปต์ด้วย./s.sh "Thu Dec 11 20:45:49 CET 2014"ไฟล์20141211.logจะถูกสร้างขึ้น อย่างไรก็ตามหากผู้ใช้พิมพ์./s.sh "Thu hec 11 20:45:49 CET 2014"ดังนั้นสคริปต์จะแสดงผล:

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

บรรทัดfname…บอกว่ามีการตรวจพบข้อมูลอินพุตที่ไม่ถูกต้องในเชลล์ย่อย แต่exit 1ในตอนท้ายของlocal …เส้นจะไม่เรียกเพราะสั่งเสมอกลับlocal 0นี่เป็นเพราะlocalมีการดำเนินการหลังจากนั้น $(fname)จึงเขียนทับโค้ดส่งคืน และด้วยเหตุนี้สคริปต์จึงดำเนินการต่อและเรียกใช้touchด้วยพารามิเตอร์ว่าง ตัวอย่างนี้ง่าย แต่พฤติกรรมของการทุบตีอาจทำให้เกิดความสับสนในการใช้งานจริง ฉันรู้ว่าโปรแกรมเมอร์จริงไม่ใช้คนในท้องถิ่น

เพื่อให้ชัดเจน: หากไม่มีlocalสคริปต์จะยกเลิกตามที่คาดไว้เมื่อป้อนวันที่ไม่ถูกต้อง

การแก้ไขคือการแบ่งบรรทัดเช่น

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

พฤติกรรมแปลก ๆ นั้นสอดคล้องกับเอกสารlocalภายในของ man page ของ bash: "สถานะการส่งคืนคือ 0 เว้นแต่ว่า local จะใช้งานนอกฟังก์ชั่นชื่อที่ไม่ถูกต้องจะได้รับหรือชื่อเป็นตัวแปรแบบอ่านอย่างเดียว"

แม้ว่าจะไม่ใช่ข้อผิดพลาด แต่ฉันก็รู้สึกว่าพฤติกรรมของการทุบตีนั้นตรงกันข้าม ฉันตระหนักถึงลำดับของการดำเนินการlocalไม่ควรปิดบังการมอบหมายที่ไม่สมบูรณ์อย่างไรก็ตาม

คำตอบแรกของฉันมีบางอย่างที่ไม่ถูกต้อง หลังจากการสนทนาที่เปิดเผยและเจาะลึกกับ mikeserv (ขอบคุณสำหรับสิ่งนั้น) ฉันไปซ่อมมัน


@mikeserv: ฉันเพิ่มตัวอย่างเพื่อแสดงความเกี่ยวข้อง
hermannk

@mikeserv: ใช่คุณพูดถูก แม้กระทั้ง แต่หลุมพรางยังคงอยู่ที่นั่น
hermannk

@mikeserv: ขอโทษตัวอย่างของฉันเสีย ฉันลืมการทดสอบdoit()ค่ะ
hermannk

ขอให้เรายังคงอภิปรายนี้ในการแชท
hermannk

1

เครื่องหมายวงเล็บจะเริ่มsubshellและทางออกจะออกจาก subshell นั้นเท่านั้น

คุณสามารถอ่าน exitcode ด้วย$?และเพิ่มสิ่งนี้ในสคริปต์ของคุณเพื่อออกจากสคริปต์ถ้าออกจาก subshell:

#!/bin/bash

function bla() {
    return 1
}

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

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

echo '2'

1

ทางออกที่แท้จริง:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

การจัดกลุ่มข้อผิดพลาดจะดำเนินการก็ต่อเมื่อblaส่งคืนสถานะข้อผิดพลาดและexitไม่ได้อยู่ในเชลล์ย่อยดังนั้นสคริปต์ทั้งหมดจึงหยุดทำงาน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.