Friday, August 7, 2009

Displaying Random Text in Gnome Terminal

The other day a friend of mine asked how he could randomly display famous quotes on his Gnome terminal. He wanted a famous quote displayed every time a new Gnome terminal was started. I went over the steps of how to implement this on his box using BASH, and thought it would be an interesting first topic for this blog.

Setting this up only requires a few lines of code. For this example, I am using BASH, the default shell for Fedora and Ubuntu. Undoubtedly, it wouldn't be difficult to adapt this setup for the CSH, KSH or any other shells the reader may be using.

Upon logging in to the computer, or starting a terminal, the contents of the ~/.bashrc file are read and executed. ~/.bashrc is a user specific file, where aliases and user preferences can be set, as well as the PATH and other important variables. I have even go so far as to include functions in my bashrc file (which is preferable to aliases under most circumstances, another blog post perhaps)

My solution was to select a random number from 1 to the line count of the file (i.e., the number of quotes in the file), then print that line of the file. For the first part, I needed two lines of code. Let me introduce the second line of code first, then I'll introduce the first line (the following code snippet won't work without the first line)
let RAND=${RANDOM}%${lines}+1
That was an arithmetic expression in BASH. Rather convoluted and difficult to read, but I find it preferable to the other method (which is downright nasty!) What it does is simple. It takes the modulus of a random number by the number of lines in the file. The modulus is the remainder in a division operation. For example, 3 % 2 results in 1. 1 is the remainder when one divides 3 by 2. The importance of this operator, is it creates an upper bound. For example, x % y will produce a number from 1 to y-1. Or, in plainer English, if y were 5, I would always get a number from 0 to 4. If y were 100, I would always get a number from 0 to 99. If I divde a random number by the number of lines in the file, say 50 lines, I will get a number 0 to 49. The rest of the expression adds 1 for a number of 1 to 50, which can be matched directly with one of the fifty lines. In short, I always get a random number that coresponds with a line in the file.

With that explained, a little more detail is in order for:
${RANDOM}%${lines}
RANDOM is a variable, so is lines. The dollar sign and curly brackets expand the variables. For example, if RANDOM was 61000, and lines was 200, that expression would become
61000%200
RANDOM is a random number generated by the computer. To get a feel for the range of numbers it produces, you could bring up a terminal, and repeatedly enter:
echo ${RANDOM}
With five runs I received the numbers:
412
12859
20649
18377
21323
Your numbers will most likely vary.

The variable lines, however, is a user defined variable that I made. If you were to run that line above as is, you would get an error, something like:
bash: let: RAND=15801%: syntax error: operand expected (error token is "%")
The reason is, the variable lines is empty. The error message gives a clue, following "bash:" is what BASH tried to execute, followed by a description. Basically, it said the operator % expected an output on both its left and right side, but didn't.

Now we need the first line of code to the my working solution. The first line of code defines the variable lines.
lines=`wc -l famous_quotes.txt | awk '{print $1}'`
As with the last line of code, there are many steps to this line. The first part is `wc -l famous_quotes.txt`. `wc` is used to count words, bytes, characters and lines in a file. For fun, try `wc -w`, `wc -c` on text files to print out the amount of words or charaters. Anyhow, the command takes a file called famous_quotes.txt and counts the number of lines in the file. What I want it to do, is provide a single number for the variable lines. But instead, it produces an output of:
6 famous_quotes.txt
Which is the line count of the file proceeded by the filename. I want a number though, not a filename. So I send its output to another utility called awk. Using the bar (the | symbol) is a pipe. It sends the output of one command to another. In the past I have written expression that involved nearly a dozen pipes. You can feed the output of one command as input to another, which goes on to the next command, followed by the next, and so on.

Moving away from my digression, the output of `wc` produces unwanted output. The solution? Pipe it to a utility that formats the output: awk. Awk is a very powerful tool, and I am not even going to begin to explain its complexities here. I will only provide the most basic coverage to understand it in this context alone.
wc -l famous_quotes.txt | awk '{print $1}'
Awk operates on each line it receives, and here prints out the correct column. The output of the above would be:
6
You could change the command to `wc -l famous_quotes.txt | awk '{print $2}'` and receive an output of:
famous_quotes.txt
In other words, $1 and $2 respond to the first and second columns of the line. Of course, you can combine outputs `awk '{print $1 $2}'` or even `awk '{print $1 " " $2}'`. Awk determines each column by the variable FS (Field Separator), which you can set. But already I have far transgressed the scope of this article. For more details, use google. Awk is horrifically powerful (for those interested in text manipulation, also review grep and sed and learn to pipe the output of one to another).

So now we have our number. And in this example, it is 6. But how do we assign it to the variable lines? That is where the backticks come in. If we were to write the code naively:
lines=wc -l famous_quotes.txt | awk '{print $1}'
It would fail. We must encapsulate the commands. We do this with the backticks (for my keyboard it is adjacent to the ESC key and number 1 key).
lines=`wc -l famous_quotes.txt | awk '{print $1}'`
All the backticks do, is they execute whatever is within them, and return the result. In this case, it is assigned to the variable lines.

Now we have our two lines. The following two lines of code should work provided that the file famous_quotes.txt exists and has some lines of text in it.
lines=`wc -l famous_quotes.txt | awk '{print $1}'`
let RAND=${RANDOM}%${lines}+1
But...it doesn't print anything does it? Let's add the last line to make it print. But first, a slight discursion. The utilities `head` and `tail` are very useful tools. `head` will print the "header" of a file, or the first 10 lines. You can adjust how many lines it prints of a file by `head -30`, which would print the first 30 lines. Similarly, `tail` prints the last 10 lines of a file. Now for the final line of code:
head -${RAND} famous_quotes.txt | tail -1
RAND was defined on the second line of code, a random number within the bounds of the total count of the famous_quotes.txt file. Let's pretend RAND is 5, then the command becomes:
head -5 famous_quotes.txt | tail -1
This prints the first 5 lines of the file. But we only want the fifth line. Hence, we pipe the output to `tail`, which only prints the last line: that is, the fifth line. Hence, the solution is:

lines=`wc -l famous_quotes.txt | awk '{print $1}'`
let RAND=${RANDOM}%${lines}+1
head -${RAND} famous_quotes.txt | tail -1
Add that to your .bashrc file (and be sure to fill out your famous_quotes.txt file!) and next time you start a terminal, you will see one of your quotes!

1 comment:

  1. And here is an improved version for multi-line quotes.

    http://llijun.blogspot.com/2009/08/play-with-random-quotes.html

    ReplyDelete