ใช้ไฟล์ config สำหรับเชลล์สคริปต์ของฉัน


26

ฉันต้องการสร้างไฟล์ปรับแต่งสำหรับสคริปต์ของตัวเอง: นี่เป็นตัวอย่าง:

สคริปต์:

#!/bin/bash
source /home/myuser/test/config
echo "Name=$nam" >&2
echo "Surname=$sur" >&2

เนื้อหาของ/home/myuser/test/config:

nam="Mark"
sur="Brown"

นั่นเป็นผลงาน!

คำถามของฉัน: นี่เป็นวิธีที่ถูกต้องในการทำเช่นนี้หรือมีวิธีอื่นหรือไม่?


ตัวแปรควรอยู่ด้านบนสุด ฉันประหลาดใจที่มันใช้งานได้ อย่างไรก็ตามทำไมคุณต้องใช้ไฟล์กำหนดค่า คุณวางแผนที่จะใช้ตัวแปรเหล่านี้ที่อื่นหรือไม่?
Faheem Mitha

คำตอบ: ฉันต้องการตัวแปรเพราะสคริปต์ของฉันมีตัวเลือกมากมาย: การใช้ไฟล์ปรับแต่งจะทำให้สคริปต์เป็นกึ่งกลาง ขอบคุณ
Pol Hallen

5
IMHO มันดี ฉันจะทำแบบนี้
Tinti

abcdeทำเช่นนี้ด้วยและนั่นเป็นโปรแกรมที่ค่อนข้างใหญ่ (สำหรับเชลล์สคริปต์) คุณสามารถดูได้ที่มันนี่
ลูคัส

คำตอบ:


19

sourceไม่ปลอดภัยเนื่องจากจะใช้รหัสโดยอำเภอใจ สิ่งนี้อาจไม่เป็นปัญหาสำหรับคุณ แต่หากสิทธิ์ของไฟล์ไม่ถูกต้องอาจเป็นไปได้ที่ผู้โจมตีที่มีสิทธิ์เข้าถึงระบบไฟล์เพื่อเรียกใช้รหัสในฐานะผู้ใช้ที่มีสิทธิ์โดยการฉีดรหัสเข้าไปในไฟล์ปรับแต่งที่โหลดโดยสคริปต์ที่ปลอดภัย สคริปต์ init

จนถึงตอนนี้ทางออกที่ดีที่สุดที่ฉันสามารถระบุได้คือวิธีแก้ปัญหาซ้ำซาก - ล้อ:

myscript.conf

password=bar
echo rm -rf /
PROMPT_COMMAND='echo "Sending your last command $(history 1) to my email"'
hostname=localhost; echo rm -rf /

การใช้sourceสิ่งนี้จะทำงานecho rm -rf /สองครั้งรวมถึงเปลี่ยนผู้ใช้ที่กำลังทำงาน$PROMPT_COMMANDอยู่ ให้ทำสิ่งนี้แทน:

myscript.sh (Bash 4)

#!/bin/bash
typeset -A config # init array
config=( # set default values in config array
    [username]="root"
    [password]=""
    [hostname]="localhost"
)

while read line
do
    if echo $line | grep -F = &>/dev/null
    then
        varname=$(echo "$line" | cut -d '=' -f 1)
        config[$varname]=$(echo "$line" | cut -d '=' -f 2-)
    fi
done < myscript.conf

echo ${config[username]} # should be loaded from defaults
echo ${config[password]} # should be loaded from config file
echo ${config[hostname]} # includes the "injected" code, but it's fine here
echo ${config[PROMPT_COMMAND]} # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

myscript.sh (รองรับ Mac / Bash 3)

#!/bin/bash
config() {
    val=$(grep -E "^$1=" myscript.conf 2>/dev/null || echo "$1=__DEFAULT__" | head -n 1 | cut -d '=' -f 2-)

    if [[ $val == __DEFAULT__ ]]
    then
        case $1 in
            username)
                echo -n "root"
                ;;
            password)
                echo -n ""
                ;;
            hostname)
                echo -n "localhost"
                ;;
        esac
    else
        echo -n $val
    fi
}

echo $(config username) # should be loaded from defaults
echo $(config password) # should be loaded from config file
echo $(config hostname) # includes the "injected" code, but it's fine here
echo $(config PROMPT_COMMAND) # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

โปรดตอบกลับหากคุณพบช่องโหว่ด้านความปลอดภัยในรหัสของฉัน


1
FYI นี่เป็นโซลูชัน Bash เวอร์ชัน 4.0 ซึ่งน่าเศร้าอยู่ภายใต้ปัญหาการออกใบอนุญาตอย่างบ้าคลั่งที่ Apple กำหนดไว้และไม่สามารถใช้งานได้บน Macs เป็นค่าเริ่มต้น
Sukima

@Sukima จุดที่ดี ฉันได้เพิ่มเวอร์ชันที่เข้ากันได้กับ Bash 3 จุดอ่อนของมันคือว่ามันจะไม่สามารถจัดการกับ*อินพุตได้อย่างถูกต้อง แต่แล้ว Bash สามารถจัดการกับตัวละครนั้นได้ดีแค่ไหน?
Mikkel

สคริปต์แรกล้มเหลวหากรหัสผ่านมีแบ็กสแลช
Kusalananda

@Kusalananda เกิดอะไรขึ้นถ้าแบ็กสแลชหนี my\\password
มิคเคล

10

นี่เป็นเวอร์ชั่นที่สะอาดและพกพาได้ซึ่งเข้ากันได้กับ Bash 3 ขึ้นไปทั้งบน Mac และ Linux

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

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (นี่คือไลบรารีดังนั้นจึงไม่มีสาย shebang):

config_read_file() {
    (grep -E "^${2}=" -m 1 "${1}" 2>/dev/null || echo "VAR=__UNDEFINED__") | head -n 1 | cut -d '=' -f 2-;
}

config_get() {
    val="$(config_read_file config.cfg "${1}")";
    if [ "${val}" = "__UNDEFINED__" ]; then
        val="$(config_read_file config.cfg.defaults "${1}")";
    fi
    printf -- "%s" "${val}";
}

test.sh (หรือสคริปต์ใด ๆ ที่คุณต้องการอ่านค่ากำหนด) :

#!/usr/bin/env bash
source config.shlib; # load the config library functions
echo "$(config_get myvar)"; # will be found in user-cfg
printf -- "%s\n" "$(config_get myvar)"; # safer way of echoing!
myvar="$(config_get myvar)"; # how to just read a value without echoing
echo "$(config_get othervar)"; # will fall back to defaults
echo "$(config_get bleh)"; # "__UNDEFINED__" since it isn't set anywhere

คำอธิบายของสคริปต์ทดสอบ:

  • โปรดทราบว่าการใช้งานทั้งหมดของ config_get ใน test.sh ถูกห่อด้วยเครื่องหมายคำพูดคู่ โดยการห่อทุก config_get ด้วยเครื่องหมายคำพูดคู่เรามั่นใจว่าข้อความในค่าตัวแปรจะไม่ถูกตีความว่าผิด และช่วยให้มั่นใจได้ว่าเรารักษาช่องว่างอย่างถูกต้องเช่นหลายช่องว่างในแถวในค่าการกำหนดค่า
  • แล้วprintfเส้นนั่นคืออะไร? มันเป็นสิ่งที่คุณควรระวัง: echoเป็นคำสั่งที่ไม่ดีสำหรับการพิมพ์ข้อความที่คุณไม่สามารถควบคุมได้ แม้ว่าคุณจะใช้อัญประกาศคู่ก็จะตีความธง ลองตั้งค่าmyvar(ในconfig.cfg) เป็น-eแล้วคุณจะเห็นบรรทัดว่างเปล่าเพราะechoจะคิดว่าเป็นธง แต่printfไม่มีปัญหานั้น คำprintf --ว่า "พิมพ์สิ่งนี้และไม่ตีความสิ่งใดเป็นแฟล็ก" และคำ"%s\n"ว่า "จัดรูปแบบเอาต์พุตเป็นสตริงที่มีบรรทัดใหม่ต่อท้ายและสุดท้ายพารามิเตอร์สุดท้ายคือค่าสำหรับ printf เพื่อจัดรูปแบบ
  • myvar="$(config_get myvar)";หากคุณกำลังจะไม่ได้สะท้อนค่าหน้าจอแล้วคุณก็จะกำหนดให้ได้ตามปกติเช่น หากคุณกำลังจะพิมพ์พวกเขาไปที่หน้าจอฉันขอแนะนำให้ใช้ printf เพื่อความปลอดภัยโดยสิ้นเชิงกับสาย echo ที่เข้ากันไม่ได้ที่อาจอยู่ในการกำหนดค่าของผู้ใช้ แต่เสียงสะท้อนนั้นใช้ได้ถ้าตัวแปรที่ผู้ใช้ให้ไม่ใช่อักขระตัวแรกของสตริงที่คุณกำลังสะท้อนเนื่องจากเป็นสถานการณ์เดียวที่สามารถตีความ "ค่าสถานะ" ได้ดังนั้นสิ่งที่คล้ายecho "foo: $(config_get myvar)";นั้นปลอดภัยเนื่องจาก "foo" ไม่ เริ่มต้นด้วยเส้นประและดังนั้นจึงบอกว่าเสียงสะท้อนที่เหลือของสตริงไม่ได้ธงสำหรับมันอย่างใดอย่างหนึ่ง :-)

@ user2993656 ขอบคุณที่ทราบว่ารหัสเดิมของฉันยังคงมีชื่อไฟล์กำหนดค่าส่วนตัว (environment.cfg) ในนั้นแทนที่จะเป็นชื่อที่ถูกต้อง สำหรับการแก้ไข "echo -n" ที่คุณทำขึ้นอยู่กับเชลล์ที่ใช้ บน Mac / Linux Bash "echo -n" หมายถึง "echo โดยไม่ขึ้นบรรทัดใหม่" ซึ่งฉันทำเพื่อหลีกเลี่ยงการขึ้นบรรทัดใหม่ แต่ดูเหมือนว่าจะทำงานได้เหมือนกันทุกประการถ้าไม่มีก็ขอขอบคุณสำหรับการแก้ไข!
gw0

ที่จริงแล้วฉันเพิ่งอ่านและเขียนใหม่เพื่อใช้งาน printf แทน echo ซึ่งทำให้แน่ใจได้ว่าเราจะกำจัดความเสี่ยงของ echo ที่แปลความหมายผิด "ค่าสถานะ" ในค่าปรับแต่ง
gw0

ฉันชอบรุ่นนี้มาก ฉันทิ้งแทนการกำหนดให้พวกเขาในช่วงเวลาของการโทรconfig.cfg.defaults tritarget.org/static/…$(config_get var_name "default_value")
Sukima

7

แยกวิเคราะห์ไฟล์การกำหนดค่าอย่าเรียกใช้งาน

ฉันกำลังเขียนแอปพลิเคชันในที่ทำงานซึ่งใช้การกำหนดค่า XML ที่ง่ายมาก:

<config>
    <username>username-or-email</username>
    <password>the-password</password>
</config>

ในเชลล์สคริปต์ ("แอปพลิเคชัน") นี่คือสิ่งที่ฉันทำเพื่อรับชื่อผู้ใช้ (มากหรือน้อยฉันได้ใส่ไว้ในฟังก์ชั่นเชลล์):

username="$( xml sel -t -v '/config/username' "$config_file" )"

xmlคำสั่งXMLStarletซึ่งสามารถใช้ได้สำหรับ Unices ที่สุด

ฉันใช้ XML เนื่องจากส่วนอื่น ๆ ของแอปพลิเคชันยังเกี่ยวข้องกับการเข้ารหัสข้อมูลในไฟล์ XML ดังนั้นจึงเป็นเรื่องง่ายที่สุด

หากคุณต้องการ JSON จะมีjqตัวแยกวิเคราะห์เชลล์ JSON ที่ใช้งานง่าย

ไฟล์กำหนดค่าของฉันจะมีลักษณะเช่นนี้ใน JSON:

{                                 
  "username": "username-or-email",
  "password": "the-password"      
}                

แล้วฉันจะได้รับชื่อผู้ใช้ในสคริปต์:

username="$( jq -r '.username' "$config_file" )"

การเรียกใช้สคริปต์มีข้อดีและข้อเสียจำนวนหนึ่ง ข้อเสียเปรียบหลักคือความปลอดภัยหากใครบางคนสามารถแก้ไขไฟล์ปรับแต่งแล้วพวกเขาก็สามารถเรียกใช้งานโค้ดได้และมันก็ยากที่จะพิสูจน์ให้เป็นจริง ข้อดีคือความเร็วในการทดสอบอย่างง่าย ๆ มันเร็วกว่า 10,000 เท่าในการแหล่งไฟล์ config มากกว่าที่จะเรียกใช้ pq และความยืดหยุ่นทุกคนที่ชอบลิงงูหลาม patching จะชื่นชมสิ่งนี้
รัส

@icarus คุณพบไฟล์การกำหนดค่าขนาดใหญ่ขนาดไหนและคุณต้องแจงในหนึ่งครั้งบ่อยแค่ไหน? โปรดสังเกตด้วยว่าอาจมีค่าหลายค่าจาก XML หรือ JSON ในครั้งเดียว
Kusalananda

มักจะมีค่าเพียงไม่กี่ (1 ถึง 3) หากคุณใช้evalเพื่อตั้งค่าหลายค่าคุณจะดำเนินการบางส่วนของไฟล์ปรับแต่ง :-)
รัส

1
@icarus ฉันคิดอาร์เรย์ ... ไม่จำเป็นต้องทำevalอะไรเลย ประสิทธิภาพการทำงานของการใช้รูปแบบมาตรฐานกับตัวแยกวิเคราะห์ที่มีอยู่ (แม้ว่าจะเป็นเครื่องมือภายนอก) นั้นมีความสำคัญน้อยมากเมื่อเปรียบเทียบกับความทนทานปริมาณโค้ดที่ใช้งานง่ายและการบำรุงรักษา
Kusalananda

1
+1 สำหรับ "แจงไฟล์กำหนดค่าอย่าเรียกใช้งาน"
Iiridayn

4

วิธีที่พบบ่อยที่สุดที่มีประสิทธิภาพและถูกต้องคือการใช้sourceหรือ.เป็นรูปแบบชวเลข ตัวอย่างเช่น:

source /home/myuser/test/config

หรือ

. /home/myuser/test/config

อย่างไรก็ตามสิ่งที่ควรพิจารณาคือปัญหาด้านความปลอดภัยที่สามารถเพิ่มไฟล์การกำหนดค่าที่มาจากภายนอกเพิ่มเติมได้เนื่องจากสามารถแทรกรหัสเพิ่มเติมได้ สำหรับข้อมูลเพิ่มเติมรวมถึงวิธีการตรวจจับและแก้ไขปัญหานี้ฉันขอแนะนำให้ดูที่ส่วน 'รักษาความปลอดภัย' ของhttp://wiki.bash-hackers.org/howto/conffile#secure_it


5
ฉันมีความหวังสูงสำหรับบทความนั้น (เกิดขึ้นในผลการค้นหาของฉันด้วย) แต่ข้อเสนอแนะของผู้เขียนเกี่ยวกับการพยายามใช้ regex เพื่อกรองรหัสที่เป็นอันตรายออกมานั้นเป็นการออกกำลังกายที่ไร้ประโยชน์
Mikkel

ขั้นตอนที่มีจุดต้องมีเส้นทางที่แน่นอน? ด้วยความที่ใช้ไม่ได้
Davide

2

ฉันใช้สิ่งนี้ในสคริปต์ของฉัน:

sed_escape() {
  sed -e 's/[]\/$*.^[]/\\&/g'
}

cfg_write() { # path, key, value
  cfg_delete "$1" "$2"
  echo "$2=$3" >> "$1"
}

cfg_read() { # path, key -> value
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" | sed "s/^$(echo "$2" | sed_escape)=//" | tail -1
}

cfg_delete() { # path, key
  test -f "$1" && sed -i "/^$(echo $2 | sed_escape).*$/d" "$1"
}

cfg_haskey() { # path, key
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" > /dev/null
}

ควรสนับสนุนการรวมกันของตัวละครทุกตัวยกเว้นปุ่มที่ไม่สามารถมีได้=ในนั้นเพราะมันเป็นตัวคั่น สิ่งอื่นใดทำงานได้

% cfg_write test.conf mykey myvalue
% cfg_read test.conf mykey
myvalue
% cfg_delete test.conf mykey
% cfg_haskey test.conf mykey || echo "It's not here anymore"
It's not here anymore

นอกจากนี้ยังมีความปลอดภัยอย่างสมบูรณ์เนื่องจากไม่ได้ใช้source/eval


0

สำหรับสถานการณ์ของฉันsourceหรือไม่.เป็นไร แต่ฉันต้องการสนับสนุนตัวแปรสภาพแวดล้อมท้องถิ่น (เช่นFOO=bar myscript.sh) โดยมีความสำคัญเหนือกว่าตัวแปรที่กำหนดค่าไว้ ฉันยังต้องการให้ไฟล์ปรับแต่งนั้นเป็นแบบที่ผู้ใช้สามารถแก้ไขได้และสะดวกสบายกับใครบางคนที่เคยใช้ไฟล์ปรับแต่งที่มาและทำให้มันเล็ก / ง่ายที่สุดเท่าที่จะทำได้เพื่อไม่เบี่ยงเบนความสนใจหลักของสคริปต์เล็ก ๆ

นี่คือสิ่งที่ฉันมาด้วย:

CONF=${XDG_CONFIG_HOME:-~/config}/myscript.sh
if [ ! -f $CONF ]; then
    cat > $CONF << CONF
VAR1="default value"
CONF
fi
. <(sed 's/^\([^=]\+\) *= *\(.*\)$/\1=${\1:-\2}/' < $CONF)

เป็นหลัก - ตรวจสอบคำจำกัดความของตัวแปร (โดยไม่ต้องมีความยืดหยุ่นมากเกี่ยวกับช่องว่าง) และเขียนบรรทัดเหล่านั้นใหม่เพื่อให้ค่าถูกแปลงเป็นค่าเริ่มต้นสำหรับตัวแปรนั้นและตัวแปรจะไม่ถูกแก้ไขหากพบเช่นXDG_CONFIG_HOMEตัวแปรข้างต้น เป็นแหล่งที่มาของไฟล์ปรับแต่งเวอร์ชันที่เปลี่ยนแปลงนี้และดำเนินการต่อ

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


0

นี่กระชับและปลอดภัย:

# Read common vars from common.vars
# the incantation here ensures (by env) that only key=value pairs are present
# then declare-ing the result puts those vars in our environment 
declare $(env -i `cat common.vars`)

ความ-iมั่นใจว่าคุณจะได้รับตัวแปรเท่านั้นcommon.vars


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