Sử dụng tập tin cấu hình cho tập lệnh shell của tôi


26

Tôi cần tạo một tệp cấu hình cho tập lệnh của riêng tôi: đây là một ví dụ:

kịch bản:

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

Nội dung của /home/myuser/test/config:

nam="Mark"
sur="Brown"

đó là công việc!

Câu hỏi của tôi: đây là cách chính xác để làm điều này hay có những cách khác?


Các biến nên ở đầu. Tôi ngạc nhiên khi nó hoạt động. Dù sao, tại sao bạn cần một tập tin cấu hình? Bạn có kế hoạch sử dụng các biến này ở một nơi khác?
Faheem Mitha

Faheem, tôi cần các biến vì tập lệnh của tôi có nhiều tùy chọn: sử dụng tệp cấu hình sẽ bán thành tập lệnh. Cảm ơn
Pol Hallen

5
IMHO tốt của nó. Tôi sẽ làm theo cách này.
Tinti

abcdecũng làm theo cách này và đó là một chương trình khá lớn (đối với kịch bản shell). Bạn có thể có một cái nhìn tại đây .
Lucas

Câu trả lời:


19

sourcekhông an toàn vì nó sẽ thực thi mã tùy ý. Điều này có thể không phải là mối quan tâm của bạn, nhưng nếu quyền truy cập tệp không chính xác, có thể kẻ tấn công có quyền truy cập hệ thống tệp để thực thi mã với tư cách là người dùng đặc quyền bằng cách tiêm mã vào tệp cấu hình được tải bởi tập lệnh được bảo mật khác, chẳng hạn như kịch bản init.

Cho đến nay, giải pháp tốt nhất mà tôi có thể xác định là giải pháp tái tạo vụng về vụng về:

myscript.conf

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

Sử dụng source, điều này sẽ chạy echo rm -rf /hai lần, cũng như thay đổi người dùng đang chạy $PROMPT_COMMAND. Thay vào đó, hãy làm điều này:

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 (Tương thích 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

Vui lòng trả lời nếu bạn tìm thấy một khai thác bảo mật trong mã của tôi.


1
FYI đây là một giải pháp Bash phiên bản 4.0, đáng buồn là phải chịu các vấn đề cấp phép điên rồ do Apple áp đặt và không có sẵn theo mặc định trên máy Mac
Sukima

@Sukima Điểm tốt. Tôi đã thêm một phiên bản tương thích với Bash 3. Điểm yếu của nó là nó sẽ không xử lý *đúng các yếu tố đầu vào, nhưng sau đó, điều gì ở Bash xử lý tốt nhân vật đó?
Mikkel

Kịch bản đầu tiên thất bại nếu mật khẩu chứa dấu gạch chéo ngược.
Kusalananda

@Kusalananda Điều gì xảy ra nếu dấu gạch chéo ngược được thoát? my\\password
Mikkel

10

Đây là phiên bản sạch và di động tương thích với Bash 3 trở lên, trên cả Mac và Linux.

Nó chỉ định tất cả các giá trị mặc định trong một tệp riêng biệt, để tránh sự cần thiết của chức năng cấu hình "mặc định" rất lớn, lộn xộn, trùng lặp trong tất cả các tập lệnh shell của bạn. Và nó cho phép bạn chọn giữa đọc có hoặc không có dự phòng mặc định:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (đây là một thư viện, vì vậy không có shebang-line):

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 (hoặc bất kỳ tập lệnh nào bạn muốn đọc giá trị cấu hình) :

#!/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

Giải thích về kịch bản thử nghiệm:

  • Lưu ý rằng tất cả các cách sử dụng config_get trong test.sh được gói trong dấu ngoặc kép. Bằng cách gói mọi config_get trong dấu ngoặc kép, chúng tôi đảm bảo rằng văn bản trong giá trị biến sẽ không bao giờ bị hiểu sai là cờ. Và nó đảm bảo rằng chúng ta bảo toàn khoảng trắng đúng cách, chẳng hạn như nhiều khoảng trắng trong một hàng trong giá trị cấu hình.
  • printfdòng đó là gì? Chà, đó là điều bạn nên chú ý: echolà một lệnh tồi để in văn bản mà bạn không kiểm soát được. Ngay cả khi bạn sử dụng dấu ngoặc kép, nó sẽ diễn giải cờ. Hãy thử đặt myvar(trong config.cfg) thành -evà bạn sẽ thấy một dòng trống, bởi vì echosẽ nghĩ rằng đó là một lá cờ. Nhưng printfkhông có vấn đề đó. Câu printf --nói "in này và không diễn giải bất cứ điều gì là cờ" và "%s\n"nói "định dạng đầu ra dưới dạng một chuỗi với một dòng mới, và cuối cùng, tham số cuối cùng là giá trị cho printf định dạng.
  • Nếu bạn sẽ không lặp lại các giá trị cho màn hình, thì bạn chỉ cần gán chúng một cách bình thường, như thế myvar="$(config_get myvar)";. Nếu bạn định in chúng ra màn hình, tôi khuyên bạn nên sử dụng printf để hoàn toàn an toàn trước mọi chuỗi không tương thích với tiếng vang có thể có trong cấu hình người dùng. Nhưng tiếng vang vẫn ổn nếu biến do người dùng cung cấp không phải là ký tự đầu tiên của chuỗi bạn đang lặp lại, vì đó là tình huống duy nhất mà "cờ" có thể được diễn giải, do đó, một cái gì đó như echo "foo: $(config_get myvar)";là an toàn, vì "foo" không bắt đầu bằng dấu gạch ngang và do đó báo cho tiếng vang rằng phần còn lại của chuỗi không phải là cờ cho nó. :-)

@ user2993656 Cảm ơn bạn đã phát hiện ra rằng mã gốc của tôi vẫn có tên tệp cấu hình riêng tư của tôi (môi trường.cfg) trong đó thay vì mã chính xác. Đối với chỉnh sửa "echo -n" bạn đã thực hiện, điều đó phụ thuộc vào trình bao được sử dụng. Trên Mac / Linux Bash, "echo -n" có nghĩa là "echo mà không theo dõi dòng mới", mà tôi đã làm để tránh theo dõi dòng mới. Nhưng nó dường như hoạt động chính xác như vậy mà không có nó, vì vậy cảm ơn bạn đã chỉnh sửa!
gw0

Trên thực tế, tôi chỉ đi qua và viết lại nó để sử dụng printf thay vì echo, điều này đảm bảo rằng chúng ta sẽ thoát khỏi nguy cơ lặp lại tiếng vang "cờ" trong các giá trị cấu hình.
gw0

Tôi thực sự thích phiên bản này. Tôi đã bỏ config.cfg.defaultsthay cho việc xác định chúng tại thời điểm gọi $(config_get var_name "default_value"). tritarget.org/static/ Kẻ
Sukima

7

Phân tích tệp cấu hình, không thực hiện nó.

Tôi hiện đang viết một ứng dụng tại nơi làm việc sử dụng cấu hình XML cực kỳ đơn giản:

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

Trong tập lệnh shell ("ứng dụng"), đây là những gì tôi làm để lấy tên người dùng (ít nhiều, tôi đã đặt nó trong hàm shell):

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

Các xmllệnh là XMLStarlet , trong đó có sẵn cho hầu hết các unices.

Tôi đang sử dụng XML vì các phần khác của ứng dụng cũng xử lý dữ liệu được mã hóa trong các tệp XML, vì vậy nó dễ nhất.

Nếu bạn thích JSON, có jqmột trình phân tích cú pháp JSON shell dễ sử dụng.

Tệp cấu hình của tôi sẽ trông giống như thế này trong JSON:

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

Và sau đó tôi sẽ nhận được tên người dùng trong kịch bản:

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

Thi công kịch bản có một số ưu điểm và nhược điểm. Nhược điểm chính là bảo mật, nếu ai đó có thể thay đổi tệp cấu hình thì họ có thể thực thi mã và khó có thể chứng minh điều đó hơn. Ưu điểm là tốc độ, trong một thử nghiệm đơn giản, nguồn tệp cấu hình nhanh hơn 10.000 lần so với chạy pq và tính linh hoạt, bất cứ ai thích con trăn vá khỉ sẽ đánh giá cao điều này.
icarus

@icarus Bạn thường gặp các tệp cấu hình lớn như thế nào và bạn có thường xuyên phải phân tích chúng trong một phiên không? Cũng lưu ý rằng một số giá trị có thể bị loại ra khỏi XML hoặc JSON trong một lần.
Kusalananda

Thông thường chỉ có một vài (1 đến 3) giá trị. Nếu bạn đang sử dụng evalđể đặt nhiều giá trị thì bạn đang thực thi các phần được chọn của tệp cấu hình :-).
icarus

1
@icarus Tôi đã suy nghĩ mảng ... Không cần evalgì cả. Hiệu suất của việc sử dụng định dạng chuẩn với trình phân tích cú pháp hiện có (mặc dù đó là tiện ích bên ngoài) không đáng kể so với độ mạnh mẽ, số lượng mã, dễ sử dụng và khả năng bảo trì.
Kusalananda

1
+1 cho "phân tích tệp cấu hình, không thực thi nó"
Iiridayn

4

Cách phổ biến nhất, hiệu quả và chính xác là sử dụng source, hoặc .như một hình thức tốc ký. Ví dụ:

source /home/myuser/test/config

hoặc là

. /home/myuser/test/config

Tuy nhiên, một cái gì đó để xem xét là các vấn đề bảo mật mà sử dụng tệp cấu hình có nguồn gốc bên ngoài bổ sung có thể nêu ra, với điều kiện là có thể chèn thêm mã. Để biết thêm thông tin, bao gồm cả cách phát hiện và giải quyết vấn đề này, tôi khuyên bạn nên xem phần 'Bảo mật' của http://wiki.bash-hackers.org/howto/conffile#secure_it


5
Tôi đã hy vọng rất cao cho bài viết đó (cũng xuất hiện trong kết quả tìm kiếm của tôi), nhưng đề xuất của tác giả về việc cố gắng sử dụng regex để lọc mã độc là một bài tập vô ích.
Mikkel

Các thủ tục với dấu chấm, đòi hỏi một đường dẫn tuyệt đối? Với người thân, nó không hoạt động
Davide

2

Tôi sử dụng điều này trong các kịch bản của tôi:

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
}

Nên hỗ trợ mọi tổ hợp ký tự, ngoại trừ các khóa không thể có =trong chúng, vì đó là phần tách biệt. Bất cứ điều gì khác làm việc.

% 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

Ngoài ra, điều này hoàn toàn an toàn vì nó không sử dụng bất kỳ loại source/eval


0

Đối với kịch bản của tôi, sourcehoặc .vẫn ổn, nhưng tôi muốn hỗ trợ các biến môi trường cục bộ (nghĩa là FOO=bar myscript.sh) được ưu tiên hơn các biến được định cấu hình. Tôi cũng muốn tập tin cấu hình có thể được người dùng chỉnh sửa và thoải mái với ai đó đã sử dụng các tập tin cấu hình có nguồn gốc và để giữ cho nó nhỏ / đơn giản nhất có thể, để không bị phân tâm khỏi lực đẩy chính của tập lệnh rất nhỏ của tôi.

Đây là những gì tôi nghĩ ra:

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)

Về cơ bản - nó kiểm tra các định nghĩa biến (không linh hoạt về khoảng trắng) và viết lại các dòng đó để giá trị được chuyển đổi thành mặc định cho biến đó và biến không được sửa đổi nếu được tìm thấy, như XDG_CONFIG_HOMEbiến ở trên. Nó lấy phiên bản thay đổi của tệp cấu hình này và tiếp tục.

Công việc trong tương lai có thể làm cho sedkịch bản mạnh mẽ hơn, lọc ra các dòng trông lạ hoặc không có định nghĩa, v.v., không phá vỡ các nhận xét cuối dòng - nhưng điều này là đủ tốt cho tôi bây giờ.


0

Điều này ngắn gọn và an toàn:

# 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`)

Các -iđảm bảo bạn chỉ nhận được các biến từcommon.vars


-2

Bạn có thể làm được:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.