AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / unix / Perguntas / 444946
Accepted
Tim
Tim
Asked: 2018-05-21 04:46:29 +0800 CST2018-05-21 04:46:29 +0800 CST 2018-05-21 04:46:29 +0800 CST

Como podemos executar um comando armazenado em uma variável?

  • 772
$ ls -l /tmp/test/my\ dir/
total 0

Eu queria saber por que as seguintes maneiras de executar o comando acima falham ou são bem-sucedidas?

$ abc='ls -l "/tmp/test/my dir"'

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0
bash shell
  • 5 5 respostas
  • 196816 Views

5 respostas

  • Voted
  1. Best Answer
    ilkkachu
    2018-05-21T04:58:21+08:002018-05-21T04:58:21+08:00

    Isso foi discutido em várias perguntas no unix.SE, tentarei coletar todos os problemas que puder surgir aqui. Abaixo está uma descrição de por que e como as várias tentativas falham, uma maneira de fazê-lo corretamente com uma função (para um comando fixo), ou com arrays de shell (Bash/ksh/zsh) ou o $@pseudo-array (POSIX sh), e notas sobre como usar evalpara fazer isso. Algumas referências no final.

    Para os propósitos aqui, não importa se são apenas os argumentos do comando ou também o nome do comando que deve ser armazenado em uma variável. Eles são processados ​​de forma semelhante até o ponto em que o comando é iniciado, ponto em que o shell apenas recebe a primeira palavra como o nome do comando a ser executado.


    Por que falha

    A razão pela qual você enfrenta esses problemas é o fato de que a divisão de palavras é bastante simples e não se presta a casos complexos, e o fato de que aspas expandidas de variáveis ​​não agem como aspas, mas são apenas caracteres comuns.

    (Observe que a parte sobre aspas é semelhante a qualquer outra linguagem de programação: por exemplo char *s = "foo()"; printf("%s\n", s), não chama a função foo()em C, mas apenas imprime a string foo(). O shell é uma linguagem de programação, não um processador de macro.)

    Lembre-se de que é o shell que processa aspas e expansões de variáveis ​​na linha de comando, transformando-a de uma única string na lista de strings que eventualmente são passadas para o comando iniciado. O programa em si não vê as aspas. Por exemplo, se for dado o comando ls -l "foo bar", o shell transforma isso nas três strings lse (removendo as aspas) -le foo baras passa para ls. (Até o nome do comando é passado, embora nem todos os programas o usem.)

    Os casos apresentados na pergunta:

    A atribuição aqui atribui a string única ls -l "/tmp/test/my dir"a abc:

    $ abc='ls -l "/tmp/test/my dir"'
    

    Abaixo, $abcé dividido em espaço em branco, e lsrecebe os três argumentos -l, "/tmp/test/mye dir"(com uma aspa na frente do segundo e outra no verso do terceiro). A opção funciona, mas o caminho é processado incorretamente:

    $ $abc
    ls: cannot access '"/tmp/test/my': No such file or directory
    ls: cannot access 'dir"': No such file or directory
    

    Aqui, a expansão é citada, então é mantida como uma única palavra. O shell tenta encontrar um programa literalmente chamado ls -l "/tmp/test/my dir", incluindo espaços e aspas.

    $ "$abc"
    bash: ls -l "/tmp/test/my dir": No such file or directory
    

    E aqui, $abcé dividido, e apenas a primeira palavra resultante é tomada como argumento para -c, então o Bash é executado lsno diretório atual. As outras palavras são argumentos para bash e são usadas para preencher $0, $1, etc.

    $ bash -c $abc
    'my dir'
    

    Com bash -c "$abc", e eval "$abc", há uma etapa adicional de processamento de shell, que faz as aspas funcionarem, mas também faz com que todas as expansões de shell sejam processadas novamente , portanto, há o risco de executar acidentalmente, por exemplo, uma substituição de comando de dados fornecidos pelo usuário, a menos que você esteja muito cuidado ao citar.


    Melhores maneiras de fazer isso

    As duas melhores maneiras de armazenar um comando são a) usar uma função em vez disso, b) usar uma variável de matriz (ou os parâmetros posicionais).

    Usando uma função:

    Simplesmente declare uma função com o comando dentro e execute a função como se fosse um comando. Expansões em comandos dentro da função são processadas apenas quando o comando é executado, não quando é definido, e você não precisa citar os comandos individuais.

    # define it
    myls() {
        ls -l "/tmp/test/my dir"
    }
    
    # run it
    myls
    

    Usando uma matriz:

    Arrays permitem criar variáveis ​​de várias palavras onde as palavras individuais contêm espaço em branco. Aqui, as palavras individuais são armazenadas como elementos de matriz distintos e a "${array[@]}"expansão expande cada elemento como palavras de shell separadas:

    # define the array
    mycmd=(ls -l "/tmp/test/my dir")
    
    # run the command
    "${mycmd[@]}"
    

    A sintaxe é um pouco horrível, mas os arrays também permitem que você construa a linha de comando peça por peça. Por exemplo:

    mycmd=(ls)               # initial command
    if [ "$want_detail" = 1 ]; then
        mycmd+=(-l)          # optional flag
    fi
    mycmd+=("$targetdir")    # the filename
    
    "${mycmd[@]}"
    

    ou mantenha partes da linha de comando constantes e use a matriz para preencher apenas uma parte dela, como opções ou nomes de arquivos:

    options=(-x -v)
    files=(file1 "file name with whitespace")
    target=/somedir
    
    transmutate "${options[@]}" "${files[@]}" "$target"
    

    A desvantagem dos arrays é que eles não são um recurso padrão, então shells POSIX simples (como dash, o padrão /bin/shno Debian/Ubuntu) não os suportam (mas veja abaixo). Bash, ksh e zsh sim, então é provável que seu sistema tenha algum shell que suporte arrays.

    Usando"$@"

    Em shells sem suporte para arrays nomeados, ainda é possível usar os parâmetros posicionais (o pseudo-array "$@") para armazenar os argumentos de um comando.

    A seguir, devem ser bits de script portáteis que fazem o equivalente aos bits de código na seção anterior. A matriz é substituída por "$@", a lista de parâmetros posicionais. A configuração "$@"é feita com set, e as aspas duplas ao redor "$@"são importantes (elas fazem com que os elementos da lista sejam citados individualmente).

    Primeiro, simplesmente armazenando um comando com argumentos "$@"e executando-o:

    set -- ls -l "/tmp/test/my dir"
    "$@"
    

    Configurando condicionalmente partes das opções de linha de comando para um comando:

    set -- ls
    if [ "$want_detail" = 1 ]; then
        set -- "$@" -l
    fi
    set -- "$@" "$targetdir"
    
    "$@"
    

    Usando apenas "$@"para opções e operandos:

    set -- -x -v
    set -- "$@" file1 "file name with whitespace"
    set -- "$@" /somedir
    
    transmutate "$@"
    

    (Claro, "$@"geralmente é preenchido com os argumentos do próprio script, então você terá que salvá-los em algum lugar antes de reaproveitar "$@".)


    Usando eval(cuidado aqui!)

    evalpega uma string e a executa como um comando, como se fosse digitado na linha de comando do shell. Isso inclui todo o processamento de cotação e expansão, que é útil e perigoso.

    No caso simples, permite fazer exatamente o que queremos:

    cmd='ls -l "/tmp/test/my dir"'
    eval "$cmd"
    

    Com eval, as aspas são processadas, então lseventualmente vê apenas os dois argumentos -le /tmp/test/my dir, como queremos. evaltambém é inteligente o suficiente para concatenar quaisquer argumentos que obtenha, portanto, eval $cmdtambém pode funcionar em alguns casos, mas, por exemplo, todas as execuções de espaços em branco seriam alteradas para espaços únicos. Ainda é melhor citar a variável lá, pois isso garantirá que ela não seja modificada para eval.

    No entanto, é perigoso incluir a entrada do usuário na string de comando paraeval . Por exemplo, isso parece funcionar:

    read -r filename
    cmd="ls -ld '$filename'"
    eval "$cmd";
    

    Mas se o usuário fornecer uma entrada que contenha aspas simples, ele poderá sair das aspas e executar comandos arbitrários! Por exemplo, com o input '$(whatever)'.txt, seu script executa alegremente a substituição do comando. Que poderia ter sido rm -rf(ou pior) em vez disso.

    O problema é que o valor de $filenamefoi incorporado na linha de comando que evalé executada. Foi expandido antes eval, que viu, por exemplo, o comando ls -l ''$(whatever)'.txt'. Você precisaria pré-processar a entrada para ser seguro.

    Se fizermos de outra forma, mantendo o nome do arquivo na variável e deixando o evalcomando expandi-lo, é mais seguro novamente:

    read -r filename
    cmd='ls -ld "$filename"'
    eval "$cmd";
    

    Observe que as aspas externas agora são aspas simples, portanto, as expansões internas não acontecem. Portanto, evalvê o comando ls -l "$filename"e expande o nome do arquivo com segurança.

    Mas isso não é muito diferente de apenas armazenar o comando em uma função ou array. Com funções ou matrizes, não há esse problema, pois as palavras são mantidas separadas o tempo todo e não há citação ou outro processamento para o conteúdo de filename.

    read -r filename
    cmd=(ls -ld -- "$filename")
    "${cmd[@]}"
    

    Praticamente a única razão para usar evalé aquela em que a parte variável envolve elementos de sintaxe de shell que não podem ser trazidos por meio de variáveis ​​(pipelines, redirecionamentos, etc.). No entanto, você precisará citar/escapar tudo o mais na linha de comando que precisa de proteção da etapa de análise adicional (consulte o link abaixo). De qualquer forma, é melhor evitar incorporar a entrada do usuário no evalcomando!


    Referências

    • Divisão de palavras no BashGuide
    • BashFAQ/050 ou "Estou tentando colocar um comando em uma variável, mas os casos complexos sempre falham!"
    • A pergunta Por que meu script de shell engasga com espaços em branco ou outros caracteres especiais? , que discute vários problemas relacionados a citações e espaços em branco, incluindo o armazenamento de comandos.
    • Escape de uma variável para uso como conteúdo de outro script
    • 205
  2. Hauke Laging
    2018-05-21T05:20:26+08:002018-05-21T05:20:26+08:00

    A maneira mais segura de executar um comando (não trivial) é eval. Então você pode escrever o comando como faria na linha de comando e ele é executado exatamente como se você tivesse acabado de digitá-lo. Mas você tem que citar tudo.

    Caso simples:

    abc='ls -l "/tmp/test/my dir"'
    eval "$abc"
    

    caso não tão simples:

    # command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
    abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
    eval "$abc"
    
    • 15
  3. ingyhere
    2022-09-08T19:13:13+08:002022-09-08T19:13:13+08:00

    Se precisar de um array para ser executado, transforme-o em um array!

    IFS=' ' read -r -a command_arr <<< "${command}"
    "${command_arr[@]}"
    

    a primeira linha converte a string em uma matriz. A segunda linha executa o comando.

    Isso não parece funcionar com comandos encadeados, por exemplo, usando &&ou ;.

    • 1
  4. Wattana Gaming
    2018-05-21T05:02:58+08:002018-05-21T05:02:58+08:00

    O segundo sinal de aspas interrompe o comando.

    Quando eu corro:

    abc="ls -l '/home/wattana/Desktop'"
    $abc
    

    Deu-me um erro.

    Mas quando eu corro

    abc="ls -l /home/wattana/Desktop"
    $abc
    

    Não há nenhum erro em tudo

    Não há como corrigir isso no momento (para mim), mas você pode evitar o erro por não ter espaço no nome do diretório.

    Esta resposta disse que o comando eval pode ser usado para corrigir isso, mas não funciona para mim :(

    • 0
  5. bloody
    2020-04-14T11:42:20+08:002020-04-14T11:42:20+08:00

    Outro truque para executar qualquer comando (trivial/não trivial) armazenado na abcvariável é:

    $ history -s $abc
    

    e pressione UpArrowou Ctrl-ppara trazê-lo na linha de comando. Ao contrário de qualquer outro método, você pode editá-lo antes da execução, se necessário.

    Este comando anexará o conteúdo da variável como uma nova entrada no histórico do Bash e você poderá recuperá-lo por UpArrow.

    • -3

relate perguntas

  • Problema estranho ao passar variáveis ​​do arquivo de texto

  • Enquanto a linha lê mantendo os espaços de escape?

  • Como salvar um caminho com ~ em uma variável?

  • ordem de substituição de processos `te` e `bash`

  • Execute um script muito lento até que seja bem-sucedido

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Como exportar uma chave privada GPG e uma chave pública para um arquivo

    • 4 respostas
  • Marko Smith

    ssh Não é possível negociar: "nenhuma cifra correspondente encontrada", está rejeitando o cbc

    • 4 respostas
  • Marko Smith

    Como podemos executar um comando armazenado em uma variável?

    • 5 respostas
  • Marko Smith

    Como configurar o systemd-resolved e o systemd-networkd para usar o servidor DNS local para resolver domínios locais e o servidor DNS remoto para domínios remotos?

    • 3 respostas
  • Marko Smith

    Como descarregar o módulo do kernel 'nvidia-drm'?

    • 13 respostas
  • Marko Smith

    apt-get update error no Kali Linux após a atualização do dist [duplicado]

    • 2 respostas
  • Marko Smith

    Como ver as últimas linhas x do log de serviço systemctl

    • 5 respostas
  • Marko Smith

    Nano - pule para o final do arquivo

    • 8 respostas
  • Marko Smith

    erro grub: você precisa carregar o kernel primeiro

    • 4 respostas
  • Marko Smith

    Como baixar o pacote não instalá-lo com o comando apt-get?

    • 7 respostas
  • Martin Hope
    rocky Como exportar uma chave privada GPG e uma chave pública para um arquivo 2018-11-16 05:36:15 +0800 CST
  • Martin Hope
    Wong Jia Hau ssh-add retorna com: "Erro ao conectar ao agente: nenhum arquivo ou diretório" 2018-08-24 23:28:13 +0800 CST
  • Martin Hope
    Evan Carroll status systemctl mostra: "Estado: degradado" 2018-06-03 18:48:17 +0800 CST
  • Martin Hope
    Tim Como podemos executar um comando armazenado em uma variável? 2018-05-21 04:46:29 +0800 CST
  • Martin Hope
    Ankur S Por que /dev/null é um arquivo? Por que sua função não é implementada como um programa simples? 2018-04-17 07:28:04 +0800 CST
  • Martin Hope
    user3191334 Como ver as últimas linhas x do log de serviço systemctl 2018-02-07 00:14:16 +0800 CST
  • Martin Hope
    Marko Pacak Nano - pule para o final do arquivo 2018-02-01 01:53:03 +0800 CST
  • Martin Hope
    Kidburla Por que verdadeiro e falso são tão grandes? 2018-01-26 12:14:47 +0800 CST
  • Martin Hope
    Christos Baziotis Substitua a string em um arquivo de texto enorme (70 GB), uma linha 2017-12-30 06:58:33 +0800 CST
  • Martin Hope
    Bagas Sanjaya Por que o Linux usa LF como caractere de nova linha? 2017-12-20 05:48:21 +0800 CST

Hot tag

linux bash debian shell-script text-processing ubuntu centos shell awk ssh

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve