parâmetros no estilo dd para um script bash

19

Gostaria de passar parâmetros para um script bash, no estilo dd. Basicamente eu quero

./script a=1 b=43

ter o mesmo efeito que

a=1 b=43 ./script

Eu pensei que poderia conseguir isso com:

for arg in "$@"; do
   eval "$arg";
done

Qual é uma boa maneira de garantir que a evalsegurança seja segura, ou seja, que "$arg"corresponda a uma atribuição estática (sem execução de código), variável?

Ou há uma maneira melhor de fazer isto? (Eu gostaria de manter isso simples).

PSkocik
fonte
Isso está marcado com bash. Você quer uma solução compatível com Posix ou aceita soluções bash?
rici
O que diz a marca é o que eu quero dizer :)
PSkocik
Bem, você pode simplesmente analisá-lo como um padrão com um =separador e fazer a tarefa com uma avaliação mais cuidadosamente construída. Apenas por segurança, para uso privado, eu faria como você fez.
orion

Respostas:

16

Você pode fazer isso no bash sem eval (e sem escape artificial):

for arg in "$@"; do
  if [[ $arg =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; then
    declare +i +a +A "$arg"
  fi
done

Edit: Com base em um comentário de Stéphane Chazelas, adicionei flags ao declare para evitar que a variável atribuída já fosse declarada como uma matriz ou variável inteira, o que evitará vários casos em que declareavaliará a parte do valor do key=valargumento. ( +aIsso causará um erro se a variável a ser definida já estiver declarada como variável de matriz, por exemplo.) Todas essas vulnerabilidades estão relacionadas ao uso dessa sintaxe para reatribuir variáveis ​​existentes (matriz ou número inteiro), que normalmente seriam bem conhecidas. variáveis ​​de shell.

De fato, essa é apenas uma instância de uma classe de ataques de injeção que afetará igualmente as evalsoluções baseadas em: seria realmente muito melhor permitir apenas nomes de argumentos conhecidos do que definir cegamente a variável que estivesse presente na linha de comando. (Considere o que acontece se a linha de comando definir PATH, por exemplo. Ou redefine PS1para incluir alguma avaliação que ocorrerá na próxima exibição de prompt.)

Em vez de usar variáveis ​​bash, prefiro usar uma matriz associativa de argumentos nomeados, que é mais fácil de definir e muito mais seguro. Como alternativa, ele pode definir variáveis ​​reais do bash, mas apenas se seus nomes estiverem em uma matriz associativa de argumentos legítimos.

Como um exemplo da última abordagem:

# Could use this array for default values, too.
declare -A options=([bs]= [if]= [of]=)
for arg in "$@"; do
  # Make sure that it is an assignment.
  # -v is not an option for many bash versions
  if [[ $arg =~ ^[[:alpha:]_][[:alnum:]_]*= &&
        ${options[${arg%%=*}]+ok} == ok ]]; then
    declare "$arg"
    # or, to put it into the options array
    # options[${arg%%=*}]=${arg#*=}
  fi
done
rici
fonte
1
O regex parece ter os colchetes incorretos. Talvez use isso ^[[:alpha:]_][[:alnum:]_]*=:?
Lcd047
1
@ lcd047: foo=é a única maneira de definir foo para a string vazia, por isso deve ser permitido (IMHO). Eu consertei os suportes, obrigado.
rici
3
declareé tão perigoso quanto eval(pode-se dizer pior ainda, pois não é tão aparente que seja tão perigoso). Tente, por exemplo, chamar isso com 'DIRSTACK=($(echo rm -rf ~))'como argumento.
Stéphane Chazelas
1
@ PSkocik: +x"não é -x". -a= matriz indexada, -A= matriz associativa, -i= variável inteira. Assim: matriz não indexada, matriz não associativa, não número inteiro.
Lcd047
1
Observe que, com a próxima versão de bash, talvez seja necessário adicionar +cpara desativar variáveis ​​compostas ou +Fdesativar variáveis flutuantes. Eu ainda usaria evalonde você sabe onde está.
Stéphane Chazelas
9

Um POSIX (define em $<prefix>varvez de $varevitar problemas com variáveis ​​especiais como IFS/ PATH...):

prefix=my_prefix_
for var do
  case $var in
    (*=*)
       case ${var%%=*} in
         "" | *[!abcdefghijiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_]*) ;;
         (*) eval "$prefix${var%%=*}="'${var#*=}'
       esac
  esac
done

Chamado como myscript x=1 PATH=/tmp/evil %=3 blah '=foo' 1=2, ele atribuiria:

my_prefix_x <= 1
my_prefix_PATH <= /tmp/evil
my_prefix_1 <= 2
Stéphane Chazelas
fonte
6

A solução do lcd047 foi refatorada com um DD_OPT_prefixo codificado :

while [[ $1 =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; do
  eval "DD_OPT_${1%%=*}"='${1#*=}'; shift;
done

frostschutz merece o crédito pela maior parte da refatoração.

Coloquei isso em um arquivo de origem com como variável global:

DD_OPTS_PARSE=$(cat <<'EOF'
  while [[ $1 =~ ^[[:alpha:]_][[:alnum:]_]*= ]]; do
    eval "DD_OPT_${1%%=*}"='${1#*=}'; shift;
  done
EOF
)

eval "$DD_OPTS_PARSE" faz toda a mágica.

Uma versão para funções seria:

DD_OPTS_PARSE_LOCAL="${PARSE_AND_REMOVE_DD_OPTS/DD_OPT_/local DD_OPT_}"

Em uso:

eval "$DD_OPTS_PARSE_LOCAL"

Fiz um repo disso, completo com testes e um README.md. Em seguida, usei isso em um wrapper da API do Github API que estava escrevendo e usei o mesmo wrapper para configurar um clone do github do referido repositório (o bootstrapping é divertido).

Passagem segura de parâmetros para scripts bash em apenas uma linha. Apreciar. :)

PSkocik
fonte
1
mas você pode se livrar *=*e parar de substituir key / val onde não houver =. (desde que você estava refatorando): P
frostschutz 4/15/15
1
na verdade, você pode se livrar do loop for e if e uso enquanto US $ 1 em vez disso, desde que você está mudando e tudo ...
frostschutz
1
Heh, a prova de que o brainstorming funciona. :)
lcd047
1
Colhendo idéias da manhã: você pode até se livrar de keye val, e apenas escrever eval "${1%%=*}"=\${1#*=}. Mas isso é praticamente o mais longe possível, eval "$1"pois o @ rici's declare "$arg"não funcionará, obviamente. Também tenha cuidado ao definir coisas como PATHou PS1.
lcd047
1
Obrigado - pensei que essa fosse a variável avaliada. Agradeço sua paciência comigo - isso é bastante óbvio. Enfim, não - além do que se imaginava, parece bom. Você sabe que você pode estender isso para trabalhar em qualquer shell com case. Provavelmente não importa, mas apenas no caso de você não sabia ...
mikeserv
5

Shell Bourne clássico suportado e shell Bash e Korn ainda suportam, uma -kopção. Quando está em vigor, qualquer ddopção de comando '-like' em qualquer lugar da linha de comando é convertida automaticamente em variáveis ​​de ambiente passadas para o comando:

$ set -k
$ echo a=1 b=2 c=3
$ 

É um pouco mais difícil convencer que são variáveis ​​de ambiente; executando isso funciona para mim:

$ set -k
$ env | grep '^[a-z]='   # No environment a, b, c
$ bash -c 'echo "Args: $*" >&2; env' a=1 b=2 c=3 | grep '^[a-z]='
Args: 
a=1
b=2
c=3
$ set +k
$ bash -c 'echo "Args: $*" >&2; env' a=1 b=2 c=3 | grep '^[a-z]='
Args: b=2 c=3
$

O primeiro env | grepnão demonstra variáveis ​​de ambiente com uma única letra minúscula. O primeiro bashmostra que não há argumentos passados ​​para o script executado via -c, e o ambiente contém as três variáveis ​​de letra única. A set +kcancela a -k, e mostra que o mesmo comando agora tem argumentos passados a ele. (Oa=1 foi tratado como $0no script; você também pode provar isso com eco apropriado.)

Isso alcança o que a pergunta faz - que digitar ./script.sh a=1 b=2deve ser o mesmo que digitara=1 b=2 ./script.sh .

Esteja ciente de que você terá problemas se tentar truques como este em um script:

if [ -z "$already_invoked_with_minus_k" ]
then set -k; exec "$0" "$@" already_invoked_with_minus_k=1
fi

O "$@"é tratado literalmente; não é re-analisado para encontrar variáveis ​​no estilo de atribuição (em ambos bashe ksh). Eu tentei:

#!/bin/bash

echo "BEFORE"
echo "Arguments:"
al "$@"
echo "Environment:"
env | grep -E '^([a-z]|already_invoked_with_minus_k)='
if [ -z "$already_invoked_with_minus_k" ]
then set -k; exec "$0" "$@" already_invoked_with_minus_k=1
fi

echo "AFTER"
echo "Arguments:"
al "$@"
echo "Environment:"
env | grep -E '^([a-z]|already_invoked_with_minus_k)='

unset already_invoked_with_minus_k

e apenas a already_invoked_with_minus_kvariável de ambiente é definida no execscript 'd.

Jonathan Leffler
fonte
Resposta muito boa! É interessante que isso não mude o PATH, embora HOME seja alterável, portanto deve haver algo como uma lista negra (contendo PATH pelo menos) de ambientes que seriam perigosos demais para serem configurados dessa maneira. Adoro como isso é ultra curto e responde à pergunta, mas vou com a solução de sanitize + eval + prefix, pois ainda é mais segura e, portanto, mais universalmente utilizável (em ambientes em que você não deseja que os usuários mexam com o ambiente) ) Obrigado e +1.
PSKocik
2

Minha tentativa:

#! /usr/bin/env bash
name='^[a-zA-Z][a-zA-Z0-9_]*$'
count=0
for arg in "$@"; do
    case "$arg" in
        *=*)
            key=${arg%%=*}
            val=${arg#*=}

            [[ "$key" =~ $name ]] && { let count++; eval "$key"=\$val; } || break

            # show time
            if [[ "$key" =~ $name ]]; then
                eval "out=\${$key}"
                printf '|%s| <-- |%s|\n' "$key" "$out"
            fi
            ;;
        *)
            break
            ;;
    esac
done
shift $count

# show time again   
printf 'arg: |%s|\n' "$@"

Trabalha com (quase) lixo arbitrário no RHS:

$ ./assign.sh Foo_Bar33='1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0' '1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0=33'
|Foo_Bar33| <-- |1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0|
arg: |1 2;3`4"5~6!7@8#9$0 1%2^3&4*5(6)7-8=9+0=33|

$ ./assign.sh a=1 b=2 c d=4
|a| <-- |1|
|b| <-- |2|
arg: |c|
arg: |d=4|
lcd047
fonte
mudança vai matar as coisas erradas se você não quebrar o laço na primeira-x não = parâmetro y
frostschutz
@frostschutz Bom ponto, editado.
Lcd047
Bom trabalho em generalizá-lo. Eu acho que pode ser um pouco simplificado.
PSKocik
Você teve a chance de dar uma olhada na minha edição?
PSKocik
Por favor, dê uma olhada na minha edição. É assim que eu gosto (+ talvez apenas em shiftvez de shift 1). Caso contrário, obrigado!
PSKocik
0

Há algum tempo, decidi aliaspor esse tipo de trabalho. Aqui está outra resposta minha:


Às vezes, pode ser possível separar a avaliação e a execução de tais declarações. Por exemplo, aliaspode ser usado para pré-avaliar um comando. No exemplo a seguir, a definição da variável é salva em um alias que só pode ser declarado com êxito se a $varvariável que está avaliando não contiver bytes que não correspondam aos alfanuméricos ASCII ou _.

LC_OLD=$LC_ALL LC_ALL=C
for var do    val=${var#*=} var=${var%%=*}
    alias  "${var##*[!_A-Z0-9a-z]*}=_$var=\$val" &&
    eval   "${var##[0-9]*}" && unalias "$var"
done;       LC_ALL=$LC_OLD

evalé usado aqui para manipular a chamada do novo a aliaspartir de um contexto de nome de variante citado - não exatamente para a atribuição. E evalsó é chamado se a aliasdefinição anterior for bem-sucedida e, embora eu saiba que muitas implementações diferentes aceitem muitos tipos diferentes de valores para nomes de alias, ainda não encontrei um shell que aceite um completamente vazio .

A definição no alias é para _$var, no entanto, e isso garante que nenhum valor significativo do ambiente seja substituído. Não conheço nenhum valor de ambiente notável que comece com _ e geralmente é uma aposta segura para uma declaração semi-privada.

De qualquer forma, se a definição de alias for bem-sucedida, ela declarará um alias nomeado para $varo valor de. E evalsó chamará isso aliasse também não começar com um número - senão evalobtém apenas um argumento nulo. Então, se ambas as condições forem atendidas evalchamadas as aliase a definição variável guardado na aliasé feito, após o qual o novo alias é prontamente removida da tabela de hash.


Também útil aliasnesse contexto é que você pode imprimir seu trabalho. aliasimprimirá uma declaração duplamente citada de segurança para reexecução quando solicitada.

sh -c "IFS=\'
    alias q=\"\$*\" q" -- \
    some args which alias \
    will print back at us

SAÍDA

q='some'"'"'args'"'"'which'"'"'alias'"'"'will'"'"'print'"'"'back'"'"'at'"'"'us'
mikeserv
fonte