Beautiful Command Line Output¶
by Brant Watson
Python has excellent tooling for making highly functional command line utilities, but misunderstanding them has lead to many inconsistent and problematic tools. In this article, I’ll introduce you to some simple concepts around command line output and some light programming concepts. You don’t need to know how to code to read this, but having a little bit of experience with the command line will definitely help!
First Program¶
To understand this, you’ll have to first learn how to write some code. Open a file and enter the following code:
print("I'm coding stuff")
Save the file as test.py
, then invoke it from the command line using the
Python interpreter—it will print the quoted text to your command prompt:
$ python test.py
I'm coding stuff
$
Tip
In examples like this, the $
represents the command prompt. Stuff
here should be typed out. The other lines are the result of the command.
Standard Out¶
So, what’s happening here? You’re invoking print
, which actually is writing
to something called standard out. The secret is that anything that gets
written to standard out is consumed by the terminal and output to your
screen. print
not only writes to standard out but it also modifies what
you wrote. It adds a newline (\n
) to the end of your text. print
is a
nice shortcut for “write something to standard out and then add a new line to
that”. Let’s do something more fancy to show what print
is doing behind the
scenes. Open that test.py
file again, and change it to look like this:
import sys
sys.stdout.write("I'm coding stuff")
Now, as before, run it:
$ python test.py
I'm coding stuff$
It’s almost the same as when you used print
, but notice that you’re cursor
is still stuck on the same line as I’m coding stuff is. That’s because we
didn’t add a newline character to the end. Open test.py
once more and
change it to look like this:
import sys
sys.stdout.write("I'm coding stuff\n")
Now, as before, save then run it:
$ python test.py
I'm coding stuff
$
Voila! Now it works exactly like print
. Of course sys.stdout.write
is
a lot more to type, and you have to import the module sys
. Still, this is
essentially what print
is doing behind the scenes.
Standard Error¶
Now an experiment: open test.py
and change stdout
to stderr
. The
resulting file should look like this:
import sys
sys.stderr.write("I'm coding stuff\n")
Run it once more:
$ python test.py
I'm coding stuff
$
Huh? It does exactly the same thing! Or at least it seems to. The reason for
this is that your terminal window (command prompt) is actually watching for
stuff to get written to two places. It monitors standard out and
standard error. Things written to either location are displayed on the
terminal. This turns out to be very useful, and we’ll get to why, but first,
lets do another experiment. Open your old friend test.py
and change it to
look like the following:
import sys
sys.stdout.write("I'm coding stuff\n")
sys.stderr.write("I'm coding stuff\n")
If you save then run this program you get:
$ python test.py
I'm coding stuff
I'm coding stuff
$
Redirects¶
Pretty redundant seeming for now. But we have a few tricks up our sleeve. It’s
time to break out the handy concept of redirection. Here we will use our
favorite standard redirect character: >
Those of you familiar with Posix
compliant systems like BSD or Linux will recognize the meaning of this
character on the command line. It redirects the output of a command into a
file. Check this out:
$ python test.py > output.txt
I'm coding stuff
$
The first thing you’ll notice is that I'm coding stuff
was only written to
your command line once. That’s weird because we told it to write it twice;
once to standard out and once to standard error. Our program didn’t change.
What happened is that everything written to standard out was written into a
file called output.txt
. It doesn’t matter that no such file existed before.
The redirect (denoted by the >
character) created that file. If you were to
look, you’d see that a new file was created and the contents of that file are
I'm coding stuff
.
Getting Fancy¶
It’s time for a more concrete example. Don’t worry about fully understanding
the following code. Our purpose here is to demonstrate some advantages of
having two output locations and the ability to distinguish between them. It’s
time to edit test.py
again, but this time we’re going to make it really
fancy:
import sys
sys.stderr.write("FILE WRITER VERSION 1.0\n")
sys.stderr.write("-----------------------\n")
sys.stderr.write("Welcome to a boring program! It's terrible!\n")
sys.stderr.write("Enter something and press [Enter]: ")
sys.stderr.flush() # This just says 'write to the terminal NOW!'
x = raw_input() # Read whatever the user inputs and store it in 'x'
sys.stderr.write("You entered: ")
sys.stderr.write(x)
sys.stderr.write("\n") # Writes a newline after whatever was in 'x'
sys.stderr.write("Bye loser!\n")
sys.stderr.flush() # Same as using flush before
sys.stdout.write(x) # Writes the contents of 'x' to standard out
First, just save the file and run the command:
$ python test.py
FILE WRITER VERSION 1.0
-----------------------
Welcome to a boring program! It's terrible!
Enter something and press [Enter]: hello
You entered: hello
Bye loser!
hello$
Note
Here we use raw_input
to read input from the user. Whatever the
user types in before pressing [Enter] will be saved in a variable called
x
.
First this is pretty stupid; we enter “hello” and get back a message that says we entered “hello”. Then at the end, “hello” shows up on the screen again! Also, things look a little bit ugly because the new command prompt is still on the same line as the word “hello”. But, this is also handy; run it once more with a redirect:
$ python test.py > output.txt
FILE WRITER VERSION 1.0
-----------------------
Welcome to a boring program! It's terrible!
Enter something and press [Enter]: hello
You entered: hello
Bye loser!
$
That’s better. We still get some information about what the program DID, but we
don’t see “hello” hucked into the terminal twice. So what happened to the
second appearance of “hello”? It was redirected into the file output.txt
.
That’s handy because it means we can separate out special stuff that we want
the user to see such as questions, messages, the program name, and anything
else user-centric, but allow only important output to be easily redirected to a
file. This leads to a very simple rule to follow: “Write all user messages to
standard error and all programmatic output to standard out.” This means we
now have a program that does something vaguely useful. It asks the user a
question, and then can create a file containing whatever they say.
Logging¶
Wasn’t the term logging mentioned previously? It was! It just took us a long time to get here because the introduction of necessary concepts was a pretty deep one. Python ships with a very nice logging system as part of it’s standard library.
The important thing to remember about the logging system, is that if you’re using a StreamHandler, then Python will log to standard error by default. That means you can use logging and all of it’s nice facilities for all output. Here is an example with an overly basic logging setup:
import sys
import logging
# Create a StreamHandler (which writes to standard error)
handler = logging.StreamHandler()
# Setup a logger with the name 'cool_app'
LOG = logging.getLogger('cool_app')
LOG.setLevel(logging.INFO)
LOG.addHandler(handler)
LOG.debug("Using logging namespace 'cool_app'")
LOG.debug("Available modules: {0}".format(sys.modules))
LOG.info("FILE WRITER VERSION 1.0")
LOG.info("-----------------------")
LOG.info("Welcome to a boring program! It's terrible!")
LOG.info("Enter a number and press [Enter]: ")
x = raw_input() # Read whatever the user inputs and store it in 'x'
try:
x = int(x) # Try turning the value into an integer
except ValueError:
LOG.warning("User did not enter a number!")
else:
LOG.debug("Sucessfully cast {0} as an integer".format(x))
sys.stdout.write(x) # Writes the contents of 'x' to standard out
LOG.debug("Entry: {0}".format(x))
LOG.info(x)
LOG.info("Bye loser!")
Log Levels¶
Whats with all this INFO and DEBUG crud? Python’s logging system has a concept called levels. What this basically means is that you can set your log level to a value and only have messages that are below that level come though.
Here are the available levels:
CRITICAL
ERROR
WARNING
INFO
DEBUG
So in our case, setting the log level to INFO means that only INFO,
WARNING, ERROR, and CRITICAL messages will get written to the
screen. DEBUG messages will be ignored entirely. This means we can setup a
bunch of extra output that gives us verbose information about what is going on
inside our application and then we can turn that on and off simply by changing
the level from LOG.setLevel(logging.INFO)
to
LOG.setLevel(logging.DEBUG)
or vica-versa. Plus all of our old tricks with
redirects will still work. In the above example, the contents of x
are
still written to standard out and as a result can be redirected to a file.
The log messages that meet the log level requirements are displayed to the
user.