การอ้างอิงภายใน $ (การทดแทนคำสั่ง) ใน Bash


126

ในสภาพแวดล้อม Bash ของฉันฉันใช้ตัวแปรที่มีช่องว่างและฉันใช้ตัวแปรเหล่านี้ภายในการทดแทนคำสั่ง น่าเสียดายที่ฉันไม่พบคำตอบใน SE

วิธีที่ถูกต้องในการอ้างอิงตัวแปรของฉันคืออะไร? และฉันควรทำอย่างไรถ้าสิ่งเหล่านี้ซ้อนอยู่?

DIRNAME=$(dirname "$FILE")

หรือฉันจะพูดนอกทดแทน?

DIRNAME="$(dirname $FILE)"

หรือทั้งคู่?

DIRNAME="$(dirname "$FILE")"

หรือฉันจะใช้เห็บกลับ?

DIRNAME=`dirname "$FILE"`

วิธีที่ถูกต้องในการทำเช่นนี้คืออะไร? และฉันจะตรวจสอบได้อย่างง่ายดายว่าราคาถูกต้องหรือไม่


ดูเพิ่มเติมที่ - unix.stackexchange.com/questions/97560/…
แกรม


1
นี่เป็นคำถามที่ดี แต่ให้ทุกปัญหากับช่องว่างในตัวทำไมคุณต้องทำให้ชีวิตตัวเองยากขึ้นโดยใช้มันอย่างตั้งใจ?
Joe

3
@ โจด้วยช่องว่างที่ฝังอยู่คุณหมายถึงพื้นที่ในชื่อไฟล์ใช่ไหม ส่วนตัวฉันไม่ได้ใช้พวกเขาที่มักจะ แต่ฉันทำงานกับไดเรกทอรีประชาชนอื่น ๆ และไฟล์ที่ฉันไม่แน่ใจว่าพวกเขามีช่องว่าง นอกจากนี้ฉันคิดว่ามันเป็นการดีที่จะทำให้ถูกต้องทันทีดังนั้นฉันจึงไม่ต้องกังวลในอนาคต
CousinCocaine

4
ใช่. เราจะทำอย่างไรกับ "คนอื่น" เหล่านั้น? <G>
Joe

คำตอบ:


188

ตามลำดับจากที่เลวร้ายที่สุดถึงดีที่สุด:

  • DIRNAME="$(dirname $FILE)" จะไม่ทำสิ่งที่คุณต้องการถ้ามีช่องว่างหรืออักขระ$FILE globbing\[?*
  • DIRNAME=`dirname "$FILE"`ถูกต้องทางเทคนิค แต่ไม่แนะนำให้ backticks สำหรับการขยายคำสั่งเนื่องจากความซับซ้อนเป็นพิเศษเมื่อซ้อนกัน
  • DIRNAME=$(dirname "$FILE")ถูกต้อง แต่เพียงเพราะนี่คือการมอบหมาย หากคุณใช้การทดแทนคำสั่งในบริบทอื่นเช่นexport DIRNAME=$(dirname "$FILE")หรือdu $(dirname "$FILE")การขาดเครื่องหมายคำพูดจะทำให้เกิดปัญหาหากผลลัพธ์ของการขยายประกอบด้วยช่องว่างหรือตัวอักษรกลม
  • DIRNAME="$(dirname "$FILE")"เป็นวิธีที่แนะนำ คุณสามารถแทนที่DIRNAME=ด้วยคำสั่งและช่องว่างโดยไม่ต้องเปลี่ยนแปลงอะไรเลยและdirnameรับสตริงที่ถูกต้อง

เพื่อปรับปรุงให้ดียิ่งขึ้น:

  • DIRNAME="$(dirname -- "$FILE")"ทำงานถ้า$FILEเริ่มต้นด้วยเส้นประ
  • DIRNAME="$(dirname -- "$FILE"; printf x)" && DIRNAME="${DIRNAME%?x}"ทำงานได้แม้ว่าจะ$FILEจบลงด้วยการขึ้นบรรทัดใหม่เนื่องจาก$()ตัดการขึ้นบรรทัดใหม่ที่ส่วนท้ายของเอาต์พุตและdirnameส่งเอาต์พุตขึ้นบรรทัดใหม่หลังจากผลลัพธ์ Sheesh dirnameทำไมคุณถึงต้องแตกต่าง

คุณสามารถซ้อนการขยายคำสั่งได้มากเท่าที่คุณต้องการ เมื่อ$()คุณสร้างบริบทข้อความใหม่เสมอคุณจึงสามารถทำสิ่งนี้ได้:

foo "$(bar "$(baz "$(ban "bla")")")"

คุณไม่ต้องการลอง backticks


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

3
มีการอ้างอิง / ทรัพยากรรายละเอียดพฤติกรรมของอัญประกาศภายในสถานีย่อยคำสั่งภายในอัญประกาศหรือไม่
AmadeusDrZaius

12
@AmadeusDrZaius "เมื่อ$()คุณสร้างบริบทข้อความใหม่เสมอ" ดังนั้นมันจึงเหมือนกับคำพูดภายนอก เท่าที่ฉันรู้
l0b0

4
@ l0b0 ขอบคุณใช่ฉันพบคำอธิบายของคุณชัดเจนมาก ฉันแค่สงสัยว่ามันอยู่ในคู่มือหรือไม่ ผมไม่คิดว่ามัน (แม้จะไม่เป็นทางการ) ที่ wooledge ฉันเดาว่าถ้าคุณอ่านเกี่ยวกับลำดับของการทดแทนอย่างระมัดระวังคุณอาจได้รับข้อเท็จจริงนี้
AmadeusDrZaius

1
คำพูดซ้อนจึงเป็นที่ยอมรับ แต่พวกมันจะโยนเราออกไปเพราะโครงร่างสีส่วนใหญ่จะไม่ตรวจจับสถานการณ์พิเศษ เรียบร้อย
ลุคเดวิส

19

คุณสามารถแสดงผลของการอ้างอิงตัวแปรprintfได้ตลอดเวลา

การแยกคำทำได้เมื่อvar1:

$ var1="hello     world"
$ printf '[%s]\n' $var1
[hello]
[world]

var1 ยกมาดังนั้นจึงไม่มีการแยกคำ:

$ printf '[%s]\n' "$var1"
[hello     world]

คำที่แยกออกมาvar1ข้างใน$()เทียบเท่ากับecho "hello" "world":

$ var2=$(echo $var1)
$ printf '[%s]\n' "$var2"
[hello world]

ไม่มีการแยกคำvar1ไม่มีปัญหากับการไม่อ้างถึง$():

$ var2=$(echo "$var1")
$ printf '[%s]\n' "$var2"
[hello     world]

คำแยกvar1อีกครั้ง:

$ var2="$(echo $var1)"
$ printf '[%s]\n' "$var2"
[hello world]

การอ้างอิงทั้งสองวิธีที่ง่ายที่สุดเพื่อให้แน่ใจ

$ var2="$(echo "$var1")"
$ printf '[%s]\n' "$var2"
[hello     world]

ปัญหาการวนรอบ

การไม่อ้างตัวแปรสามารถนำไปสู่การขยายตัวของเนื้อหาได้

$ mkdir test; cd test; touch file1 file2
$ var="*"
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]

โปรดทราบว่าสิ่งนี้เกิดขึ้นหลังจากตัวแปรถูกขยายเท่านั้น ไม่จำเป็นต้องอ้างกลมระหว่างการมอบหมาย:

$ var=*
$ printf '[%s]\n' $var
[file1]
[file2]
$ printf '[%s]\n' "$var"
[*]

ใช้set -fเพื่อปิดใช้งานลักษณะการทำงานนี้:

$ set -f
$ var=*
$ printf '[%s]\n' $var
[*]

และset +fเพื่อเปิดใช้งานอีกครั้ง:

$ set +f
$ printf '[%s]\n' $var
[file1]
[file2]

8
ผู้คนมักจะลืมว่าการแยกคำไม่ใช่ปัญหาเดียวคุณอาจต้องการเปลี่ยนตัวอย่างของคุณvar1='hello * world'เพื่อแสดงให้เห็นถึงปัญหาที่เกิดขึ้นเช่นกัน
Stéphane Chazelas

10

นอกจากคำตอบที่ยอมรับแล้ว:

แม้ว่าโดยทั่วไปฉันจะเห็นด้วยกับคำตอบของ @ l0b0ที่นี่ฉันสงสัยว่าตำแหน่งของ backticks เปล่าในรายการ "แย่ที่สุดถึงดีที่สุด" อย่างน้อยก็ส่วนหนึ่งเป็นผลมาจากสมมติฐานที่$(...)มีอยู่ทุกที่ ฉันรู้ว่าคำถามนั้นระบุ Bash แต่มีหลายครั้งที่ Bash เปลี่ยนไปเป็นค่าเฉลี่ย/bin/shซึ่งจริงๆแล้วอาจไม่ใช่เชลล์เต็มรูปแบบของ Bourne Again

โดยเฉพาะอย่างยิ่งเชลล์ Bourne ธรรมดาจะไม่ทราบว่าจะทำอย่างไร$(...)ดังนั้นสคริปต์ที่อ้างว่าใช้งานร่วมกับมันได้ (เช่นผ่านทาง#!/bin/shแถวชีบ) จะมีพฤติกรรมที่ไม่เหมาะสมหากพวกเขาทำงานโดย "ตัวจริง" /bin/sh- นี่เป็นของ ความสนใจเป็นพิเศษเมื่อพูดผลิตสคริปต์เริ่มต้นหรือบรรจุภัณฑ์ก่อนและหลังสคริปต์และสามารถลงจอดหนึ่งในสถานที่ที่น่าแปลกใจในระหว่างการติดตั้งระบบฐาน

หากสิ่งใดที่ดูเหมือนสิ่งที่คุณวางแผนจะทำกับตัวแปรนี้การซ้อนอาจเป็นเรื่องที่น่ากังวลน้อยกว่าการใช้สคริปต์จริง เมื่อเป็นกรณีที่ง่ายและสะดวกในการพกพาเป็นสิ่งที่น่ากังวลแม้ว่าฉันคาดว่าสคริปต์จะทำงานบนระบบที่/bin/shเป็น Bash ฉันมักจะใช้ backticks ด้วยเหตุผลนี้ด้วยการมอบหมายหลายอย่างแทนที่จะซ้อนกัน

ต้องบอกว่าทุกอย่างความแพร่หลายของกระสุนที่ใช้$(...)(Bash, Dash, et al.) ทำให้เราอยู่ในจุดที่ดีที่จะติดกับ prettier ที่ง่ายต่อการทำรัง ทุกเหตุผลที่ @ l0b0 กล่าวถึง

นอกเหนือ: สิ่งนี้ปรากฏขึ้นเป็นครั้งคราวใน StackOverflow ด้วย -


//, คำตอบที่ดีเลิศ ฉันพบปัญหาความเข้ากันได้ย้อนหลังกับ / bin / sh ในอดีตเช่นกัน คุณมีคำแนะนำเกี่ยวกับวิธีจัดการกับปัญหาของเขาโดยใช้วิธีการทำงานร่วมกันได้หรือไม่?
Nathan Basanese

จากประสบการณ์ของฉัน: บางครั้งคุณอาจต้องเปลี่ยน backticks เป็นเงินดอลลาร์ที่ปิดล้อมและไม่เคยช่วย (เคยเป็นเฉพาะเจาะจง) เพื่อเปลี่ยนเงินดอลลาร์ที่ถูกล้อมรอบเป็น backticks ฉันเขียน backticks ในรหัส javascript เท่านั้นไม่ใช่ใน shell code
Steven Lu
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.