7

Is it possible to get the output of a command - for example tar - to write each line of output to one line only?

Example usage:

tar -options -f dest source | [insert trickery here]

and the output would show every file being processed without making the screen move: each output overwrites the last one. Can it be done?


Edit: we seem to have a working answer, but lets take it further: How about doing the same, but over 5 lines? You see a scrolling output that doesn't affect the rest of the terminal. I think I've got an answer, but I'd like to see what you guys think.

Chris Watts
  • 6,197
  • 7
  • 49
  • 98

3 Answers3

6

Replace the newlines with carriage returns.

 tar -options -f dest source | cut -b1-$(tput cols) | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo

Explanation:

  • cut -b1-$(tput cols): Truncates the output of tar if it is longer than the terminal is wide. Depending on how little you want your terminal to move, it isn't strictly necessary.

  • sed -u 'i\\o033[2K': Inserts a line blank at the beginning of each line. The -u option to sed puts it in unbuffered mode. stdbuf -oL sed 'i\\033[2K' would work equally as well.

  • stdbuf -o0 tr '\n' '\r': Uses tr to exchange newlines with carriage returns. Stdbuf makes sure that the output is unbuffered; without the \n's, on a line buffered terminal, we'd see no output.

  • echo: Outputs a final newline, so that the terminal prompt doesn't eat up the final line.

For the problem your edit proposes:

x=0; 
echo -e '\e[s'; 
tar -options -f dest source | while read line; do
      echo -en "\e[u" 
      if [ $x gt 0 ]; then echo -en "\e["$x"B"; fi;
      echo -en "\e[2K"
      echo -n $line | cut -b1-$(tput cols);
      let "x = ($x+1)%5";
done; echo;

Feel free to smush all that onto one line. This actually yields an alternative solution for the original problem:

echo -e '\e[s'; tar -options -f dest source | while read line; do echo -en "\e[u\e2K"; echo -n $line | cut -b1-$(tput cols); done; echo

which neatly relies on nothing except VT100 codes.

Dave
  • 10,964
  • 3
  • 32
  • 54
  • 2
    Or with carriage returns, causing the next line to overwrite: `tr '\012' '\015'` – tripleee Jan 05 '12 at 18:56
  • 1
    I think you want to print a single newline at the end so that when it finishes it doesn't screw up your prompt. – mrj Jan 05 '12 at 18:59
  • 1
    I don't think this is what the OP is looking for at all. He wants some kind of control-character trickery (with backspaces or something) so the output is all written to the _same spot_. – Dan Fego Jan 05 '12 at 19:05
  • @DanFego: That's what this theoretically does... except `tr` doesn't flush stdout after writing `\r` so it doesn't actually print anything until the end, and also replaces the last newline with a carriage return, so the prompt overwrites the last line of output. – mrj Jan 05 '12 at 19:39
  • I wonder if there's some `setterm` magic that'll make stdout more flushy... – Dave Jan 05 '12 at 20:18
  • Tar doesn't like this. It ignores the pipe and takes the rest as standard parameters =[ – Chris Watts Jan 07 '12 at 11:34
  • 1
    I'm not sure why everyone's upvoting this; it doesn't work, since stdout doesn't get flushed. – mrj Jan 07 '12 at 14:26
  • +1 This works perfectly for me, *if* none of the lines are wider that the current terminal width. Ubuntu 11.04. – Aaron McDaid Jan 07 '12 at 15:03
  • @CJxD, that's not possible ("It ignores the pipe and takes the rest as standard parameters"). What exactly happens when you try it? – Aaron McDaid Jan 07 '12 at 15:05
  • .. on second thoughts, it's less than perfect at the moment. 'old' lines are left behind and sometimes the row is a combination of recent lines and old lines. What's the xterm code to clear the current line? – Aaron McDaid Jan 07 '12 at 15:10
  • 1
    @Dave, good use of `sed`, but I think it should be `| cut -b1-$(tput cols) | sed -u 'i\\o033[2K' | tr '\n' '\r'; echo` , using cut to trim the overwide lines. – Aaron McDaid Jan 07 '12 at 15:58
  • To clarify: There are two distinct 'line-width' problems. Both must be solved. (1) We need to clear the lines, so that a short line isn't mixed up with older, longer lines. (2) If a line is very long, *and is wider than the current terminal width*, then we need to trim it. These two problems need two separate solutions. – Aaron McDaid Jan 07 '12 at 16:02
  • `/minecraft/mcbackup: line 252: tar -cpvC /backups/TEST/serv -f /backups/TEST/back/120108-af.tar * | cut -b1-80 | sed -u 'i\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo: No such file or directory` Alsooo, the next command I have is `status=$?`. I assume that this variable now wont hold the result of the tar command, but probably the tr command. Is this true? – Chris Watts Jan 08 '12 at 01:07
  • My usage was: `$command" | cut -b1-80 | sed -u 'i\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo"` where $command stores the tar command – Chris Watts Jan 08 '12 at 01:37
  • Is that quote actually there in `$command"`? You may be running into issues if it is. What happens under a simple test case? Try `ls / | cut -b1-80 | sed -u 'i\\o033[2K' | stdbuf -o0 tr '\n' '\r'; echo`. Don't forget to include **both** backslashes in `'i\\o033[2K'`. – Dave Jan 08 '12 at 01:54
  • There were two, it just escaped one on the output: I miscopied there. It seemed to work, as all I saw was 'var' - the last folder, but I had to take out `stdbuf` and its params because it wasn't recognised as a command. – Chris Watts Jan 08 '12 at 11:20
  • Okay, it now works like this: `$command | cut -b1-80 | sed -u 'i\\o033[2K' | tr '\n' '\r'; echo` BUT how do I get the exit code of $command? This tells me if it succeeded or not. – Chris Watts Jan 08 '12 at 11:28
4

Thanks to Dave/tripleee for the core mechanic (replacing newlines with carriage returns), here's a version that actually works:

tar [opts] [args] | perl -e '$| = 1; while (<>) { s/\n/\r/; print; } print "\n"'

Setting $| causes perl to automatically flush after every print, instead of waiting for newlines, and the trailing newline keeps your last line of output from being (partially) overwritten when the command finishes and bash prints a prompt. (That's really ugly if it's partial, with the prompt and cursor followed by the rest of the line of output.)

It'd be nice to accomplish this with tr, but I'm not aware of how to force tr (or anything similarly standard) to flush stdout.

Edit: The previous version is actually ugly, since it doesn't clear the rest of the line after what's been output. That means that shorter lines following longer lines have leftover trailing text. This (admittedly ugly) fixes that:

tar [opts] [args] | perl -e '$| = 1; $f = "%-" . `tput cols` . "s\r"; $f =~ s/\n//; while (<>) {s/\n//; printf $f, $_;} print "\n"'

(You can also get the terminal width in more perl-y ways, as described here; I didn't want to depend on CPAN modules though.

Community
  • 1
  • 1
mrj
  • 1,571
  • 1
  • 10
  • 7
  • It doens't like this method either. The pipe is being ignored for whatever reason. Also, will Perl work for everyone, or does it have to be installed first? – Chris Watts Jan 07 '12 at 11:35
  • Doesn't work with `cp` either. `cp` doesn't complain, but it doesn't do what it should either. Nether does the previous answer. =[ – Chris Watts Jan 07 '12 at 13:07
  • @CJxD: I tested this. It absolutely works for me on Ubuntu. Are you possibly on a different platform, where `tar` sends its output to stderr instead of stdout? In that case, you'd need to add `2>&1` before the pipe, to redirect stderr to stdout so the pipe will catch it. – mrj Jan 07 '12 at 14:30
  • @CJxD: Oh, I saw your comment on the other answer about tar taking everything as arguments. Are you sure you don't just have a quoting problem in your tar command? Bash interprets the command line, doing its own thing with the pipe, and giving the arguments to `tar`; `tar` has no way to grab the pipe unless you escape it so bash doesn't give it special meaning. I tested using this command: `tar xvf foo.tar.gz | perl ...` – Cascabel Jan 07 '12 at 14:35
  • @jefromi: I suppose that sounds more likely than a platform sending normal output to stderr... – mrj Jan 07 '12 at 15:00
4
tar -options -f dest source | cut -b1-$(tput cols) | perl -ne 's/^/\e[2K/; s/\n/\r/; print' ;echo

Explanations:

  • | cut -b1-$(tput cols) This is in order to make sure that the columns aren't too wide.
  • (In perl -ne) s/^/\e[2K/ This code clears the current line, erasing 'old' lines. This should be at the start of the line, in order to ensure that the final line of output is preserved and also to ensure that we don't delete a line until the next line is available.
  • (In perl -ne) s/\n/\r/ The tr command could be used here of course. But once I started using perl, I stuck with it

PS To clarify: There are two distinct 'line-width' problems. Both must be solved. (1) We need to clear the lines, so that a short line isn't mixed up with older, longer lines. (2) If a line is very long, and is wider than the current terminal width, then we need to trim it.

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • If it was at the end of the line, There would be no output; Every line it prints, it would immediately erase – Dave Jan 07 '12 at 18:14
  • @Dave, I've updated the working a little. Is this OK: It's at the start in order to ".. ensure that we don't delete a line until the next line is available." – Aaron McDaid Jan 07 '12 at 18:27
  • @AaronMcDaid that's fine – Dave Jan 07 '12 at 18:34