Linux Gazette - Shell Programming

A Publication of the Linux HomeBoy WebPage Series

"The Linux Gazette...making Linux just a little more fun...!"

Copyright (c) 1996 Geoff Taylor geoff@marburg.dnet.co.uk

The LINUX GAZETTE is a member of the LINUX DOCUMENTATION PROJECT.
For information regarding copying and distribution of this material see the COPYING document.
Linux Home Boy Pages logo created using David Koblas' excellent program XPAINT 2.1.1


Shell Programming

Hi there, and welcome to an occasional series on shells and shell programming. Just how occasional this column is depends on you! I'm always happy to learn new tips and tricks, and (since I don't intend to write everything that appears here) this is your opportunity to share your knowledge with others. Email me at geoff@marburg.dnet.co.uk

I tend to use GNU's bash most of the time, because I think it's pretty nifty and I like some of it's features. But I'm not tied to it. I've used sh, csh, ksh and perl in the past, and they all have their advantages and disadvantages, so there'll be no Holy Wars here! What I'd like to collect and provide here are some ready-to-use scripts you can copy and paste into your environment. I'll try and cater for both beginners and more advanced users, but again this wll be entirely contribution-driven - if you think things aren't advanced enough, send me something!.


px

We'll start with a simple script. One of the more common commands I enter is ps -ax | fgrep -i xxx, to find out more information about the running program xxx. The ps command generates a detailed listing of all processes running on the system and then the output is piped into fgrep to strip out irrelevant information. After a while (being a lazy sort) I put the following into a file I called "px"

	#!/bin/bash
	if [ $# = 1 ]
	then
	{
	    ps -ax | fgrep -i $1;
	}
	else
	{
	    ps -ax;
	}
	fi;

[Note - to run this script, just enter the above text into a file called "px" and make the file executable by chmod +x px. The command can now be run by typing "./px", or just "px" if the current directory is listed in your $PATH.]

Let's take a closer look at what's in the script. The first line looks like a comment, because bash comments start with a "#" symbol. It is a comment as far as the script is concerned, but as well as that it tells the operating system that this script should be interpreted using the executable /bin/bash. The second line is a check on the number of arguments on the command line. The environment variable $# tells you how many arguments the script was passed, and the variables $1, $2, $3 etc hold the arguments themselves. If we are passed one argument, we execute the command:

ps -ax | fgrep -i $1;

$1 will be substituted with the parameter you entered on the command line, so if you typed px bash, the command executed will be ps -ax | fgrep -i bash. Easy, eh? If we enter only the command px, the comparison on the second line fails and we execute the other branch of the if statement. This simply executes the ps -ax command with no filtering of the output.


search

Now, a slightly more complicated script. One of the things I always wanted in my younger days was a grep option to search subdirectories recursively. I usually only wanted to know which files contained the substring, so the output from grep was all irrelevant except for the filename. I'm a little older and wiser now, and I've seen this problem solved a couple of different ways (Un*x motto - there's always another way!). Here's one way a colleague of mine came up with:

	search ()
	{
	    if [ $# = 1 ]
	    then
	    {
	        for i in `find . -path './dev' -prune -o -print 2> /dev/null`
	        do
	        {
	            fgrep -i $1 $i > /dev/null 2>&1;
	            if [ $? = 0 ]
	            then
	            {
	                echo $i
	            } fi;
	        } done;
	    }
	    else
	    {
	        echo "Wrong number of arguments!"
	    } fi;
	}

Let's take a look at it. For a start, it's a shell "function" instead of a script. Functions are basically shell scripts loaded into the shell's memory (usually via your .profile file when you log in), and are therefore available as commands without having to put the executable file on your $PATH. More importantly, they execute in your current shell, without starting a child or subshell process.

Again the first thing the function does is check that it has been given the right number of arguments. In this case we want just 1 argument, and we return an error message if we have too many or not enough.

The next line contains 2 important snippets - a for loop and a find command. We'll look at the find command first.

find . -path './dev' -prune -o -print 2> /dev/null

This command is used to generate a list of all files in the current directory and any subdirectories, with the exception of any directory called dev. (We generally want to avoid looking in any dev directory because it is traditionally where device files and other special files are kept, so we "prune" that subtree from the search. Performing a search on special files can produce some interesting results, but it's definitely not recommended!) We print out any other filenames we come across, and we redirect any errors to /dev/null because we don't really want them and they would only confuse matters.

The for loop is fairly straightforward:

for i in `find . -path './dev' -prune -o -print 2> /dev/null`

We execute the find statement by placing it in backquotes, which have a special meaning in bash. In effect the expression in the backquotes evaluates to the output of the command when it's executed, so if the find command printed out the name of three files as: "file1 file2 file3" the for loop would effectively be: for i in "file1 file2 file3"B>.

The general look of a for loop is "B>for variable in textlist" where variable is the name of the reference variable we are going to use, and textlist is a list of one or more strings. Each time we go through the loop, variable is set to the next item in textlist, so (using the above example) the first time through the loop $i is set to file1, second time through $i is set to file2, and third and final time through $i is set to file3.

Inside the loop, we call fgrep with:

fgrep -i $1 $i > /dev/null 2>&1;

That is, we call fgrep for a case-insensitive search (the "i" option) using the pattern passed on the command line ($1) on the currently active filename ($i), and we ignore everything it prints out. The normal action for fgrep is to print out the line of text that matches the search pattern, but we're not interested in that here. All we want is the name of the file that contains the search pattern. We can tell if fgrep found anything by looking at the value it returns to the calling environment. This value is accessible like any other shell variable, and it is called "$?". If $? is zero, we know that the search expression was found in the filename contained in $i. So, all that remains is to print this filename out, which we do with the straightforward echo command.


As it stands there are a couple of problems with both the px script and the search function, but they're not too bad. I'd be really interested in hearing what you think of the functions, what problems you see with them, and how you could improve or rewrite them. Drop me a line at geoff@marburg.dnet.co.uk to let me know what you think, or to show me some simple yet wonderful scripts of your own.



Back to Linux Gazette #8