¿Captura automáticamente la salida del último comando en una variable usando Bash?

139

Me gustaría poder usar el resultado del último comando ejecutado en un comando posterior. Por ejemplo,

$ find . -name foo.txt
./home/user/some/directory/foo.txt

Ahora digamos que quiero poder abrir el archivo en un editor, o eliminarlo, o hacer algo más con él, por ejemplo

mv <some-variable-that-contains-the-result> /some/new/location

¿Cómo puedo hacerlo? Tal vez usando alguna variable bash?

Actualizar:

Para aclarar, no quiero asignar cosas manualmente. Lo que busco es algo así como variables de bash incorporadas, por ejemplo

ls /tmp
cd $_

$_contiene el último argumento del comando anterior. Quiero algo similar, pero con la salida del último comando.

Actualización final:

La respuesta de Seth ha funcionado bastante bien. Un par de cosas a tener en cuenta:

  • no olvides touch /tmp/xprobar la solución por primera vez
  • el resultado solo se almacenará si el código de salida del último comando fue exitoso
Armandino
fuente
Después de ver tu edición, pensé en eliminar mi respuesta. Me pregunto si hay algo incorporado que esté buscando.
taskinoor
No pude encontrar nada incorporado. Me preguntaba si sería posible implementarlo ... ¿tal vez a través de .bahsrc? Creo que sería una característica bastante útil.
armandino
Me temo que todo lo que puede hacer es redirigir la salida al archivo o canalizarla o capturarla, de lo contrario no se guardará.
Bandi
3
No se puede hacer eso sin la cooperación del shell y el terminal, y generalmente no cooperan. Consulte también ¿Cómo reutilizo la última salida de la línea de comando? y Usar texto de la salida de comandos anteriores en Unix Stack Exchange .
Gilles 'SO- deja de ser malvado'
66
Una de las razones principales por las que no se captura la salida de comandos es porque la salida puede ser arbitrariamente grande, muchos megabytes a la vez. Por supuesto, no siempre es tan grande, pero los grandes resultados causan problemas.
Jonathan Leffler

Respuestas:

71

Esta es una solución realmente hacky, pero parece funcionar en su mayor parte del tiempo. Durante las pruebas, noté que a veces no funcionaba muy bien al obtener un ^Cen la línea de comando, aunque lo modifiqué un poco para que se comportara un poco mejor.

Este truco es solo un modo interactivo, y estoy bastante seguro de que no se lo recomendaría a nadie. Es probable que los comandos en segundo plano causen un comportamiento aún menos definido de lo normal. Las otras respuestas son una mejor manera de obtener resultados mediante programación.


Dicho esto, aquí está la "solución":

PROMPT_COMMAND='LAST="`cat /tmp/x`"; exec >/dev/tty; exec > >(tee /tmp/x)'

Establezca esta variable de entorno bash y emita comandos como desee. $LASTgeneralmente tendrá el resultado que está buscando:

startide seth> fortune
Courtship to marriage, as a very witty prologue to a very dull play.
                -- William Congreve
startide seth> echo "$LAST"
Courtship to marriage, as a very witty prologue to a very dull play.
                -- William Congreve
Seth Robertson
fuente
1
@armandino: Por supuesto, esto hace que los programas que esperan interactuar con un terminal en la salida estándar no funcionen como se esperaba (más / menos) o almacenen cosas extrañas en $ LAST (emacs). Pero creo que es tan bueno como lo que vas a conseguir. La única otra opción es usar un script (tipo) para guardar una copia de TODO en un archivo y luego usar PROMPT_COMMAND para verificar los cambios desde la última PROMPT_COMMAND. Sin embargo, esto incluirá cosas que no quieres. Sin embargo, estoy bastante seguro de que no encontrarás nada más cercano a lo que quieres que esto.
Seth Robertson
2
Para ir un poco más lejos: lo PROMPT_COMMAND='last="$(cat /tmp/last)";lasterr="$(cat /tmp/lasterr)"; exec >/dev/tty; exec > >(tee /tmp/last); exec 2>/dev/tty; exec 2> >(tee /tmp/lasterr)'que proporciona ambos $lasty $lasterr.
phaphink
@cdosborn: man por defecto envía la salida a través de un localizador. Como decía mi comentario anterior: "los programas que esperan interactuar con un terminal en salida estándar no funcionan como se esperaba (más / menos)". más y menos son buscapersonas.
Seth Robertson
Obtengo:No such file or directory
Francisco Corrales Morales
@Raphink Solo quería señalar una corrección: su sugerencia debería terminar 2> >(tee /tmp/lasterr 1>&2)porque la salida estándar de teedebe ser redirigida nuevamente al error estándar.
Hugues
90

No conozco ninguna variable que haga esto automáticamente . Para hacer algo aparte de simplemente copiar y pegar el resultado, puede volver a ejecutar lo que acaba de hacer, por ejemplo

vim $(!!)

¿Dónde !!está la expansión de la historia que significa 'el comando anterior'?

Si espera que haya un solo nombre de archivo con espacios u otros caracteres que puedan evitar el análisis adecuado de los argumentos, cite el resultado ( vim "$(!!)"). Dejarlo sin comillas permitirá que se abran varios archivos a la vez, siempre que no incluyan espacios u otros tokens de análisis de shell.

Daenyth
fuente
1
Copiar y pegar es lo que generalmente hago en ese caso, también porque normalmente solo necesita una parte de la salida del comando. Me sorprende que seas el primero en mencionarlo.
Bruno De Fraine
44
Puede hacer lo mismo con menos pulsaciones de teclas con:vim `!!`
psmears
Para ver la diferencia con la respuesta aceptada, ejecute date "+%N"y luegoecho $(!!)
Marinos An
20

Bash es una especie de lenguaje feo. Sí, puedes asignar la salida a la variable

MY_VAR="$(find -name foo.txt)"
echo "$MY_VAR"

Pero es mejor que espere lo más difícil que findsolo haya devuelto un resultado y que ese resultado no tuviera ningún carácter "extraño", como retornos de carro o saltos de línea, ya que se modificarán en silencio cuando se asignen a una variable Bash.

¡Pero mejor tenga cuidado de citar su variable correctamente cuando la use!

Es mejor actuar directamente sobre el archivo, por ejemplo, con find's -execdir(consulte el manual).

find -name foo.txt -execdir vim '{}' ';'

o

find -name foo.txt -execdir rename 's/\.txt$/.xml/' '{}' ';'
rlibby
fuente
55
Están no modificados en silencio cuando se asigna, se modifican cuando echo! Solo tiene que hacer echo "${MY_VAR}"para ver que este es el caso.
paxdiablo
13

Hay más de una forma de hacer esto. Una forma es usar v=$(command)cuál asignará la salida del comando a v. Por ejemplo:

v=$(date)
echo $v

Y también puedes usar comillas inversas.

v=`date`
echo $v

De Bash Beginners Guide ,

Cuando se usa la forma de sustitución con comillas anteriores de estilo antiguo, la barra diagonal inversa conserva su significado literal, excepto cuando va seguida de "$", "` "o" \ ". Los primeros backticks no precedidos por una barra invertida terminan la sustitución del comando. Cuando se utiliza el formulario "$ (COMANDO)", todos los caracteres entre paréntesis forman el comando; ninguno es tratado especialmente.

EDITAR: después de la edición en la pregunta, parece que esto no es lo que el OP está buscando. Hasta donde yo sé, no hay una variable especial como $_la salida del último comando.

taskinoor
fuente
11

Es bastante fácil Use comillas inversas:

var=`find . -name foo.txt`

Y luego puedes usar eso en cualquier momento en el futuro

echo $var
mv $var /somewhere
Wes Hardaker
fuente
11

Descargos de responsabilidad:

  • Esta respuesta llega tarde medio año: D
  • Soy un gran usuario de tmux
  • Tienes que ejecutar tu shell en tmux para que esto funcione

Al ejecutar un shell interactivo en tmux, puede acceder fácilmente a los datos que se muestran actualmente en un terminal. Echemos un vistazo a algunos comandos interesantes:

  • panel de captura de tmux : este copia los datos mostrados a uno de los búferes internos de tmux. Puede copiar el historial que actualmente no está visible, pero no estamos interesados ​​en eso ahora
  • tmux list-buffers : muestra la información sobre los buffers capturados. El más nuevo tendrá el número 0.
  • tmux show-buffer -b (número de búfer) : imprime el contenido del búfer dado en un terminal
  • tmux paste-buffer -b (buffer num) : esto pega el contenido del búfer dado como entrada

Sí, esto nos da muchas posibilidades ahora :) En cuanto a mí, configuré un alias simple: alias L="tmux capture-pane; tmux showb -b 0 | tail -n 3 | head -n 1"y ahora cada vez que necesito acceder a la última línea simplemente uso $(L)para obtenerla.

Esto es independiente de la secuencia de salida que utiliza el programa (ya sea stdin o stderr), el método de impresión (ncurses, etc.) y el código de salida del programa; los datos solo deben mostrarse.

Wiesław Herr
fuente
Hola, gracias por este consejo de tmux. Estaba buscando básicamente lo mismo que el OP, encontré su comentario y codifiqué un script de shell para permitirle elegir y pegar parte de la salida de un comando anterior utilizando los comandos tmux que menciona y los movimientos de Vim: github.com/ bgribble / lw
Bill Gribble
9

Creo que es posible que pueda hackear una solución que implica configurar su shell en un script que contenga:

#!/bin/sh
bash | tee /var/log/bash.out.log

Luego, si configura la $PROMPT_COMMANDsalida de un delimitador, puede escribir una función auxiliar (tal vez llamada _) que le permita obtener el último fragmento de ese registro, para que pueda usarlo como:

% find lots*of*files
...
% echo "$(_)"
... # same output, but doesn't run the command again
Jay Adkisson
fuente
7

Puede configurar el siguiente alias en su perfil de bash:

alias s='it=$($(history | tail -2 | head -1 | cut -d" " -f4-))'

Luego, al escribir 's' después de un comando arbitrario, puede guardar el resultado en una variable de shell 'it'.

Entonces, el uso de ejemplo sería:

$ which python
/usr/bin/python
$ s
$ file $it
/usr/bin/python: symbolic link to `python2.6'
Joe Tallett
fuente
¡Gracias! Esto es lo que necesitaba para implementar mi grabfunción que copia la enésima línea del último comando al portapapeles gist.github.com/davidhq/f37ac87bc77f27c5027e
davidhq
6

Acabo de destilar esta bashfunción de las sugerencias aquí:

grab() {     
  grab=$("$@")
  echo $grab
}

Entonces, solo haces:

> grab date
Do 16. Feb 13:05:04 CET 2012
> echo $grab
Do 16. Feb 13:05:04 CET 2012

Actualización : un usuario anónimo sugirió reemplazar echopor el printf '%s\n'cual tiene la ventaja de que no procesa opciones como -een el texto capturado. Entonces, si espera o experimenta tales peculiaridades, considere esta sugerencia. Otra opción es usar cat <<<$graben su lugar.

Tilman Vogel
fuente
1
Ok, estrictamente hablando, esto no es una respuesta porque falta la parte "automática".
Tilman Vogel
¿Puedes explicar la cat <<<$grabopción?
leoj
1
Citando de man bash: "Aquí cadenas: una variante de los documentos aquí, el formato es: <<<wordla palabra se expande y se suministra al comando en su entrada estándar". Por lo tanto, el valor de $grabse alimenta a catstdin, y catsolo lo retroalimenta a stdout.
Tilman Vogel
5

Al decir "Me gustaría poder usar el resultado del último comando ejecutado en un comando posterior", supongo que se refiere al resultado de cualquier comando, no solo a encontrar.

Si ese es el caso, xargs es lo que estás buscando.

find . -name foo.txt -print0 | xargs -0 -I{} mv {} /some/new/location/{}

O si está interesado en ver primero la salida:

find . -name foo.txt -print0

!! | xargs -0 -I{} mv {} /some/new/location/{}

Este comando trata con múltiples archivos y funciona como un encanto, incluso si la ruta y / o el nombre del archivo contienen espacios.

Observe la parte mv {} / some / new / location / {} del comando. Este comando se crea y ejecuta para cada línea impresa por el comando anterior. Aquí la línea impresa por el comando anterior se reemplaza en lugar de {} .

Extracto de la página de manual de xargs:

xargs: crea y ejecuta líneas de comando desde la entrada estándar

Para más detalles vea la página del manual: man xargs

ssapkota
fuente
5

Capture la salida con backticks:

output=`program arguments`
echo $output
emacs $output
Bandi
fuente
4

Usualmente hago lo que los otros aquí han sugerido ... sin la tarea

$find . -iname '*.cpp' -print
./foo.cpp
./bar.cpp
$vi `!!`
2 files to edit

Puedes ponerte más elegante si quieres:

$grep -R "some variable" * | grep -v tags
./foo/bar/xxx
./bar/foo/yyy
$vi `!!`
halm
fuente
2

Si todo lo que desea es volver a ejecutar su último comando y obtener la salida, una variable bash simple funcionaría:

LAST=`!!`

Entonces puede ejecutar su comando en la salida con:

yourCommand $LAST

Esto generará un nuevo proceso y volverá a ejecutar su comando, luego le dará la salida. Parece que lo que realmente le gustaría sería un archivo de historial de bash para la salida del comando. Esto significa que necesitará capturar la salida que bash envía a su terminal. Podría escribir algo para ver el / dev o / proc necesario, pero eso es desordenado. También podría crear una "tubería especial" entre su término y bash con un comando tee en el medio que redirige a su archivo de salida.

Pero ambas son soluciones extravagantes. Creo que lo mejor sería Terminator, que es un terminal más moderno con registro de salida. Simplemente revise su archivo de registro para ver los resultados del último comando. Una variable bash similar a la anterior lo haría aún más simple.

Spencer Rathbun
fuente
1

Aquí hay una forma de hacerlo después de que haya ejecutado su comando y haya decidido que desea almacenar el resultado en una variable:

$ find . -name foo.txt
./home/user/some/directory/foo.txt
$ OUTPUT=`!!`
$ echo $OUTPUT
./home/user/some/directory/foo.txt
$ mv $OUTPUT somewhere/else/

O si sabe de antemano que deseará obtener el resultado en una variable, puede usar los backticks:

$ OUTPUT=`find . -name foo.txt`
$ echo $OUTPUT
./home/user/some/directory/foo.txt
Nate W.
fuente
1

Como alternativa a las respuestas existentes: utilícelo whilesi los nombres de sus archivos pueden contener espacios en blanco como este:

find . -name foo.txt | while IFS= read -r var; do
  echo "$var"
done

Como escribí, la diferencia solo es relevante si tiene que esperar espacios en blanco en los nombres de archivo.

NB: el único elemento integrado no se trata del resultado, sino del estado del último comando.

0xC0000022L
fuente
1

puedes usar !!: 1. Ejemplo:

~]$ ls *.~
class1.cpp~ class1.h~ main.cpp~ CMakeList.txt~ 

~]$ rm !!:1
rm class1.cpp~ class1.h~ main.cpp~ CMakeList.txt~ 


~]$ ls file_to_remove1 file_to_remove2
file_to_remove1 file_to_remove2

~]$ rm !!:1
rm file_to_remove1

~]$ rm !!:2
rm file_to_remove2
rkm
fuente
Esta notación toma el argumento numerado de un comando: consulte la documentación de "$ {parámetro: desplazamiento}" aquí: gnu.org/software/bash/manual/html_node/… . Ver también aquí para más ejemplos: howtogeek.com/howto/44997/…
Alexander Bird
1

Tenía una necesidad similar, en la que quería usar la salida del último comando en el siguiente. Muy parecido a un | (tubo). p.ej

$ which gradle 
/usr/bin/gradle
$ ls -alrt /usr/bin/gradle

a algo como

$ which gradle |: ls -altr {}

Solución: creó esta tubería personalizada. Muy simple, usando xargs -

$ alias :='xargs -I{}'

Básicamente nada al crear una mano corta para xargs, funciona de maravilla y es realmente útil. Solo agrego el alias en el archivo .bash_profile.

Exc ª
fuente
1

Se puede hacer usando la magia de los descriptores de archivos y la lastpipeopción de shell.

Tiene que hacerse con un script: la opción "lastpipe" no funcionará en modo interactivo.

Aquí está la secuencia de comandos que he probado con:

$ cat shorttest.sh 
#!/bin/bash
shopt -s lastpipe

exit_tests() {
    EXITMSG="$(cat /proc/self/fd/0)"
}

ls /bloop 2>&1 | exit_tests

echo "My output is \"$EXITMSG\""


$ bash shorttest.sh 
My output is "ls: cannot access '/bloop': No such file or directory"

Lo que estoy haciendo aquí es:

  1. configurando la opción de shell shopt -s lastpipe. No funcionará sin esto, ya que perderá el descriptor de archivo.

  2. asegurándome de que mi stderr también sea capturado con 2>&1

  3. canalizar la salida a una función para que se pueda hacer referencia al descriptor de archivo stdin.

  4. configurando la variable obteniendo el contenido del /proc/self/fd/0descriptor de archivo, que es stdin.

Estoy usando esto para capturar errores en un script, por lo que si hay un problema con un comando, puedo dejar de procesar el script y salir de inmediato.

shopt -s lastpipe

exit_tests() {
    MYSTUFF="$(cat /proc/self/fd/0)"
    BADLINE=$BASH_LINENO
}

error_msg () {
    echo -e "$0: line $BADLINE\n\t $MYSTUFF"
    exit 1
}

ls /bloop 2>&1 | exit_tests ; [[ "${PIPESTATUS[0]}" == "0" ]] || error_msg

De esta manera, puedo agregar 2>&1 | exit_tests ; [[ "${PIPESTATUS[0]}" == "0" ]] || error_msgdetrás de cada comando que me interesa verificar.

¡Ahora puedes disfrutar de tu salida!

Montjoy
fuente
0

Esta no es estrictamente una solución bash, pero puede usar tuberías con sed para obtener la última fila de salida de comandos anteriores.

Primero veamos lo que tengo en la carpeta "a"

rasjani@helruo-dhcp022206::~$ find a
a
a/foo
a/bar
a/bat
a/baz
rasjani@helruo-dhcp022206::~$ 

Entonces, su ejemplo con ls y cd se convertiría en sed y tubería en algo como esto:

rasjani@helruo-dhcp022206::~$ cd `find a |sed '$!d'`
rasjani@helruo-dhcp022206::~/a/baz$ pwd
/home/rasjani/a/baz
rasjani@helruo-dhcp022206::~/a/baz$

Por lo tanto, la magia real ocurre con sed, canaliza cualquier salida de lo que se ordena en sed y sed imprime la última fila que puede usar como parámetro con ticks de retroceso. O puedes combinar eso con xargs también. ("man xargs" en shell es tu amigo)

rasjani
fuente
0

El shell no tiene símbolos especiales similares a Perl que almacenan el resultado de eco del último comando.

Aprende a usar el símbolo de tubería con awk.

find . | awk '{ print "FILE:" $0 }'

En el ejemplo anterior, podrías hacer:

find . -name "foo.txt" | awk '{ print "mv "$0" ~/bar/" | "sh" }'
ascotan
fuente
0
find . -name foo.txt 1> tmpfile && mv `cat tmpfile` /path/to/some/dir/

es otra forma, aunque sucia.

Kevin
fuente
encontrar . -name foo.txt 1> tmpfile && mv `cat tmpfile` ruta / a / some / dir && rm tmpfile
Kevin
0

Me resulta molesto recordar que canalizar la salida de mis comandos en un archivo específico es un poco molesto, mi solución es una función en mi .bash_profile que captura la salida en un archivo y devuelve el resultado cuando lo necesita.

La ventaja de este es que no tiene que volver a ejecutar todo el comando (al usar find u otros comandos de ejecución prolongada que pueden ser críticos)

Bastante simple, pegue esto en su .bash_profile :

Guión

# catch stdin, pipe it to stdout and save to a file
catch () { cat - | tee /tmp/catch.out}
# print whatever output was saved to a file
res () { cat /tmp/catch.out }

Uso

$ find . -name 'filename' | catch
/path/to/filename

$ res
/path/to/filename

En este punto, tiendo a agregar | catchal final de todos mis comandos, porque no tiene costo hacerlo y me ahorra tener que volver a ejecutar comandos que tardan mucho tiempo en finalizar.

Además, si desea abrir la salida del archivo en un editor de texto, puede hacer esto:

# vim or whatever your favorite text editor is
$ vim <(res)
Connor
fuente