Cattura automaticamente l'output dell'ultimo comando in una variabile usando Bash?


139

Vorrei poter utilizzare il risultato dell'ultimo comando eseguito in un comando successivo. Per esempio,

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

Ora diciamo che voglio essere in grado di aprire il file in un editor, o eliminarlo o fare qualcos'altro con esso, ad es

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

Come posso farlo? Forse usando qualche variabile bash?

Aggiornare:

Per chiarire, non voglio assegnare le cose manualmente. Quello che sto cercando è qualcosa come le variabili bash integrate, ad es

ls /tmp
cd $_

$_contiene l'ultimo argomento del comando precedente. Voglio qualcosa di simile, ma con l'output dell'ultimo comando.

Aggiornamento finale:

La risposta di Seth ha funzionato abbastanza bene. Un paio di cose da tenere a mente:

  • non dimenticare di touch /tmp/xprovare la soluzione per la prima volta
  • il risultato verrà memorizzato solo se il codice di uscita dell'ultimo comando ha avuto esito positivo

Dopo aver visto la tua modifica ho pensato di eliminare la mia risposta. Mi chiedo se ci sia qualcosa incorporato che stai cercando.
taskinoor

Non ho trovato nulla incorporato. Mi chiedevo se sarebbe stato possibile implementarlo .. forse attraverso .bahsrc? Penso che sarebbe una funzionalità abbastanza utile.
Armandino,

Temo che tutto ciò che puoi fare sia reindirizzare l'output su file o pipe o acquisirlo, altrimenti non verrà salvato.
bandi

3
Non puoi farlo senza la collaborazione della shell e del terminale e generalmente non collaborano. Vedi anche Come riutilizzare l'ultimo output dalla riga di comando? e Utilizzo del testo dall'output dei comandi precedenti su Unix Stack Exchange .
Gilles 'SO- smetti di essere malvagio'

6
Uno dei motivi principali per cui l'output dei comandi non viene acquisito è perché l'output può essere arbitrariamente grande - molti megabyte alla volta. Certo, non sempre così grandi, ma le grandi uscite causano problemi.
Jonathan Leffler,

Risposte:


71

Questa è una soluzione davvero confusa, ma sembra funzionare per lo più qualche volta. Durante i test, ho notato che a volte non funzionava molto bene quando si entrava ^Cnella riga di comando, anche se l'ho modificato un po 'per comportarsi un po' meglio.

Questo hack è solo un hack in modalità interattiva e sono abbastanza sicuro che non lo consiglierei a nessuno. È probabile che i comandi in background causino comportamenti ancora meno definiti del normale. Le altre risposte sono un modo migliore per ottenere programmaticamente i risultati.


Detto questo, ecco la "soluzione":

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

Impostare questa variabile ambientale bash ed emettere i comandi come desiderato. $LASTdi solito avrà l'output che stai cercando:

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

1
@armandino: questo ovviamente fa sì che i programmi che prevedono di interagire con un terminale in uscita standard non funzionino come previsto (più / meno) o memorizzino cose strane in $ LAST (emacs). Ma penso che sia buono come hai intenzione di ottenere. L'unica altra opzione è utilizzare (tipo) lo script per salvare una copia di TUTTO in un file e quindi utilizzare PROMPT_COMMAND per verificare le modifiche dall'ultimo PROMPT_COMMAND. Questo includerà cose che non vuoi, però. Sono abbastanza sicuro che non troverai nulla di più vicino a ciò che desideri che questo, però.
Seth Robertson,

2
Per andare un po 'oltre: 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)'che fornisce sia $laste $lasterr.
inkaphink,

@cdosborn: man di default invia l'output tramite un cercapersone. Come diceva il mio precedente commento: "i programmi che prevedono di interagire con un terminale in uscita standard non funzionano come previsto (più / meno)". più e meno sono cercapersone.
Seth Robertson,

Ottengo:No such file or directory
Francisco Corrales Morales,

@Raphink Volevo solo sottolineare una correzione: il tuo suggerimento dovrebbe finire 2> >(tee /tmp/lasterr 1>&2)perché l'output standard di teedeve essere reindirizzato all'errore standard.
Hugues,

90

Non conosco alcuna variabile che lo fa automaticamente . Per fare qualcosa a parte semplicemente incollare il risultato, puoi rieseguire tutto ciò che hai appena fatto, ad es

vim $(!!)

Dove !! espansione della storia che significa "il comando precedente".

Se ti aspetti che ci sia un singolo nome file con spazi o altri caratteri in esso che potrebbero impedire l'analisi corretta dell'argomento, cita il risultato ( vim "$(!!)"). Se non viene quotato, sarà possibile aprire contemporaneamente più file, purché non includano spazi o altri token di analisi della shell.


1
Il copia-incolla è ciò che di solito faccio in questo caso, anche perché in genere è necessaria solo una parte dell'output del comando. Sono sorpreso che tu sia il primo a parlarne.
Bruno De Fraine,

4
Puoi fare lo stesso con meno tasti con:vim `!!`
psmears

Per vedere la differenza dalla risposta accettata, esegui date "+%N"e poiecho $(!!)
Marinos Il

20

Bash è una specie di brutto linguaggio. Sì, è possibile assegnare l'output alla variabile

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

Ma meglio sperare che sia il più difficile find restituito un solo risultato e che quel risultato non contenga caratteri "dispari", come ritorni a capo o avanzamenti di riga, poiché verranno silenziosamente modificati quando assegnati a una variabile Bash.

Ma fai attenzione a citare correttamente la tua variabile quando la usi!

È meglio agire direttamente sul file, ad es. Con find's -execdir(consultare il manuale).

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

o

find -name foo.txt -execdir rename 's/\.txt$/.xml/' '{}' ';'

5
Essi sono non in silenzio modificati quando si assegnano, vengono modificati quando si echo! Devi solo fare echo "${MY_VAR}"per vedere questo è il caso.
paxdiablo,

13

Esistono più modi per farlo. Un modo è usare v=$(command)che assegnerà l'output del comando a v. Per esempio:

v=$(date)
echo $v

E puoi anche usare i backquotes.

v=`date`
echo $v

Dalla guida per principianti di Bash ,

Quando viene utilizzata la forma di sostituzione retroquadrata in vecchio stile, la barra rovesciata conserva il suo significato letterale tranne quando è seguita da "$", "` "o" \ ". I primi backtick non preceduti da una barra rovesciata terminano la sostituzione del comando. Quando si utilizza il modulo "$ (COMANDO)", tutti i caratteri tra parentesi compongono il comando; nessuno è trattato in modo speciale.

EDIT: Dopo la modifica della domanda, sembra che questa non sia la cosa che l'OP sta cercando. Per quanto ne so, non esiste una variabile speciale come $_per l'output dell'ultimo comando.


11

È abbastanza facile Usa le virgolette:

var=`find . -name foo.txt`

E poi puoi usarlo in qualsiasi momento in futuro

echo $var
mv $var /somewhere

11

Disclamers:

  • Questa risposta è in ritardo di sei mesi: D
  • Sono un utente tmux pesante
  • Devi far funzionare la tua shell in tmux perché questo funzioni

Quando si esegue una shell interattiva in tmux, è possibile accedere facilmente ai dati attualmente visualizzati su un terminale. Diamo un'occhiata ad alcuni comandi interessanti:

  • tmux capture-pane : questo copia i dati visualizzati su uno dei buffer interni di tmux. Può copiare la cronologia che al momento non è visibile, ma non ci interessa ora
  • tmux list-buffers : visualizza le informazioni sui buffer acquisiti. Il più recente avrà il numero 0.
  • tmux show-buffer -b (buffer num) : stampa il contenuto del buffer dato su un terminale
  • tmux paste-buffer -b (buffer num) : incolla il contenuto del buffer dato come input

Sì, questo ci offre molte possibilità ora :) Per quanto mi riguarda, ho creato un semplice alias: alias L="tmux capture-pane; tmux showb -b 0 | tail -n 3 | head -n 1"e ora ogni volta che devo accedere all'ultima riga che uso semplicemente $(L)per ottenerlo.

Questo è indipendente dal flusso di output utilizzato dal programma (sia esso stdin o stderr), il metodo di stampa (ncurses, ecc.) E il codice di uscita del programma - i dati devono solo essere visualizzati.


Ehi, grazie per questo consiglio. Stavo cercando praticamente la stessa cosa dell'OP, ho trovato il tuo commento e ho codificato uno script di shell per consentirti di scegliere e incollare parte dell'output di un comando precedente usando i comandi tmux che menzioni e i movimenti di Vim: github.com/ bgribble / lw
Bill Gribble,

9

Penso che potresti essere in grado di hackerare una soluzione che comporta l'impostazione della shell su uno script contenente:

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

Quindi, se si imposta $PROMPT_COMMANDl'output di un delimitatore, è possibile scrivere una funzione di supporto (forse chiamata _) che ti fornisce l'ultimo blocco di quel registro, in modo da poterlo usare come:

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

7

È possibile impostare il seguente alias nel profilo bash:

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

Quindi, digitando 's' dopo un comando arbitrario è possibile salvare il risultato in una variabile di shell 'it'.

Quindi un esempio di utilizzo sarebbe:

$ which python
/usr/bin/python
$ s
$ file $it
/usr/bin/python: symbolic link to `python2.6'

Grazie! Questo è ciò di cui avevo bisogno per implementare la mia grabfunzione che copia l'ennesima riga dall'ultimo comando negli appunti gist.github.com/davidhq/f37ac87bc77f27c5027e
davidhq

6

Ho appena distillato questa bashfunzione dai suggerimenti qui:

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

Quindi, fai semplicemente:

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

Aggiornamento : un utente anonimo ha suggerito di sostituire echocon il printf '%s\n'quale ha il vantaggio di non elaborare opzioni come -enel testo acquisito. Quindi, se ti aspetti o provi tali peculiarità, considera questo suggerimento. Un'altra opzione è quella di utilizzare cat <<<$grabinvece.


1
Ok, a rigor di termini questa non è una risposta perché, la parte "automatica" è completamente mancante.
Tilman Vogel,

Puoi spiegare l' cat <<<$grabopzione?
leoj,

1
Citando da man bash: "Here Strings: una variante dei documenti qui, il formato è: <<<wordla parola viene espansa e fornita al comando sul suo input standard." Quindi, il valore di $grabviene alimentato catsu stdin e catlo riporta semplicemente a stdout.
Tilman Vogel,

5

Dicendo "Mi piacerebbe poter usare il risultato dell'ultimo comando eseguito in un comando successivo", presumo - intendi il risultato di qualsiasi comando, non solo trovare.

Se è così, xargs è ciò che stai cercando.

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

O se sei interessato a vedere prima l'output:

find . -name foo.txt -print0

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

Questo comando si occupa di più file e funziona come un incantesimo anche se il percorso e / o il nome file contiene spazi.

Notare la parte mv {} / some / new / location / {} del comando. Questo comando viene creato ed eseguito per ogni riga stampata dal comando precedente. Qui la riga stampata dal comando precedente viene sostituita al posto di {} .

Estratto dalla pagina man di xargs:

xargs - crea ed esegue le righe di comando dall'input standard

Per maggiori dettagli vedi la pagina man: man xargs


5

Cattura l'output con i backtick:

output=`program arguments`
echo $output
emacs $output

4

Di solito faccio quello che gli altri qui hanno suggerito ... senza il compito:

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

Puoi diventare più elaborato se ti piace:

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

2

Se tutto ciò che vuoi è eseguire nuovamente il tuo ultimo comando e ottenere l'output, una semplice variabile bash funzionerebbe:

LAST=`!!`

Quindi puoi eseguire il tuo comando sull'output con:

yourCommand $LAST

Questo genererà un nuovo processo ed eseguirà nuovamente il tuo comando, quindi ti darà l'output. Sembra che quello che vorresti fosse un file di cronologia bash per l'output del comando. Questo significa che dovrai catturare l'output che bash invia al tuo terminale. Potresti scrivere qualcosa per guardare il / dev o / proc necessario, ma è disordinato. Potresti anche creare una "pipe speciale" tra il tuo termine e bash con un comando tee nel mezzo che reindirizza al tuo file di output.

Ma entrambi sono una sorta di soluzioni confuse. Penso che la cosa migliore sarebbe terminator che è un terminale più moderno con registrazione dell'output. Basta controllare il file di registro per i risultati dell'ultimo comando. Una variabile bash simile a quanto sopra renderebbe questo ancora più semplice.


1

Ecco un modo per farlo dopo aver eseguito il comando e deciso di voler archiviare il risultato in una variabile:

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

Oppure, se sai in anticipo che vorrai il risultato in una variabile, puoi usare i backtick:

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

1

In alternativa alle risposte esistenti: utilizzare whilese i nomi dei file possono contenere spazi vuoti come questo:

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

Come ho scritto, la differenza è rilevante solo se devi aspettarti spazi vuoti nei nomi dei file.

NB: l'unico elemento incorporato non riguarda l'output ma lo stato dell'ultimo comando.


1

puoi usare !!: 1. Esempio:

~]$ 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

Questa notazione prende l'argomento numerato da un comando: vedere la documentazione per "$ {parametro: offset}" qui: gnu.org/software/bash/manual/html_node/… . Vedi anche qui per altri esempi: howtogeek.com/howto/44997/…
Alexander Bird

1

Avevo un bisogno simile, in cui volevo usare l'output dell'ultimo comando nel successivo. Proprio come un | (tubo). per esempio

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

a qualcosa del tipo -

$ which gradle |: ls -altr {}

Soluzione: creato questo tubo personalizzato. Davvero semplice, usando xargs -

$ alias :='xargs -I{}'

Fondamentalmente nulla creando una scorciatoia per xargs, funziona come un incantesimo ed è davvero utile. Ho appena aggiunto l'alias nel file .bash_profile.


1

Può essere fatto usando la magia dei descrittori di file e l' lastpipeopzione di shell.

Deve essere fatto con uno script: l'opzione "lastpipe" non funzionerà in modalità interattiva.

Ecco lo script con cui ho provato:

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

Quello che sto facendo qui è:

  1. impostazione dell'opzione shell shopt -s lastpipe. Non funzionerà senza questo poiché perderai il descrittore di file.

  2. assicurandomi che anche il mio stderr venga catturato 2>&1

  3. reindirizzare l'output in una funzione in modo che sia possibile fare riferimento al descrittore del file stdin.

  4. impostando la variabile ottenendo il contenuto del /proc/self/fd/0descrittore di file, che è stdin.

Sto usando questo per catturare errori in uno script, quindi se c'è un problema con un comando posso interrompere l'elaborazione dello script e uscire immediatamente.

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

In questo modo posso aggiungere 2>&1 | exit_tests ; [[ "${PIPESTATUS[0]}" == "0" ]] || error_msg dietro ogni comando che mi interessa controllare.

Ora puoi goderti il ​​tuo output!


0

Questa non è una soluzione bash ma puoi usare piping con sed per ottenere l'output dell'ultima riga dei comandi precedenti.

Per prima cosa vediamo cosa ho nella cartella "a"

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

Quindi, il tuo esempio con ls e cd si trasformerebbe in sed & piping in qualcosa del genere:

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

Quindi, la magia vera accade con sed, si inoltra qualsiasi output di qualsiasi comando in sed e sed stampa l'ultima riga che è possibile utilizzare come parametro con tick posteriori. Oppure puoi combinarlo anche con xargs. ("man xargs" in shell è tuo amico)


0

La shell non ha simboli speciali simili a perl che memorizzano il risultato dell'eco dell'ultimo comando.

Impara a usare il simbolo della pipa con awk.

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

Nell'esempio sopra puoi fare:

find . -name "foo.txt" | awk '{ print "mv "$0" ~/bar/" | "sh" }'

0
find . -name foo.txt 1> tmpfile && mv `cat tmpfile` /path/to/some/dir/

è ancora un altro modo, anche se sporco.


trova . -name foo.txt 1> tmpfile && mv `cat tmpfile` path / to / some / dir && rm tmpfile
Kevin

0

Trovo che ricordare di reindirizzare l'output dei miei comandi in un file specifico sia un po 'fastidioso, la mia soluzione è una funzione nel mio .bash_profile che cattura l'output in un file e restituisce il risultato quando ne hai bisogno.

Il vantaggio di questo è che non è necessario eseguire nuovamente l'intero comando (durante l'utilizzo find o altri comandi di lunga durata che possono essere critici)

Abbastanza semplice, incollalo nel tuo .bash_profile :

copione

# 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

A questo punto, tendo ad aggiungere | catch alla fine di tutti i miei comandi, perché non ci sono costi per farlo e mi fa risparmiare dover ripetere i comandi che impiegano molto tempo per finire.

Inoltre, se si desidera aprire l'output del file in un editor di testo, è possibile:

# vim or whatever your favorite text editor is
$ vim <(res)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.