Usar arquivo de configuração para o meu shell script

26

Eu preciso criar um arquivo de configuração para o meu próprio script: aqui está um exemplo:

roteiro:

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

Conteúdo de /home/myuser/test/config:

nam="Mark"
sur="Brown"

isso funciona!

Minha pergunta: essa é a maneira correta de fazer isso ou existem outras maneiras?

Pol Hallen
fonte
As variáveis ​​devem estar no topo. Estou surpreso que funcione. Enfim, por que você precisa de um arquivo de configuração? Você está planejando usar essas variáveis ​​em outro lugar?
Faheem Mitha
Faheem, eu preciso das variáveis ​​porque meu script tem muitas opções: usar um arquivo de configuração semplificará o script. Obrigado
Pol Hallen
5
IMHO está bem. Eu faria assim.
Tinti
abcdetambém faz desta maneira e esse é um programa bastante grande (para um script de shell). Você pode dar uma olhada aqui .
Lucas

Respostas:

19

sourcenão é seguro, pois executará código arbitrário. Isso pode não ser uma preocupação para você, mas se as permissões do arquivo estiverem incorretas, pode ser possível que um invasor com acesso ao sistema de arquivos execute o código como um usuário privilegiado injetando código em um arquivo de configuração carregado por um script protegido de outra forma, como um script de inicialização.

Até agora, a melhor solução que consegui identificar é a desajeitada solução de reinventar a roda:

myscript.conf

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

Usando source, isso seria executado echo rm -rf /duas vezes, além de alterar o usuário em execução $PROMPT_COMMAND. Em vez disso, faça o seguinte:

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 (compatível com 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

Por favor, responda se você encontrar uma falha de segurança no meu código.

Mikkel
fonte
11
FYI esta é uma solução Bash versão 4.0 que, infelizmente, está sujeito a problemas de licenciamento insanas impostas pela Apple e não está disponível por padrão em Macs
Sukima
@Sukima Bom ponto. Eu adicionei uma versão que é compatível com o Bash 3. Seu ponto fraco é que ele não manipula as *entradas corretamente, mas o que no Bash trata bem esse caractere?
Mikkel
O primeiro script falhará se a senha contiver uma barra invertida.
Kusalananda
@Kusalananda E se a barra invertida for escapada? my\\password
Mikkel
10

Aqui está uma versão limpa e portátil, compatível com o Bash 3 e superior, no Mac e no Linux.

Ele especifica todos os padrões em um arquivo separado, para evitar a necessidade de uma enorme, confusa e duplicada função de configuração de "padrões" em todos os seus scripts de shell. E permite escolher entre ler com ou sem fallbacks padrão:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (esta é uma biblioteca, portanto não há linha de 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 (ou qualquer script em que você queira ler os valores de configuração) :

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

Explicação do script de teste:

  • Observe que todos os usos de config_get no test.sh estão entre aspas duplas. Ao agrupar cada config_get entre aspas duplas, garantimos que o texto no valor da variável nunca seja mal interpretado como sinalizador. E garante que preservemos os espaços em branco corretamente, como vários espaços seguidos no valor de configuração.
  • E qual é essa printflinha? Bem, é algo que você deve estar ciente: echoé um mau comando para imprimir texto sobre o qual você não tem controle. Mesmo se você usar aspas duplas, ele interpretará sinalizadores. Tente definir myvar(in config.cfg) para -ee você verá uma linha vazia, porque echopensará que é uma bandeira. Mas printfnão tem esse problema. O printf --diz "imprima isso e não interprete nada como sinalizadores" e o "%s\n"diz "formate a saída como uma string com uma nova linha à direita e, por último, o parâmetro final é o valor para o formato printf.
  • Se você não estiver ecoando valores na tela, basta atribuí-los normalmente, como myvar="$(config_get myvar)";. Se você quiser imprimi-los na tela, sugiro usar printf para ser totalmente seguro contra quaisquer seqüências incompatíveis com eco que possam estar na configuração do usuário. Mas eco é bom se a variável fornecida pelo usuário não for o primeiro caractere da string em que você está ecoando, já que essa é a única situação em que "sinalizadores" podem ser interpretados, portanto, algo como echo "foo: $(config_get myvar)";é seguro, pois o "foo" não comece com um traço e, portanto, diz ao eco que o restante da string também não é sinalizador. :-)
gw0
fonte
@ user2993656 Obrigado por descobrir que meu código original ainda tinha meu nome de arquivo de configuração privado (environment.cfg), em vez do código correto. Quanto à edição "echo -n" que você fez, isso depende do shell usado. No Mac / Linux Bash, "echo -n" significa "eco sem rastrear nova linha", o que fiz para evitar rastrear novas linhas. Mas parece funcionar exatamente da mesma forma sem isso, então obrigado pelas edições!
Gw0
Na verdade, acabei de reescrevê-lo para usar printf em vez de echo, o que garante que vamos nos livrar do risco de eco interpretar erroneamente "sinalizadores" nos valores de configuração.
Gw0
Eu realmente gosto desta versão. Eu deixei config.cfg.defaultsde defini-los no momento da ligação $(config_get var_name "default_value"). tritarget.org/static/…
Sukima 22/10
7

Analise o arquivo de configuração, não o execute.

Atualmente, estou escrevendo um aplicativo no trabalho que usa uma configuração XML extremamente simples:

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

No script shell (o "aplicativo"), é isso que faço para obter o nome de usuário (mais ou menos, coloquei-o em uma função shell):

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

O xmlcomando é XMLStarlet , disponível para a maioria dos Unices.

Estou usando XML, já que outras partes do aplicativo também lidam com dados codificados em arquivos XML, por isso foi mais fácil.

Se você preferir JSON, existe jqum analisador de shell JSON fácil de usar.

Meu arquivo de configuração seria parecido com isto em JSON:

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

E então eu estaria recebendo o nome de usuário no script:

username="$( jq -r '.username' "$config_file" )"
Kusalananda
fonte
A execução do script tem várias vantagens e desvantagens. As principais desvantagens são a segurança; se alguém puder alterar o arquivo de configuração, poderá executar o código, e é mais difícil torná-lo à prova de idiotas. As vantagens são a velocidade, em um teste simples, é mais de 10.000 vezes mais rápido a origem de um arquivo de configuração do que o pq e a flexibilidade, e qualquer pessoa que goste de usar o patch de macaco em python vai gostar disso.
icarus
@icarus Quantos arquivos de configuração você costuma encontrar e com que frequência precisa analisá-los em uma sessão? Observe também que vários valores podem ser obtidos fora do XML ou JSON de uma só vez.
Kusalananda
Geralmente, apenas alguns (1 a 3) valores. Se você estiver usando evalpara definir vários valores, estará executando partes selecionadas do arquivo de configuração :-).
Icarus
11
@icarus Eu estava pensando em matrizes ... Não há necessidade de evalnada. O impacto no desempenho do uso de um formato padrão com um analisador existente (embora seja um utilitário externo) é insignificante em comparação com a robustez, a quantidade de código, a facilidade de uso e a manutenção.
Kusalananda
11
+1 para "analisar o arquivo de configuração, não o execute"
Iiridayn 3/17/17
4

A maneira mais comum, eficiente e correta é usar sourceou .como forma abreviada. Por exemplo:

source /home/myuser/test/config

ou

. /home/myuser/test/config

Algo a considerar, no entanto, são os problemas de segurança que o uso de um arquivo de configuração adicional de origem externa pode gerar, dado que um código adicional pode ser inserido. Para obter mais informações, inclusive sobre como detectar e resolver esse problema, eu recomendo dar uma olhada na seção 'Secure it' em http://wiki.bash-hackers.org/howto/conffile#secure_it

NFarrington
fonte
5
Eu esperava muito desse artigo (também apareceu nos meus resultados de pesquisa), mas a sugestão do autor de tentar usar o regex para filtrar códigos maliciosos é um exercício de futilidade.
Mikkel
O procedimento com ponto exige um caminho absoluto? Com o relativo não funciona #
Davide
2

Eu uso isso em meus scripts:

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
}

Deve suportar todas as combinações de caracteres, exceto que as chaves não podem ter =nelas, pois esse é o separador. Qualquer outra coisa funciona.

% 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

Além disso, isso é completamente seguro, pois não usa nenhum tipo de source/eval

therealfarfetchd
fonte
0

Para o meu cenário, sourceou .estava bom, mas eu queria dar suporte a variáveis ​​de ambiente local (ou seja, FOO=bar myscript.sh) tendo precedência sobre variáveis ​​configuradas. Eu também queria que o arquivo de configuração fosse editável pelo usuário e confortável para alguém acostumado a obter arquivos de configuração, e mantê-lo o mais pequeno / simples possível, para não distrair o principal impulso do meu pequeno script.

Isto é o que eu vim com:

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)

Essencialmente - ele verifica definições de variáveis ​​(sem ser muito flexível sobre espaços em branco) e reescreve essas linhas para que o valor seja convertido em um padrão para essa variável e a variável não seja modificada se encontrada, como a XDG_CONFIG_HOMEvariável acima. Ele origina esta versão alterada do arquivo de configuração e continua.

Trabalhos futuros podem tornar o sedscript mais robusto, filtrar linhas que parecem estranhas ou não são definições, etc., sem interromper os comentários no final da linha - mas isso é bom o suficiente para mim por enquanto.

Iiridayn
fonte
0

Isso é sucinto e seguro:

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

Os -igarante que você só obter as variáveis decommon.vars

Marcin
fonte
-2

Você consegue:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
PersianGulf
fonte