Bash Templating: จะสร้างไฟล์คอนฟิกจากเทมเพลตด้วย Bash ได้อย่างไร?


134

ฉันกำลังเขียนสคริปต์เพื่อสร้างไฟล์กำหนดค่าอัตโนมัติสำหรับ Apache และ PHP สำหรับเว็บเซิร์ฟเวอร์ของฉันเอง ฉันไม่ต้องการใช้ GUI ใด ๆ เช่น CPanel หรือ ISPConfig

ฉันมีเทมเพลตของไฟล์กำหนดค่า Apache และ PHP สคริปต์ Bash จำเป็นต้องอ่านเทมเพลตทำการแทนที่ตัวแปรและเทมเพลตที่แยกวิเคราะห์เอาต์พุตในบางโฟลเดอร์ วิธีที่ดีที่สุดคืออะไร? ฉันคิดได้หลายวิธี วิธีไหนดีที่สุดหรืออาจมีวิธีที่ดีกว่านั้นบ้าง ฉันต้องการทำแบบนั้นด้วย Bash ล้วนๆ (ตัวอย่างเช่น PHP ทำได้ง่าย)

1) จะแทนที่ตัวยึดตำแหน่ง $ {} ในไฟล์ข้อความได้อย่างไร

template.txt:

the number is ${i}
the word is ${word}

script.sh:

#!/bin/sh

#set variables
i=1
word="dog"
#read in template one line at the time, and replace variables
#(more natural (and efficient) way, thanks to Jonathan Leffler)
while read line
do
    eval echo "$line"
done < "./template.txt"

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

2) การใช้ cat & sed เพื่อแทนที่ตัวแปรแต่ละตัวด้วยค่า:

ให้ template.txt:

The number is ${i}
The word is ${word}

คำสั่ง:

cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"

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

คุณนึกถึงโซลูชันที่หรูหราและปลอดภัยอื่น ๆ ได้หรือไม่?


คำตอบ:


62

คุณสามารถใช้สิ่งนี้:

perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < template.txt

เพื่อแทนที่${...}สตริงทั้งหมดด้วยตัวแปรสภาพแวดล้อมที่สอดคล้องกัน (อย่าลืมส่งออกก่อนที่จะเรียกใช้สคริปต์นี้)

สำหรับการทุบตีที่แท้จริงสิ่งนี้ควรใช้งานได้ (สมมติว่าตัวแปรไม่มีสตริง $ {... }):

#!/bin/bash
while read -r line ; do
    while [[ "$line" =~ (\$\{[a-zA-Z_][a-zA-Z_0-9]*\}) ]] ; do
        LHS=${BASH_REMATCH[1]}
        RHS="$(eval echo "\"$LHS\"")"
        line=${line//$LHS/$RHS}
    done
    echo "$line"
done

. วิธีแก้ปัญหาที่ไม่ค้างหาก RHS อ้างอิงตัวแปรบางตัวที่อ้างอิงตัวเอง:

#!/bin/bash
line="$(cat; echo -n a)"
end_offset=${#line}
while [[ "${line:0:$end_offset}" =~ (.*)(\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})(.*) ]] ; do
    PRE="${BASH_REMATCH[1]}"
    POST="${BASH_REMATCH[4]}${line:$end_offset:${#line}}"
    VARNAME="${BASH_REMATCH[3]}"
    eval 'VARVAL="$'$VARNAME'"'
    line="$PRE$VARVAL$POST"
    end_offset=${#PRE}
done
echo -n "${line:0:-1}"

คำเตือน : ฉันไม่รู้วิธีจัดการอินพุตด้วย NUL อย่างถูกต้องในการทุบตีหรือรักษาจำนวนของการขึ้นบรรทัดใหม่ต่อท้าย ตัวแปรสุดท้ายถูกนำเสนอเนื่องจากเชลล์ "รัก" อินพุตไบนารี:

  1. read จะตีความเครื่องหมายแบ็กสแลช
  2. read -r จะไม่ตีความเครื่องหมายแบ็กสแลช แต่จะยังคงวางบรรทัดสุดท้ายหากไม่ได้ลงท้ายด้วยขึ้นบรรทัดใหม่
  3. "$(…)"จะตัดบรรทัดใหม่ต่อท้ายให้มากที่สุดเท่าที่มีอยู่ดังนั้นฉันจึงลงท้ายด้วย; echo -n aและใช้echo -n "${line:0:-1}": สิ่งนี้จะลดอักขระสุดท้าย (ซึ่งก็คือa) และรักษาบรรทัดใหม่ต่อท้ายให้มากที่สุดเท่าที่มีในอินพุต (รวมถึงไม่)

3
ฉันจะเปลี่ยน[^}]เป็น[A-Za-Z_][A-Za-z0-9_]ในเวอร์ชัน bash เพื่อป้องกันไม่ให้เชลล์เกินกว่าการทดแทนที่เข้มงวด (เช่นหากพยายามประมวลผล${some_unused_var-$(rm -rf $HOME)})
Chris Johnsen

2
@FractalizeR คุณอาจต้องการเปลี่ยน$&วิธีแก้ปัญหา perl เพื่อ"": ใบแรก${...}ไม่ถูกแตะต้องหากมันเปลี่ยนเป็นสิ่งที่สองแทนที่ด้วยสตริงว่าง
ZyX

5
หมายเหตุ: เห็นได้ชัดว่ามีการเปลี่ยนแปลงจาก bash 3.1 เป็น 3.2 (ขึ้นไป) ซึ่งเครื่องหมายคำพูดเดียวรอบ regex - ถือว่าเนื้อหาของ regex เป็นสตริงตามตัวอักษร ดังนั้น regex ด้านบนควรเป็น ... (\ $ \ {[a-zA-Z _] [a-zA-Z_0-9] * \}) stackoverflow.com/questions/304864/…
Blue Waters

2
ที่จะทำให้ห่วงอ่านบรรทัดสุดท้ายถึงแม้ว่ามันจะไม่ได้ยกเลิกโดยการขึ้นบรรทัดใหม่ใช้while while read -r line || [[ -n $line ]]; doนอกจากนี้readคำสั่งของคุณจะตัดช่องว่างที่นำหน้าและต่อท้ายจากแต่ละบรรทัด เพื่อหลีกเลี่ยงสิ่งนั้นให้ใช้while IFS= read -r line || [[ -n $line ]]; do
mklement0

2
เพียงเพื่อสังเกตข้อ จำกัด สำหรับผู้ที่กำลังมองหาโซลูชันที่ครอบคลุม: โซลูชันที่มีประโยชน์เหล่านี้ไม่อนุญาตให้คุณเลือกป้องกันการอ้างอิงตัวแปรจากการขยาย (เช่นโดยการ\ เว้นวรรค)
mklement0

139

ลอง envsubst

FOO=foo
BAR=bar
export FOO BAR

envsubst <<EOF
FOO is $FOO
BAR is $BAR
EOF

12
เพื่อการอ้างอิงเท่านั้นenvsubstไม่จำเป็นต้องใช้เมื่อใช้ heredoc เนื่องจาก bash ถือว่า heredoc เป็นสตริงที่ยกมาสองครั้งตามตัวอักษรและแก้ไขตัวแปรในนั้นอยู่แล้ว เป็นทางเลือกที่ดีเมื่อคุณต้องการอ่านเทมเพลตจากไฟล์อื่น m4ทดแทนที่ดีสำหรับยุ่งยากมากขึ้น
beporter

2
ฉันรู้สึกประหลาดใจมากที่ได้เรียนรู้เกี่ยวกับคำสั่งนี้ ฉันพยายามขยายฟังก์ชันการทำงานของ envsubst ด้วยตนเองโดยไม่ประสบความสำเร็จ ขอบคุณ yottatsa!
Tim Stewart

4
หมายเหตุ: envsubstเป็นยูทิลิตี้ GNU gettext และไม่ได้มีประสิทธิภาพทั้งหมด (เนื่องจาก gettext มีไว้สำหรับการแปลข้อความของมนุษย์) ที่สำคัญที่สุดคือไม่รู้จักการแทนที่ $ {VAR} ที่ใช้เครื่องหมายแบ็กสแลช (ดังนั้นคุณจึงไม่สามารถมีเทมเพลตที่ใช้การแทนที่ $ VAR ในรันไทม์ได้เช่นเชลล์สคริปต์หรือไฟล์ Nginx conf) ดูคำตอบของฉันสำหรับวิธีแก้ปัญหาที่จัดการกับแบ็กสแลช
Stuart P. Bentley

4
@beporter ในกรณีนี้หากคุณต้องการส่งเทมเพลตนี้ไปยัง envsubst ด้วยเหตุผลบางประการคุณต้องการใช้<<"EOF"ซึ่งไม่ได้แก้ไขตัวแปร
Stuart P. Bentley

2
ฉันใช้มันเช่น: cat template.txt | envsubst
truthadjustr

47

envsubst เป็นเรื่องใหม่สำหรับฉัน น่าอัศจรรย์

สำหรับเร็กคอร์ดการใช้ heredoc เป็นวิธีที่ดีในการสร้างเทมเพลตไฟล์ conf

STATUS_URI="/hows-it-goin";  MONITOR_IP="10.10.2.15";

cat >/etc/apache2/conf.d/mod_status.conf <<EOF
<Location ${STATUS_URI}>
    SetHandler server-status
    Order deny,allow
    Deny from all
    Allow from ${MONITOR_IP}
</Location>
EOF

1
ฉันชอบสิ่งนี้ดีกว่าenvsubstเพราะมันบันทึกของฉันจากไฟล์เพิ่มเติมapt-get install gettext-baseใน Dockerfile ของฉัน
truthadjustr

เชลล์เป็นสคริปต์ที่เหมือนเทมเพลตอย่างไรก็ตามไม่มีการติดตั้งไลบรารีภายนอกหรือความเครียดจากการรับมือกับนิพจน์ที่ยุ่งยาก
千木郷

36

ฉันเห็นด้วยกับการใช้ sed: เป็นเครื่องมือที่ดีที่สุดสำหรับการค้นหา / แทนที่ นี่คือแนวทางของฉัน:

$ cat template.txt
the number is ${i}
the dog's name is ${name}

$ cat replace.sed
s/${i}/5/
s/${name}/Fido/

$ sed -f replace.sed template.txt > out.txt

$ cat out.txt
the number is 5
the dog's name is Fido

1
สิ่งนี้ต้องการไฟล์ชั่วคราวสำหรับสตริงการทดแทนใช่ไหม มีวิธีดำเนินการโดยไม่มีไฟล์ชั่วคราวหรือไม่?
Vladislav Rastrusny

@FractalizeR: sedบางเวอร์ชันมี-iตัวเลือก (แก้ไขไฟล์ในสถานที่) ที่คล้ายกับตัวเลือกperl ตรวจสอบ manpage สำหรับคุณsed
Chris Johnsen

@FractalizeR ใช่ sed -i จะแทนที่อินไลน์ หากคุณพอใจกับ Tcl (ภาษาสคริปต์อื่น) โปรดดูหัวข้อนี้: stackoverflow.com/questions/2818130/…
Hai Vu

ฉันสร้าง replace.sed จาก propertyfiles โดยใช้คำสั่ง sed ต่อไปนี้: sed -e 's / ^ / s \ / $ {/ g' -e 's / = /} \ // g' -e 's / $ / \ // g 'the.properties> replace.sed
Jaap D

รหัสของ @hai vu สร้างโปรแกรม sed และส่งผ่านโปรแกรมนั้นโดยใช้แฟล็ก sed's -f หากคุณต้องการคุณสามารถส่งผ่านแต่ละบรรทัดของโปรแกรม sed ไปยัง sed โดยใช้แฟล็ก -e FWIW ฉันชอบแนวคิดในการใช้ sed สำหรับเทมเพลต
Andrew Allbright

23

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

$ cat template.txt
variable1 = ${variable1}
variable2 = $variable2
my-ip = \"$(curl -s ifconfig.me)\"

$ echo $variable1
AAA
$ echo $variable2
BBB
$ eval "echo \"$(<template.txt)\"" 2> /dev/null
variable1 = AAA
variable2 = BBB
my-ip = "11.22.33.44"

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

นอกจากนี้คุณยังสามารถใช้เอกสารที่นี่ถ้าคุณต้องการcatที่จะecho

$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null

@plockc ได้สร้างโซลูชันที่หลีกเลี่ยงปัญหาการอ้างสิทธิ์ทุบตี:

$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

แก้ไข:ลบส่วนที่เกี่ยวกับการเรียกใช้สิ่งนี้ในฐานะรูทโดยใช้ sudo ...

แก้ไข:เพิ่มความคิดเห็นเกี่ยวกับการใช้เครื่องหมายคำพูดเพิ่มวิธีแก้ปัญหาของ plockc ในการผสม!


สิ่งนี้จะตัดเครื่องหมายคำพูดที่คุณมีอยู่ในเทมเพลตของคุณและจะไม่แทนที่ภายในเครื่องหมายคำพูดเดียวดังนั้นอาจทำให้เกิดข้อบกพร่องเล็กน้อยขึ้นอยู่กับรูปแบบเทมเพลตของคุณ สิ่งนี้อาจใช้ได้กับวิธีการเทมเพลตแบบ Bash ใด ๆ ก็ตาม
Alex B

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

@AlexB: วิธีการนี้จะแทนที่ระหว่างเครื่องหมายคำพูดเดี่ยวเนื่องจากเป็นเพียงอักขระตามตัวอักษรภายในสตริงที่มีเครื่องหมายอัญประกาศคู่ที่แนบมาแทนที่จะเป็นตัวคั่นสตริงเมื่อevaled echo / catคำสั่งประมวลผล ลองeval "echo \"'\$HOME'\"".
mklement0

21

ฉันมีวิธีแก้ปัญหาทุบตีเช่น mogsie แต่ใช้ heredoc แทนการต่อท้ายเพื่อให้คุณหลีกเลี่ยงการหลีกเลี่ยงเครื่องหมายคำพูดคู่

eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null

4
โซลูชันนี้รองรับการขยายพารามิเตอร์ Bashในเทมเพลต รายการโปรดของฉันเป็นพารามิเตอร์ที่ต้องการพร้อม${param:?}และการซ้อนข้อความรอบ ๆพารามิเตอร์ที่เป็นทางเลือก ตัวอย่าง: ${DELAY:+<delay>$DELAY</delay>}ขยายเป็นค่าว่างเมื่อ DELAY ไม่ได้กำหนดและ <delay> 17 </delay> เมื่อ DELAY = 17
Eric Bolinger

2
Oh! และคั่น EOF สามารถใช้สตริงแบบไดนามิกเช่น _EOF_$$PID
Eric Bolinger

1
@ mklement0 วิธีแก้ปัญหาสำหรับการลากเส้นใหม่คือการใช้ส่วนขยายบางอย่างเช่นตัวแปรว่าง$trailing_newlineหรือใช้$NL5และตรวจสอบให้แน่ใจว่าได้ขยายเป็น 5 บรรทัดใหม่
xebeche

@xebeche: ใช่การวางสิ่งที่คุณแนะนำที่ปลายสุดภายในtemplate.txtจะทำงานเพื่อที่จะรักษาบรรทัดใหม่ต่อท้าย
mklement0

1
วิธีแก้ปัญหาที่สวยงาม แต่โปรดทราบว่าการแทนที่คำสั่งจะตัดบรรทัดใหม่ที่ต่อท้ายออกจากไฟล์อินพุตแม้ว่าโดยทั่วไปแล้วจะไม่เป็นปัญหาก็ตาม อีกกรณีหนึ่งของขอบ: เนื่องจากการใช้งานevalหากtemplate.txtมีEOFอยู่ในบรรทัดของตัวเองมันจะยกเลิก here-doc ก่อนกำหนดและทำให้คำสั่งแตก (เคล็ดลับของหมวกเพื่อ @xebeche)
mklement0

18

แก้ไข 6 ม.ค. 2017

ฉันต้องการเก็บเครื่องหมายคำพูดคู่ไว้ในไฟล์การกำหนดค่าของฉันดังนั้นการหลีกเลี่ยงเครื่องหมายคำพูดคู่สองครั้งด้วย sed ช่วย:

render_template() {
  eval "echo \"$(sed 's/\"/\\\\"/g' $1)\""
}

ฉันไม่สามารถคิดที่จะลากเส้นใหม่ต่อไปได้ แต่จะมีการเว้นบรรทัดว่างไว้ระหว่างนั้น


แม้ว่าจะเป็นหัวข้อเก่า แต่ IMO ฉันพบวิธีแก้ปัญหาที่หรูหรากว่าที่นี่: http://pempek.net/articles/2013/07/08/bash-sh-as-template-engine/

#!/bin/sh

# render a template configuration file
# expand variables + preserve formatting
render_template() {
  eval "echo \"$(cat $1)\""
}

user="Gregory"
render_template /path/to/template.txt > path/to/configuration_file

เครดิตทั้งหมดในGrégory Pakosz


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

2
ฉันต้องการแบ็กสแลชน้อยลงสองตัวเพื่อให้มันใช้งานได้นั่นคือ eval "echo \"$(sed 's/\"/\\"/g' $1)\""
David Bau

น่าเสียดายที่วิธีนี้ไม่อนุญาตให้คุณสร้างไฟล์ php (มี$variables)
IStranger

10

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

หากบน mac ตรวจสอบให้แน่ใจว่าคุณมีhomebrewจากนั้นเชื่อมโยงจาก gettext:

brew install gettext
brew link --force gettext

./template.cfg

# We put env variables into placeholders here
this_variable_1 = ${SOME_VARIABLE_1}
this_variable_2 = ${SOME_VARIABLE_2}

./.env:

SOME_VARIABLE_1=value_1
SOME_VARIABLE_2=value_2

./configure.sh

#!/bin/bash
cat template.cfg | envsubst > whatever.cfg

ตอนนี้ใช้มัน:

# make script executable
chmod +x ./configure.sh
# source your variables
. .env
# export your variables
# In practice you may not have to manually export variables 
# if your solution depends on tools that utilise .env file 
# automatically like pipenv etc. 
export SOME_VARIABLE_1 SOME_VARIABLE_2
# Create your config file
./configure.sh

ลำดับการร้องขอนี้envsubstใช้งานได้จริง
Alex

สำหรับคนอื่นมองenvsubstไม่ทำงานบน MacOS, คุณจะต้องติดตั้งโดยใช้ brew install gettextHomebrew:
Matt

9

คำตอบที่ยอมรับในเวอร์ชันที่ยาวกว่า แต่มีประสิทธิภาพมากขึ้น:

perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})?;substr($1,0,int(length($1)/2)).($2&&length($1)%2?$2:$ENV{$3||$4});eg' template.txt

สิ่งนี้จะขยายอินสแตนซ์ทั้งหมด$VAR หรือ ${VAR}เป็นค่าสภาพแวดล้อม (หรือหากไม่ได้กำหนดสตริงว่าง)

มันหลีกเลี่ยงแบ็กสแลชอย่างถูกต้องและยอมรับ $ ที่หลีกเลี่ยงแบ็กสแลชเพื่อยับยั้งการทดแทน (ไม่เหมือนกับ envsubst ซึ่งปรากฎว่าไม่ทำเช่นนี้ )

ดังนั้นหากสภาพแวดล้อมของคุณเป็น:

FOO=bar
BAZ=kenny
TARGET=backslashes
NOPE=engi

และแม่แบบของคุณคือ:

Two ${TARGET} walk into a \\$FOO. \\\\
\\\$FOO says, "Delete C:\\Windows\\System32, it's a virus."
$BAZ replies, "\${NOPE}s."

ผลลัพธ์จะเป็น:

Two backslashes walk into a \bar. \\
\$FOO says, "Delete C:\Windows\System32, it's a virus."
kenny replies, "${NOPE}s."

หากคุณต้องการหลีกเลี่ยงแบ็กสแลชก่อน $ (คุณสามารถเขียน "C: \ Windows \ System32" ในเทมเพลตได้โดยไม่เปลี่ยนแปลง) ให้ใช้เวอร์ชันที่แก้ไขเล็กน้อยนี้:

perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\});substr($1,0,int(length($1)/2)).(length($1)%2?$2:$ENV{$3||$4});eg' template.txt

8

ฉันทำแบบนี้แล้วอาจมีประสิทธิภาพน้อยกว่า แต่อ่าน / ดูแลรักษาง่ายกว่า

TEMPLATE='/path/to/template.file'
OUTPUT='/path/to/output.file'

while read LINE; do
  echo $LINE |
  sed 's/VARONE/NEWVALA/g' |
  sed 's/VARTWO/NEWVALB/g' |
  sed 's/VARTHR/NEWVALC/g' >> $OUTPUT
done < $TEMPLATE

10
คุณสามารถทำได้โดยไม่ต้องอ่านทีละบรรทัดและมีคำวิงวอนเพียงครั้งเดียว:sed -e 's/VARONE/NEWVALA/g' -e 's/VARTWO/NEWVALB/g' -e 's/VARTHR/NEWVALC/g' < $TEMPLATE > $OUTPUT
Brandon Bloom

8

หากคุณต้องการที่จะใช้Jinja2แม่แบบเห็นโครงการนี้: j2cli

รองรับ:

  • เทมเพลตจากไฟล์ JSON, INI, YAML และอินพุตสตรีม
  • การจำลองจากตัวแปรสภาพแวดล้อม

5

รับคำตอบจาก ZyX โดยใช้ pure bash แต่ด้วยการจับคู่ regex รูปแบบใหม่และการแทนที่พารามิเตอร์ทางอ้อมจะกลายเป็น:

#!/bin/bash
regex='\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}'
while read line; do
    while [[ "$line" =~ $regex ]]; do
        param="${BASH_REMATCH[1]}"
        line=${line//${BASH_REMATCH[0]}/${!param}}
    done
    echo $line
done

5

หากใช้Perlเป็นตัวเลือกและคุณพอใจกับการขยายตัวแปรสภาพแวดล้อมเท่านั้น (ซึ่งตรงข้ามกับตัวแปรเชลล์ทั้งหมด) ให้พิจารณาคำตอบที่ชัดเจนของ Stuart P.Bentley

คำตอบนี้มีวัตถุประสงค์เพื่อให้การแก้ปัญหาทุบตีอย่างเดียวว่า - แม้จะมีการใช้eval- ควรจะมีความปลอดภัยในการใช้งาน

เป้าหมายคือ:

  • รองรับการขยายการอ้างอิงทั้ง${name}และ$nameตัวแปร
  • ป้องกันการขยายอื่น ๆ ทั้งหมด:
    • การแทนที่คำสั่ง ( $(...)และไวยากรณ์เดิม`...`)
    • การแทนที่ทางคณิตศาสตร์ ( $((...))และไวยากรณ์ดั้งเดิม$[...])
  • อนุญาตให้มีการปราบปรามการขยายตัวแปรโดยเลือกขึ้นต้นด้วย\( \${name})
  • เก็บรักษาอักขระพิเศษ ในอินพุตโดยเฉพาะอย่างยิ่ง"และ\อินสแตนซ์
  • อนุญาตให้ป้อนข้อมูลผ่านอาร์กิวเมนต์หรือผ่านทาง stdin

ฟังก์ชันexpandVars() :

expandVars() {
  local txtToEval=$* txtToEvalEscaped
  # If no arguments were passed, process stdin input.
  (( $# == 0 )) && IFS= read -r -d '' txtToEval
  # Disable command substitutions and arithmetic expansions to prevent execution
  # of arbitrary commands.
  # Note that selectively allowing $((...)) or $[...] to enable arithmetic
  # expressions is NOT safe, because command substitutions could be embedded in them.
  # If you fully trust or control the input, you can remove the `tr` calls below
  IFS= read -r -d '' txtToEvalEscaped < <(printf %s "$txtToEval" | tr '`([' '\1\2\3')
  # Pass the string to `eval`, escaping embedded double quotes first.
  # `printf %s` ensures that the string is printed without interpretation
  # (after processing by by bash).
  # The `tr` command reconverts the previously escaped chars. back to their
  # literal original.
  eval printf %s "\"${txtToEvalEscaped//\"/\\\"}\"" | tr '\1\2\3' '`(['
}

ตัวอย่าง:

$ expandVars '\$HOME="$HOME"; `date` and $(ls)'
$HOME="/home/jdoe"; `date` and $(ls)  # only $HOME was expanded

$ printf '\$SHELL=${SHELL}, but "$(( 1 \ 2 ))" will not expand' | expandVars
$SHELL=/bin/bash, but "$(( 1 \ 2 ))" will not expand # only ${SHELL} was expanded
  • ด้วยเหตุผลด้านประสิทธิภาพฟังก์ชันจะอ่านอินพุต stdin ทั้งหมดในหน่วยความจำในครั้งเดียวแต่ง่ายต่อการปรับฟังก์ชันให้เข้ากับวิธีการทีละบรรทัด
  • ยังสนับสนุนการขยายตัวแปรที่ไม่ใช่พื้นฐานเช่น${HOME:0:10}ตราบใดที่ไม่มีคำสั่งฝังตัวหรือการแทนที่ทางคณิตศาสตร์เช่น${HOME:0:$(echo 10)}
    • การแทนที่ที่ฝังไว้ดังกล่าวจริง ๆ แล้ว BREAK ฟังก์ชัน (เนื่องจากอินสแตนซ์ทั้งหมด$(และ`อินสแตนซ์จะถูกหลีกเลี่ยงโดยไม่ได้ตั้งใจ)
    • ในทำนองเดียวกันการอ้างอิงตัวแปรผิดรูปแบบเช่น${HOME(ไม่มีการปิด}) BREAK ฟังก์ชัน
  • เนื่องจากการจัดการสตริงที่ยกมาสองครั้งของ bash จึงมีการจัดการแบ็กสแลชดังนี้:
    • \$name ป้องกันการขยายตัว
    • รายการเดียวที่\ไม่ตามด้วย$จะถูกเก็บรักษาไว้ตามที่เป็นอยู่
    • หากคุณต้องการที่จะเป็นตัวแทนที่อยู่ติดกันหลาย \กรณีคุณจะต้องเป็นสองเท่าของพวกเขา ; เช่น:
      • \\-> \- เช่นเดียวกับ\
      • \\\\ -> \\
    • การป้อนข้อมูลที่ไม่ต้องมีดังต่อไปนี้ตัวละคร (ที่ไม่ค่อยได้ใช้) ซึ่งถูกนำมาใช้เพื่อวัตถุประสงค์ภายใน: 0x1, ,0x20x3
  • มีความกังวลอย่างมากในเชิงสมมุติว่าหาก bash ควรแนะนำไวยากรณ์ส่วนขยายใหม่ฟังก์ชันนี้อาจไม่ป้องกันการขยายดังกล่าว - ดูโซลูชันที่ไม่ได้ใช้ด้านevalล่าง

หากคุณกำลังมองหาวิธีการแก้ปัญหาที่เข้มงวดมากขึ้นซึ่งรองรับเฉพาะการ${name}ขยายเท่านั้นเช่นการใช้เครื่องหมายปีกกาบังคับโดยไม่สนใจ$nameข้อมูลอ้างอิงโปรดดูคำตอบของฉัน


นี่คือโซลูชันbash-only เวอร์ชันปรับปรุงใหม่evalจากคำตอบที่ยอมรับ :

การปรับปรุง ได้แก่ :

  • รองรับการขยายการอ้างอิงทั้ง${name}และ$nameตัวแปร
  • รองรับการ\อ้างอิงตัวแปร -escaping ที่ไม่ควรขยาย
  • ซึ่งแตกต่างจากevalวิธีแก้ปัญหาด้านบน
    • การขยายที่ไม่ใช่พื้นฐานจะถูกละเว้น
    • การอ้างอิงตัวแปรที่ผิดรูปแบบจะถูกละเว้น (ไม่ทำลายสคริปต์)
 IFS= read -d '' -r lines # read all input from stdin at once
 end_offset=${#lines}
 while [[ "${lines:0:end_offset}" =~ (.*)\$(\{([a-zA-Z_][a-zA-Z_0-9]*)\}|([a-zA-Z_][a-zA-Z_0-9]*))(.*) ]] ; do
      pre=${BASH_REMATCH[1]} # everything before the var. reference
      post=${BASH_REMATCH[5]}${lines:end_offset} # everything after
      # extract the var. name; it's in the 3rd capture group, if the name is enclosed in {...}, and the 4th otherwise
      [[ -n ${BASH_REMATCH[3]} ]] && varName=${BASH_REMATCH[3]} || varName=${BASH_REMATCH[4]}
      # Is the var ref. escaped, i.e., prefixed with an odd number of backslashes?
      if [[ $pre =~ \\+$ ]] && (( ${#BASH_REMATCH} % 2 )); then
           : # no change to $lines, leave escaped var. ref. untouched
      else # replace the variable reference with the variable's value using indirect expansion
           lines=${pre}${!varName}${post}
      fi
      end_offset=${#pre}
 done
 printf %s "$lines"

5

นี่เป็นอีกวิธีแก้ปัญหาทุบตีที่บริสุทธิ์:

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

$ cat code

#!/bin/bash
LISTING=$( ls )

cat_template() {
  echo "cat << EOT"
  cat "$1"
  echo EOT
}

cat_template template | LISTING="$LISTING" bash

$ cat template (มีบรรทัดใหม่ต่อท้ายและเครื่องหมายคำพูดคู่)

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
$( echo "$LISTING" | sed 's/^/        /' )
      <pre>
    </p>
  </body>
</html>

เอาท์พุต

<html>
  <head>
  </head>
  <body> 
    <p>"directory listing"
      <pre>
        code
        template
      <pre>
    </p>
  </body>
</html>

4

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

word=dog           
i=1                
cat << EOF         
the number is ${i} 
the word is ${word}

EOF                

หากเราป้อนสคริปต์นี้ลงใน bash มันจะให้ผลลัพธ์ที่ต้องการ:

the number is 1
the word is dog

นี่คือวิธีสร้างสคริปต์นั้นและฟีดสคริปต์นั้นลงใน bash:

(
    # Variables
    echo word=dog
    echo i=1

    # add the template
    echo "cat << EOF"
    cat template.txt
    echo EOF
) | bash

อภิปรายผล

  • วงเล็บจะเปิดเชลล์ย่อยโดยมีวัตถุประสงค์เพื่อจัดกลุ่มผลลัพธ์ทั้งหมดที่สร้างขึ้น
  • ภายในเชลล์ย่อยเราสร้างการประกาศตัวแปรทั้งหมด
  • นอกจากนี้ในเชลล์ย่อยเราสร้างcatคำสั่งด้วย HEREDOC
  • สุดท้ายเราป้อนเอาต์พุตเชลล์ย่อยเพื่อทุบตีและสร้างเอาต์พุตที่ต้องการ
  • หากคุณต้องการเปลี่ยนทิศทางผลลัพธ์นี้ไปยังไฟล์ให้แทนที่บรรทัดสุดท้ายด้วย:

    ) | bash > output.txt


3

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

เพียงดำเนินการ:

$ i=1 word=dog sh -c "$( shtpl template.txt )"

ผลลัพธ์คือ:

the number is 1
the word is dog

มีความสุข.


1
ถ้ามันห่วยมันก็ลดลงอยู่ดี และฉันก็โอเคกับสิ่งนั้น แต่โอเคชี้ให้เห็นว่ามันเป็นโครงการของฉันจริงๆ ไปให้เห็นมากขึ้นในอนาคต. ขอบคุณสำหรับความคิดเห็นและเวลาของคุณ
zstegi

ฉันต้องการเพิ่มว่าฉันค้นหา usecases เมื่อวานนี้โดยที่ shtpl จะเป็นโซลูชันที่สมบูรณ์แบบ ใช่ฉันเบื่อ ...
zstegi

3
# Usage: template your_file.conf.template > your_file.conf
template() {
        local IFS line
        while IFS=$'\n\r' read -r line ; do
                line=${line//\\/\\\\}         # escape backslashes
                line=${line//\"/\\\"}         # escape "
                line=${line//\`/\\\`}         # escape `
                line=${line//\$/\\\$}         # escape $
                line=${line//\\\${/\${}       # de-escape ${         - allows variable substitution: ${var} ${var:-default_value} etc
                # to allow arithmetic expansion or command substitution uncomment one of following lines:
#               line=${line//\\\$\(/\$\(}     # de-escape $( and $(( - allows $(( 1 + 2 )) or $( command ) - UNSECURE
#               line=${line//\\\$\(\(/\$\(\(} # de-escape $((        - allows $(( 1 + 2 ))
                eval "echo \"${line}\"";
        done < "$1"
}

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



0

นี่คือฟังก์ชั่นทุบตีที่รักษาช่องว่าง:

# Render a file in bash, i.e. expand environment variables. Preserves whitespace.
function render_file () {
    while IFS='' read line; do
        eval echo \""${line}"\"
    done < "${1}"
}

0

นี่คือperlสคริปต์ที่ได้รับการแก้ไขตามคำตอบอื่น ๆ บางส่วน:

perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template

คุณสมบัติ (ตามความต้องการของฉัน แต่ควรปรับเปลี่ยนได้ง่าย):

  • ข้ามการขยายพารามิเตอร์ที่ใช้ Escape (เช่น \ $ {VAR})
  • รองรับการขยายพารามิเตอร์ของรูปแบบ $ {VAR} แต่ไม่ใช่ $ VAR
  • แทนที่ $ {VAR} ด้วยสตริงว่างถ้าไม่มี VAR envar
  • รองรับเฉพาะอักขระ az, AZ, 0-9 และขีดล่างในชื่อ (ไม่รวมตัวเลขในตำแหน่งแรก)


0

หากต้องการติดตามคำตอบของ plockcในหน้านี้นี่คือเวอร์ชันที่เหมาะสมสำหรับผู้ที่ต้องการหลีกเลี่ยงการทุบตี

eval "cat <<EOF >outputfile
$( cat template.in )
EOF
" 2> /dev/null

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