------------------------------------------------ Practice Unix/Linux Scripts - Chapters 5, 10, 11 - Part 2 ------------------------------------------------ -IAN! idallen@ncf.ca All scripts written must conform to the script_checklist.txt checklist. All user input (arguments or via "read") must be validated before being used. Do not process bad or missing input data! All scripts written must begin with a proper script header (including PATH and umask), as given in the script_checklist.txt file. All prompts for user input must appear on the terminal, even if standard output is redirected. (Do not send prompts for input into the output file!) Quote all your variables - never let the shell expand glob patterns (wildcards) unexpectedly. Test this! Pay close attention to the names and locations of input/output files. Not all of these scripts are equally difficult to write. Find the easy ones first! TimeSaver: Use filename completion in the shell to complete the names of these scripts when you want to test them or edit them. Don't keep typing the full path names of the scripts! Let your Shell do your typing for you! --) Type in all the scripts in the parts of Chapter 11 that we study. Start with "if1" and "chkargs". Add the correct script headers to the scripts. Fix the prompts. Test them to make sure that they work. --) Write this executable script named "10_top_five.sh" Verify that the script has exactly one command line argument that is a readable file. Sort the file and display the first five lines. --) Write this executable script named "11_string_compare.sh" Prompt for and read a string. Echo the string back to the user. Compare this string against the first command line argument (test to make sure it exists!) and print whether or not the string is an exact match: $ ./foo 'this is four' Enter your one line of input: this is three You entered: this is three String 'this is three' does not match argument 'this is four'. $ ./foo "happy coding" Enter your one line of input: happy coding You entered: happy coding String 'happy coding' is a match. $ ./foo ./foo: Expecting one argument, found 0: ()" --) Write this executable script named "12_file_mailer.sh" Verify that the single command line argument exists and is a non-empty, readable file. Prompt for and read an email userid. Echo the userid back to the user. Email the content of the file named on the command line to the given userid. The script must check to see that the mail command works (has a good return status). Print an error message if the mail command has an error. --) Write this executable script named "13_comment_extractor.sh" Verify that the script has exactly one command line argument, that it is a file, and that it is readable. Extract and display on standard output all the comment lines (lines starting with "#") from the argument file. Bonus: Don't display lines that begin "#!". --) Write this executable script named "15_optional_arguments_demo.sh" This script takes zero to three arguments. Without arguments, the contents of three predefined shell keyword variables are displayed. Each command line argument replaces one of the variables in the output: With no arguments, display: PWD MAIL HOME With 1 argument, display: arg1 MAIL HOME With 2 arguments, display: arg1 arg2 HOME With 3 arguments, display: arg1 arg2 arg3 Examples: $ ./foo Output is: /home/idallen/tmp /var/spool/mail/idallen /home/idallen $ ./foo blue Output is: blue /var/spool/mail/idallen /home/idallen $ ./foo blue green Output is: blue green /home/idallen $ ./foo blue green red Output is: blue green red $ ./foo blue green red yellow ./foo: Expecting less than 4 arguments; found 4 (blue green red yellow) Restrictions: You may only expand $PWD, $MAIL, and $HOME *once* each in the script. You may only use *one* echo command to produce the output and *one* echo command to produce the error message. Hint: Use three variables to hold the three output values. Initialize the variables to the values of PWD, MAIL, and HOME at the start of the script. Depending on the number of arguments, replace one or more of the variables with the values from the command line. At the end of the script, use the three variables in your "echo" output line. --) Write this executable script named "16_file_sort_self_compare_v1.sh" Verify that the script has exactly one argument. Echo the argument that will be processed back to the user. Verify that the argument is a file, and that it is readable. Sort the file into the /tmp directory under a unique temporary name. (Use the pid $$ somewhere in the name to help ensure uniqueness.) The script must check exit status to make sure that the sort worked. Run a "diff" on the original file and the temporary file and count the number of lines of output. The output should look like this: $ ./foo /etc/resolv.conf Processing: /etc/resolv.conf The file differs from its sorted version by 4 lines. $ ./foo /etc/passwd /etc/group ./foo: Expecting 1 argument; found 2 (/etc/passwd /etc/group) Remove the temporary file when you are done with it. --) Write this executable script named "17_path_validator.sh" The purpose of this script is to validate a single command line argument. If the pathname is a file, make sure it is readable, writable, and not zero size. If the pathname is a directory, make sure it is readable and searchable. Print appropriate messages if any of these validations fail. --) Write this executable script named "18_file_size_classer.sh" Process one command line argument. Make sure it is readable and that it is a file (not a directory). Classify the file according to the count of the number of lines it contains: 0-99: small 100-499: medium over 499: large Sample output: $ ./foo a File 'a' contains 4 lines and is: small $ ./foo b File 'b' contains 500 lines and is: large $ ./foo c ./foo: Argument 'c' is not a readable file. $ ./foo /bin ./foo: Argument '/bin' is is a directory; nothing done. $ ./foo a b c ./foo: Expecting 1 argument; found 3 (a b c) --) Write this executable script named "19_two_number_sort.sh" Get two inputs. You may use two command line arguments or you may prompt for two inputs from standard input (your choice). The two inputs will be treated as two unsigned integer numbers. Your script does not have to check that the inputs are really numbers; but, you do have to make sure they both exist and are not empty strings (or missing arguments, if you use arguments). Output the smaller number followed by the larger number, in this form: You entered 100 and 12. In order, they are: 12 100 You may use only one "echo" statement to produce the above message. The "echo" statement that produces the standard output of the script must appear in only one place in the script. Hint: Use variables to hold the smaller and larger numbers. Use the two variables in the "echo" statment at the end of the script. --) Write this executable script named "20_range_tester_v1.sh" Prompt for and read two "range" integers. In the prompts, tell the user that the second range integer must be bigger than the first range integer. Your script does not have to make sure that the inputs are numbers; assume the user will always enter numbers. (However - You cannot assume that the user will enter both inputs - make sure that neither of the inputs is empty.) Validate to make sure the second integer is bigger than the first one. Compare a single command-line argument (an integer) against the range and print where it lies in the range. The output must look like this (except that your prompts should be much more informative): $ ./foo 12 Input: 20 40 Argument 12 lies below low range 20. $ ./foo 300 Input: 99 1000 Argument 300 lies between 99 and 1000. $ ./foo 2000 Input: 500 600 Argument 2000 lies above high range 600. $ ./foo a b c ./foo: Expecting 1 argument; found 3 (a b c) $ ./foo ./foo: Expecting 1 argument; found 0 () --) Write this executable script named "21_range_tester_v2.sh" As above; but, use two command line arguments as the two range integers and read the integer to be tested from standard input. $ ./foo 20 40 Input: 12 Input 12 lies below low range 20. $ ./foo 99 1000 Input: 300 Input 300 lies between 99 and 1000. $ ./foo 500 600 Input: 2000 Input 2000 lies above high range 600. $ ./foo 500 600 Input: ./foo: Expecting 1 input number - nothing found. $ ./foo 123 ./foo: Expecting 2 arguments; found 1 (123) Remember: Validate your inputs. Do not use any input that is missing or empty. Do not ignore "extra" arguments - produce an error message. --) Write this executable script named "22_range_tester_v3.sh" As above, but allow the user to enter the two range integers in any order. (Smallest could be first *or* second.) Your program will put the two integers in order before using them as the range. Just to be sure, echo back the low range integer and the high range integer after your program has put them in order. Generate the same output as the previous problem. --) Write this executable script named "25_file_sort_self_compare_v2.sh" Produce the same output as the file_sort_self_compare_v1 script; but, accept either zero or one command line argument. (The argument is now optional.) If there is no command line argument, prompt for and read a filename. If there is a command line argument, use that for the filename. $ ./foo /etc/resolv.conf Processing: /etc/resolv.conf The file differs from its sorted version by 4 lines. $ ./foo Input file name: /etc/resolv.conf Processing: /etc/resolv.conf The file differs from its sorted version by 4 lines. $ ./foo a b ./foo: Expecting 0 or 1 argument; found 2 (a b) Notes: Do not duplicate code! The processing part of the script should *not* be written twice, once for command line arguments and a second time for standard input. Do not duplicate the code. Write it only once. Hint: If using a command line argument, put the command line argument into a shell variable. If there is no command line argument, read standard input into the same variable. Use the shell variable in the rest of the script. Do not duplicate the code. --) Write this executable script named "26_script_permission_validator.sh" Make sure that the given single pathname command line argument is a readable file and that the first line of the file starts with "#!". (Hint: The grep command [like most Unix commands] can read standard input. If the grep pattern is found in standard input, grep returns zero, otherwise it returns non-zero. You already know a command that will extract just the first line of a file.) You may echo the line found for debugging purposes. Next, make sure that this pathname is executable. Print a warning message and a line showing the permissions of the file if it is not. $ ./foo goodscript.sh Found this first line: #!/bin/sh -u $ ./foo wrongexec.sh Found this first line: #!/bin/sh -u Script 'wrongexec.sh' is not executable by you. -rw-r--r-- 1 alleni alleni 123 Jul 31 15:13 wrongexec.sh $ whoami alleni $ ./foo bad.sh Found this first line: !#/bin/sh -u Script 'bad.sh' does not start with '#!'. Script 'bad.sh' is not executable by you. -rw-r-xr-x 1 alleni alleni 123 Jul 31 13:15 bad.sh $ ./foo /bin ./foo: Argument '/bin' is is a directory; nothing done. Bonus: Prompt for a file name if none is given on the command line. --) Write this executable script named "27_script_interpreter_validator.sh" Make sure that the script has a single command line argument and that it is a readable file - don't try to process directories or unreadable files given as arguments. Extract the first line of the file. Validate this first line as a valid first line for a shell script. (Validate the entire line, not just the beginning of the line.) To be valid, the line must be one of the lines you use in your scripts to date. Print a special message for invalid first lines that are blank. Sample usage (showing the first three lines of each of my test files): bash$ head -3 script1.sh <== show my test file #!/bin/sh -u # $0 (no arguments) # This script does the following: bash$ ./foo script1.sh Found this first line: #!/bin/sh -u This is a valid first line of a script. bash$ head -3 script2.sh <== show my test file #/bin/sh -u # $0 pathname # This script does the following: bash$ ./foo script2.sh Found this first line: #/bin/sh -u *** '#/bin/sh -u' is not a valid first line of a shell script. *** bash$ touch empty.sh bash$ ./foo empty.sh Found this first line: *** The first line of this script is missing or empty. *** Suggestion: Use a set of simple IF statements to compare the line you found with each possible valid first line, one after the other. If the line matches in any of the IF statements, exit the script immediately with a good return code. If the first line does not match any of the valid lines (if it fails all the IF tests), then after all the IF statements, print an error message at the end of the script (include the text of the line that doesn't match) and exit non-zero. Bonus: Prompt for a file name if none is given on the command line. --) Write this executable script named "28_script_path_validator.sh" As above, except the script must pick out the "PATH=" line from the script file that is the first command line argument and validate it as being a correct PATH line. (What Unix command will find a line that contains the string PATH= in a file?) --) Write this executable script named "29_script_umask_validator.sh" As above, except the script must pick out the "umask" line from the script file that is the first command line argument and validate it as being a correct umask line (umask 22 or umask 022). --) Write this executable script named "30_script_validator.sh" Write a master script that validates its first argument (a filename) as being a valid script by passing the filename to each of the four permission, interpreter, path, and umask validator scripts that you wrote above. This is a very handy script to have, especially for tests. --) Write this executable script named "31_empty_directory_tester_v1.sh" This script validates its only command-line argument as a directory, tells whether or not it is searchable (has execute permissions), and counts the number of non-hidden names in it. (What Unix command shows the names in a directory? What Unix command counts words?) The output should look like this: $ ./foo /bin Directory '/bin' contains 90 non-hidden names. $ ./foo /nosuchfile ./foo: error: argument '/nosuchfile' is not a directory. $ mkdir empty $ touch empty/.hidden empty/.morehidden $ ./foo empty Directory 'empty' contains 0 non-hidden names. $ chmod u-x empty $ ./foo empty ./foo: warning: Directory 'empty' is not searchable. Directory 'empty' contains 0 non-hidden names. $ chmod u-rx empty $ ./foo empty ./foo: error: directory 'empty' is not readable; nothing done. Notes: - warnings and errors print on standard error, not standard output Bonus: In your output, say that the directory contains no non-hidden names instead of displaying the number 0. (Hint: Put the count of names into a variable. Change the variable contents to "no" if it has the value zero.) Bonus: Prompt for a directory name if none is given on the command line. --) Write this executable script named "32_empty_directory_tester_v2.sh" This produces output similar to the previous problem; but, hidden names except "." and ".." are now included in the count. Include hidden names in the name count; but, don't include the names "." and "..". If a directory contains only "." and ".." you must still call it "empty" in the output: $ mkdir empty $ ./foo empty Directory 'empty' is empty. $ touch empty/.hidden empty/.another $ ./foo empty Directory 'empty' contains 2 names. Hints: Read about the "-A" option to the "ls" command. Bonus: If there is only one hidden name, say "1 name" not "1 names". (Hint: put either "s" or "" [nothing] into a variable, depending on whether the count is 1 or not 1. Use the variable at the end of the word "name" in the output message.) --) Write this executable script named "33_make_backup.sh" Verify that the script has exactly one command line argument. Verify that the argument is a file, and that it is readable. See if a "backup" version of the given file exists. The backup version is named with ".bak" appended to the file name. If no backup exists, create a backup version of the file by copying it to the same name with ".bak" appended and then exit the script. Use the "-p" option of the copy command to preserve the modify time on the backup copy of the file. Remember: Test the return code of the copy and exit the script non-zero if the copy command fails. If a backup copy already exists, determine if it is identical to the original file. (Hint: use diff, and optionally wc.) If it is identical, print a message saying it is identical and exit the script. If the existing backup copy is different from the original, prompt and ask the user if the existing backup copy should be overwritten. (Bonus: Tell the user how many lines of differences there are between the original file and its backup version.) If the backup copy should be overwritten (if the user responds yes), do so, otherwise exit the script. $ ls foo zork $ ./foo zork Creating new backup: zork.bak $ ls foo zork zork.bak $ ./foo zork Files 'zork' and 'zork.bak' are identical; nothing done. $ date >zork.bak $ ./foo zork File 'zork.bak' already exists; should I overwrite it (yes/no)? yes Creating new backup: zork.bak $ Bonus: Structure the code so that the copy code that creates the backup copy is only present in one place in the script; don't duplicate the code. Bonus: Prompt for a file name if none is given on the command line. --) Write this executable script named "34_integer_sorter_v1.sh" This scripting is easy; the sorting algorithm is the item most students find difficult to get right. Prompt the user and read three integers from standard input. Your script does not have to check that the three inputs are really integers; you may assume that they are numbers. (However - You cannot assume that the user will enter all three inputs - make sure none of the three inputs are empty.) Output the three integers in order from smallest to largest, in this form: The numbers are, from smallest to largest: 23 156 9823 You probably need to write a correct pseudocode algorithm to do this first, before you write the script to do it. You might also verify your pseudocode with a small C program, first. Hint: The shortest solution is based on the "bubble sort" algorithm. Bubble the largest integer to the top of the list and then repeat for the remaining two numbers. You don't need a loop or array here, since there are only 3 numbers. Bonus: Check that the user entered only three inputs, not four or more. (Hint: If you use four varibles in the shell "read" statement, the fourth variable must be empty; if not, there are more than three words typed as input and you should issue an error message.) --) Write this executable script named "35_integer_sorter_v2.sh" Same as above; but, use exactly three command line arguments instead of reading three inputs from standard input. Your script does not have to check that the three arguments are really numbers; you may assume that they are numbers. (However - You cannot assume that there will be three of them - validate the number of arguments!) Generate the same output as the previous problem. --) Write this executable script named "36_integer_sorter_v3.sh" Generate the same output as above; but, allow a mixture of command line arguments and prompts for input. For example: If the user supplies three command line arguments, don't prompt for any input; use the command line arguments. If the user supplies only two command line arguments, prompt for and read the missing third integer. If the user supplies only one command line argument, prompt for and read the missing two ingegers. If the user supplies no command line arguments, prompt for and read all three missing integers. Echo the input numbers back to the user before sorting them. Note: Do not duplicate code! Use shell variables to hold the command line arguments (if any). See the optional_arguments_demo and file_sort_self_compare scripts for examples of how to do this. --) Write this executable script named "37_file_ranger.sh" Expect two command arguments to be integers establishing a number range. The two integers may be supplied either smallest and largest or largest and smallest. Output the smaller range number and then the larger range number. (e.g. both "5 9" and "9 5" as command line arguments would generate the ordered output "5 9".) Prompt for and read a file name from standard input. Generate a special error message if the user's supplied input is empty (if the user didn't enter any name). Make sure the file is a readable file (not a directory). Count the number of lines in the file. Output the name of the file, the number of lines in the file, and whether or not the number of lines lies within the range numbers given on the command line. $ ./foo 500 10 The range is from 10 to 500 Input: /etc/passwd Yes '/etc/passwd' has 101 lines and is in range 10 to 500. $ ./foo 99 1000 The range is from 99 to 1000 Input: /etc/passwd Yes '/etc/passwd' has 101 lines and is in range 99 to 1000. $ ./foo 9 3 The range is from 3 to 9 Input: /etc/passwd No '/etc/passwd' has 101 lines, which is greater than 9. $ ./foo 1000 99 The range is from 99 to 1000 Input: /bin ./foo: unknown input: '/bin' is a directory, not a file. $ ./foo 99 1000 The range is from 99 to 1000 Input: ./foo: missing file name; nothing done --) Write this executable script named "38_file_ranger_v2.sh" As above; but, reverse where you obtain the integers and the pathname. Prompt for and read the two numbers from standard input. Get the pathname as the only command line argument. Generate the same output as the previous problem. As with all scripts, you must validate all user input, whether coming from the command line arguments or when read from standard input. The script should give good error messages if any input is missing or empty. Bonus: Make sure the user enters exactly two numbers, not more. --) Following the model of the previous problem that used optional command line arguments (file_sort_self_compare_v2), go back and modify all the scripts you have written so far and make their command line arguments optional. Read the missing arguments from standard input if there are arguments missing on the command line. Do not duplicate the code; modify the code to use shell variables and put the command line arguments (if any) into those variables.