Writing a pipe-friendly program
In this recipe, we will learn how to write a program that is pipe-friendly. It will take input from standard input and output the result on standard output. Any error messages are going to be printed on standard error.
Getting ready
We'll need the GCC compiler, GNU Make, and preferably the Bash shell for this recipe.
How to do it…
In this recipe, we are going to write a program that converts miles per hour into kilometers per hour. As a test, we are going to pipe data to it from a text file that contains measurements from a car trial run with average speeds. The text file is in miles per hour (mph), but we want them in kilometers per hour (kph) instead. Let's get started:
- Start by creating the following text file or download it from GitHub from https://github.com/PacktPublishing/Linux-System-Programming-Techniques/blob/master/ch2/avg.txt. If you are creating it yourself, name it
avg.txt
. This text will be used as the input for a program we will write. The text simulates measurement values from a car trial run:10-minute average: 61 mph 30-minute average: 55 mph 45-minute average: 54 mph 60-minute average: 52 mph 90-minute average: 52 mph 99-minute average: nn mph
- Now, create the actual program. Type in the following code and save it as
mph-to-kph.c
, or download it from GitHub from https://github.com/PacktPublishing/Linux-System-Programming-Techniques/blob/master/ch2/mph-to-kph.c. This program will convert miles per hour into kilometers per hour. This conversion is performed in theprintf()
statement:#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { char mph[10] = { 0 }; while(fgets(mph, sizeof(mph), stdin) != NULL) { /* Check if mph is numeric * (and do conversion) */ if( strspn(mph, "0123456789.-\n") == strlen(mph) ) { printf("%.1f\n", (atof(mph)*1.60934) ); } /* If mph is NOT numeric, print error * and return */ else { fprintf(stderr, "Found non-numeric" " value\n"); return 1; } } return 0; }
- Compile the program:
$> gcc mph-to-kph.c -o mph-to-kph
- Test the program by running it interactively. Type in some miles per hour values and hit Enter after each value. The program will print out the corresponding value in kilometers per hour:
$> ./mph-to-kph 50 80.5 60 96.6 100 160.9 hello Found non-numeric value $> echo $? 1 $> ./mph-to-kph 50 80.5 Ctrl+D $> echo $? 0
- Now, it's time to use our program as a filter to transform the table containing miles per hour into kilometers per hour. But first, we must filter out only the mph values. We can do this with
awk
:$> cat avg.txt | awk '{ print $3 }' 61 55 54 52 52 nn
- Now that we have a list of the numbers only, we can add our
mph-to-kph
program at the end to convert the values:$> cat avg.txt | awk '{ print $3 }' | ./mph-to-kph 98.2 88.5 86.9 83.7 83.7 Found non-numeric value
- Since the last value is
nn
, a non-numeric value, which is an error in the measurement, we don't want to show the error message in the output. Therefore, we redirect stderr to/dev/null
. Note the parenthesis around the expression, before the redirect:$> (cat avg.txt | awk '{ print $3 }' | \ > ./mph-to-kph) 2> /dev/null 98.2 88.5 86.9 83.7 83.7
- This is much prettier! However, we also want to add km/h at the end of every line to know what the value is. We can use
sed
to accomplish this:$> (cat avg.txt | awk '{ print $3 }' | \ > ./mph-to-kph) 2> /dev/null | sed 's/$/ km\/h/' 98.2 km/h 88.5 km/h 86.9 km/h 83.7 km/h 83.7 km/h
How it works…
This program is similar to the one from the previous recipe. The features we added here check if the input data is numeric or not, and if it isn't, the program aborts with an error message that is printed to stderr. The regular output is still printed to stdout, as far as it goes without an error.
The program is only printing the numeric values, no other information. This makes it better as a filter, since the km/h text can be added by the user with other programs. That way, the program can be useful for many more scenarios that we haven't thought about.
The line where we check for numeric input might require some explanation:
if( strspn(mph, "0123456789.-\n") == strlen(mph) )
The strspn()
function only reads the characters that we specified in the second argument to the function and then returns the number of read characters. We can then compare the number of characters read by strspn()
with the entire length of the string, which we get with strlen()
. If those match, we know that every character is either numeric, a dot, a minus, or a newline. If they don't match, this means an illegal character was found in the string.
For strspn()
and strlen()
to work, we included string.h
. For atof()
to work, we included stdlib.h
.
Piping data to the program
In Step 5, we selected only the third field—the mph value—using the awk
program. The awk $3
variable means field number 3. Each field is a new word, separated by a space.
In Step 6, we redirected the output from the awk
program—the mph values—into our mph-to-kph
program. As a result, our program printed the km/h values on the screen.
In Step 7, we redirected the error messages to /dev/null
so that the output from the program is clean.
Finally, in Step 8, we added the text km/h after the kph values in the output. We did this by using the sed
program. The sed
program can look a bit cryptic, so let's break it down:
sed 's/$/ km\/h/'
This sed
script is similar to the previous ones we have seen. But this time, we substituted the end of the line with a $
sign instead of the beginning with ^
. So, what we did here is substitute the end of the line with the text "km/h". Note, though, that we needed to escape the slash in "km/h" with a backslash.
There's more…
There's a lot of useful information about strlen()
and strspn()
in the respective manual pages. You can read them with man 3 strlen
and man 3 strspn
.