Використовувати конфігураційний файл для мого сценарію оболонки


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

Faheem, мені потрібні змінні, оскільки мій сценарій має багато варіантів: використання конфігураційного файлу посилить сценарій. Спасибі
Пол Халлен

5
ІМХО свій штраф. Я би зробив так.
Тінті

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 добре обробляє цей символ?
Міккель

Перший сценарій виходить з ладу, якщо пароль містить зворотну косу рису.
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 (це бібліотека, тому немає шебанг-рядка):

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 у подвійні лапки, ми гарантуємо, що текст у значенні змінної ніколи не буде трактуватися неправильно як прапори. І це гарантує правильне збереження пробілів, таких як декілька пробілів підряд у значенні config.
  • І що це за printfрядок? Ну, це те, про що слід пам’ятати: echoце погана команда для друку тексту, над яким ти не маєш контролю. Навіть якщо ви використовуєте подвійні лапки, він буде інтерпретувати прапори. Спробуйте встановити myvarconfig.cfg) на, -eі ви побачите порожній рядок, бо echoподумаєте, що це прапор. Але printfце не має проблеми. printf --Каже «надрукувати це, і нічого , як прапори не інтерпретують», і "%s\n"говорить , що «формат виведення у вигляді рядка з завершальним символом нового рядка, і , нарешті, останній параметр є значенням Printf в формат.
  • Якщо ви не збираєтесь повторювати значення на екрані, ви просто призначите їх нормально, як-от myvar="$(config_get myvar)";. Якщо ви збираєтеся надрукувати їх на екран, я пропоную використовувати printf, щоб бути абсолютно безпечним від будь-яких ехо-несумісних рядків, які можуть бути в налаштуваннях користувача. Але луна чудово, якщо надана користувачем змінна не є першим символом рядка, який ви повторюєте, оскільки це єдина ситуація, коли "прапори" можуть бути інтерпретовані, тож щось подібне echo "foo: $(config_get myvar)";є безпечним, оскільки "foo" не Почніть з тире і тому повідомляє відлуння, що решта рядка теж не є прапорами для нього. :-)

@ user2993656 Дякуємо, що помітили, що в моєму оригінальному коді все-таки було моє приватне ім'я файлу config (environment.cfg) замість правильного. Щодо редагування "echo -n", яке ви зробили, це залежить від використовуваної оболонки. На Mac / Linux Bash, "echo -n" означає "ехо без затримки нового рядка", що я зробив, щоб уникнути затримки нових рядків. Але, здається, працює точно так само без нього, тому дякую за правки!
gw0

Насправді, я просто пройшов і переписав це, щоб використовувати printf замість echo, що гарантує, що ми позбудемося ризику неправильного трактування "прапорів" echo у значеннях config.
gw0

Мені дуже подобається ця версія. Я кинув config.cfg.defaultsзамість того, щоб визначити їх під час дзвінка $(config_get var_name "default_value"). tritarget.org/static/…
Сукіма

7

Розбирайте файл конфігурації, не виконуйте його.

Зараз я пишу заявку на роботі, яка використовує надзвичайно просту конфігурацію XML:

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

У скрипті оболонки ("додаток") це те, що я роблю, щоб отримати ім'я користувача (більш-менш я поставив це у функції оболонки):

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

xmlКоманда XMLStarlet , яка доступна для більшості Юнікси.

Я використовую XML, оскільки інші частини програми також мають справу з даними, закодованими у файлах XML, тому це було найпростіше.

Якщо ви віддаєте перевагу JSON, jqє простий у використанні аналізатор оболонки JSON.

Мій файл конфігурації виглядатиме приблизно так у JSON:

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

І тоді я отримаю ім'я користувача в сценарії:

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

Виконання сценарію має ряд переваг і недоліків. Основні недоліки - це безпека, якщо хтось може змінити конфігураційний файл, то він може виконати код, і важче зробити це доказів ідіотом. Переваги полягають у швидкості, на простому тесті - це в 10 000 разів швидше джерело конфігураційного файлу, ніж запуск pq, та гнучкість, кожен, хто любить мавповий патч-пітон, оцінить це.
ікар

@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
Я покладав великі надії на цю статтю (з'явився і в моїх результатах пошуку), але пропозиція автора спробувати використати регулярний вираз для фільтрації шкідливого коду - це марність.
Міккель

Процедура з крапкою вимагає абсолютного шляху? З відносною це не працює
Девіде

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


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.