Como defino uma variável para a saída de um comando no Bash?

1679

Eu tenho um script bastante simples que é algo como o seguinte:

#!/bin/bash

VAR1="$1"
MOREF='sudo run command against $VAR1 | grep name | cut -c7-'

echo $MOREF

Quando executo esse script na linha de comando e passo os argumentos, não estou obtendo nenhuma saída. No entanto, quando executo os comandos contidos na $MOREFvariável, sou capaz de obter saída.

Como se pode pegar os resultados de um comando que precisa ser executado em um script, salvá-lo em uma variável e, em seguida, produzir essa variável na tela?

John
fonte
1
Uma questão relacionada stackoverflow.com/questions/25116521/…
Sandeepan Nath
40
Além disso, as variáveis ​​all-caps são definidas pelo POSIX para nomes de variáveis ​​com significado para o sistema operacional ou o próprio shell, enquanto nomes com pelo menos um caractere minúsculo são reservados para uso do aplicativo. Portanto, considere usar nomes em minúsculas para suas próprias variáveis ​​de shell para evitar conflitos não intencionais (lembrando que a configuração de uma variável de shell substituirá qualquer variável de ambiente com nome semelhante).
Charles Duffy
1
Como uma saída para o lado, a captura em uma variável só assim você pode, em seguida, echoa variável é um uso inútil de echo, e um uso inútil de variáveis.
Tripleee
1
Além disso, o armazenamento de saída em variáveis ​​é muitas vezes desnecessário. Para cadeias pequenas e curtas, você precisará fazer referência várias vezes no seu programa, isso é completamente bom e exatamente o caminho a percorrer; mas para processar qualquer quantidade não trivial de dados, você deseja remodelar seu processo em um pipeline ou usar um arquivo temporário.
Tripleee 19/01/19

Respostas:

2378

Além dos backticks `command`, a substituição de comandos pode ser feita com $(command)or "$(command)", que eu acho mais fácil de ler, e permite o aninhamento.

OUTPUT=$(ls -1)
echo "${OUTPUT}"

MULTILINE=$(ls \
   -1)
echo "${MULTILINE}"

Citar ( ") importa preservar os valores das variáveis ​​com várias linhas ; é opcional no lado direito de uma tarefa, pois a divisão de palavras não é executada e , portanto OUTPUT=$(ls -1), funcionaria bem.

Andy Lester
fonte
59
Podemos fornecer um separador para saída em várias linhas?
Aryan
20
O espaço em branco (ou a falta de espaço em branco) importa
Ali
8
@ timhc22, as chaves são irrelevantes; são apenas as aspas que são importantes: se os resultados da expansão são divididos por cadeias e expandidos por glob antes de serem passados ​​para o echocomando.
Charles Duffy
4
Ah obrigado! Então, há algum benefício para o aparelho?
precisa saber é o seguinte
14
Os chavetas podem ser usadas quando a variável é imediatamente seguida por mais caracteres que podem ser interpretados como parte do nome da variável. por exemplo ${OUTPUT}foo . Eles também são obrigados ao executar operações de cadeia em linha sobre a variável, tais como${OUTPUT/foo/bar}
Remer rica
282

O caminho certo é

$(sudo run command)

Se você vai usar um apóstrofo, precisa `, não' . Esse personagem é chamado de "backticks" (ou "sotaque grave").

Como isso:

#!/bin/bash

VAR1="$1"
VAR2="$2"

MOREF=`sudo run command against "$VAR1" | grep name | cut -c7-`

echo "$MOREF"
Ilya Kogan
fonte
31
A sintaxe do backtick é obsoleta e você realmente precisa colocar aspas duplas em torno da interpolação da variável no echo.
Tripleee
10
Eu acrescentaria que você deve ter cuidado com os espaços em torno de '=' na tarefa acima. Você shouln't tem nenhum espaços , caso contrário, você vai ter uma atribuição incorreta
zbstof
4
O comentário de tripleeee está correto. Em cygwin (maio de 2016), `` não funciona enquanto $()trabalha. Não foi possível corrigir até que eu vi esta página.
Toddwz 13/05
2
Elaboração como um exemplo na Atualização (2018) seria apreciada.
Eduard
90

Alguns truques do Bash que eu uso para definir variáveis ​​de comandos

2nd Edit 2018-02-12: Adicionada uma maneira diferente, pesquise na parte inferior desta por tarefas de longa duração !

2018-01-25 Edit: adicionada uma função de amostra (para preencher variáveis ​​sobre o uso do disco)

Primeira maneira simples, antiga e compatível

myPi=`echo '4*a(1)' | bc -l`
echo $myPi 
3.14159265358979323844

Principalmente compatível, segunda via

Como o aninhamento pode se tornar pesado, parênteses foram implementados para esse

myPi=$(bc -l <<<'4*a(1)')

Amostra aninhada:

SysStarted=$(date -d "$(ps ho lstart 1)" +%s)
echo $SysStarted 
1480656334

Lendo mais de uma variável (com Bashisms )

df -k /
Filesystem     1K-blocks   Used Available Use% Mounted on
/dev/dm-0         999320 529020    401488  57% /

Se eu apenas quero um valor usado :

array=($(df -k /))

você pode ver uma variável de matriz :

declare -p array
declare -a array='([0]="Filesystem" [1]="1K-blocks" [2]="Used" [3]="Available" [
4]="Use%" [5]="Mounted" [6]="on" [7]="/dev/dm-0" [8]="999320" [9]="529020" [10]=
"401488" [11]="57%" [12]="/")'

Então:

echo ${array[9]}
529020

Mas eu prefiro isso:

{ read foo ; read filesystem size using avail prct mountpoint ; } < <(df -k /)
echo $using
529020

O primeiro read foosó vai pular linha de cabeçalho, mas em apenas um comando, você vai preencher 7 diferentes variáveis:

declare -p avail filesystem foo mountpoint prct size using
declare -- avail="401488"
declare -- filesystem="/dev/dm-0"
declare -- foo="Filesystem     1K-blocks   Used Available Use% Mounted on"
declare -- mountpoint="/"
declare -- prct="57%"
declare -- size="999320"
declare -- using="529020"

Ou até:

{ read foo ; read filesystem dsk[{6,2,9}] prct mountpoint ; } < <(df -k /)
declare -p mountpoint dsk
declare -- mountpoint="/"
declare -a dsk=([2]="529020" [6]="999320" [9]="401488")

... também funcionará com matrizes associativas :read foo disk[total] disk[used] ...

Função de amostra para preencher algumas variáveis:

#!/bin/bash

declare free=0 total=0 used=0

getDiskStat() {
    local foo
    {
        read foo
        read foo total used free foo
    } < <(
        df -k ${1:-/}
    )
}

getDiskStat $1
echo $total $used $free

Nota: a declarelinha não é necessária, apenas para facilitar a leitura.

Sobre sudo cmd | grep ... | cut ...

shell=$(cat /etc/passwd | grep $USER | cut -d : -f 7)
echo $shell
/bin/bash

(Evite inútil cat! Portanto, este é apenas um garfo a menos:

shell=$(grep $USER </etc/passwd | cut -d : -f 7)

Todos os tubos ( |) implica garfos. Onde outro processo precisa ser executado, acessando disco, chamadas de bibliotecas e assim por diante.

Portanto, o uso sedde amostra limitará o subprocesso a apenas um garfo :

shell=$(sed </etc/passwd "s/^$USER:.*://p;d")
echo $shell

E com Bashisms :

Mas, para muitas ações, principalmente em arquivos pequenos, o Bash poderia fazer o trabalho sozinho:

while IFS=: read -a line ; do
    [ "$line" = "$USER" ] && shell=${line[6]}
  done </etc/passwd
echo $shell
/bin/bash

ou

while IFS=: read loginname encpass uid gid fullname home shell;do
    [ "$loginname" = "$USER" ] && break
  done </etc/passwd
echo $shell $loginname ...

Indo mais longe sobre a divisão de variáveis ...

Dê uma olhada na minha resposta para Como eu divido uma string em um delimitador no Bash?

Alternativa: reduzindo garfos usando tarefas em execução de longa duração em segundo plano

2a Edição 2018-02-12:

Para evitar vários garfos como

myPi=$(bc -l <<<'4*a(1)'
myRay=12
myCirc=$(bc -l <<<" 2 * $myPi * $myRay ")

ou

myStarted=$(date -d "$(ps ho lstart 1)" +%s)
mySessStart=$(date -d "$(ps ho lstart $$)" +%s)

Isso funciona bem, mas executar muitos garfos é pesado e lento.

E comandos como datee bcpodem fazer muitas operações, linha por linha !

Vejo:

bc -l <<<$'3*4\n5*6'
12
30

date -f - +%s < <(ps ho lstart 1 $$)
1516030449
1517853288

Portanto, poderíamos usar um processo em segundo plano de longa execução para realizar muitos trabalhos, sem precisar iniciar uma nova bifurcação para cada solicitação.

Nós apenas precisamos de alguns descritores de arquivo e fifos para fazer isso corretamente:

mkfifo /tmp/myFifoForBc
exec 5> >(bc -l >/tmp/myFifoForBc)
exec 6</tmp/myFifoForBc
rm /tmp/myFifoForBc

(Claro, o FD 5e 6deve ser não utilizado!) ... A partir daí, você pode usar esse processo:

echo "3*4" >&5
read -u 6 foo
echo $foo
12

echo >&5 "pi=4*a(1)"
echo >&5 "2*pi*12"
read -u 6 foo
echo $foo
75.39822368615503772256

Em uma função newConnector

Você pode encontrar minha newConnectorfunção no GitHub.Com ou no meu próprio site (Nota no GitHub: existem dois arquivos no meu site. A função e a demonstração são agrupadas em um arquivo que pode ser usado para uso ou apenas executado para demonstração.)

Amostra:

. shell_connector.sh

tty
/dev/pts/20

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30745 pts/20   R+     0:00  \_ ps --tty pts/20 fw

newConnector /usr/bin/bc "-l" '3*4' 12

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30944 pts/20   S      0:00  \_ /usr/bin/bc -l
  30952 pts/20   R+     0:00  \_ ps --tty pts/20 fw

declare -p PI
bash: declare: PI: not found

myBc '4*a(1)' PI
declare -p PI
declare -- PI="3.14159265358979323844"

A função myBcpermite que você use a tarefa em segundo plano com sintaxe simples e para a data:

newConnector /bin/date '-f - +%s' @0 0
myDate '2000-01-01'
  946681200
myDate "$(ps ho lstart 1)" boottime
myDate now now ; read utm idl </proc/uptime
myBc "$now-$boottime" uptime
printf "%s\n" ${utm%%.*} $uptime
  42134906
  42134906

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30944 pts/20   S      0:00  \_ /usr/bin/bc -l
  32615 pts/20   S      0:00  \_ /bin/date -f - +%s
   3162 pts/20   R+     0:00  \_ ps --tty pts/20 fw

A partir daí, se você quiser finalizar um dos processos em segundo plano, basta fechar o seu fd :

eval "exec $DATEOUT>&-"
eval "exec $DATEIN>&-"
ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
   4936 pts/20   Ss     0:00 bash
   5256 pts/20   S      0:00  \_ /usr/bin/bc -l
   6358 pts/20   R+     0:00  \_ ps --tty pts/20 fw

o que não é necessário, porque todos fecham quando o processo principal termina.

F. Hauri
fonte
A amostra aninhada acima é o que eu estava procurando. Pode haver uma maneira mais simples, mas o que eu estava procurando era a maneira de descobrir se um contêiner de docker já existe, recebendo seu nome em uma variável de ambiente. Então, para mim: EXISTING_CONTAINER=$(docker ps -a | grep "$(echo $CONTAINER_NAME)")foi a afirmação que eu estava procurando.
Capricórnio1
2
@ capricorn1 Esse é um uso inútilecho ; você quer simplesmentegrep "$CONTAINER_NAME"
tripleee
Provavelmente sinto falta de algo aqui: kubectl get ns | while read -r line; do echo $ line | termo grep | cut -d '' -f1 ; doneimprime para cada $lineuma uma linha vazia e depois bash: xxxx: command not found. No entanto, eu esperaria que fosse impressa apenasxxx
papanito 14/02
77

Como eles já indicaram, você deve usar 'backticks'.

A alternativa proposta também $(command)funciona, e também é mais fácil de ler, mas observe que é válida apenas com o Bash ou o KornShell (e os shells derivados deles); portanto, se seus scripts precisam ser realmente portáteis em vários sistemas Unix, você deve preferir a velha notação de backticks.

bitwelder
fonte
23
Eles são abertamente cautelosos. Os backticks foram descontinuados pelo POSIX há muito tempo; a sintaxe mais moderna deve estar disponível na maioria dos shells deste milênio. (Há ainda ambientes legados tosse HP-UX tosse que está preso firmemente no início dos anos noventa.)
tripleee
25
Incorreta. $()é totalmente compatível com o POSIX sh, conforme padronizado há mais de duas décadas.
Charles Duffy
3
Observe que /bin/shno Solaris 10 ainda não reconhece $(…)- e o AFAIK também é válido no Solaris 11.
Jonathan Leffler
2
@ JonathanLeffler Na verdade, não é mais o caso do Solaris 11 onde /bin/shestá ksh93.
Jlliagre
2
@tripleee - resposta três anos atrasado :-) mas eu usei $()no shell POSIX no HP-UX nos últimos 10 anos.
Bob Jarvis - Restabelece Monica
54

Conheço três maneiras de fazer isso:

  1. As funções são adequadas para essas tarefas: **

    func (){
        ls -l
    }

    Invoque-o dizendo func.

  2. Outra solução adequada também pode ser avaliada:

    var="ls -l"
    eval $var
  3. O terceiro está usando variáveis ​​diretamente:

    var=$(ls -l)
    
        OR
    
    var=`ls -l`

Você pode obter a saída da terceira solução em um bom caminho:

echo "$var"

E também de uma maneira desagradável:

echo $var
MLSC
fonte
1
Os dois primeiros não parecem responder à pergunta como está atualmente, e a segunda é geralmente considerada duvidosa.
tripleee
1
Como alguém que é inteiramente novo no bash, por que é "$var"bom e $vardesagradável?
Peter
30

Apenas para ser diferente:

MOREF=$(sudo run command against $VAR1 | grep name | cut -c7-)
DigitalRoss
fonte
22

Ao definir uma variável, verifique se NÃO há espaços antes e / ou depois do sinal = . Literalmente, passei uma hora tentando descobrir isso, tentando todos os tipos de soluções! Isso não é legal.

Corrigir:

WTFF=`echo "stuff"`
echo "Example: $WTFF"

Falhará com o erro "stuff: not found" ou similar

WTFF= `echo "stuff"`
echo "Example: $WTFF"
Emil
fonte
2
A versão com o espaço significa algo diferente : var=value somecommandé executado somecommandcom varem seu ambiente tendo o valor value. Portanto, var= somecommandestá exportando varno ambiente somecommandcom um valor vazio (zero byte).
Charles Duffy
Sim, uma pegadinha do Bash.
Peter Mortensen
14

Se você quiser fazer isso com várias linhas / vários comandos / s, poderá fazer o seguinte:

output=$( bash <<EOF
# Multiline/multiple command/s
EOF
)

Ou:

output=$(
# Multiline/multiple command/s
)

Exemplo:

#!/bin/bash
output="$( bash <<EOF
echo first
echo second
echo third
EOF
)"
echo "$output"

Resultado:

first
second
third

Usando o heredoc , você pode simplificar as coisas facilmente, dividindo seu código longo de linha única em um código de várias linhas. Outro exemplo:

output="$( ssh -p $port $user@$domain <<EOF
# Breakdown your long ssh command into multiline here.
EOF
)"
Jahid
fonte
2
O que há com o segundo bashdentro da substituição de comando? Você já está criando um subshell pela própria substituição de comando. Se você deseja colocar vários comandos, basta separá-los por nova linha ou ponto e vírgula. output=$(echo first; echo second; ...)
Tripleee
Da mesma forma 'bash -c "bash -c \"bash -c ...\""', também seria "diferente"; mas não vejo o objetivo disso.
Tripleee
@ heredoc triplo significa algo mais do que isso. Você pode fazer o mesmo com alguns outros comandos como ssh sudo -sexecutar comandos mysql dentro, etc .. (em vez de bash)
Jahid
1
Não acho que estamos nos comunicando adequadamente. Eu estou desafiando a utilidade mais variable=$(bash -c 'echo "foo"; echo "bar"')longo variable=$(echo "foo"; echo "bar")- o aqui documento é apenas um mecanismo de cotação e realmente não acrescenta nada, exceto uma outra complicação inútil.
Tripleee
2
Quando uso o heredoc com ssh, preciso o comando a ser executado ssh -p $port $user@$domain /bin/bash <<EOFpara evitar Pseudo-terminal will not be allocated because stdin is not a terminal.avisos
F. Hauri
9

Você precisa usar

$(command-here)

ou

`command-here`

Exemplo

#!/bin/bash

VAR1="$1"
VAR2="$2"

MOREF="$(sudo run command against "$VAR1" | grep name | cut -c7-)"

echo "$MOREF"
Diego Velez
fonte
2
$()é muito melhor do que backticks. Veja: Qual é o benefício de usar $ () em vez de backticks em scripts de shell?
código é o seguinte
1
Eu não sabia que você poderia aninhar, mas faz todo o sentido, muito obrigado pela informação!
Diego Velez
6

Essa é outra maneira e é bom usar com alguns editores de texto que não conseguem destacar corretamente todos os códigos complexos que você cria:

read -r -d '' str < <(cat somefile.txt)
echo "${#str}"
echo "$str"
Poder do Aquário
fonte
Isso não lida com a pergunta do OP, que é realmente sobre substituição de comando , não substituição de processo .
precisa saber é o seguinte
6

Você pode usar retalhos (também conhecidos como túmulos com sotaque) ou $().

Gostar:

OUTPUT=$(x+2);
OUTPUT=`x+2`;

Ambos têm o mesmo efeito. Mas OUTPUT = $ (x + 2) é mais legível e o mais recente.

Pratik Patil
fonte
2
Parênteses foram implementados para permitir o aninhamento.
21716 F. Hauri
5

Se o comando que você está tentando executar falhar, ele gravará a saída no fluxo de erros e será impresso no console.

Para evitá-lo, você deve redirecionar o fluxo de erros:

result=$(ls -l something_that_does_not_exist 2>&1)
cafebabe1991
fonte
4

Alguns podem achar isso útil. Valores inteiros na substituição de variáveis, em que o truque é usar $(())colchetes duplos:

N=3
M=3
COUNT=$N-1
ARR[0]=3
ARR[1]=2
ARR[2]=4
ARR[3]=1

while (( COUNT < ${#ARR[@]} ))
do
  ARR[$COUNT]=$((ARR[COUNT]*M))
  (( COUNT=$COUNT+$N ))
done
Gus
fonte
1
Isso não parece ter nenhuma relevância para esta questão. Seria uma resposta razoável se alguém perguntasse como multiplicar um número em uma matriz por um fator constante, embora eu não me lembro de ter visto alguém perguntar isso (e então um for ((...))loop pareceria uma melhor correspondência para a variável do loop) ) Além disso, você não deve usar maiúsculas para suas variáveis ​​privadas.
Tripleee
Não concordo com a parte "relevância". A pergunta diz claramente: Como definir uma variável igual à saída de um comando no Bash? E adicionei esta resposta como complemento, porque cheguei aqui à procura de uma solução que me ajudou com o código que posteriormente postei. Em relação aos vars maiúsculos, obrigado por isso.
Gus
1
Isso pode ser escrito, ARR=(3 2 4 1);for((N=3,M=3,COUNT=N-1;COUNT < ${#ARR[@]};ARR[COUNT]*=M,COUNT+=N)){ :;}mas eu concordo com o @tripleee: eu não entendo o que fazer isso!
F. Hauri
@ F.Hauri ... o bash está ficando cada vez mais parecido com o perl, quanto mais fundo você entra nele!
Roblogic 7/11
4

Aqui estão mais duas maneiras:

Lembre-se de que o espaço é muito importante no Bash. Portanto, se você deseja que seu comando seja executado, use como está sem introduzir mais espaços.

  1. A seguir atribui harshila Le, em seguida, imprime

    L=$"harshil"
    echo "$L"
  2. A seguir, atribui a saída do comando tra L2. trestá sendo operado em outra variável, L1.

    L2=$(echo "$L1" | tr [:upper:] [:lower:])
Harshil
fonte
4
1. $"..."provavelmente não faz o que você pensa que faz . 2. Isso já é dado na resposta de Andy Lester.
gniourf_gniourf
@gniourf_gniourf está certo: veja a localização do bash não funcionará com várias linhas . Mas no bash , você pode usar echo ${L1,,}para fazer downcase ou upcase echo ${L1^^}.
F. Hauri