Capturar automaticamente a saída do último comando em uma variável usando o Bash?

139

Eu gostaria de poder usar o resultado do último comando executado em um comando subsequente. Por exemplo,

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

Agora, digamos que eu queira abrir o arquivo em um editor ou excluí-lo ou fazer outra coisa com ele, por exemplo

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

Como eu posso fazer isso? Talvez usando alguma variável bash?

Atualizar:

Para esclarecer, não quero atribuir coisas manualmente. O que eu estou procurando é algo como variáveis ​​bash embutidas, por exemplo

ls /tmp
cd $_

$_mantém o último argumento do comando anterior. Eu quero algo semelhante, mas com a saída do último comando.

Atualização final:

A resposta de Seth funcionou muito bem. Algumas coisas a ter em mente:

  • não se esqueça de touch /tmp/xexperimentar a solução pela primeira vez
  • o resultado será armazenado apenas se o código de saída do último comando for bem-sucedido
armandino
fonte
Depois de ver sua edição, pensei em excluir minha resposta. Gostaria de saber se há algo embutido que você está procurando.
taskinoor
Não consegui encontrar nada embutido. Eu queria saber se seria possível implementá-lo .. talvez através de .bahsrc? Eu acho que seria um recurso bastante útil.
Armandino
Receio que tudo o que você pode fazer é redirecionar a saída para arquivo ou canalizar ou capturá-la, caso contrário, ela não será salva.
bandi
3
Você não pode fazer isso sem a cooperação do shell e do terminal, e eles geralmente não cooperam. Consulte também Como reutilizar a última saída da linha de comando? e Usando texto da saída de comandos anteriores no Unix Stack Exchange .
Gilles 'SO- stop be evil'
6
Uma das principais razões pelas quais a saída dos comandos não é capturada é porque a saída pode ser arbitrariamente grande - muitos megabytes por vez. Concedido, nem sempre que grandes, mas grandes resultados causam problemas.
Jonathan Leffler

Respostas:

71

Esta é uma solução realmente hacky, mas parece funcionar na maior parte do tempo. Durante o teste, notei que às vezes não funcionava muito bem ao obter uma ^Clinha de comando, embora eu a ajustava um pouco para me comportar um pouco melhor.

Esse hack é apenas um hack de modo interativo, e estou bastante confiante de que não o recomendaria a ninguém. É provável que os comandos em segundo plano causem um comportamento ainda menos definido que o normal. As outras respostas são uma maneira melhor de obter resultados programaticamente.


Dito isto, aqui está a "solução":

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

Defina essa variável de ambiente bash e emita comandos conforme desejado. $LASTnormalmente terá a saída que você está procurando:

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
fonte
1
@armandino: Isso obviamente faz com que os programas que esperam interagir com um terminal na saída padrão não funcionem conforme o esperado (mais / menos) ou armazenem coisas estranhas em $ LAST (emacs). Mas acho que é tão bom quanto você conseguirá. A única outra opção é usar o script (tipo) para salvar uma cópia de EVERYTHING em um arquivo e, em seguida, usar PROMPT_COMMAND para verificar se há alterações desde o último PROMPT_COMMAND. Isso incluirá coisas que você não deseja. Tenho certeza que você não vai encontrar nada mais perto do que você quer que isso, no entanto.
Seth Robertson
2
Para ir um pouco mais longe: o 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 fornece ambos $laste $lasterr.
ℝaphink
@cdosborn: man, por padrão, envia a saída através de um pager. Como meu comentário anterior disse: "os programas que esperam interagir com um terminal na saída padrão não funcionam como o esperado (mais / menos)". mais e menos são pagers.
Seth Robertson
Eu recebo:No such file or directory
Francisco Corrales Morales
@Raphink Eu só queria apontar uma correção: sua sugestão deve terminar 2> >(tee /tmp/lasterr 1>&2)porque a saída padrão de teedeve ser redirecionada de volta ao erro padrão.
Hugues
90

Não conheço nenhuma variável que faça isso automaticamente . Para fazer algo além de copiar e colar o resultado, você pode executar novamente o que acabou de fazer, por exemplo

vim $(!!)

Onde !!está a expansão da história que significa 'o comando anterior'.

Se você espera que exista um único nome de arquivo com espaços ou outros caracteres que possam impedir a análise adequada de argumentos, cite o resultado ( vim "$(!!)"). Deixá-lo entre aspas permitirá que vários arquivos sejam abertos ao mesmo tempo, desde que não incluam espaços ou outros tokens de análise de shell.

Daenyth
fonte
1
Copiar e colar é o que costumo fazer nesse caso, também porque você normalmente precisa apenas de uma parte da saída do comando. Estou surpreso que você seja o primeiro a mencionar.
Bruno De Fraine
4
Você pode fazer o mesmo com menos pressionamentos de tecla com:vim `!!`
psmears
Para ver a diferença entre a resposta aceita, executar date "+%N"e depoisecho $(!!)
Marinos Um
20

Bash é uma linguagem feia. Sim, você pode atribuir a saída à variável

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

Mas é melhor esperar que você tenha findretornado apenas um resultado e que esse resultado não tenha caracteres "ímpares", como retornos de carro ou feeds de linha, pois eles serão modificados silenciosamente quando atribuídos a uma variável Bash.

Mas é melhor ter cuidado para citar sua variável corretamente ao usá-la!

É melhor agir diretamente no arquivo, por exemplo, com find's -execdir(consulte o manual).

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

ou

find -name foo.txt -execdir rename 's/\.txt$/.xml/' '{}' ';'
rlibby
fonte
5
Eles não são modificados silenciosamente quando você atribui, eles são modificados quando você ecoa! Você só precisa fazer echo "${MY_VAR}"para ver que é esse o caso.
23411
13

Existem mais de uma maneira de fazer isso. Uma maneira é usar a v=$(command)qual atribuirá a saída do comando v. Por exemplo:

v=$(date)
echo $v

E você também pode usar aspas.

v=`date`
echo $v

No Guia para Iniciantes do Bash ,

Quando a forma de substituição com aspas antigas do estilo antigo é usada, a barra invertida mantém seu significado literal, exceto quando seguido por "$", "` "ou" \ ". Os primeiros backticks não precedidos por uma barra invertida encerram a substituição do comando. Ao usar o formulário "$ (COMMAND)", todos os caracteres entre parênteses formam o comando; nenhum é tratado especialmente.

EDIT: Após a edição da pergunta, parece que isso não é o que o OP está procurando. Até onde eu sei, não há variável especial como $_a saída do último comando.

taskinoor
fonte
11

É bem fácil Use aspas:

var=`find . -name foo.txt`

E então você pode usar isso a qualquer momento no futuro

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

Disclamers:

  • Esta resposta está atrasada meio ano: D
  • Eu sou um usuário pesado do tmux
  • Você precisa executar seu shell no tmux para que isso funcione

Ao executar um shell interativo no tmux, você pode acessar facilmente os dados atualmente exibidos em um terminal. Vamos dar uma olhada em alguns comandos interessantes:

  • Painel de captura do tmux : este copia os dados exibidos para um dos buffers internos do tmux. Ele pode copiar o histórico que não está visível no momento, mas não estamos interessados ​​nisso agora
  • tmux list-buffers : exibe as informações sobre os buffers capturados. O mais novo terá o número 0.
  • tmux show-buffer -b (buffer num) : imprime o conteúdo do buffer fornecido em um terminal
  • tmux paste-buffer -b (buffer num) : cola o conteúdo do buffer fornecido como entrada

Sim, isso nos oferece muitas possibilidades agora :) Quanto a mim, montei um apelido simples: alias L="tmux capture-pane; tmux showb -b 0 | tail -n 3 | head -n 1"e agora toda vez que preciso acessar a última linha, simplesmente uso $(L)para obtê-la.

Isso é independente do fluxo de saída que o programa usa (seja stdin ou stderr), o método de impressão (ncurses etc.) e o código de saída do programa - os dados precisam apenas ser exibidos.

Wiesław Herr
fonte
Ei, obrigado por esta dica tmux. Eu estava procurando basicamente a mesma coisa que o OP, encontrei seu comentário e codifiquei um shell script para permitir que você escolha e cole parte da saída de um comando anterior usando os comandos tmux mencionados e os movimentos do Vim: github.com/ bgribble / LW
Bill Gribble
9

Eu acho que você pode ser capaz de hackear uma solução que envolve a configuração do shell para um script que contém:

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

Então, se você definir $PROMPT_COMMANDa saída de um delimitador, poderá escrever uma função auxiliar (talvez chamada _) que obtenha o último bloco desse log, para que você possa usá-lo como:

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

Você pode configurar o seguinte alias no seu perfil do bash:

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

Então, digitando 's' após um comando arbitrário, você pode salvar o resultado em uma variável do shell 'it'.

Assim, o exemplo de uso seria:

$ which python
/usr/bin/python
$ s
$ file $it
/usr/bin/python: symbolic link to `python2.6'
Joe Tallett
fonte
Obrigado! Isto é o que eu precisava para implementar minha grabfunção dessa linha cópias enésimo do último comando para clipboard gist.github.com/davidhq/f37ac87bc77f27c5027e
davidhq
6

Acabei de destilar essa bashfunção a partir das sugestões aqui:

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

Então, você apenas faz:

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

Atualização : um usuário anônimo sugerido para substituir echopelo printf '%s\n'qual tem a vantagem de não processar opções como -eno texto capturado. Portanto, se você espera ou experimenta essas peculiaridades, considere esta sugestão. Outra opção é usar em seu cat <<<$grablugar.

Tilman Vogel
fonte
1
Ok, estritamente falando, isso não é uma resposta, porque a parte "automática" está completamente ausente.
Tilman Vogel
Você pode explicar a cat <<<$grabopção?
leoj
1
Citando man bash: "Here Strings: Uma variante dos documentos aqui, o formato é: <<<wordA palavra é expandida e fornecida ao comando em sua entrada padrão". Portanto, o valor de $grabé alimentado catno stdin e catapenas o alimenta de volta ao stdout.
Tilman Vogel
5

Ao dizer "Gostaria de poder usar o resultado do último comando executado em um comando subseqüente", presumo - você quer dizer o resultado de qualquer comando, e não apenas encontrar.

Se for esse o caso - xargs é o que você está procurando.

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

OU se você estiver interessado em ver a saída primeiro:

find . -name foo.txt -print0

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

Este comando lida com vários arquivos e funciona como um encanto, mesmo que o caminho e / ou o nome do arquivo contenham espaço (s).

Observe a parte mv {} / some / new / location / {} do comando. Este comando é construído e executado para cada linha impressa pelo comando anterior. Aqui a linha impressa pelo comando anterior é substituída no lugar de {} .

Trecho da página de manual do xargs:

xargs - cria e executa linhas de comando a partir da entrada padrão

Para mais detalhes, consulte a página do manual: man xargs

ssapkota
fonte
5

Capture a saída com backticks:

output=`program arguments`
echo $output
emacs $output
bandi
fonte
4

Eu costumo fazer o que os outros aqui sugeriram ... sem a atribuição:

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

Você pode ficar mais chique se quiser:

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

Se tudo o que você deseja é executar novamente o seu último comando e obter a saída, uma variável bash simples funcionaria:

LAST=`!!`

Então você pode executar seu comando na saída com:

yourCommand $LAST

Isso irá gerar um novo processo e executar novamente o seu comando, fornecendo a saída. Parece que o que você realmente gostaria seria um arquivo de histórico do bash para saída do comando. Isso significa que você precisará capturar a saída que o bash envia para o seu terminal. Você pode escrever algo para observar o / dev ou / proc necessário, mas isso é confuso. Você também pode criar um "canal especial" entre o seu termo e o bash com um comando tee no meio, que redireciona para o seu arquivo de saída.

Mas essas duas são soluções hacky. Eu acho que a melhor coisa seria o terminator, que é um terminal mais moderno com registro de saída. Basta verificar seu arquivo de log para obter os resultados do último comando. Uma variável bash semelhante à anterior tornaria isso ainda mais simples.

Spencer Rathbun
fonte
1

Aqui está uma maneira de fazer isso depois de executar seu comando e decidir que você deseja armazenar o resultado em uma variável:

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

Ou, se você souber antecipadamente que deseja o resultado em uma variável, poderá usar backticks:

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

Como alternativa às respostas existentes: Use whilese os nomes dos arquivos puderem conter espaços em branco como este:

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

Como escrevi, a diferença é relevante apenas se você precisar de espaços em branco nos nomes dos arquivos.

Nota: o único material incorporado não é sobre a saída, mas sobre o status do último comando.

0xC0000022L
fonte
1

você pode usar !!: 1. Exemplo:

~]$ 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
fonte
Esta notação captura o argumento numerado de um comando: Consulte a documentação para "$ {parameter: offset}" aqui: gnu.org/software/bash/manual/html_node/… . Veja também aqui para mais exemplos: howtogeek.com/howto/44997/…
Alexander Bird
1

Eu tinha uma necessidade semelhante, na qual queria usar a saída do último comando no próximo. Muito parecido com um | (tubo). por exemplo

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

para algo como -

$ which gradle |: ls -altr {}

Solução: criou este canal personalizado. Realmente simples, usando xargs -

$ alias :='xargs -I{}'

Basicamente, nada criando uma mão curta para xargs, funciona como charme e é realmente útil. Acabei de adicionar o alias no arquivo .bash_profile.

eXc
fonte
1

Isso pode ser feito usando a mágica dos descritores de arquivos e a lastpipeopção shell.

Isso deve ser feito com um script - a opção "lastpipe" não funcionará no modo interativo.

Aqui está o script que eu testei:

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

O que estou fazendo aqui é:

  1. definindo a opção shell shopt -s lastpipe. Não funcionará sem isso, pois você perderá o descritor de arquivo.

  2. certificando-se de que meu stderr também seja capturado com 2>&1

  3. canalizando a saída em uma função para que o descritor de arquivo stdin possa ser referenciado.

  4. definindo a variável obtendo o conteúdo do /proc/self/fd/0descritor de arquivo, que é stdin.

Estou usando isso para capturar erros em um script, portanto, se houver um problema com um comando, posso parar de processar o script e sair imediatamente.

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

Dessa forma, posso adicionar 2>&1 | exit_tests ; [[ "${PIPESTATUS[0]}" == "0" ]] || error_msgpor trás de cada comando que gostaria de verificar.

Agora você pode apreciar sua produção!

montjoy
fonte
0

Esta não é estritamente uma solução bash, mas você pode usar o piping com sed para obter a última linha de saída dos comandos anteriores.

Primeiro vamos ver o que tenho na pasta "a"

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

Então, seu exemplo com ls e cd se tornaria sed & piping em algo assim:

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

Portanto, a mágica real acontece com o sed, você canaliza o que sempre sai do que já foi mandado para o sed e sed imprime a última linha que você pode usar como parâmetro com back ticks. Ou você pode combinar isso com xargs também. ("man xargs" com casca é seu amigo)

rasjani
fonte
0

O shell não possui símbolos especiais do tipo perl que armazenam o resultado do eco do último comando.

Aprenda a usar o símbolo de tubo com awk.

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

No exemplo acima, você pode fazer:

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

é ainda outra maneira, ainda que suja.

Kevin
fonte
encontrar . -name foo.txt 1> tmpfile && mv `cat tmpfile` caminho / para / some / dir && rm tmpfile
Kevin
0

Acho que lembrar de canalizar a saída dos meus comandos em um arquivo específico é um pouco chato, minha solução é uma função na minha .bash_profileque captura a saída em um arquivo e retorna o resultado quando você precisar.

A vantagem deste é que você não precisa executar novamente o comando inteiro (ao usar findou outros comandos de execução longa que podem ser críticos)

Simples o suficiente, cole isso no seu .bash_profile :

Roteiro

# 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

Neste ponto, costumo adicionar apenas | catchao final de todos os meus comandos, porque não há custo para fazê-lo e isso me poupa de ter que executar novamente os comandos que demoram muito tempo para serem finalizados.

Além disso, se você deseja abrir a saída do arquivo em um editor de texto, você pode fazer isso:

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