Como excluir e substituir a última linha no terminal usando o bash?

99

Quero implementar uma barra de progresso mostrando os segundos decorridos no bash. Para isso, preciso apagar a última linha mostrada na tela (o comando "limpar" apaga toda a tela, mas preciso apagar apenas a linha da barra de progresso e substituí-la pelas novas informações).

O resultado final deve ser semelhante a:

$ Elapsed time 5 seconds

Depois de 10 segundos, quero substituir essa frase (na mesma posição na tela) por:

$ Elapsed time 15 seconds
Depurador
fonte

Respostas:

116

ecoar um retorno de carro com \ r

seq 1 1000000 | while read i; do echo -en "\r$i"; done

do homem echo:

-n     do not output the trailing newline
-e     enable interpretation of backslash escapes

\r     carriage return
Ken
fonte
19
for i in {1..100000}; do echo -en "\r$i"; donepara evitar a chamada de
sequência
Isso faz exatamente o que eu preciso, obrigado !! E o uso do comando seq de fato congela o termianl :-)
Debugger
11
Quando você usa coisas como "for i in $ (...)" ou "for i in {1..N}", você está gerando todos os elementos antes de iterar, isso é muito ineficiente para grandes entradas. Você pode aproveitar as vantagens dos tubos: "seq 1 100000 | while read i; do ..." ou usar o bash c-style for loop: "for ((i = 0 ;; i ++)); do ..."
Tokland
Obrigado Douglas e tokland - embora a sequência de produção não fosse diretamente parte da questão, mudei para o tubo mais eficiente de tokland
Ken
1
Minha impressora matricial está bagunçando completamente meu papel. Ele fica atolando pontos no mesmo pedaço de papel que não está mais lá, por quanto tempo este programa funciona?
Rob
185

O retorno de carro por si só move o cursor para o início da linha. Tudo bem se cada nova linha de saída for pelo menos tão longa quanto a anterior, mas se a nova linha for mais curta, a linha anterior não será completamente substituída, por exemplo:

$ echo -e "abcdefghijklmnopqrstuvwxyz\r0123456789"
0123456789klmnopqrstuvwxyz

Para realmente limpar a linha do novo texto, você pode adicionar \033[Kdepois de \r:

$ echo -e "abcdefghijklmnopqrstuvwxyz\r\033[K0123456789"
0123456789

http://en.wikipedia.org/wiki/ANSI_escape_code

Derek Veit
fonte
3
Isso funciona muito bem no meu ambiente. Algum conhecimento de compatibilidade?
Alexander Olsson
21
No Bash, pelo menos isso pode ser encurtado para em \e[Kvez de \033[K.
Dave James Miller
Ótima resposta! Funciona perfeitamente usando puts do ruby. Agora parte do meu kit de ferramentas.
phatmann de
@ Desenvolvedor de Pixel: Obrigado por tentar melhorá-lo, mas sua edição estava incorreta. O retorno deve acontecer primeiro para mover o cursor para o início da linha, então o kill limpa tudo desde a localização do cursor até o final, deixando a linha inteira em branco, que é a intenção. Você os coloca no início de cada nova linha, de maneira semelhante à resposta de Ken, de modo que seja escrito em uma linha vazia.
Derek Veit
1
Só para constar, também se pode fazer \033[Gio \rantes, \033[Kembora obviamente \rseja muito mais simples. Também invisible-island.net/xterm/ctlseqs/ctlseqs.html fornece mais detalhes do que a Wikipedia e é do desenvolvedor xterm.
jamadagni
19

A resposta de Derek Veit funciona bem, desde que o comprimento da linha nunca exceda a largura do terminal. Se este não for o caso, o código a seguir evitará a saída de lixo:

antes que a linha seja escrita pela primeira vez, faça

tput sc

que salva a posição atual do cursor. Agora, sempre que quiser imprimir sua linha, use

tput rc
tput ed
echo "your stuff here"

para primeiro retornar à posição salva do cursor, limpar a tela do cursor para baixo e, finalmente, escrever a saída.

Hum.
fonte
Estranho, isso não faz nada no terminator. Você sabe se existem limitações de compatibilidade?
Jamie Pate
1
Nota para Cygwin: Você precisa instalar o pacote "ncurses" para usar "tput".
Jack Miller
Hm ... Parece não funcionar se houver mais de uma linha na saída. Isso não funciona:tput sc # save cursor echo '' > sessions.log.json while [ 1 ]; do curl 'http://localhost/jolokia/read/*:type=Manager,*/activeSessions,maxActiveSessions' >> sessions.log.json echo '' >> sessions.log.json cat sessions.log.json | jq '.' tput rc;tput el # rc = restore cursor, el = erase to end of line sleep 1 done
Nux
@Nux Você precisa usar em tput edvez de tput el. O exemplo de @ Um usado ed(talvez ele consertou depois que você comentou).
Studgeek
Para sua informação, aqui está a lista de cores dos
studgeek
12

O método \ 033 não funcionou para mim. O método \ r funciona, mas na verdade não apaga nada, apenas coloca o cursor no início da linha. Portanto, se a nova string for mais curta do que a antiga, você poderá ver o texto restante no final da linha. No final das contas, tput foi o melhor caminho a seguir. Ele tem outros usos além do cursor e vem pré-instalado em muitas distros Linux e BSD, então deve estar disponível para a maioria dos usuários do bash.

#/bin/bash
tput sc # save cursor
printf "Something that I made up for this string"
sleep 1
tput rc;tput el # rc = restore cursor, el = erase to end of line
printf "Another message for testing"
sleep 1
tput rc;tput el
printf "Yet another one"
sleep 1
tput rc;tput el

Aqui está um pequeno script de contagem regressiva para brincar:

#!/bin/bash
timeout () {
    tput sc
    time=$1; while [ $time -ge 0 ]; do
        tput rc; tput el
        printf "$2" $time
        ((time--))
        sleep 1
    done
    tput rc; tput ed;
}

timeout 10 "Self-destructing in %s"
lee8oi
fonte
Isso limpa toda a linha, de fato, o problema cintila muito para mim: '(
smarber
5

Caso a saída de progresso seja de várias linhas ou o script já tenha impresso o caractere de nova linha, você pode pular as linhas com algo como:

printf "\033[5A"

o que fará com que o cursor salte 5 linhas para cima. Em seguida, você pode sobrescrever o que for necessário.

Se isso não funcionar, você pode tentar printf "\e[5A"ou echo -e "\033[5A", que deve ter o mesmo efeito.

Basicamente, com as sequências de escape, você pode controlar quase tudo na tela.

Sr. Goferito
fonte
O equivalente portátil disso é tput cuu 5, onde 5 é o número de linhas ( cuupara mover para cima, cudpara mover para baixo).
Maëlan
4

Use o caractere de retorno de carro:

echo -e "Foo\rBar" # Will print "Bar"
Mikael S
fonte
Essa resposta é a maneira mais simples. Você pode até obter o mesmo efeito usando: printf "Foo \ rBar"
lee8oi
1

Pode alcançá-lo colocando o retorno do carro \r.

Em uma única linha de código com printf

for i in {10..1}; do printf "Counting down: $i\r" && sleep 1; done

ou com echo -ne

for i in {10..1}; do echo -ne "Counting down: $i\r" && sleep 1; done
Akif
fonte