将配置文件用于我的Shell脚本


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 2014年

Faheem,我需要变量,因为我的脚本有很多选择:使用配置文件可以简化脚本。谢谢
Pol Hallen 2014年

5
恕我直言,罚款。我会这样做。
Tinti 2014年

abcde也这样做,那是一个很大的程序(对于shell脚本)。你可以在这里看看。
卢卡斯

Answers:


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

@Sukima好点。我添加了一个与Bash 3兼容的版本。其缺点是它将无法*正确处理输入,但是Bash中的什么可以很好地处理该字符?
米克尔(Mikkel)

如果密码包含反斜杠,则第一个脚本将失败。
库沙兰丹

@Kusalananda如果反斜杠转义了怎么办?my\\password
Mikkel

10

这是一个干净便携的版本,在Mac和Linux上均与Bash 3及更高版本兼容。

它在一个单独的文件中指定所有默认值,以避免所有shell脚本中都需要庞大,混乱,重复的“默认值”配置功能。它使您可以选择是否读取默认后备:

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

测试脚本的说明:

  • 请注意,test.sh中config_get的所有用法都用双引号引起来。通过将每个config_get用双引号引起来,我们确保变量值中的文本不会被误解为标志。并且它确保我们正确保留空白,例如config值中一行中有多个空格。
  • 那是什么printf线?嗯,这是您应该意识到的事情:这echo是打印您无法控制的文本的错误命令。即使您使用双引号,它也会解释标志。尝试将myvar(in config.cfg)设置为-e,您会看到一个空行,因为echo会认为这是一个标志。但是printf没有那个问题。在printf --写着“打印本,并没有解释任何东西作为标志”,并"%s\n"说“格式化输出与尾随换行符的字符串,最后是最后一个参数是对的printf格式的值。
  • 如果您不打算在屏幕上回显值,则只需简单地为其分配值,例如myvar="$(config_get myvar)";。如果要将它们打印到屏幕上,建议您使用printf来防止用户配置中可能存在任何与回声不兼容的字符串,这是绝对安全的。但是,如果用户提供的变量不是您要回显的字符串的第一个字符,则echo会很好,因为这是唯一可以解释“ flags”的情况,因此echo "foo: $(config_get myvar)";很安全,因为“ foo”不会以破折号开头,因此告诉echo字符串的其余部分也不是其标志。:-)

@ user2993656感谢您发现我的原始代码中仍然包含我的私有配置文件名(environment.cfg),而不是正确的文件名。至于您执行的“ echo -n”编辑,则取决于所使用的shell。在Mac / Linux Bash上,“ echo -n”表示“无尾随换行符的回声”,我这样做是为了避免尾随换行符。但是,如果没有它,它似乎可以完全一样地工作,因此感谢您的修改!
gw0

实际上,我只是仔细阅读并重写了它,以使用printf而不是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>

在shell脚本(“应用程序”)中,这就是我想要获得用户名的方法(或多或少,我将其放在了shell函数中):

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

xml命令是XMLStarlet,可用于大多数Unices。

我正在使用XML,因为应用程序的其他部分还处理XML文件中编码的数据,因此这是最简单的。

如果您更喜欢JSON,那么有jq一个易于使用的Shell JSON解析器。

我的配置文件在JSON中看起来像这样:

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

然后我将在脚本中获取用户名:

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

执行脚本有很多优点和缺点。主要缺点是安全性,如果有人可以更改配置文件,则他们可以执行代码,并且很难使它成为白痴证明。优点是速度快,在一个简单的测试中,提供配置文件的源代码比运行pq快10,000倍以上,并且灵活性强,任何喜欢猴子修补python的人都会喜欢的。
icarus

@icarus您通常会遇到多少个配置文件,并且一次需要解析一次?还要注意,一次性使用XML或JSON可能会有多个值。
库桑兰达

通常只有几个(1到3)值。如果eval用于设置多个值,那么您正在执行配置文件的选定部分:-)。
icarus

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的“保护它”部分


5
我对那篇文章寄予厚望(也出现在我的搜索结果中),但是作者提出的尝试使用正则表达式来过滤掉恶意代码的建议是徒劳的。
米克尔2015年

带点的程序,需要绝对路径吗?与亲戚一起行不通
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


-2

你能行的:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.