¿Cómo configuro una variable a la salida de un comando en Bash?

1679

Tengo un script bastante simple que es algo como lo siguiente:

#!/bin/bash

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

echo $MOREF

Cuando ejecuto este script desde la línea de comando y le paso los argumentos, no obtengo ningún resultado. Sin embargo, cuando ejecuto los comandos contenidos dentro de la $MOREFvariable, puedo obtener resultados.

¿Cómo se pueden tomar los resultados de un comando que debe ejecutarse dentro de un script, guardarlo en una variable y luego mostrar esa variable en la pantalla?

Juan
fuente
1
Una pregunta relacionada stackoverflow.com/questions/25116521/…
Sandeepan Nath
40
Por otro lado, POSIX define las variables en mayúsculas para los nombres de variables con significado para el sistema operativo o el propio shell, mientras que los nombres con al menos un carácter en minúscula están reservados para el uso de la aplicación. Por lo tanto, considere usar nombres en minúsculas para sus propias variables de shell para evitar conflictos no deseados (teniendo en cuenta que establecer una variable de shell sobrescribirá cualquier variable de entorno con un nombre similar).
Charles Duffy
1
Como una salida a un lado, la captura en una variable sólo para que pueda entonces echola variable es un uso inútil de echo, y un uso inútil de variables.
tripleee
1
Además, el almacenamiento de resultados en variables a menudo es innecesario. Para cadenas pequeñas y cortas, deberá hacer referencia varias veces en su programa, esto está completamente bien y es exactamente el camino a seguir; pero para procesar cualquier cantidad de datos no trivial, desea cambiar la forma de su proceso en una tubería, o usar un archivo temporal.
tripleee

Respuestas:

2379

Además de los backticks `command`, la sustitución de comandos se puede hacer con $(command)o "$(command)", que me resulta más fácil de leer, y permite anidar.

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

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

Las comillas ( ") son importantes para preservar los valores de las variables de varias líneas ; es opcional en el lado derecho de una tarea, ya que no se realiza la división de palabras , por OUTPUT=$(ls -1)lo que funcionaría bien.

Andy Lester
fuente
59
¿Podemos proporcionar un separador para la salida de varias líneas?
Aria
20
El espacio en blanco (o la falta de espacio en blanco) es importante
Ali
8
@ timhc22, las llaves son irrelevantes; son solo las comillas las que son importantes con respecto a: si los resultados de expansión se dividen en cadenas y se expanden globalmente antes de pasar al echocomando.
Charles Duffy
44
¡Ah gracias! Entonces, ¿hay algún beneficio para las llaves?
timhc22
14
Las llaves se pueden usar cuando la variable es seguida inmediatamente por más caracteres que podrían interpretarse como parte del nombre de la variable. por ej ${OUTPUT}foo . También son necesarios cuando se realizan operaciones de cadena en línea en la variable, como${OUTPUT/foo/bar}
rich remer
284

La forma correcta es

$(sudo run command)

Si vas a usar un apóstrofe, es necesario `, no '. Este personaje se llama "backticks" (o "acento grave").

Me gusta esto:

#!/bin/bash

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

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

echo "$MOREF"
Ilya Kogan
fuente
31
La sintaxis de retroceso es obsoleta, y realmente necesita poner comillas dobles alrededor de la interpolación variable en el echo.
tripleee
10
Agregaría que debe tener cuidado con los espacios alrededor de '=' en la asignación anterior. No debería tener ningún espacio allí, de lo contrario obtendrá una asignación incorrecta
zbstof
44
El comentario de tripleeee es correcto. En cygwin (mayo de 2016), `` no funciona mientras $()funciona. No se pudo solucionar hasta que vi esta página.
toddwz
2
Se agradecería la elaboración, como un ejemplo de Actualización (2018) .
Eduard
90

Algunos trucos de Bash que uso para establecer variables a partir de comandos

2nd Edit 2018-02-12: Agregado de una manera diferente, ¡busca en la parte inferior de este para tareas de larga duración !

25-01-2018 Editar: se agregó una función de muestra (para rellenar variables sobre el uso del disco)

Primera forma simple, antigua y compatible

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

Mayormente compatible, segunda forma

Como la anidación podría volverse pesada, se implementó un paréntesis para esto

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

Muestra anidada:

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

Lectura de más de una variable (con Bashisms )

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

Si solo quiero un valor usado :

array=($(df -k /))

podrías ver una variable 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]="/")'

Entonces:

echo ${array[9]}
529020

Pero prefiero esto:

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

El primero read foosimplemente saltará la línea del encabezado, pero en un solo comando, completará 7 variables diferentes :

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"

O incluso:

{ 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")

... también funcionará con matrices asociativas :read foo disk[total] disk[used] ...

Función de muestra para rellenar algunas variables:

#!/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: declareno se requiere línea, solo para facilitar la lectura.

Acerca de sudo cmd | grep ... | cut ...

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

(¡Evite inútil cat! Así que esto es solo un tenedor menos:

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

Todos los tubos ( |) implican horquillas. Donde se debe ejecutar otro proceso, acceder al disco, llamadas a bibliotecas, etc.

Por lo tanto, usar sedpara la muestra limitará el subproceso a solo una bifurcación :

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

Y con Bashisms :

Pero para muchas acciones, principalmente en archivos pequeños, Bash podría hacer el trabajo por sí mismo:

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

o

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

Yendo más lejos sobre la división de variables ...

Eche un vistazo a mi respuesta a ¿Cómo divido una cadena en un delimitador en Bash?

Alternativa: reducir las horquillas utilizando tareas de fondo de larga duración

2nd Edit 2018-02-12:

Para evitar múltiples horquillas como

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

o

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

Esto funciona bien, pero ejecutar muchos tenedores es pesado y lento.

¡Y los comandos como datey bcpodrían hacer muchas operaciones, línea por línea !

Ver:

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

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

Por lo tanto, podríamos utilizar un proceso en segundo plano de larga ejecución para realizar muchos trabajos, sin tener que iniciar una nueva bifurcación para cada solicitud.

Solo necesitamos algunos descriptores de archivo y quince para hacerlo correctamente:

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

(¡Por supuesto, FD 5y 6tiene que estar sin usar!) ... A partir de ahí, puede usar este proceso de la siguiente manera:

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

En una función newConnector

Puede encontrar mi newConnectorfunción en GitHub.Com o en mi propio sitio (Nota sobre GitHub: hay dos archivos en mi sitio. La función y la demostración están agrupadas en un solo archivo que puede obtenerse para su uso o simplemente ejecutarse para la demostración).

Muestra:

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

La función le myBcpermite usar la tarea en segundo plano con una sintaxis simple y para la fecha:

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 de ahí, si desea finalizar uno de los procesos en segundo plano, solo tiene que cerrar su 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

lo cual no es necesario, porque todos los archivos se cierran cuando finaliza el proceso principal.

F. Hauri
fuente
La muestra anidada arriba es lo que estaba buscando. Puede haber una forma más simple, pero lo que estaba buscando era la forma de averiguar si ya existe un contenedor acoplable dado su nombre en una variable de entorno. Entonces para mí: EXISTING_CONTAINER=$(docker ps -a | grep "$(echo $CONTAINER_NAME)")fue la declaración que estaba buscando.
Capricornio1
2
@ capricorn1 Ese es un uso inútil deecho ; simplemente quieresgrep "$CONTAINER_NAME"
tripleee
Probablemente extraño algo aquí: kubectl get ns | while read -r line; do echo $ line | grep Término | cut -d '' -f1 ; doneimprime para cada $lineuna una línea vacía y luego bash: xxxx: command not found. Sin embargo, esperaría que se imprima soloxxx
papanito
77

Como ya te han indicado, debes usar 'backticks'.

La alternativa propuesta también $(command)funciona, y también es más fácil de leer, pero tenga en cuenta que solo es válida con Bash o KornShell (y shells derivados de ellos), por lo que si sus scripts tienen que ser realmente portátiles en varios sistemas Unix, debería preferir La antigua notación de backticks.

Bitwelder
fuente
23
Son abiertamente cautelosos. Los backticks han sido obsoletos por POSIX hace mucho tiempo; la sintaxis más moderna debería estar disponible en la mayoría de los shells de este milenio. (Todavía hay entornos heredados tos HP-UX para la tos que se pegan firmemente a principios de los años noventa.)
tripleee
25
Incorrecto. $()es totalmente compatible con POSIX sh, estandarizado hace más de dos décadas.
Charles Duffy
3
Tenga /bin/shen cuenta que en Solaris 10 todavía no reconoce $(…), y AFAIK también es cierto en Solaris 11.
Jonathan Leffler
2
@JonathanLeffler En realidad, es no más el caso de Solaris 11, donde /bin/shes ksh93.
jlliagre
2
@tripleee - respuesta tres años tarde :-) pero lo he usado $()en el shell POSIX en HP-UX durante los últimos 10 años.
Bob Jarvis - Restablece a Monica el
54

Sé tres formas de hacerlo:

  1. Las funciones son adecuadas para tales tareas: **

    func (){
        ls -l
    }

    Invocarlo diciendo func.

  2. También otra solución adecuada podría ser eval:

    var="ls -l"
    eval $var
  3. El tercero está usando variables directamente:

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

Puede obtener el resultado de la tercera solución de una buena manera:

echo "$var"

Y también de una manera desagradable:

echo $var
MLSC
fuente
1
Los dos primeros no parecen responder a la pregunta tal como está actualmente, y el segundo comúnmente se considera dudoso.
tripleee
1
Como alguien completamente nuevo en bash, ¿por qué es "$var"bueno y $vardesagradable?
Peter
30

Solo para ser diferente:

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

Al configurar una variable, asegúrese de NO tener espacios antes y / o después del signo = . ¡Literalmente pasé una hora tratando de resolver esto, probando todo tipo de soluciones! Esto no es guay.

Correcto:

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

Fallará con el error "cosas: no encontrado" o similar

WTFF= `echo "stuff"`
echo "Example: $WTFF"
Emil
fuente
2
La versión con el espacio significa algo diferente : var=value somecommandse ejecuta somecommandcon varen su entorno que tenga el valor value. Por lo tanto, var= somecommandse exporta varen el entorno de somecommandcon un valor vacío (cero bytes).
Charles Duffy
Sí, un golpe de Bash.
Peter Mortensen el
14

Si desea hacerlo con comandos / s multilínea / múltiples, puede hacerlo:

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

O:

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

Ejemplo:

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

Salida:

first
second
third

Con heredoc , puede simplificar las cosas con bastante facilidad al dividir su código de línea única larga en uno de varias líneas. Otro ejemplo:

output="$( ssh -p $port $user@$domain <<EOF
# Breakdown your long ssh command into multiline here.
EOF
)"
Jahid
fuente
2
¿Qué pasa con el segundo bashdentro de la sustitución de comandos? Ya está creando una subshell mediante la sustitución del comando en sí. Si desea poner varios comandos, simplemente sepárelos por nueva línea o punto y coma. output=$(echo first; echo second; ...)
tripleee
Entonces, de manera similar 'bash -c "bash -c \"bash -c ...\""', también sería "diferente"; pero no veo el punto de eso.
tripleee
@tripleee heredoc significa algo más que eso. Puede hacer lo mismo con algunos otros comandos como ssh sudo -sejecutar comandos mysql dentro, etc. (en lugar de bash)
Jahid
1
No siento que nos estamos comunicando correctamente. Estoy desafiando la utilidad una vez variable=$(bash -c 'echo "foo"; echo "bar"')más variable=$(echo "foo"; echo "bar"): el documento aquí es solo un mecanismo de citas y realmente no agrega nada excepto otra complicación inútil.
tripleee
2
Cuando uso heredoc con ssh, preciso el comando a ejecutar ssh -p $port $user@$domain /bin/bash <<EOFpara evitar Pseudo-terminal will not be allocated because stdin is not a terminal.advertencias
F. Hauri
9

Necesitas usar cualquiera

$(command-here)

o

`command-here`

Ejemplo

#!/bin/bash

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

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

echo "$MOREF"
Diego Velez
fuente
2
$()Es mucho mejor que los backticks. Ver: ¿Cuál es el beneficio de usar $ () en lugar de backticks en scripts de shell?
codeforester
1
No sabía que podrías anidar, pero tiene mucho sentido, ¡muchas gracias por la información!
Diego Velez
6

Esta es otra forma y es bueno usarla con algunos editores de texto que no pueden resaltar correctamente cada código complejo que cree:

read -r -d '' str < <(cat somefile.txt)
echo "${#str}"
echo "$str"
Poder de acuario
fuente
Esto no trata con la pregunta de OP, que realmente se trata de la sustitución de comandos , no de la sustitución de procesos .
codeforester
6

Puedes usar backticks (también conocidos como tumbas decorativas) o $().

Me gusta:

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

Ambos tienen el mismo efecto. Pero OUTPUT = $ (x + 2) es más legible y el último.

Pratik Patil
fuente
2
El paréntesis se implementó para permitir el anidamiento.
F. Hauri
5

Si el comando que está intentando ejecutar falla, escribiría la salida en la secuencia de error y luego se imprimiría en la consola.

Para evitarlo, debe redirigir la secuencia de error:

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

Algunos pueden encontrar esto útil. Valores enteros en sustitución de variables, donde el truco es usar $(())corchetes dobles:

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
fuente
1
Esto no parece tener ninguna relevancia para esta pregunta. Sería una respuesta razonable si alguien preguntara cómo multiplicar un número en una matriz por un factor constante, aunque no recuerdo haber visto a nadie preguntar eso (y luego un for ((...))bucle parecería una mejor coincidencia para la variable de bucle ) Además, no debe usar mayúsculas para sus variables privadas.
tripleee
No estoy de acuerdo con la parte de "relevancia". La pregunta dice claramente: ¿Cómo establecer una variable igual a la salida de un comando en Bash? Y agregué esta respuesta como complemento porque llegué aquí buscando una solución que me ayudó con el código que luego publiqué. En cuanto a las mayúsculas, gracias por eso.
Gus
1
Esto podría escribirse ARR=(3 2 4 1);for((N=3,M=3,COUNT=N-1;COUNT < ${#ARR[@]};ARR[COUNT]*=M,COUNT+=N)){ :;}pero estoy de acuerdo con @tripleee: ¡No entiendo qué hacer esto allí!
F. Hauri
@ F.Hauri ... ¡bash se está volviendo más y más como Perl cuanto más profundizas en ello!
roblogic
4

Aquí hay dos formas más:

Tenga en cuenta que el espacio es muy importante en Bash. Por lo tanto, si desea que se ejecute su comando, úselo tal cual sin introducir más espacios.

  1. El siguiente asigna harshila Ly luego lo imprime

    L=$"harshil"
    echo "$L"
  2. Lo siguiente asigna la salida del comando tra L2. trestá siendo operado en otra variable, L1.

    L2=$(echo "$L1" | tr [:upper:] [:lower:])
Harshil
fuente
44
1. $"..."probablemente no hace lo que crees que hace . 2. Esto ya se da en la respuesta de Andy Lester.
gniourf_gniourf
@gniourf_gniourf tiene razón: vea que la localización bash no funcionará con líneas múltiples . Pero bajo bash , puedes usar echo ${L1,,}para minúsculas o echo ${L1^^}mayúsculas.
F. Hauri