มีวิธีรัดกุมในการตรวจสอบว่าตัวแปรสภาพแวดล้อมถูกตั้งค่าใน Unix shell script อย่างไร


500

ฉันมีสคริปต์ Unix shell บางตัวที่ฉันต้องตรวจสอบว่าตัวแปรสภาพแวดล้อมบางตัวถูกตั้งค่าก่อนที่ฉันจะเริ่มทำสิ่งต่าง ๆ ดังนั้นฉันจึงทำสิ่งนี้:

if [ -z "$STATE" ]; then
    echo "Need to set STATE"
    exit 1
fi  

if [ -z "$DEST" ]; then
    echo "Need to set DEST"
    exit 1
fi

ซึ่งพิมพ์เยอะมาก มีสำนวนที่สง่างามกว่าสำหรับการตรวจสอบว่าชุดของตัวแปรสภาพแวดล้อมถูกตั้งค่าหรือไม่?

แก้ไข: ฉันควรพูดถึงว่าตัวแปรเหล่านี้ไม่มีค่าเริ่มต้นที่มีความหมาย - สคริปต์ควรผิดพลาดหากมีการตั้งค่า


2
หลายคำตอบของคำถามนี้รู้สึกเหมือนสิ่งที่คุณจะได้เห็นในรหัสกอล์ฟกองแลกเปลี่ยน
Daniel Schilling

คำตอบ:


587

การขยายพารามิเตอร์

คำตอบที่ชัดเจนคือใช้รูปแบบพิเศษของการขยายพารามิเตอร์:

: ${STATE?"Need to set STATE"}
: ${DEST:?"Need to set DEST non-empty"}

หรือดีกว่า (ดูหัวข้อ 'ตำแหน่งของเครื่องหมายคำพูดคู่' ด้านล่าง):

: "${STATE?Need to set STATE}"
: "${DEST:?Need to set DEST non-empty}"

ตัวแปรแรก (ใช้เพียง?) ต้องตั้งค่าสถานะ แต่ STATE = "" (สตริงว่าง) คือตกลง - ไม่ใช่สิ่งที่คุณต้องการอย่างแน่นอน แต่เป็นทางเลือกและสัญลักษณ์เก่า

ชุดตัวเลือกที่สอง (ใช้:?) ต้องการ DEST ที่จะตั้งค่าและไม่ว่างเปล่า

หากคุณไม่มีข้อความเชลล์จะแสดงข้อความเริ่มต้น

${var?}สร้างกลับมาแบบพกพารุ่น 7 UNIX และบอร์นเชลล์ (1978 หรือราว) การ${var:?}สร้างล่าสุดเล็กน้อย: ฉันคิดว่ามันอยู่ใน System III UNIX ประมาณปี 1981 แต่มันอาจจะอยู่ใน PWB UNIX ก่อนหน้านั้น ดังนั้นจึงอยู่ใน Korn Shell และใน POSIX เชลล์รวมถึง Bash โดยเฉพาะ

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

${parameter:?word}

แสดงข้อผิดพลาดถ้า Null หรือ Unset หากพารามิเตอร์เป็นโมฆะหรือไม่มีการตั้งค่าการขยายตัวของคำ (หรือข้อความไปยังผลกระทบที่ถ้าไม่มีคำ) จะถูกเขียนไปยังข้อผิดพลาดมาตรฐานและเปลือกถ้ามันไม่ได้โต้ตอบออกจาก มิฉะนั้นค่าของพารามิเตอร์จะถูกแทนที่

คำสั่งลำไส้ใหญ่

ฉันควรจะเพิ่มว่าคำสั่งโคลอนก็มีการประเมินผลอาร์กิวเมนต์แล้วก็ประสบความสำเร็จ มันเป็นสัญกรณ์แสดงความคิดเห็นเปลือกเดิม (ก่อน ' #' ถึงจุดสิ้นสุดของบรรทัด) เป็นเวลานานสคริปต์เชลล์เป้าหมายมีเครื่องหมายทวิภาคเป็นอักขระตัวแรก C เชลล์จะอ่านสคริปต์และใช้อักขระตัวแรกเพื่อตรวจสอบว่าเป็นของ C เชลล์ ( #แฮช '') หรือบอร์นเชลล์ (a ' :' โคลอน) หรือไม่ จากนั้นเคอร์เนลก็เข้ามามีบทบาทและเพิ่มการรองรับความคิดเห็นของ ' #!/path/to/program' และบอร์นเชลล์ได้รับ ' #' และการประชุมลำไส้ใหญ่ก็อยู่ข้างทาง แต่ถ้าคุณเจอสคริปต์ที่ขึ้นต้นด้วยโคลอนตอนนี้คุณจะรู้ว่าทำไม


ตำแหน่งของเครื่องหมายคำพูดคู่

blongถามในความคิดเห็น :

มีความคิดเห็นเกี่ยวกับการสนทนานี้หรือไม่? https://github.com/koalaman/shellcheck/issues/380#issuecomment-145872749

ส่วนสำคัญของการอภิปรายคือ:

…อย่างไรก็ตามเมื่อฉันshellcheck(เวอร์ชัน 0.4.1) ฉันได้รับข้อความนี้:

In script.sh line 13:
: ${FOO:?"The environment variable 'FOO' must be set and non-empty"}
  ^-- SC2086: Double quote to prevent globbing and word splitting.

คำแนะนำเกี่ยวกับสิ่งที่ฉันควรทำในกรณีนี้?

คำตอบสั้น ๆ คือ "ทำตามคำshellcheckแนะนำ":

: "${STATE?Need to set STATE}"
: "${DEST:?Need to set DEST non-empty}"

เพื่อแสดงเหตุผลว่าทำไมจึงศึกษาสิ่งต่อไปนี้ โปรดทราบว่า:คำสั่งไม่ได้สะท้อนข้อโต้แย้งของมัน (แต่เชลล์ทำการประเมินข้อโต้แย้ง) เราต้องการที่จะเห็นการขัดแย้งดังนั้นโค้ดด้านล่างการนำมาใช้ประโยชน์ในสถานที่ของprintf "%s\n":

$ mkdir junk
$ cd junk
$ > abc
$ > def
$ > ghi
$ 
$ x="*"
$ printf "%s\n" ${x:?You must set x}    # Careless; not recommended
abc
def
ghi
$ unset x
$ printf "%s\n" ${x:?You must set x}    # Careless; not recommended
bash: x: You must set x
$ printf "%s\n" "${x:?You must set x}"  # Careful: should be used
bash: x: You must set x
$ x="*"
$ printf "%s\n" "${x:?You must set x}"  # Careful: should be used
*
$ printf "%s\n" ${x:?"You must set x"}  # Not quite careful enough
abc
def
ghi
$ x=
$ printf "%s\n" ${x:?"You must set x"}  # Not quite careful enough
bash: x: You must set x
$ unset x
$ printf "%s\n" ${x:?"You must set x"}  # Not quite careful enough
bash: x: You must set x
$ 

โปรดสังเกตว่าค่าใน$xถูกขยายเป็นอันดับแรก*จากนั้นรายการของชื่อไฟล์เมื่อนิพจน์โดยรวมไม่อยู่ในเครื่องหมายคำพูดคู่ นี่คือสิ่งที่shellcheckแนะนำควรได้รับการแก้ไข ฉันยังไม่ได้ตรวจสอบว่ามันไม่ได้คัดค้านรูปแบบที่การแสดงออกถูกล้อมรอบด้วยเครื่องหมายคำพูดคู่ แต่มันก็เป็นข้อสันนิษฐานที่สมเหตุสมผลว่ามันจะตกลง


2
นั่นคือสิ่งที่ฉันต้องการ ฉันใช้ Unix หลายรุ่นตั้งแต่ปี 1987 และฉันไม่เคยเห็นไวยากรณ์นี้เลย - เพิ่งไปแสดง ...
AndrewR

มันทำงานยังไงและมันอยู่ที่ไหนกันบ้าง? ฉันสงสัยว่ามันสามารถแก้ไขได้เพื่อตรวจสอบตัวแปรที่มีอยู่และตั้งค่าเป็นค่าเฉพาะหรือไม่
jhabbott

6
มีการบันทึกไว้ในหน้าคู่มือเปลือกหรือคู่มือทุบตีโดยทั่วไปภายใต้หัวข้อ 'การขยายพารามิเตอร์' วิธีมาตรฐานในการตรวจสอบว่าตัวแปรที่มีอยู่และมีการตั้งค่าที่เฉพาะเจาะจง (พูดว่า 'abc') if [ "$variable" = "abc" ]; then : OK; else : variable is not set correctly; fiเป็นเพียง:
Jonathan Leffler

ฉันจะใช้ไวยากรณ์นี้กับตัวแปรสตริงที่มีช่องว่างในนั้นได้อย่างไร ฉันพบข้อผิดพลาดว่า "ไม่พบคำสั่งนี้" เมื่อฉันตั้งค่าตัวแปรฉันต้องการตรวจสอบกับ "นี่คือการทดสอบ" ความคิดใด ๆ
user2294382

1
มีความคิดเห็นเกี่ยวกับการสนทนานี้หรือไม่? github.com/koalaman/shellcheck/issues/…
blong

138

ลองสิ่งนี้:

[ -z "$STATE" ] && echo "Need to set STATE" && exit 1;

8
นั่นค่อนข้าง verbose เซมิโคลอนซ้ำซ้อน
Jonathan Leffler

3
หรือสั้นกว่านี้[ -z "$STATE" ] && { echo "Need to set STATE"; exit 1; } ดูโพสต์บล็อกนี้
zipizap

36
สิ่งนั้นคือสามารถอ่านได้ ฉันทั้งหมดสำหรับอันนี้ สคริปต์ทุบตีต้องการความช่วยเหลือทั้งหมดที่พวกเขาสามารถได้รับในแผนกนั้น!
เลนดันแคน

คำตอบเดิมมีความสอดคล้องในไวยากรณ์
ฟันเร็ว

4
อย่างไรก็ตามสิ่งนี้ไม่ได้แยกความแตกต่างระหว่างตัวแปร unset และตัวแปรที่ตั้งค่าเป็นสตริงว่าง
chepner

57

คำถามของคุณขึ้นอยู่กับเชลล์ที่คุณใช้

เชลล์เป้าหมายปล่อยให้สิ่งที่คุณตามหาอยู่น้อยมาก

แต่...

มันใช้งานได้ทุกที่

เพียงแค่พยายามอยู่ห่างจาก csh มันดีสำหรับระฆังและนกหวีดที่เพิ่มเข้ามาเปรียบเทียบกับ Bourne shell แต่ตอนนี้มันกำลังคืบคลานเข้ามาจริงๆ หากคุณไม่เชื่อฉันให้ลองแยก STDERR ออกเป็น csh! (-:

มีความเป็นไปได้สองอย่างที่นี่ ตัวอย่างข้างต้นคือการใช้:

${MyVariable:=SomeDefault}

เป็นครั้งแรกที่คุณต้องอ้างถึง $ MyVariable สิ่งนี้ต้องใช้ env var MyVariable และหากไม่ได้ตั้งค่าไว้ในปัจจุบันให้กำหนดค่าของ SomeDefault ให้กับตัวแปรเพื่อใช้ในภายหลัง

คุณยังมีความเป็นไปได้ที่:

${MyVariable:-SomeDefault}

ซึ่งเพิ่งทดแทน SomeDefault สำหรับตัวแปรที่คุณใช้โครงสร้างนี้ ไม่ได้กำหนดค่า SomeDefault ให้กับตัวแปรและค่าของ MyVariable จะยังคงเป็นโมฆะหลังจากพบคำสั่งนี้


3
CSH: (foo> foo.out)> & foo.err
Mr.Ree

เชลล์เป้าหมายทำสิ่งที่ต้องการ
Jonathan Leffler

51

วิธีที่ง่ายที่สุดคือการเพิ่ม-uสวิตช์ไปยัง shebang (บรรทัดที่ด้านบนสุดของสคริปต์) โดยสมมติว่าคุณกำลังใช้bash:

#!/bin/sh -u

สิ่งนี้จะทำให้สคริปต์ออกหากตัวแปรที่ไม่ได้ผูกไว้ซึ่งแฝงตัวอยู่ภายใน


40
${MyVariable:=SomeDefault}

หากMyVariableตั้งไว้และไม่เป็นโมฆะมันจะรีเซ็ตค่าตัวแปร (= ไม่มีอะไรเกิดขึ้น)
อื่นMyVariableถูกตั้งค่าเป็นSomeDefaultมีการตั้งค่า

ด้านบนจะพยายามดำเนินการ${MyVariable}ดังนั้นหากคุณต้องการตั้งค่าตัวแปรให้ทำ:

MyVariable=${MyVariable:=SomeDefault}

สิ่งนี้ไม่สร้างข้อความ - และ Q บอกว่าไม่มีค่าเริ่มต้น (แม้ว่าจะไม่ได้บอกว่าเมื่อคุณพิมพ์คำตอบ)
Jonathan Leffler

10
รุ่นที่ถูกต้อง: (1): $ {MyVariable: = SomeDefault} หรือ (2): MyVariable = $ {MyVariable: -SomeDefault}
Jonathan Leffler

23

ในความคิดของฉันการตรวจสอบที่ง่ายและเข้ากันได้มากที่สุดสำหรับ #! / bin / shคือ:

if [ "$MYVAR" = "" ]
then
   echo "Does not exist"
else
   echo "Exists"
fi

อีกครั้งนี้ใช้สำหรับ / bin / sh และเข้ากันได้กับระบบ Solaris เก่า


สิ่งนี้ 'ใช้งานได้' แต่แม้แต่ระบบ Solaris รุ่นเก่าก็มีระบบ/bin/shที่รองรับ${var:?}สัญกรณ์ ฯลฯ
Jonathan Leffler

คำตอบที่ดีที่สุด
โอเล

4
การตั้งค่าเป็นสตริงว่างจะแตกต่างจากการไม่ได้ตั้งเลย การทดสอบนี้จะพิมพ์ "ไม่อยู่" หลังจากที่ทั้งสองและunset MYVAR MYVAR=
chepner

@ chepner ฉัน upvote ความคิดเห็นของคุณสำหรับ heads-up ที่มีค่าน่าจะเป็นไปได้ แต่จากนั้นฉันก็ทำการทดสอบ (ด้วย bash) และไม่สามารถตั้งค่า var ให้เป็นสตริงว่างได้โดยที่มันไม่ได้ถูกตั้งค่า คุณจะทำอย่างไร
Sz.

@Sz ฉันไม่แน่ใจว่าคุณหมายถึงอะไร "Unset" หมายถึงชื่อไม่มีค่าที่ได้รับมอบหมายทั้งหมด หากfooไม่ได้ตั้งค่า"$foo"ยังคงขยายเป็นสตริงว่าง
chepner

17

bash4.2 แนะนำตัว-vดำเนินการซึ่งจะทดสอบว่าชื่อถูกตั้งเป็นค่าใด ๆแม้แต่สตริงว่าง

$ unset a
$ b=
$ c=
$ [[ -v a ]] && echo "a is set"
$ [[ -v b ]] && echo "b is set"
b is set
$ [[ -v c ]] && echo "c is set"
c is set

16

ฉันมักจะใช้:

if [ "x$STATE" == "x" ]; then echo "Need to set State"; exit 1; fi

ไม่รัดกุมกว่านี้มากนักฉันกลัว

ภายใต้ CSH คุณมี $? STATE


2
การตรวจสอบนี้STATEจะว่างเปล่าหรือไม่ได้ตั้งค่าไม่ใช่แค่ยกเลิกการตั้งค่า
chepner

7

สำหรับคนในอนาคตอย่างฉันฉันอยากก้าวไปข้างหน้าและตั้งชื่อตัวแปร var ดังนั้นฉันสามารถวนรอบรายการตัวแปรชื่อตัวแปร:

#!/bin/bash
declare -a vars=(NAME GITLAB_URL GITLAB_TOKEN)

for var_name in "${vars[@]}"
do
  if [ -z "$(eval "echo \$$var_name")" ]; then
    echo "Missing environment variable $var_name"
    exit 1
  fi
done

3

เราสามารถเขียนคำยืนยันที่ดีเพื่อตรวจสอบตัวแปรหลายตัวพร้อมกัน:

#
# assert if variables are set (to a non-empty string)
# if any variable is not set, exit 1 (when -f option is set) or return 1 otherwise
#
# Usage: assert_var_not_null [-f] variable ...
#
function assert_var_not_null() {
  local fatal var num_null=0
  [[ "$1" = "-f" ]] && { shift; fatal=1; }
  for var in "$@"; do
    [[ -z "${!var}" ]] &&
      printf '%s\n' "Variable '$var' not set" >&2 &&
      ((num_null++))
  done

  if ((num_null > 0)); then
    [[ "$fatal" ]] && exit 1
    return 1
  fi
  return 0
}

ตัวอย่างการร้องขอ:

one=1 two=2
assert_var_not_null one two
echo test 1: return_code=$?
assert_var_not_null one two three
echo test 2: return_code=$?
assert_var_not_null -f one two three
echo test 3: return_code=$? # this code shouldn't execute

เอาท์พุท:

test 1: return_code=0
Variable 'three' not set
test 2: return_code=1
Variable 'three' not set

ยืนยันดังกล่าวเพิ่มเติมได้ที่นี่: https://github.com/codeforester/base/blob/master/lib/assertions.sh



1

ไม่มีวิธีแก้ปัญหาข้างต้นทำงานได้ตามวัตถุประสงค์ส่วนหนึ่งเป็นเพราะฉันตรวจสอบสภาพแวดล้อมเพื่อหารายการตัวแปรปลายเปิดที่ต้องตั้งค่าก่อนเริ่มกระบวนการที่มีความยาว ฉันลงเอยด้วยสิ่งนี้:

mapfile -t arr < variables.txt

EXITCODE=0

for i in "${arr[@]}"
do
   ISSET=$(env | grep ^${i}= | wc -l)
   if [ "${ISSET}" = "0" ];
   then
      EXITCODE=-1
      echo "ENV variable $i is required."
   fi
done

exit ${EXITCODE}

-1

แทนที่จะใช้สคริปต์เปลือกภายนอกฉันมักจะโหลดในฟังก์ชั่นในเปลือกเข้าสู่ระบบของฉัน ฉันใช้สิ่งนี้เป็นฟังก์ชันผู้ช่วยในการตรวจสอบตัวแปรสภาพแวดล้อมมากกว่าตัวแปรชุดใด ๆ :

is_this_an_env_variable ()
    local var="$1"
    if env |grep -q "^$var"; then
       return 0
    else
       return 1
    fi
 }

1
นี่เป็นสิ่งที่ผิด is_this_an_env_variable Pจะกลับมาประสบความสำเร็จเพราะPATHมีอยู่
Eric

มันมีอยู่เพราะมันเป็นตัวแปรสภาพแวดล้อม ไม่ว่าจะเป็นชุดสตริง eqaul nonnull เป็นอีกเรื่อง
nafdef

-7

$?ไวยากรณ์เป็นระเบียบสวย:

if [ $?BLAH == 1 ]; then 
    echo "Exists"; 
else 
    echo "Does not exist"; 
fi

ทำงานได้ แต่ทุกคนที่นี่รู้ว่าฉันสามารถหาเอกสารในไวยากรณ์นั้นได้ที่ไหน $? โดยปกติคือค่าส่งคืนก่อนหน้า
Danny Staple

ไม่ดีของฉัน - สิ่งนี้ไม่ทำงาน ลองใช้สคริปต์ง่ายๆโดยมีและไม่มีชุดนั้น
Danny Staple

11
ใน Bourne เชลล์ Korn, Bash และ POSIX $?คือสถานะการออกของคำสั่งก่อนหน้า $?BLAHคือสตริงที่ประกอบด้วยสถานะการออกของคำสั่งก่อนหน้าที่ต่อกับ 'BLAH' นี่ไม่ได้ทดสอบตัวแปร$BLAHเลย (มีข่าวลือว่ามันอาจทำอะไรมากหรือน้อยกว่าที่ต้องการในcshแต่เปลือกหอยนั้นดีที่สุดที่เหลืออยู่บนชายฝั่งทะเล)
Jonathan Leffler

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