Используйте конфигурационный файл для моего сценария оболочки

26

Мне нужно создать файл конфигурации для моего собственного скрипта: вот пример:

сценарий:

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

Содержание /home/myuser/test/config:

nam="Mark"
sur="Brown"

это работает!

Мой вопрос: это правильный способ сделать это или есть другие способы?

Пол Халлен
источник
Переменные должны быть вверху. Я удивлен, что это работает. В любом случае, зачем вам файл конфигурации? Планируете ли вы использовать эти переменные в другом месте?
Фахим Митха
Фахим, мне нужны переменные, потому что у моего скрипта есть много опций: использование файла конфигурации приведет к упрощению скрипта. Спасибо
Пол Халлен
5
ИМХО это нормально. Я бы так и сделал.
Тинти
abcdeтакже делает это таким образом, и это довольно большая программа (для сценария оболочки). Вы можете посмотреть на это здесь .
Лукас

Ответы:

19

sourceне является безопасным, поскольку он будет выполнять произвольный код. Это может вас не беспокоить, но если права доступа к файлам неверны, злоумышленник с правами доступа к файловой системе может выполнить код от имени привилегированного пользователя, внедрив код в файл конфигурации, загруженный сценарием, защищенным иным способом, таким как сценарий инициализации.

Пока что лучшее решение, которое я смог определить, - это неуклюжее решение «изобретать за рулем»:

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
К вашему сведению, это решение Bash версии 4.0, которое, к сожалению, подвержено безумным лицензионным проблемам, навязанным Apple, и по умолчанию недоступно на компьютерах Mac
Sukima
@ Сукима Хороший вопрос. Я добавил версию, совместимую с Bash 3. Его слабость в том, что он не будет *правильно обрабатывать входные данные, но что тогда в Bash хорошо обрабатывает этот символ?
Миккель
Первый скрипт завершается ошибкой, если пароль содержит обратную косую черту.
Кусалананда
@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это плохая команда для печати текста, которую вы не можете контролировать. Даже если вы используете двойные кавычки, он будет интерпретировать флаги. Попробуйте установить myvarconfig.cfg) значение, -eи вы увидите пустую строку, потому что echoбудете думать, что это флаг. Но printfне имеет этой проблемы. Они printf --говорят: «напечатайте это, и ничего не интерпретируйте как флаги», и "%s\n"говорят: «форматируйте вывод как строку с завершающим переводом строки, и, наконец, последний параметр - это значение для printf для форматирования.
  • Если вы не собираетесь отображать значения на экране, вы просто назначаете их как обычно myvar="$(config_get myvar)";. Если вы собираетесь распечатывать их на экране, я предлагаю использовать printf для полной защиты от любых несовместимых с эхом строк, которые могут быть в конфигурации пользователя. Но echo - это хорошо, если предоставленная пользователем переменная не является первым символом строки, которую вы выводите, поскольку это единственная ситуация, в которой «флаги» могут быть интерпретированы, поэтому что-то вроде этого echo "foo: $(config_get myvar)";безопасно, поскольку «foo» не начинаться с тире и, следовательно, говорит echo, что остальная часть строки также не является для него флажком. :-)
gw0
источник
@ user2993656 Спасибо, что заметили, что в моем исходном коде все еще было мое личное имя конфигурации (environment.cfg) вместо правильного. Что касается редактирования "echo -n", которое вы сделали, это зависит от используемой оболочки. В Mac / Linux Bash «echo -n» означает «эхо без завершающей строки», что я сделал, чтобы избежать конечных строк. Но, похоже, без него все работает точно так же, так что спасибо за правки!
gw0
На самом деле, я только что прошел и переписал его, чтобы использовать printf вместо echo, что гарантирует, что мы избавимся от риска неправильного толкования echo «флагов» в значениях конфигурации.
gw0
Мне очень нравится эта версия. Я бросил config.cfg.defaultsвместо определения их во время вызова $(config_get var_name "default_value"). tritarget.org/static/...
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 , которая доступна для большинства юниксы.

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

Если вы предпочитаете JSON, есть jqпростой в использовании анализатор оболочки JSON.

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

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

И тогда я получу имя пользователя в сценарии:

username="$( jq -r '.username' "$config_file" )"
Кусалананда
источник
Выполнение скрипта имеет ряд преимуществ и недостатков. Основными недостатками являются безопасность, если кто-то может изменить конфигурационный файл, он может выполнить код, и сделать его более идиотским доказательством труднее. Преимущества - скорость: в простом тесте исходный файл конфигурации более чем в 10 000 раз быстрее, чем для запуска pq, а также гибкость, и любой, кто любит python для исправления обезьян, это оценит.
17
@icarus С какими большими конфигурационными файлами вы обычно сталкиваетесь и как часто вам нужно разбирать их за один сеанс? Обратите внимание, что несколько значений могут быть получены из XML или JSON за один раз.
Кусалананда
Обычно только несколько (от 1 до 3) значений. Если вы используете evalдля установки нескольких значений, вы выполняете выбранные части файла конфигурации :-).
17
1
@icarus Я думал о массивах ... Ничего не надо eval. Увеличение производительности при использовании стандартного формата с существующим синтаксическим анализатором (даже если это внешняя утилита) незначительно по сравнению с надежностью, объемом кода, простотой использования и ремонтопригодностью.
Кусалананда
1
+1 для "разбора файла конфигурации, не выполняйте его"
Iiridayn
4

Наиболее распространенным, эффективным и правильным способом является использование sourceили .в качестве сокращенной формы. Например:

source /home/myuser/test/config

или

. /home/myuser/test/config

Однако необходимо учитывать проблемы безопасности, которые могут возникнуть при использовании дополнительного файла конфигурации из внешних источников, учитывая возможность добавления дополнительного кода. Для получения дополнительной информации, в том числе о том, как обнаружить и решить эту проблему, я бы порекомендовал взглянуть на раздел «Безопасное использование» на http://wiki.bash-hackers.org/howto/conffile#secure_it.

NFarrington
источник
5
Я возлагал большие надежды на эту статью (также появился в моих результатах поиска), но предложение автора попытаться использовать регулярное выражение для фильтрации вредоносного кода - бесполезное занятие.
Миккель
Процедура с точкой, требует абсолютного пути? С относительным это не работает
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

therealfarfetchd
источник
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скрипт более устойчивым, отфильтровывать строки, которые выглядят странно или не являются определениями и т. Д., Не прерывать комментарии в конце строки - но для меня этого пока достаточно.

Iiridayn
источник
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

Marcin
источник
-2

Ты можешь это сделать:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Персидский залив
источник