วิธีกำหนดค่าที่มีช่องว่างให้กับตัวแปรใน bash โดยใช้ eval


19

evalฉันต้องการที่จะแบบไดนามิกค่ากำหนดตัวแปรโดยใช้ ตัวอย่างตัวอย่างต่อไปนี้ใช้งานได้:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

อย่างไรก็ตามเมื่อค่าตัวแปรมีช่องว่างevalส่งคืนข้อผิดพลาดแม้ว่า$var_valueจะอยู่ระหว่างเครื่องหมายคำพูดคู่:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

วิธีใดที่จะหลีกเลี่ยงสิ่งนี้

คำตอบ:



11

อย่าใช้evalเพื่อสิ่งนี้ declareใช้

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

โปรดทราบว่าการแยกคำไม่ใช่ปัญหาเพราะทุกอย่างที่อยู่หลังค่า=จะถือว่าเป็นค่าโดยdeclareไม่ใช่แค่คำแรก

ในbash4.3 การอ้างอิงแบบมีชื่อทำให้สิ่งนี้ง่ายขึ้นเล็กน้อย

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

คุณสามารถทำให้evalการทำงาน แต่คุณยังไม่ควร :) ใช้evalเป็นนิสัยที่ดีที่จะได้รับใน

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"

2
ใช้eval วิธีการที่ผิด คุณกำลังขยายตัว$var_valueก่อนที่จะส่งผ่านevalซึ่งหมายความว่ามันจะถูกตีความว่าเป็นรหัสเชลล์! (ลองกับvar_value="';:(){ :|:&};:'")
Stéphane Chazelas

1
จุดดี; มีสตริงบางตัวที่คุณไม่สามารถใช้งานได้อย่างปลอดภัยeval(ซึ่งเป็นเหตุผลหนึ่งที่ฉันบอกว่าคุณไม่ควรใช้eval)
chepner

@chepner - ฉันไม่เชื่อว่าเป็นเรื่องจริง อาจจะเป็น แต่ไม่ใช่คนนี้อย่างน้อย การแทนที่พารามิเตอร์อนุญาตให้มีการขยายตามเงื่อนไขและเพื่อให้คุณสามารถขยายเฉพาะค่าที่ปลอดภัยในกรณีส่วนใหญ่ฉันคิดว่า ยังคงปัญหาหลักของคุณ$var_valueคือหนึ่งในการอ้างคำพูด - สมมติว่าค่าที่ปลอดภัยสำหรับ$var_name (ซึ่งอาจเป็นอันตรายสมมุติฐานจริง ๆ )จากนั้นคุณควรจะใส่เครื่องหมายคำพูดคู่ด้านขวาภายในคำพูดเดียว - ไม่ ในทางกลับกัน
mikeserv

ฉันคิดว่าฉันคงที่evalใช้printfและรูปแบบbashเฉพาะของมัน %qนี่ยังไม่แนะนำให้ใช้evalแต่ฉันคิดว่ามันปลอดภัยกว่าเดิม ความจริงที่ว่าคุณต้องใช้ความพยายามอย่างมากเพื่อให้มันทำงานได้เป็นข้อพิสูจน์ว่าคุณควรใช้declareหรือตั้งชื่อการอ้างอิงแทน
chepner

ที่จริงแล้วในความเห็นของฉันการอ้างอิงชื่อเป็นปัญหา วิธีที่ดีที่สุดที่จะใช้มัน - ในประสบการณ์ของฉัน - เป็นเหมือน ... set -- a bunch of args; eval "process2 $(process1 "$@")"ที่เพียงตัวเลขจะพิมพ์ที่ยกมาเช่นprocess1 "${1}" "${8}" "${138}"นั่นมันง่ายมาก - และง่ายเหมือน'"${'$((i=$i+1))'}" 'ในกรณีส่วนใหญ่ การจัดทำดัชนีการอ้างอิงให้ปลอดภัยและมีประสิทธิภาพและรวดเร็ว ยัง - ฉัน upvoted
mikeserv

4

วิธีที่ดีในการทำงานกับevalคือแทนที่ด้วยechoสำหรับการทดสอบ echoและevalทำงานเหมือนเดิม (ถ้าเราแยกการ\xขยายที่ดำเนินการโดยechoการใช้งานบางอย่างเช่นbash'ภายใต้เงื่อนไขบางอย่าง)

ทั้งสองคำสั่งเข้าร่วมการโต้เถียงกับหนึ่งช่องว่างในระหว่าง ความแตกต่างคือecho แสดงผลในขณะที่eval ประเมิน / ตีความเป็นรหัสเชลล์ผลลัพธ์

ดังนั้นเพื่อดูว่ารหัสเปลือก

eval $(echo $var_name=$var_value)

จะประเมินคุณสามารถเรียกใช้:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

นั่นไม่ใช่สิ่งที่คุณต้องการสิ่งที่คุณต้องการคือ:

fruit=$var_value

นอกจากนี้การใช้$(echo ...)ที่นี่ก็ไม่สมเหตุสมผล

ในการแสดงผลด้านบนคุณจะต้องเรียกใช้:

$ echo "$var_name=\$var_value"
fruit=$var_value

ดังนั้นในการตีความมันนั่นเป็นเพียง:

eval "$var_name=\$var_value"

โปรดทราบว่ามันยังสามารถใช้เพื่อตั้งค่าองค์ประกอบอาร์เรย์แต่ละรายการ:

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

อย่างที่คนอื่นพูดถ้าคุณไม่สนใจรหัสที่bashเจาะจงคุณสามารถใช้declareเป็น:

declare "$var_name=$var_value"

อย่างไรก็ตามโปรดทราบว่ามันมีผลข้างเคียงบางอย่าง

มัน จำกัด ขอบเขตของตัวแปรไว้ที่ฟังก์ชั่นที่มันทำงานอยู่ดังนั้นคุณไม่สามารถใช้มันเป็นตัวอย่างในสิ่งต่าง ๆ เช่น:

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

เพราะนั่นจะประกาศfooตัวแปรท้องถิ่นเพื่อsetvarดังนั้นจะไร้ประโยชน์

bash-4.2เพิ่ม-gตัวเลือกสำหรับdeclareการประกาศตัวแปรทั่วโลกแต่นั่นไม่ใช่สิ่งที่เราต้องการอย่างใดอย่างหนึ่งตามที่เราsetvarจะกำหนดvar ทั่วโลกเมื่อเทียบกับที่ของผู้โทรถ้าโทรเป็นฟังก์ชั่นเช่นใน:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

ซึ่งจะเอาท์พุท:

1:
2: some value

นอกจากนี้โปรดทราบว่าในขณะที่declareเรียกว่าdeclare(จริง ๆ แล้วbashยืมแนวคิดจากtypesetbuiltin ของ Korn เชลล์) ถ้าตัวแปรถูกตั้งค่าไว้แล้วdeclareจะไม่ประกาศตัวแปรใหม่และวิธีการมอบหมายนั้นขึ้นอยู่กับประเภทของตัวแปร

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

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

จะผลิตได้ผลที่แตกต่างกัน (และอาจมีผลข้างเคียงที่น่ารังเกียจ) ถ้าvarnameมีการประกาศก่อนหน้านี้ว่าเป็นสเกลาร์ , อาเรย์หรืออาเรย์


2
มีอะไรผิดปกติกับการทุบตีเฉพาะ? OP ใส่แท็ก bash กับคำถามดังนั้นเขาจึงใช้ bash การให้ทางเลือกนั้นเป็นสิ่งที่ดี แต่ฉันคิดว่าการบอกใครบางคนว่าไม่ควรใช้คุณสมบัติของเปลือกเพราะมันไม่ใช่แบบพกพามันไร้สาระ
Patrick

@ แพทริคเห็นรอยยิ้ม? ต้องบอกว่าการใช้ไวยากรณ์แบบพกพาหมายถึงความพยายามน้อยลงเมื่อคุณต้องการย้ายรหัสของคุณไปยังระบบอื่นที่bashไม่สามารถใช้งานได้ (หรือเมื่อคุณรู้ว่าคุณต้องการเชลล์ที่ดีกว่า / เร็วกว่า) evalไวยากรณ์การทำงานในทุกบอร์นเหมือนเปลือกหอยและเป็น POSIX ดังนั้นระบบทั้งหมดจะมีshว่าที่งาน (นั่นก็หมายความว่าคำตอบของฉันใช้ได้กับทุกเชลล์และไม่ช้าก็เร็วคุณจะเห็นคำถามเฉพาะที่ไม่ใช้การทุบตีปิดซ้ำกับคำถามนี้
Stéphane Chazelas

แต่ถ้า$var_nameมีโทเค็นล่ะ ... ชอบ;ไหม
mikeserv

@mikeserv แล้วไม่ใช่ชื่อตัวแปร หากคุณไม่สามารถไว้วางใจเนื้อหาของมันแล้วคุณจะต้อง sanitize กับทั้งสองevalและdeclare(คิดPATH, TMOUT, PS4, SECONDS... )
Stéphane Chazelas

แต่ในครั้งแรกที่ผ่านมันมักจะขยายตัวของตัวแปรและไม่เคยชื่อตัวแปรจนกระทั่งที่สอง ในคำตอบของฉันฉันฆ่าเชื้อด้วยการขยายตัวพารามิเตอร์ แต่ถ้าคุณอ้างว่าทำฆ่าเชื้อใน subshell ในรหัสผ่านแรกที่สามารถรวมทั้งจะทำ portably w export/ ฉันไม่ทำตามบิตที่เกี่ยวกับผู้ปกครองในตอนท้ายว่า
mikeserv

1

ถ้าคุณทำ:

eval "$name=\$val"

... และ$nameมี;- หรือโทเค็นอื่น ๆ ของเชลล์อาจตีความว่าเป็นการ จำกัด คำสั่งง่ายๆ - นำหน้าด้วยไวยากรณ์เชลล์ที่เหมาะสมซึ่งจะถูกดำเนินการ

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

เอาท์พุท

hi
be careful with eval

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

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

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

อย่างไรก็ตามคำจำกัดความภายในใช้aliasสำหรับ_$nmและนี่คือเพื่อให้แน่ใจว่าไม่มีการเขียนค่าสภาพแวดล้อมที่สำคัญ ฉันไม่รู้ค่าของสภาพแวดล้อมที่สำคัญที่เริ่มต้นด้วย a _และมันมักจะเป็นทางออกที่ปลอดภัยสำหรับการประกาศแบบกึ่งส่วนตัว

อย่างไรก็ตามหากaliasคำจำกัดความประสบความสำเร็จก็จะประกาศaliasชื่อสำหรับ$nmค่าของ และevalจะเรียกว่าaliasหากไม่ได้ขึ้นต้นด้วยตัวเลข - evalเท่านั้นจะได้รับอาร์กิวเมนต์เป็นโมฆะเท่านั้น ดังนั้นหากทั้งสองเงื่อนไขตรงกับการevalเรียกนามแฝงและคำจำกัดความของตัวแปรที่บันทึกไว้ในนามแฝงจะทำหลังจากที่aliasมีการลบสิ่งใหม่ออกจากตารางแฮชทันที


;ไม่อนุญาตในชื่อตัวแปร หากคุณไม่สามารถควบคุมเนื้อหาของได้$nameคุณจะต้องทำให้ถูกสุขลักษณะสำหรับexport/ declareเช่นกัน ในขณะที่exportไม่ได้รันโค้ด, การตั้งค่าตัวแปรบางอย่างเช่นPATH, PS4และจำนวนของผู้ที่info -f bash -n 'Bash Variables'มีผลข้างเคียงที่เป็นอันตรายอย่างเท่าเทียมกัน
Stéphane Chazelas

@ StéphaneChazelas - แน่นอนว่ามันไม่ได้รับอนุญาต แต่อย่างที่ผ่านมามันไม่ได้เป็นชื่อตัวแปรในการevalผ่านครั้งแรกของมัน - มันเป็นการขยายตัวของตัวแปร อย่างที่คุณพูดในที่อื่น ๆ ในบริบทนั้นอนุญาตอย่างมาก ข้อโต้แย้ง $ PATH ยังคงเป็นสิ่งที่ดีมาก - ฉันทำการแก้ไขเล็กน้อยและจะเพิ่มในภายหลัง
mikeserv

@ StéphaneChazelas - ดีกว่าไม่สาย ...
mikeserv

ในทางปฏิบัติzsh, pdksh, mksh, ไม่ได้บ่นเกี่ยวกับyash unset 'a;b'
Stéphane Chazelas

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