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?


10
Use uma função!
— gniourf_gniourf

Respostas:


146

Use eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"

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[@]}"

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!

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

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.


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

-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.

-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".


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.