Como armazenar um comando em uma variável em um script de shell?

113

Eu gostaria de armazenar um comando para usar em um período posterior em uma variável (não a saída do comando, mas o próprio comando)

Tenho um script simples como segue:

command="ls";
echo "Command: $command"; #Output is: Command: ls

b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)

No entanto, quando tento algo um pouco mais complicado, falha. Por exemplo, se eu fizer

command="ls | grep -c '^'";

O resultado é:

Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory

Alguma ideia de como eu poderia armazenar esse comando (com tubos / comandos múltiplos) em uma variável para uso posterior?

Benjamin
fonte
10
Use uma função!
gniourf_gniourf

Respostas:

146

Use eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Erik
fonte
27
$ (...) agora é recomendado em vez de backticks. y = $ (eval $ x) mywiki.wooledge.org/BashFAQ/082
James Broadhead
14
evalé uma prática aceitável apenas se você confiar no conteúdo de suas variáveis. Se você estiver executando, digamos, x="ls $name | wc"(ou mesmo x="ls '$name' | wc"), então este código é um caminho rápido para vulnerabilidades de injeção ou escalonamento de privilégio se essa variável puder ser definida por alguém com menos privilégios. (Iterando sobre todos os subdiretórios /tmp, por exemplo? É melhor você confiar que cada usuário do sistema não fará nenhum chamado $'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/').
Charles Duffy
9
evalé um grande ímã de bug que nunca deve ser recomendado sem um aviso sobre o risco de comportamento de análise inesperado (mesmo sem strings maliciosas, como no exemplo de @CharlesDuffy). Por exemplo, tente x='echo $(( 6 * 7 ))'e então eval $x. Você pode esperar que imprima "42", mas provavelmente não irá. Você pode explicar por que não funciona? Você pode explicar por que eu disse "provavelmente"? Se as respostas a essas perguntas não forem óbvias para você, nunca toque eval.
Gordon Davisson
1
@Student, tente executar set -xantes para registrar a execução dos comandos, o que tornará mais fácil ver o que está acontecendo.
Charles Duffy de
1
@Student Eu também recomendo shellcheck.net por apontar erros comuns (e maus hábitos que você não deve adquirir).
Gordon Davisson
41

Você não usar eval! Há um grande risco de introdução de execução arbitrária de código.

BashFAQ-50 - Estou tentando colocar um comando em uma variável, mas os casos complexos sempre falham.

Coloque-o em uma matriz e expanda todas as palavras com aspas duplas "${arr[@]}"para não permitir que IFSas palavras sejam separadas devido à Divisão de Palavras .

cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')

e veja o conteúdo da matriz interna. O declare -ppermite que você veja o conteúdo do array dentro de cada parâmetro de comando em índices separados. Se um desses argumentos contiver espaços, aspas internas ao adicionar ao array evitarão que ele seja dividido devido à divisão de palavras.

declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'

e execute os comandos como

"${cmdArgs[@]}"
23:15:18

(ou) usar uma bashfunção para executar o comando,

cmd() {
   date '+%H:%M:%S'
}

e chamar a função apenas

cmd

POSIX shnão tem matrizes, então o mais próximo que você pode chegar é construir uma lista de elementos nos parâmetros posicionais. Esta é uma shmaneira POSIX de executar um programa de e-mail

# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
    subject=$1
    shift
    first=1
    for addr; do
        if [ "$first" = 1 ]; then set --; first=0; fi
        set -- "$@" --recipient="$addr"
    done
    if [ "$first" = 1 ]; then
        echo "usage: sendto subject address [address ...]"
        return 1
    fi
    MailTool --subject="$subject" "$@"
}

Observe que essa abordagem só pode lidar com comandos simples, sem redirecionamentos. Ele não pode lidar com redirecionamentos, pipelines, loops for / while, instruções if, etc.

Outro caso de uso comum é ao executar curlcom vários campos de cabeçalho e carga útil. Você sempre pode definir args como abaixo e invocar curlno conteúdo da matriz expandida

curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[@]}"

Outro exemplo,

payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'

agora que as variáveis ​​estão definidas, use uma matriz para armazenar seus argumentos de comando

curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")

e agora faça uma expansão adequada entre aspas

curl "${curlCMD[@]}"
Inian
fonte
Isso não funciona para mim, eu tentei Command=('echo aaa | grep a')e "${Command[@]}", esperando que execute literalmente o comando echo aaa | grep a. Não é verdade. Gostaria de saber se existe uma maneira segura de substituir eval, mas parece que cada solução que tem a mesma força evalpode ser perigosa. Não é?
Estudante de
Resumindo, como isso funciona se a string original contém uma barra vertical '|'?
Aluno de
@Student, se sua string original contém um tubo, essa string precisa passar pelas partes inseguras do analisador bash para ser executada como código. Não use uma string nesse caso; em vez disso, use uma função: Command() { echo aaa | grep a; }- após a qual você pode simplesmente executar Command, ou result=$(Command), ou algo semelhante.
Charles Duffy
1
@Student, certo; mas isso falha intencionalmente , porque o que você está pedindo é inerentemente inseguro .
Charles Duffy
1
@Student: Eu adicionei uma nota no último para mencionar que não funciona sob certas condições
Inian
25
var=$(echo "asdf")
echo $var
# => asdf

Usando este método, o comando é avaliado imediatamente e seu valor de retorno é armazenado.

stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015

Mesmo com backtick

stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015

Usar eval no $(...)não fará com que seja avaliado mais tarde

stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015

Usando eval, ele é avaliado quando evalé usado

stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
#                     ^^ Time changed

No exemplo acima, se você precisa executar um comando com argumentos, coloque-os na string que você está armazenando

stored_date="date -u"
# ...

Para scripts bash, isso raramente é relevante, exceto uma última observação. Tenha cuidado com eval. Avalie apenas as strings que você controla, nunca as strings vindas de um usuário não confiável ou criadas a partir de entrada de usuário não confiável.

  • Obrigado a @CharlesDuffy por me lembrar de citar o comando!
Nate
fonte
Isso não resolve o problema original em que o comando contém um tubo '|'.
Aluno de
@Nate, observe que eval $stored_datepode ser suficiente quando stored_dateapenas contém date, mas eval "$stored_date"é muito mais confiável. Execute str=$'printf \' * %s\\n\' *'; eval "$str"com e sem as aspas no final "$str"para um exemplo. :)
Charles Duffy
@CharlesDuffy Obrigado, esqueci de citar. Aposto que meu linter reclamaria se eu tivesse me dado ao trabalho de executá-lo.
Nate
1

Para o bash, armazene seu comando assim:

command="ls | grep -c '^'"

Execute seu comando assim:

echo $command | bash
Derek Hazell
fonte
1
Não tenho certeza, mas talvez essa maneira de executar o comando tenha os mesmos riscos que o uso de 'eval' tem.
Derek Hazell
0

Tentei vários métodos diferentes:

printexec() {
  printf -- "\033[1;37m$\033[0m"
  printf -- " %q" "$@"
  printf -- "\n"
  eval -- "$@"
  eval -- "$*"
  "$@"
  "$*"
}

Resultado:

$ printexec echo  -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
 bar
bash: echo -e foo\n bar: command not found

Como você pode ver, apenas o terceiro "$@"deu o resultado correto.

mpen
fonte
0

Tenha cuidado ao registrar um pedido com: X=$(Command)

Este ainda é executado Mesmo antes de ser chamado. Para verificar e confirmar isso, você pode fazer:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed
Azerty
fonte
-1
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.

TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT                         #This will return 0 
                                    #if you don't have tune0 interface.
                                    #Or count of installed tune0 interfaces.
Silentexpert
fonte
-8

Não é necessário armazenar comandos em variáveis, mesmo que você precise usá-los posteriormente. apenas execute-o normalmente. Se você armazenar em uma variável, precisará de algum tipo de evalinstrução ou invocará algum processo shell desnecessário para "executar sua variável".

kurumi
fonte
1
O comando que armazenarei dependerá das opções que envio, portanto, em vez de ter toneladas de instruções condicionais na maior parte do meu programa, é muito mais fácil armazenar o comando de que preciso para uso posterior.
Benjamin
1
@Benjamin, então, pelo menos, armazene as opções como variáveis ​​e não o comando. por exemplovar='*.txt'; find . -name "$var"
kurumi