Odds and Ends

Topics in this section are miscellaneous odds and ends which did not have direct relevance in other sections, or topics which are more advanced. The reader should have a good understanding of previous and related sections before preceding with these topics.


Most modern Unix shells support the implementation of user defined functions (although older shells will not support this). Functions can be defined on a single line as follows:
	fname () { command1 ; command2 ; ... commandn ; }
or split across multiple lines:
	fname () { 
In either case, fname is the name used to define (and invoke) the function; the parenthesis inform the shell that the following is a function definition; and the commands compose the body of the function. Functions are typically defined for frequently used tasks that generally require more than one command. Frequently used functions can be defined within a startup file such as .profile so that they are available each time the user logs in. There are two distinct advantages to using functions:

  1. functions run in the current process space by default (i.e. no child process created)

  2. once functions are defined the reside in RAM, thus execution is faster since no directories need to be searched, scripts loaded and then executed

Note however that functions only exist in the environment (i.e. shell/process) in which they are defined1. This means that their definition and usage cannot be inherited by child shells or processes. However, if the function is defined in a startup script (e.g. .profile), the function will be (re) defined when the child shell is created since the startup script will be executed (note this may take a command line option when starting the child shell such as ksh -l (ell)). Another option is to define frequently used functions in a single script that can easily be invoked in child processes.

If we wish to create a function to indicate the number of users on the system at a given time, we can do something like:

	$ usernum () { who | wc -l ; } [Enter]
The function is then run (or invoked) as follows:
	$ usernum [Enter]
Syntatically, if your function is going to be on a single line, (at least) a single space must follow the opening curly bracket and a semicolon must precede the closing curly bracket. Failure to follow this syntax will result in somewhat cryptic errors as follows:
	$ usernum () {who | wc -l; } [Enter]
	ksh: syntax error: `}' unexpected
Functions can also be split across multiple lines either from the command line or from within shell scripts. If a function is split across multiple lines from the command prompt, you will see the PS2 prompt until the function is completed. Thus the function from above might look as follows when entered on multiple lines:
	$ usernum () { [Enter]
	> who | wc -l [Enter]
	> } [Enter]
Parameters can be passed into functions using the positional parameter mechanism as described in a previous section. Thus we can create a function called sp (for show processes) to show all processes for a given user:
	$ sp () { ps -eaf | grep $1 ; } [Enter]
The function is then executed with the command line argument as follows:
	$ sp mthomas [Enter]
	mthomas   10929  7632  0 22:34 pts/2    00:00:00 -ksh
	mthomas    9420 10929  0 22:36 pts/2    00:00:00 ps -eaf
	mthomas    9421 10929  0 22:36 pts/2    00:00:00 grep mthomas
As with other languages, shell functions can return a single value using the return command as follows:



return n

where n is an integer value. This value can then be queried using the $? construct. If no return statement is present, $? contains the return status from the last executed command.

Function definitions can be removed from existance using the undef command (as with undefining environment variables) as follows:

	$ undef -f usernum [Enter]
	$ usernum  [Enter]
ksh: usernum: not found
1 Some (but not all) newer shell implemenations allow for functions to be globally accessed, i.e. globally exporting the function definition. For example, the bash shell provides the
	export -f function_name 
statement for making functions global. This capability has been purposfully omitted from some shell implemenations, e.g. ksh (more at opengroup.org).

Finding Files

Unix provides the ability to locate files contained within the file system hierarchy by the use of the find command. The find command is a very flexible and powerful command which allows not only the ability to find files, but also to process the resulting files in many different ways. The subtle behavior and sophisticated syntax of find sometimes proves formidible as with most powerful Unix commands. In a very general sense, the find command looks as follows:
find search_path selection_criteria action_if_found
Thus, if we simply want to locate one or more occurances of a file by name (foo for example), starting from where we currently are including all subdirectories, and examine the results, the command would look as follows:
	$ find . -name foo -print [Enter]
The dot (.) in this command specifies to start our search from our current directory (and implies all subdirectories); the -name option specifies we wish search for a file by its name (of foo); and the -print option tells the find command to print the results (note the -print option may be the default and thus be omitted in some newer versions of find, specifically those on Linux distros.)

Similarly, if we have no idea where a file might live, we can start at the top of the file system as follows:

	$ find / -name services [Enter]
In this example, we are starting the search from / (including all subdirectories), and looking for all files with the name of services. Typically when using find, we don't have good knowledge of where files live, or we wouldn't be searching to find them. Frequently then, it's useful to start our search from / (note that starting from / is time consuming on large filesystems).

While this command is useful, it brings up an interesting behavior of find. If you are starting from /, and not the root user, it is typical to get voluminous errors trying to search directories in which you don't have privilege to search through (which is understandable). In this case, erroneous output will overwhelm the valid output you are looking for, as in the following example:

	$ find / -name services [Enter]
	find: /var/lib/slocate: Permission denied
	find: /var/lib/xdm/authdir: Permission denied
	find: /var/lib/nfs/statd: Permission denied
	find: /var/lib/ldap: Permission denied
	find: /var/log/samba: Permission denied	
	find: /var/log/squid: Permission denied
	find: /var/log/iptraf: Permission denied
	find: /var/log/junkbuster: Permission denied
	find: /var/log/zebra: Permission denied
	find: /etc/zebra: Permission denied
A quick (and useful) workaround to this is the following:
	$ find / -name services 2> /dev/null [Enter]
Recall that 2> redirects the standard error to the destination file; in this example, the black hole known as /dev/null. Thus the only output in this example are the occurances of files named services.

The find command also allows the use of wildcard characters and pattern matching expressions with the -name selector, but when doing this the pattern (usually) must be enclosed in quotes (either as "pattern" or 'pattern' or escaped using \) as follows:

	$ find / -name "serv*" 2> /dev/null [Enter]
Logical operators can be used with the find command also. Some of the common logical operators (in order of precedence, from highest to lowest) are:

( expr )forced precedence
! exprNOT expr, TRUE if expr is FALSE
expr1 expr2implied AND, expr2 not evaluated if expr1 is FALSE
expr1 -a expr2AND, expr2 not evaluated if expr1 is FALSE
expr1 -o expr2OR, expr2 not evaluated if expr1 is TRUE

If we examine the following find command:

	$ find $HOME -name a.out -o -name "*.o" [Enter]
where our intent is to search all directories below the home directory for files named a.out or files ending in a .o extension and display them (by default). We might also try to accomplish the same task with the following variation:
	$ find $HOME -name a.out -o -name "*.o" -print [Enter]
Notice the difference in ouput from the two commands. While the second example seems logically similar to the first example, here is an example where the subtleties of find emerge. This command will not work as intended due to the fact that there exists an implied and between the -name "*.o" and the -print arguments. Thus, the implied and has a higher precedence than the -o (or) so only the *.o file matches will display. We can force the precedence we wish with the use of parentheses as follows:
	$ find $HOME \( -name a.out -o -name "*.o" \) -print [Enter]
When using parentheses to force precedence, pay close attention to the mandatory escape characters preceding each parenthesis as well as the mandatory spaces following the first and preceding the terminating parenthesis.

The find command can also locate files based upon attributes other than file names. For example, one can use the find command to locate files based upon the time the file was last accessed. Thus to generate a list of files (residing in a temporary directory) older than 30 days (30 * 24 hours), we use find as follows:

	$ find ./temp -atime +30 [Enter]
Note here that the + indicates more than 30 days, a - would match less than 30 days, and no leading sign would match exactly 30 days. The find command allows actions other than printing on files matched from find. The most common action other than print is the -exec action, which allows the execution of other Unix commands performed on the matched files returned from find. The general syntax when using -exec is:
	-exec command {} \;
noting the space between the } and \. For example, if one wanted to change protection modes on all files ending with .html extensions in a directory to rwxr-xr-x (755), the find command would look as follows:
	$ find ./html_docs -name "*.html" -exec chmod 755 {} \; [Enter]
Notice that no output appears from this command; find silently changes the protection modes and terminates. If one desires to see what files find has selected, you can add the -print command as follows:
	$ find ./html_docs -name "*.html" -exec chmod 755 {} \; -print [Enter]
Combining several of the features of find, we can examine the following example:
	$ find $HOME \( -name a.out -o -name "*.o" \) -atime +30 -exec rm {} \; [Enter]
where the search path begins in the $HOME directory; the search criteria are all files that match the name of a.out or *.o (or precedence forced or by inclusion of parentheses) and have not been accessed in the last 30 days (where the and is implied); the resulting action is to use -exec to remove all matched files.

Below you will find a table listing some of the more common find search criteria and resulting actions:

Search Criteria Description
-atime [+-]nfile was accessed [+-]n * 24 hours ago (note this is days)
-mtime [+-]nfile was modified [+-]n * 24 hours ago
-name patternselects files with names that match pattern
-newer patternselects files newer than pattern
-size [+-]n[bck]file has size [+-]n, where b=512 byte blocks,c=bytes k=kBytes
-type xselects files of type x, f=ordinary file, d=directory
-exec commandexecute command
-ok commandlike -exec, except prompt the user (Y/N) before running command
-printdisplays results of find command

Note: [+-] above implies an optional + or - character, where a + implies more than, a minus implies less than, and no preceding sign means exactly.

Conditional Control Operators

The operators && and || are two special conditional control operators which are often used in conjunction with the test command. These operators are sometimes referred to as the conditional AND and the OR operators, respectively. While referred to as AND and OR operators, these do not behave as one would expect from standard ANDs and ORs. Rather, these operators provide a somewhat shorthand notation for the logical if statement. The AND control operator has the general form as follows:
	command1 && command2

where command2 is executed if and only if command1 returns a TRUE (zero) exit status.

Similarly, the OR command operator has the general form as follows:

	command1 || command2

where command2 is executed if and only if command1 returns a FALSE (non-zero) exit status.

An example of using the conditional control operators follows:

	[ -d /proc/$1 ] || exit 1
In this example, the test operator queries the existance of the directory file referenced by /proc/$1; if this directory does not exist, test will return a FALSE status, in which case the OR will cause the exit (with a status of 1) command to execute. If the file does exist (test returns TRUE), exit command will not execute.

Another more sophisticated example comes from the (Linux) /etc/rc.d/init.d/network script as follows:

	[ ! -x /sbin/ipx_internal_net -o ! -x /sbin/ipx_configure ] && IPX=
While this code snippet may appear complex, if we examine the pieces, its intent should be quite understandable. First we identify the square brackets as those of the test operator. This tells us all text contained within the brackets are test commands/operators. Examining the pieces and referring to the test operators presented earlier, we note the -x is the file operator to test if the following file specification exists and has execute mode set; the -o is the or operator, and the ! is the NOT operator. Thus, the test portioncan be interpreted as:
	return TRUE if either file /sbin/ipx_internal_net or /sbin/ipx_configure  
	do not exist as executable files
The next portion of the command is the AND conditional control opeator which will only force execution of the command following if the preceding command returns a zero exit status. And the final piece is the variable being assigned the value of NULL (see Env Variables). Thus, this entire commands can be interpreted as:
	if either file /sbin/ipx_internal_net or /sbin/ipx_configure  
	do not exist as executable files, set the variable IPX to NULL

Command Summary

©2021, Mark A. Thomas. All Rights Reserved.