การใช้ตัวแปรเชลล์สำหรับตัวเลือกคำสั่ง


19

ในสคริปต์ Bash ฉันพยายามจัดเก็บตัวเลือกที่ใช้rsyncในตัวแปรแยกต่างหาก ใช้งานได้ดีกับตัวเลือกง่าย ๆ (เช่น--recursive) แต่ฉันพบปัญหากับ--exclude='.*':

$ find source
source
source/.bar
source/foo

$ rsync -rnv --exclude='.*' source/ dest
sending incremental file list
foo

sent 57 bytes  received 19 bytes  152.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

$ RSYNC_OPTIONS="-rnv --exclude='.*'"

$ rsync $RSYNC_OPTIONS source/ dest
sending incremental file list
.bar
foo

sent 78 bytes  received 22 bytes  200.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

อย่างที่คุณเห็นการส่งผ่าน--exclude='.*'ไปที่rsync"ด้วยตนเอง" ทำงานได้ดี ( .barไม่ได้คัดลอก) มันจะไม่ทำงานเมื่อตัวเลือกถูกเก็บไว้ในตัวแปรก่อน

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



1
หรือดูคำตอบนี้ในเว็บไซต์ของเราเอง
สกอตต์

คำตอบ:


38

โดยทั่วไปแล้วมันเป็นความคิดที่ดีที่จะลดระดับรายการของรายการแยกเป็นสตริงเดียวไม่ว่าจะเป็นรายการตัวเลือกบรรทัดคำสั่งหรือรายการชื่อพา ธ

ใช้อาร์เรย์แทน:

rsync_options=( -rnv --exclude='.*' )

หรือ

rsync_options=( -r -n -v --exclude='.*' )

และหลังจากนั้น...

rsync "${rsync_options[@]}" source/ target

ด้วยวิธีนี้การอ้างถึงตัวเลือกแต่ละรายการจะได้รับการปรับปรุง (ตราบใดที่คุณเพิ่มการเสนอราคาเป็นสองเท่า${rsync_options[@]}) นอกจากนี้ยังช่วยให้คุณสามารถจัดการกับแต่ละรายการของอาร์เรย์ได้อย่างง่ายดายคุณจะต้องทำก่อนที่จะโทรrsyncนอกจากนี้ยังช่วยให้คุณสามารถจัดการกับรายการแต่ละรายการของอาร์เรย์ที่คุณจะต้องทำเช่นนั้นก่อนที่จะเรียก

ใน POSIX เชลล์หนึ่งอาจใช้รายการพารามิเตอร์ตำแหน่งสำหรับสิ่งนี้:

set -- -rnv --exclude='.*'

rsync "$@" source/ target

อีกครั้งการอ้างถึงการขยายตัวของสองครั้ง$@เป็นสิ่งสำคัญที่นี่

เกี่ยวข้องกับตัวตน:


ปัญหาคือเมื่อคุณใส่ตัวเลือกสองชุดลงในสตริงการเสนอราคาเดียวของค่า--excludeตัวเลือกจะกลายเป็นส่วนหนึ่งของค่านั้น ดังนั้น

RSYNC_OPTIONS='-rnv --exclude=.*'

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


¹หาก$IFSไม่ได้รับการแก้ไขและไม่มีไฟล์ที่ชื่อขึ้นต้นด้วย--exclude=.ในไดเรกทอรีปัจจุบันและไม่ได้ตั้งค่าตัวเลือกnullglobหรือfailglobเปลือก


ใช้อาร์เรย์ทำงานได้ดีขอบคุณสำหรับคำตอบโดยละเอียด!
Florian Brucker

3

@Kusalananda ได้อธิบายปัญหาพื้นฐานและวิธีการแก้ไขแล้วและรายการBash FAQ ที่ลิงก์โดย @glenn jackmann ยังให้ข้อมูลที่มีประโยชน์มากมาย นี่คือคำอธิบายโดยละเอียดเกี่ยวกับสิ่งที่เกิดขึ้นในปัญหาของฉันตามทรัพยากรเหล่านี้

เราจะใช้สคริปต์ขนาดเล็กที่พิมพ์แต่ละข้อโต้แย้งของมันในบรรทัดแยกต่างหากเพื่อแสดงสิ่งต่าง ๆ ( argtest.bash):

#!/bin/bash

for var in "$@"
do
    echo "$var"
done

ตัวเลือกผ่าน "ด้วยตนเอง":

$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*

ตามที่คาดไว้ชิ้นส่วน-rnvและ--exclude='.*'ถูกแบ่งออกเป็นสองอาร์กิวเมนต์ตามที่ถูกคั่นด้วย whitespace ที่ไม่ได้กล่าวถึง (เรียกว่าการแยกคำ )

นอกจากนี้ทราบว่าคำพูดที่อยู่รอบ ๆ.*ได้ถูกลบออก: ราคาเดียวบอกเปลือกจะผ่านเนื้อหาของพวกเขาโดยไม่ต้องตีความพิเศษ , แต่คำพูดตัวเองไม่ได้ส่งผ่านไปยังคำสั่ง

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

$ OPTS="--exclude='.*'"

$ ./argtest.bash $OPTS
--exclude='.*'

นี่เป็นเพราะเหตุผลสองประการคือ: อัญประกาศคู่ที่ใช้เมื่อกำหนด$OPTSป้องกันการดูแลพิเศษของราคาเดียวดังนั้นหลังเป็นส่วนหนึ่งของค่า:

$ echo $OPTS
--exclude='.*'

เมื่อตอนนี้เราใช้$OPTSเป็นอาร์กิวเมนต์ให้กับคำสั่งแล้วอัญประกาศจะถูกประมวลผลก่อนที่จะขยายพารามิเตอร์ดังนั้นคำพูดในที่$OPTSเกิดขึ้น "สายเกินไป"

ซึ่งหมายความว่า (ในปัญหาดั้งเดิมของฉัน) rsyncใช้รูปแบบการแยก'.*'(พร้อมอัญประกาศ!) แทนรูปแบบ.*- ยกเว้นไฟล์ที่มีชื่อขึ้นต้นด้วยอัญประกาศเดี่ยวตามด้วยจุดและลงท้ายด้วยเครื่องหมายคำพูดเดี่ยว เห็นได้ชัดว่านั่นไม่ใช่สิ่งที่ตั้งใจไว้

วิธีแก้ปัญหาคือการละเว้นเครื่องหมายคำพูดคู่เมื่อกำหนด$OPTS:

$ OPTS2=--exclude='.*'

$ ./argtest.bash $OPTS2
--exclude=.*

อย่างไรก็ตามมันเป็นแนวปฏิบัติที่ดีที่จะอ้างอิงการกำหนดตัวแปรเสมอเนื่องจากความแตกต่างที่ลึกซึ้งในกรณีที่ซับซ้อนมากขึ้น

ในฐานะที่เป็น @Kusalananda ตั้งข้อสังเกตไม่อ้าง.*ยังจะได้ทำงาน ฉันได้เพิ่มเครื่องหมายคำพูดเพื่อป้องกันการขยายตัวของรูปแบบแต่นั่นไม่จำเป็นอย่างยิ่งในกรณีพิเศษนี้ :

$ ./argtest.bash --exclude=.*
--exclude=.*

แต่กลับกลายเป็นว่าทุบตีไม่ดำเนินการขยายรูปแบบ แต่รูปแบบ--exclude=.*ไม่ตรงกับไฟล์ใด ๆ เพื่อให้รูปแบบจะถูกส่งผ่านไปยังคำสั่ง เปรียบเทียบ:

$ touch some_file

$ ./argtest.bash some_*
some_file

$ ./argtest.bash does_not_exit_*
does_not_exit_*

อย่างไรก็ตามการไม่อ้างอิงรูปแบบเป็นสิ่งที่อันตรายเพราะหาก (ด้วยเหตุผลใดก็ตาม) มีการจับคู่ไฟล์--exclude=.*ดังนั้นรูปแบบจะขยายออก:

$ touch -- --exclude=.special-filenames-happen

$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen

ในที่สุดเรามาดูกันว่าทำไมการใช้อาร์เรย์จึงป้องกันปัญหาการอ้างถึงของฉัน (นอกเหนือจากข้อดีอื่น ๆ ของการใช้อาร์เรย์เพื่อเก็บอาร์กิวเมนต์ของคำสั่ง)

เมื่อกำหนดอาร์เรย์การแบ่งคำและการจัดการคำพูดจะเกิดขึ้นตามที่คาดไว้:

$ ARRAY_OPTS=( -rnv --exclude='.*' )

$ echo length of the array: "${#ARRAY_OPTS[@]}"
length of the array: 2

$ echo first element: "${ARRAY_OPTS[0]}"
first element: -rnv

$ echo second element: "${ARRAY_OPTS[1]}"
second element: --exclude=.*

เมื่อส่งตัวเลือกไปยังคำสั่งเราจะใช้ไวยากรณ์"${ARRAY[@]}"ซึ่งขยายแต่ละองค์ประกอบของอาร์เรย์เป็นคำแยก:

$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*

สิ่งนี้ทำให้ฉันสับสนเป็นเวลานานดังนั้นคำอธิบายโดยละเอียดเช่นนี้มีประโยชน์
โจ

0

เมื่อเราเขียนฟังก์ชั่นและเชลล์สคริปต์ซึ่งมีการส่งผ่านข้อโต้แย้งเพื่อดำเนินการอาร์กิวเมนต์จะถูกส่งผ่านตัวแปรที่มีชื่อเป็นตัวเลขเช่น $ 1, $ 2, $ 3

ตัวอย่างเช่น :

bash my_script.sh Hello 42 World

ภายในmy_script.shคำสั่งจะใช้$1เพื่ออ้างถึง Hello, $2to 42และ$3forWorld

การอ้างอิงตัวแปร$0จะขยายเป็นชื่อสคริปต์ปัจจุบันเช่นmy_script.sh

อย่าเล่นรหัสทั้งหมดโดยใช้คำสั่งเป็นตัวแปร

โปรดทราบ :

1 หลีกเลี่ยงการใช้ชื่อตัวแปรตัวพิมพ์ใหญ่ทั้งหมดในสคริปต์

2 อย่าใช้ backquotes ใช้ $ (... ) แทนมันจะดีกว่า

if [ $# -ne 2 ]
then
    echo "Usage: $(basename $0) DIRECTORY BACKUP_DIRECTORY"
    exit 1
fi

directory=$1
backup_directory=$2
current_date=$(date +%Y-%m-%dT%H-%M-%S)
backup_file="${backup_directory}/${current_date}.backup"

tar cv "$directory" | openssl des3 -salt | split -b 1024m - "$backup_file"
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.