96

My bash-script looks as following:

echo "Description:"
while [ $finishInput -eq 0 ]; do
  read tmp
  desc="$desc"$'\n'"$tmp"
  if [ -z "$tmp" ]; then
    finishInput="1"
  fi
done
echo -n "Maintainer:"
read maintainer

It reads to the desc var until a empty line is passed. After that, i want to read in other stuff.

When executing my current script it looks like this:

Description:
Line 1
Line 2

Maintainer:

I would like to overwrite the last empty line with the "Maintainer:".

I searched for a solution but only found suggestions which were like

echo -n "Old line"
echo -e "\r new line"

which stays on the line and overwrites it. This is not possible in my case.

Christian Fritz
  • 20,641
  • 3
  • 42
  • 71
Zulakis
  • 7,859
  • 10
  • 42
  • 67

6 Answers6

144

In your example you delete the text at the same line. When you want to return to the previous line use \e[1A, and to clear that line, use \e[K:

echo 'Old line'
echo -e '\e[1A\e[Knew line'

When you want to go N lines up, use \e[<N>A

oguz ismail
  • 1
  • 16
  • 47
  • 69
Igor Chubin
  • 61,765
  • 13
  • 122
  • 144
  • 3
    You also stay on the same line using -n. This is not possible in my case as written above. – Zulakis Jul 01 '12 at 16:28
  • 2
    Better to have the `\r` before the `\e[0K`, otherwise you'll wipe to the right and then move left, possibly leaving junk between where you were and are. Though since it should be a blank line, neither are strictly necessary. – Kevin Jul 01 '12 at 16:38
  • Thanks, this worked. Where can you find such special characters? – Zulakis Jul 01 '12 at 16:53
  • 3
    just google `terminal ansi sequences`. One of results: http://www.termsys.demon.co.uk/vtansi.htm – Igor Chubin Jul 01 '12 at 16:57
  • 24
    @Zulakis: See `man 5 terminfo` and use `tput` instead of hard-coded sequences. `tput cuu1` goes up one line, `tput cuu 4` goes up 4 lines, `tput el` clears to end of line. You can save these in variables and reduce the overhead of repeatedly calling `tput`: `ceol=$(tput el)` and later `echo "foo\r${ceol}A"` – Dennis Williamson Jul 01 '12 at 17:04
  • Just wondering - is there support for doing something like that but from within a compiled program? i.e. some C program, for example. – sherrellbc Jul 28 '15 at 15:24
  • @sherrellbc if you're still wondering: http://stackoverflow.com/a/3219471/2954547 – shadowtalker Dec 10 '16 at 16:06
  • @IgorChubin The link you posted is incredibly helpful. However, it only mentions `\e[K`, `\e[1K`, and `\e[2K`. I'm assuming `\e[0K` is the same as `\e[K`? If so, why not use `\e[2K` instead of `\r\e[0K`? – Swivel Jul 30 '17 at 04:37
  • @Kevin Your opinion on the above would be nice too. Just not sure if there's any edge-case/caveats/weird-quirks that would make one more reliable than the other. – Swivel Jul 30 '17 at 04:38
79

Found a great guide on escape sequences and wanted to expand on some of the discussions here.

When you write out to a terminal, you move an invisible cursor around, much like you do when you write in any text editor. When using echo, it will automatically end the output with a new line character which moves the cursor to the next line.

$ echo "Hello" && echo " World"
Hello
 World

You can use -n to prevent the new line and if you echo again after this, it will append it to the end of that line

$ echo -n "Hello" && echo " World"
Hello World

The cursor remains where it was so, on it's own, we can't use -n to overwrite the previous line, we need to move the cursor to the left. To do that we need to give it an escape sequence, which we let echo know we're going to use with -e and then move the cursor by providing a return carriage \r which puts the cursor at the beginning of the line.

$ echo -n "Hello" && echo -e "\rWorld"
World

That may look like it worked, but see what happens with

$ echo -n "A longer sentance" && echo -e "\rShort sentance"
Short sentancence

See the extra characters? Simply writing over the line only changes the characters where we wrote them.

To fix this, the accepted answer above uses the escape character \e[0K to erase everything after the cursor, after the cursor has moved left. i.e. \r move to beginning \e[0K erase to end.

$ echo -n "A longer sentance" && echo -e "\r\e[0KShort sentance"
Short sentance

Important \e to begin escape sequences works in zsh but not in sh and not necessarily in bash, however \033 works in all of them. If you want your script to work anywhere, you should preference \033

$ echo -n "A longer sentance" && echo -e "\r\033[0KShort sentance"
Short sentance

But escape characters can provide even more utility. For example \033[1A moves the cursor to the previous line so we don't need the -n on the previous echo:

$ echo "A longer sentance" && echo -e "\r\033[1A\033[0KShort sentance"
Short sentance

\r move to the beginning \033[1A move up \033[0K erase to the end

Finally, this is all a bit messy in my book, so you can turn this into a function:

overwrite() { echo -e "\r\033[1A\033[0K$@"; }

Using $@ just puts all the parameters of the function into the string

$ echo Longer sentance && overwrite Short sentence
Short sentence
starball
  • 20,030
  • 7
  • 43
  • 238
DanielM
  • 6,380
  • 2
  • 38
  • 57
25

I built a function from Dennis Williamsons Comment:

function clearLastLine() {
        tput cuu 1 && tput el
}

Thanks to Dennis Williamson

Community
  • 1
  • 1
Murmel
  • 5,402
  • 47
  • 53
  • Can you explain what those `tput` do? – Enrico Feb 24 '16 at 10:59
  • 2
    @Zim "cuu #" is short for "cursor up # lines" and "el" -> "Clear to end of line", see: [gnu.org - tput](https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html) – Murmel Feb 24 '16 at 18:41
17

If you echo without the newline character echo -n "Something", you can use \r with your next echo to move the 'cursor' to the beginning of the line echo -e "\\rOverwrite something".

bash overwrite terminal line

#!/bin/bash

CHECK_MARK="\033[0;32m\xE2\x9C\x94\033[0m"

echo -e "\n\e[4mDoing Things\e[0m"
echo -n "doing thing 1..."
sleep 1
echo -e "\\r${CHECK_MARK} thing 1 done"

Just be aware that if your new string is shorter that your old string, the tail of your old string will still be visible. Note the done.. in the gif above.

Derek Soike
  • 11,238
  • 3
  • 79
  • 74
4

If you want to run a script in a loop and not blow up your scrollback, you can use the following pattern:

while sleep 10s; do 
  echo -n $(script)
  echo -n -e "\e[0K\r"
done

Just replace the script command with your own.

maxywb
  • 2,275
  • 1
  • 19
  • 25
1
#!/bin/bash

echo "Description:"
while test -z $finishInput; do
 read -s tmp
 desc="$desc"$'\n'"$tmp"
 if [ -z "$tmp" ]; then
 finishInput=1
 else
    echo $tmp
 fi
 #echo "fi="$finishInput;
done
echo -n "Maintainer:"
read maintainer

This solution avoids the empty line, but input is not echoed before the lines are complete.

Hint: My version of bash did not accept "[ $finishInput -eq 0 ]".

Michael Besteck
  • 2,415
  • 18
  • 10
  • I would rather not like this, because users won't see their input until they enter a newline. – Zulakis Jul 01 '12 at 16:54
  • 1
    Since you say "Hint" I presume you know why that line was not "accepted". In case you or someone else may not know, it's because the variable is empty and unquoted. You should use `[ "$finishInput" -eq 0 ]` or better: `[[ $finishInput -eq 0 ]]`. – Dennis Williamson Jul 01 '12 at 17:06
  • [ "$finishInput" -eq 0 ] causes error message "integer expression expected" with my bash. My bash version is 4.2-2ubuntu2. – Michael Besteck Jul 01 '12 at 17:45