วิธีการจัดเก็บคำสั่งในตัวแปรในเชลล์สคริปต์


114

ฉันต้องการเก็บคำสั่งที่จะใช้ในช่วงเวลาต่อมาในตัวแปร (ไม่ใช่ผลลัพธ์ของคำสั่ง แต่เป็นคำสั่งเอง)

ฉันมีสคริปต์ง่ายๆดังนี้:

command="ls";
echo "Command: $command"; #Output is: Command: ls

b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)

อย่างไรก็ตามเมื่อฉันลองทำอะไรที่ซับซ้อนขึ้นเล็กน้อยมันก็ล้มเหลว ตัวอย่างเช่นถ้าฉันทำ

command="ls | grep -c '^'";

ผลลัพธ์คือ:

Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory

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


10
ใช้ฟังก์ชัน!
gniourf_gniourf

คำตอบ:


146

ใช้ eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"

27
$ ( ... ) backticksขอแนะนำตอนนี้แทน y = $ (eval $ x) mywiki.wooledge.org/BashFAQ/082
James Broadhead

14
evalเป็นแนวทางปฏิบัติที่ยอมรับได้ก็ต่อเมื่อคุณเชื่อถือเนื้อหาของตัวแปรของคุณ หากคุณกำลังใช้งานอยู่ให้พูดx="ls $name | wc"(หรือแม้กระทั่งx="ls '$name' | wc") รหัสนี้จะเป็นช่องโหว่ในการแทรกหรือเพิ่มสิทธิ์อย่างรวดเร็วหากผู้ที่มีสิทธิ์น้อยกว่าตัวแปรนั้นสามารถตั้งค่าได้ (เช่นการวนซ้ำในไดเรกทอรีย่อยทั้งหมดใน/tmpหรือไม่คุณควรไว้วางใจผู้ใช้ทุกคนในระบบว่าจะไม่เรียกใคร$'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/')
Charles Duffy

9
evalเป็นแม่เหล็กดักฟังขนาดใหญ่ที่ไม่ควรแนะนำโดยไม่มีคำเตือนเกี่ยวกับความเสี่ยงของพฤติกรรมการแยกวิเคราะห์ที่ไม่คาดคิด (แม้ว่าจะไม่มีสายอักขระที่เป็นอันตรายก็ตามเช่นในตัวอย่างของ @ CharlesDuffy) ตัวอย่างเช่นลองแล้วx='echo $(( 6 * 7 ))' eval $xคุณอาจคาดหวังว่าจะพิมพ์ "42" แต่อาจไม่เป็นเช่นนั้น คุณช่วยอธิบายได้ไหมว่าทำไมถึงใช้ไม่ได้? คุณอธิบายได้ไหมว่าทำไมฉันถึงพูดว่า "น่าจะ" evalถ้าคำตอบของคำถามเหล่านี้จะไม่ชัดเจนกับคุณคุณไม่ควรสัมผัส
Gordon Davisson

1
@ นักเรียนลองรันset -xก่อนเพื่อบันทึกคำสั่งที่รันซึ่งจะทำให้ง่ายต่อการดูว่าเกิดอะไรขึ้น
Charles Duffy

1
@ นักเรียนฉันขอแนะนำให้ใช้shellcheck.netเพื่อชี้ให้เห็นข้อผิดพลาดทั่วไป (และนิสัยที่ไม่ดีที่คุณไม่ควรรับ)
Gordon Davisson

41

ทำไม่ได้ใช้eval! มีความเสี่ยงอย่างมากในการแนะนำการใช้รหัสโดยอำเภอใจ

ทุบตี - ฉันพยายามใส่คำสั่งในตัวแปร แต่กรณีที่ซับซ้อนมักจะล้มเหลวเสมอ

วางไว้ในอาร์เรย์และขยายคำทั้งหมดที่มีราคาสองครั้ง"${arr[@]}"เพื่อไม่ให้IFSแยกคำที่เกิดจากการแยกโปรแกรม Word

cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')

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

declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'

และดำเนินการคำสั่งเป็น

"${cmdArgs[@]}"
23:15:18

(หรือ) ใช้bashฟังก์ชันเพื่อเรียกใช้คำสั่งร่วมกัน

cmd() {
   date '+%H:%M:%S'
}

และเรียกใช้ฟังก์ชันเป็นเพียง

cmd

POSIX shไม่มีอาร์เรย์ดังนั้นสิ่งที่ใกล้เคียงที่สุดที่คุณสามารถทำได้คือการสร้างรายการองค์ประกอบในพารามิเตอร์ตำแหน่ง นี่คือshวิธีPOSIX ในการเรียกใช้โปรแกรมเมล

# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
    subject=$1
    shift
    first=1
    for addr; do
        if [ "$first" = 1 ]; then set --; first=0; fi
        set -- "$@" --recipient="$addr"
    done
    if [ "$first" = 1 ]; then
        echo "usage: sendto subject address [address ...]"
        return 1
    fi
    MailTool --subject="$subject" "$@"
}

โปรดทราบว่าวิธีนี้สามารถจัดการได้เฉพาะคำสั่งธรรมดาที่ไม่มีการเปลี่ยนเส้นทาง ไม่สามารถจัดการการเปลี่ยนเส้นทางไปป์ไลน์สำหรับ / while ลูปคำสั่ง if ฯลฯ

การใช้งานทั่วไปอีกกรณีหนึ่งคือเมื่อรันcurlกับฟิลด์ส่วนหัวและเพย์โหลดหลายฟิลด์ คุณสามารถกำหนด args ดังต่อไปนี้และเรียกใช้curlเนื้อหาอาร์เรย์ที่ขยายได้

curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[@]}"

ตัวอย่างอื่น,

payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'

เมื่อกำหนดตัวแปรแล้วให้ใช้อาร์เรย์เพื่อจัดเก็บ args คำสั่งของคุณ

curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")

และตอนนี้ทำการขยายคำพูดที่เหมาะสม

curl "${curlCMD[@]}"

สิ่งนี้ไม่ได้ผลสำหรับฉันฉันได้ลองCommand=('echo aaa | grep a')แล้วและ"${Command[@]}"หวังว่ามันจะทำงานตามคำสั่งecho aaa | grep aอย่างแท้จริง มันไม่ ฉันสงสัยว่ามีวิธีเปลี่ยนที่ปลอดภัยหรือไม่evalแต่ดูเหมือนว่าวิธีแก้ปัญหาแต่ละอย่างที่มีแรงเท่ากันevalอาจเป็นอันตรายได้ ไม่ใช่เหรอ?
นักศึกษา

ในระยะสั้นสิ่งนี้จะทำงานอย่างไรหากสตริงเดิมมีไปป์ "|"
นักเรียน

@ นักเรียนถ้าสตริงเดิมของคุณมีไพพ์สตริงนั้นจะต้องผ่านส่วนที่ไม่ปลอดภัยของตัวแยกวิเคราะห์ bash เพื่อเรียกใช้งานเป็นโค้ด อย่าใช้สตริงในกรณีนั้น ใช้ฟังก์ชันแทน: Command() { echo aaa | grep a; }- หลังจากนั้นคุณสามารถเรียกใช้Commandหรือresult=$(Command)หรือสิ่งที่คล้ายกันได้
Charles Duffy

1
@ นักศึกษาครับ; แต่ที่ไม่เจตนาเพราะสิ่งที่คุณขอที่จะทำคือเนื้อแท้ไม่ปลอดภัย
Charles Duffy

1
@ นักเรียน: ฉันได้เพิ่มบันทึกไว้ในตอนท้ายเพื่อพูดถึงมันไม่ทำงานภายใต้เงื่อนไขบางประการ
Inian

26
var=$(echo "asdf")
echo $var
# => asdf

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

stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015

เช่นเดียวกับ backtick

stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015

การใช้ eval ใน$(...)จะไม่ทำให้ประเมินในภายหลัง

stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015

การใช้ eval จะถูกประเมินเมื่อevalมีการใช้งาน

stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
#                     ^^ Time changed

ในตัวอย่างข้างต้นหากคุณต้องการรันคำสั่งที่มีอาร์กิวเมนต์ให้ใส่ไว้ในสตริงที่คุณจัดเก็บ

stored_date="date -u"
# ...

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

  • ขอบคุณ @CharlesDuffy ที่เตือนให้ฉันพูดคำสั่ง!

สิ่งนี้ไม่ได้แก้ปัญหาเดิมที่คำสั่งมีไพพ์ '|'
นักศึกษา

@Nate โปรดทราบว่าeval $stored_dateอาจใช้ได้ดีเมื่อstored_dateมีเพียงdateแต่eval "$stored_date"มีความน่าเชื่อถือกว่ามาก เรียกใช้str=$'printf \' * %s\\n\' *'; eval "$str"โดยไม่มีเครื่องหมายคำพูดรอบสุดท้าย"$str"สำหรับตัวอย่าง :)
Charles Duffy

@CharlesDuffy ขอบคุณฉันลืมเกี่ยวกับการอ้างอิง ฉันจะพนันได้เลยว่าเศษของฉันจะบ่นถ้าฉันใส่ใจที่จะเรียกใช้
เนท

1

สำหรับ bash ให้เก็บคำสั่งของคุณดังนี้:

command="ls | grep -c '^'"

เรียกใช้คำสั่งของคุณดังนี้:

echo $command | bash

1
ไม่แน่ใจ แต่บางทีวิธีการเรียกใช้คำสั่งนี้อาจมีความเสี่ยงเช่นเดียวกับการใช้ 'eval'
Derek Hazell

0

ฉันลองใช้วิธีต่างๆมากมาย:

printexec() {
  printf -- "\033[1;37m$\033[0m"
  printf -- " %q" "$@"
  printf -- "\n"
  eval -- "$@"
  eval -- "$*"
  "$@"
  "$*"
}

เอาท์พุต:

$ printexec echo  -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
 bar
bash: echo -e foo\n bar: command not found

อย่างที่คุณเห็นมีเพียงอันที่สามเท่านั้นที่"$@"ให้ผลลัพธ์ที่ถูกต้อง


0

ระมัดระวังในการลงทะเบียนคำสั่งซื้อด้วย: X=$(Command)

อันนี้ยังคงถูกดำเนินการก่อนที่จะถูกเรียก ในการตรวจสอบและยืนยันสิ่งนี้คุณต้องทำ:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed

-1
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.

TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT                         #This will return 0 
                                    #if you don't have tune0 interface.
                                    #Or count of installed tune0 interfaces.

-8

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


1
คำสั่งที่ฉันจะจัดเก็บจะขึ้นอยู่กับตัวเลือกที่ฉันส่งเข้ามาดังนั้นแทนที่จะมีคำสั่งเงื่อนไขจำนวนมากในโปรแกรมจำนวนมากของฉันมันง่ายกว่ามากในการจัดเก็บคำสั่งที่ฉันต้องการเพื่อใช้ในภายหลัง
Benjamin

1
@Benjamin อย่างน้อยเก็บตัวเลือกเป็นตัวแปรไม่ใช่คำสั่ง เช่นvar='*.txt'; find . -name "$var"
kurumi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.