Bash: passando o comando com parâmetros cotados para funcionar

3

Eu tenho a seguinte função bash:

function exe {
    echo -e "Execute: $1"
    # Loops every 3s, outputting '...' until command finished executing
    LOOP=0
    while true;
    do
        if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
        sleep 3;
        LOOP=$LOOP+1
    done & ERROR="$($2 2>&1)" # Execute the command and capture output to variable

    status=$?
    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo -e "✖ Error" >&2
        echo -e "$ERROR" >&2
    else
        echo -e "✔ Success"
    fi
    return $status
}

A intenção é chamá-lo da seguinte forma:

exe "Update apt indexes" \
    "sudo apt-get update"

Quais resultados:

Execute: Update apt indexes
...
...
...
...
✔ Success

Isso funciona corretamente, exceto quando uma string entre aspas é usada como um parâmetro no comando passado.

Por exemplo, o seguinte não funciona:

exe "Create self signed certificate" \
    "sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""

set -x revela que o comando acima é transformado no seguinte para execução:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '"/C=GB/ST=London/L=London/O=Confetti' Celebrations Ltd/OU=IT 'Department/CN=dev.sign-in.confetti.local"'

Parece que pegou um número de aspas simples e torna o comando ineficaz.

Eu gostaria de uma versão que não tenha essa limitação. Alguma idéia?

===============

Meu código final, após as sugestões e algumas outras correções de bugs, é:

exe () {
    echo -e "Execute: $1"
    LOOP=0
    while true;
    do
        if ! [ $LOOP == 0 ]; then echo -e "..."; fi;
        sleep 3;
        LOOP=$((LOOP+1))
    done & ERROR=$("${@:2}" 2>&1)
    status=$?
    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo -e "✖ Error" >&2
        echo -e "$ERROR" >&2
    else
        echo -e "✔ Success"
    fi
    return $status
}

A função destina-se como um 'embelezador' para um script de shell de provisionamento vagrant e pode ser chamado como

exe "Update apt indexes" sudo apt-get update

A saída aparece como

Execute: Update apt indexes
...
...
...
...
✔ Success

Comandos que duram menos de 3 segundos não veem pontos de saída de progresso

A menos que haja um erro, quando você receber um status de erro de volta e a saída completa do comando.

A principal intenção é livrar os scripts de provisionamento vagrant das linhas vermelhas mostradas ao enviar mensagens para stderr. Muitos comandos geram corretamente as informações para o stderr, uma vez que se destinam a mensagens que não devem ser canalizadas para outros comandos. Mensagens de impressão do Vagrant para stdout como Isso deixa um número de mensagens de provisionamento que se parecem com erros, mas não são.

Esta função não imprime para stderr, a menos que o comando executado retorne um status diferente de zero. Isso significa que, a menos que um comando indique uma falha, você não verá mensagens vermelhas. Quando um comando indicou falha com uma mensagem diferente de zero, então nós enviamos a saída completa do comando para stderr, dando linhas vermelhas.

Torna o aprovisionamento de vagabundos com um shell script muito mais ordenado e significa que realmente podemos olhar para as mensagens vermelhas sabendo que elas significam algo.

A função completa para usar com vagrant, incluindo um pouco de fluff visual que deixei de fora nos trechos acima, pode ser vista aqui: link

Exemplo de saída usando a função exe, sem erros:

Exemplodesaídausandoafunçãoexe,comerro:

Saídapadrãodaexecuçãodecomandosdiretamente,semerrosreais:

    
por michaelward82 03.03.2016 / 12:26

3 respostas

2

Você pode não querer passar o comando inteiro como uma string. Nós temos listas no shell, como a lista de argumentos, e passando uma lista como uma lista é muito mais simples.

Em vez de escrever exe blah "blahh cmd" , escreva o comando diretamente como exe blah blahh cmd . Então, quando você precisar usar o comando inteiro diretamente, use a expansão de fatiamento para obter tudo após o primeiro argumento: ERROR=$("${@:1}" 2>&1) .

Tradicionalmente, as pessoas podem usar shift para mudar toda a lista de argumentos 'esquerda' (veja help shift ):

f(){
    local j="$1"
    shift
    echo "$j,$3"
    shift 50
    echo "$1" # guess what "$@" is now?
}

f {1..100}

Mas isso não é necessário para bash aparentemente.

Falando sobre o slicing thing, você também pode conferir os arrays no bash.

Ugh ainda .. Você pode usar eval para executar uma string diretamente, mas isso geralmente é considerado uma coisa ruim, já que você está permitindo muito mais do que simples comandos.

E como uma dica de estilo, prefira o mais curto e mais (POSIX-) portável xxx() over function xxx e function xxx() . Na festa eles são apenas idênticos.

    
por 04.03.2016 / 23:19
1

O problema central da sua pergunta está em "como dividir uma string" dentro de um $var .

A maneira "malvada" (porque é propensa a erros e execução de código) é usar eval:

 eval set -- $var           ### Dangerous, not recommended, do not use.

Isso define a string dividida nos argumentos posicionais (uma matriz é um pouco mais complexa). Mas que a variável $var não é citada (algo a ser evitado a menos que você realmente saiba o que está fazendo) a sujeita à "divisão de palavras" (que queremos), mas que também permite que a "expansão do nome do caminho" aconteça. Você pode tentar este comando (use um diretório com poucos arquivos)

$ var='hello * world'
$ eval set -- $var
$ echo "$@"

É seguro executar, não há valores definidos externamente e a expansão de * apenas definirá os valores nos parâmetros posicionais.

Para evitar a "expansão do nome do caminho", é usado um set -f e, nesse caso, é fácil integrá-lo ao comando:

$ var='hello * world'
$ set -f
$ eval set -- $var
$ echo "$@"
hello * world

Isto é com um padrão IFS de espaço Tab Nova Linha .

As coisas podem ficar complexas se o IFS puder ser definido externamente.

Vários problemas podem ser resolvidos usando read :

$ IFS=' ' read -ra arr <<<"$var"
$ echo "${arr[@]}"
hello * world

Isso define o IFS para o comando (evite IFS definido externamente), leia sem processar a barra invertida (a opção -r), coloque tudo dentro de uma variável de matriz (a opção -a) e esteja usando a variável citada "$var" . A única ressalva é que os espaços repetidos entre as palavras serão apagados (porque o IFS é um espaço). Isso não é um problema para uma linha de comando executável.

Mas tentar executar comandos que precisam de argumentos com espaços falhará:

$ var='date -d "-1 day" +"%Y.%m.%d-%H:%M:%S"'
$ IFS=' ' read -ra arr <<<"$var"
$ "${arr[@]}"
date: extra operand '+"%Y.%m.%d-%H:%M:%S"'

A única solução real é que você construa a matriz do comando corretamente desde o início:

$ arr=( date -d "-1 day" +"%Y.%m.%d-%H:%M:%S" )
$ "${arr[@]}"
2016.03.05-00:25:17

Pense nessa solução como CSV "Comma (space) Separated Values" (valores separados por vírgula).

Este script funcionará:

#!/bin/bash

function exe {
    echo "Execute: $1"
    # Loops every 3s, outputting '...' until command finished executing
    LOOP=0
    while true; do
        if [ $LOOP -gt 0 ]; then echo -e "..."; fi;
            sleep 3;
            (( LOOP++ ))
    done &

    ERROR="$("${@:2}" 2>&1)" # Execute command and capture output.
    status=$?

    kill $!; trap 'kill $!' SIGTERM

    if [ $status -ne 0 ];
    then
        echo "✖ Error" >&2
        echo "$ERROR" >&2
    else
        echo "✔ Success"
    fi
    return $status
}

cmd=( date -d '-1 day' +'%Y.%m.%d-%H:%M:%S' )
exe "give me yesterday date" "${cmd[@]}" 

cmd=( sudo apt-get update )
exe "update package list" "${cmd[@]}" 
    
por 06.03.2016 / 01:37
0

No caso de aspas nas sequências de parâmetros a serem executadas como código, é possível recuperar a sequência de parâmetros em uma matriz, como a matriz de parâmetros posicionais $@ . Isso pode ser alcançado - pelo menos para o exemplo dado - usando ... & ERROR="$( printf "%s" "$2" | xargs sh -c 'exec "$0" "$@" 2>&1' ) ... . (Há casos com aspas duplas adicionais nessa cadeia já citada que pode causar xargs: unterminated quote mensagens).

Para mais sugestões, consulte: Linux / Bash: Como sair da lista? .

# test cases
# help :
#set -- '' "ls -ld / 'a bc'" 
set -- '' ": sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\""

printf "%s" "$2" | 
    xargs sh -c '
       echo "arg 0: ${0}"
       for ((i=1; i<=$#; i++)); do
          echo "arg $i: ${@:i:1}"
       done
       set -xv
       "$0" "$@"
    ' 

# output
arg 0: :
arg 1: sudo
arg 2: openssl
arg 3: req
arg 4: -x509
arg 5: -nodes
arg 6: -days
arg 7: 365
arg 8: -newkey
arg 9: rsa:2048
arg 10: -keyout
arg 11: /etc/apache2/ssl/apache.key
arg 12: -out
arg 13: /etc/apache2/ssl/apache.crt
arg 14: -subj
arg 15: /C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local
   "$0" "$@"
+ : sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local'

(E LOOP=$LOOP+1 no seu código acima deve ser LOOP=$((LOOP+1)) btw.)

    
por 03.03.2016 / 18:53

Tags