Carriage return

Carriage return character, progress bar and progressive enhancement.

Imagine building a progress indicator in Node. We could start with low level "print" calls

1
2
process.stdout.write('long message first')
process.stdout.write('short message')

This prints messages one after another on the same line

1
long message firstshort message

Hmm, not the best output, the words are printed on the same line. If we want to print the second message on the second line, we have to print "new line" character \n

1
2
process.stdout.write('long message first\n')
process.stdout.write('short message\n')

which outputs

1
2
long message first
short message

Note, this is what console.log does by default - it adds \n after each printed line.

But what if we send carriage return character \r after the first message?

1
2
process.stdout.write('long message first\r')
process.stdout.write('short message\n')

Our output is garbled!

1
short messagefirst

The second message is shorter and thus it overwrites only a part of the first line of text. We cannot have this; we need to clear the rest of the line every time we print it. We can print a string of spaces to clear the line - but we need to know how many spaces to print. Luckily there is a property process.stdout.columns that tells us exactly how many characters are in the terminal. We can clear the current line by print an empty line + \r before we print new text.

1
2
3
4
5
6
7
8
console.log('stdout width', process.stdout.columns)
const emptyLine = ''.padEnd(process.stdout.columns, ' ')
process.stdout.write('long message first\r')
// delay to better see the first message
setTimeout(() => {
process.stdout.write(emptyLine + '\r')
process.stdout.write('short message\n')
}, 1000)
1
2
stdout width 99
long message first

Then a second later the output becomes

1
2
stdout width 99
short message

Beautiful!

Docker

But what happens if our code runs in a terminal that is really feature-limited, like the output piped from the Docker build command? Here is a Docker file I will use

1
2
3
FROM node:8
COPY index.js .
RUN node .
1
2
3
4
5
6
7
8
9
10
11
Status: Downloaded newer image for node:8
---> ed145ef978c4
Step 2/3 : COPY index.js .
---> 44db58d93738
Step 3/3 : RUN node .
---> Running in 1bc9d0e3b0d3
stdout width undefined
short messagefirst
Removing intermediate container 1bc9d0e3b0d3
---> cb8ac000c993
Successfully built cb8ac000c993

There is no process.stdout.columns number, even if the carriage return works! So how do we show the second line? Well, we can take a shortcut and just do the "newline" instead!

1
const emptyLine = process.stdout.columns ? ''.padEnd(process.stdout.columns, ' ') : '\n'

and it works nicely in the terminal and in Docker

1
2
3
4
 ---> Running in 8d63833b96ac
stdout width undefined
long message first
short message

Nice, except when you have progress bars ... which output thousands of messages when showing percentage increments for example. We have this problem when showing Cypress installation (see issue #1243) progress. The output log would just flood with thousands of identical lines like these

Terminal output

In this case we should treat using progress bars as an enhancement. By default the program should show only the text messages at the start and end of the action.

1
2
3
4
5
6
Installing Cypress (version: 1.4.2)

[10:27:23] Downloading Cypress [started]
[10:27:52] Downloading Cypress [completed]
[10:27:52] Unzipping Cypress [started]
[10:27:58] Unzipping Cypress [completed]

Only if we find that the terminal has process.stdout.columns set, then we can use a more advanced printing and you can show progress indicators.

Note: the code is in repo bahmutov/test-line-return