parâmetros dd-style para um script bash

18

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

./script a=1 b=43

para 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 eval seja seguro, ou seja, que "$arg" corresponda a uma atribuição de variável estática (sem execução de código)?

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

    
por PSkocik 04.06.2015 / 18:33

6 respostas

6

A solução lcd047 foi refatorada com um prefixo DD_OPT_ codificado:

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

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

Eu coloquei isso em um arquivo de origem com uma 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 magia.

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"

Eu fiz um repositório disso, completo com testes e um README.md. Então eu usei isso em um wrapper CLI da API do Github que eu estava escrevendo, e usei o mesmo wrapper para configurar um clone do github do referido repositório (bootstrapping é divertido).

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

    
por 04.06.2015 / 20:44
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

Editar: Com base em um comentário de Stéphane Chazelas, adicionei sinalizadores ao declarar para evitar que a variável atribuída já esteja declarada como uma variável de matriz ou número inteiro, o que evitará vários casos em qual declare avaliará a parte do valor do argumento key=val . (O +a causará um erro se a variável a ser definida já estiver declarada como uma variável de matriz, por exemplo). Todas essas vulnerabilidades referem-se ao uso dessa sintaxe para reatribuir variáveis existentes (matriz ou inteiro), que normalmente seriam variáveis de shell conhecidas.

Na verdade, isso é apenas uma instância de uma classe de ataques de injeção que afetará igualmente as soluções eval : seria realmente muito melhor apenas permitir nomes de argumentos conhecidos do que definir cegamente qualquer variável que estivesse presente na linha de comando. (Considere o que acontece se a linha de comando definir PATH , por exemplo. Ou redefine PS1 para incluir alguma avaliação que acontecerá na próxima exibição do prompt.)

Em vez de usar variáveis bash, prefiro usar uma matriz associativa de argumentos nomeados, que é mais fácil de configurar e muito mais segura. Alternativamente, pode definir variáveis bash reais, mas somente se seus nomes estiverem em uma matriz associativa de argumentos legítimos.

Como 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
    
por 04.06.2015 / 20:26
9

Um POSIX 1 (define $<prefix>var em vez de $var para evitar 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 , seria atribuído:

my_prefix_x <= 1
my_prefix_PATH <= /tmp/evil
my_prefix_1 <= 2
    
por 04.06.2015 / 21:32
5

O shell Bourne clássico é suportado e o shell Bash e Korn ainda suporta uma opção -k . Quando está em vigor, qualquer opção de comando ' dd -like' em qualquer lugar na 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-se de 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 | grep não demonstra nenhuma variável de ambiente com uma única letra minúscula. O primeiro bash mostra que não há argumentos passados para o script executado via -c e o ambiente contém as três variáveis de uma letra. O set +k cancela o -k e mostra que o mesmo comando agora tem argumentos passados para ele. (O a=1 foi tratado como $0 para o script; você pode provar isso também, com o eco apropriado.)

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

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

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

O "$@" é tratado textualmente; não é re-analisado para encontrar variáveis no estilo de atribuição (em bash e 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 variável de ambiente already_invoked_with_minus_k está definida no script exec 'd.

    
por 05.06.2015 / 16:49
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' "$@"

Funciona com lixo (quase) 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|
    
por 04.06.2015 / 20:58
0

Algum tempo atrás eu me decidi por alias para esse tipo de trabalho. Aqui está outra resposta minha:

Às vezes, é possível separar a avaliação e execução de tais declarações. Por exemplo, alias pode 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 variável $var que está avaliando não contiver bytes que não correspondam a 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 o novo alias de um contexto varname citado - não exatamente para a atribuição. E eval só é chamado se a definição alias anterior for bem-sucedida e, embora eu saiba que muitas implementações diferentes aceitarão muitos tipos diferentes de valores para nomes de alias, ainda não encontrei uma casca que aceitará um completamente vazio.

A definição dentro do alias é para _$var , no entanto, e isso é para garantir que nenhum valor de ambiente significativo seja gravado. Eu não conheço nenhum valor de ambiente digno de nota começando com um _ e normalmente é uma aposta segura para uma declaração semi-privada.

De qualquer forma, se a definição do alias for bem-sucedida, ela declarará um alias nomeado para o valor de $var . E eval só chamará esse alias se também não começar com um número - senão eval obtém apenas um argumento nulo. Portanto, se ambas as condições forem atendidas, eval chama o alias e a definição da variável salva no alias é feita, após o qual o novo alias é prontamente removido da tabela de hash.

Também é útil sobre alias neste contexto que você pode imprimir seu trabalho. alias imprimirá uma declaração segura para shell-rexecution duplamente citada quando for solicitada.

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

OUTPUT

q='some'"'"'args'"'"'which'"'"'alias'"'"'will'"'"'print'"'"'back'"'"'at'"'"'us'
    
por 07.06.2015 / 15:14

Tags