diff --git a/001-inpath.sh b/001-inpath.sh new file mode 100755 index 0000000..361cfb6 --- /dev/null +++ b/001-inpath.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# inpath - verify that a specified program is either valid as-is, +# or can be found in the PATH directory list. + +in_path() +{ + # given a command and the PATH, try to find the command. Returns + # 0 if found and executable, 1 if not. Note that this temporarily modifies + # the the IFS (input field seperator), but restores it upon completion. + + cmd=$1 path=$2 retval=1 + oldIFS=$IFS IFS=":" + + for directory in $path + do + if [ -x $directory/$cmd ] ; then + retval=0 # if we're here, we found $cmd in $directory + fi + done + IFS=$oldIFS + return $retval +} + +checkForCmdInPath() +{ + var=$1 + + # The variable slicing notation in the following conditional + # needs some explanation: ${var#expr} returns everything after + # the match for 'expr' in the variable value (if any), and + # ${var%expr} returns everything that doesn't match (in this + # case just the very first character. You can also do this in + # Bash with ${var:0:1} and you could use cut too: cut -c1 + + if [ "$var" != "" ] ; then + if [ "${var%${var#?}}" = "/" ] ; then + if [ ! -x $var ] ; then + return 1 + fi + elif ! in_path $var $PATH ; then + return 2 + fi + fi +} + +if [ $# -ne 1 ] ; then + echo "Usage: $0 command" >&2 ; exit 1 +fi + +checkForCmdInPath "$1" +case $? in + 0 ) echo "$1 found in PATH" ;; + 1 ) echo "$1 not found or not executable" ;; + 2 ) echo "$1 not found in PATH" ;; +esac + +exit 0 diff --git a/002-validalnum.sh b/002-validalnum.sh new file mode 100755 index 0000000..e2b1658 --- /dev/null +++ b/002-validalnum.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# validalAlphaNum - Ensures that input only consists of alphabetical +# and numeric characters. + +validAlphaNum() +{ + # validate arg: returns 0 if all upper+lower+digits, 1 otherwise + + # Remove all unacceptable chars + compressed="$(echo $1 | sed -e 's/[^[:alnum:]]//g')" + + if [ "$compressed" != "$input" ] ; then + return 1 + else + return 0 + fi +} + +# Sample usage of this function in a script + +echo -n "Enter input: " +read input + +if ! validAlphaNum "$input" ; then + echo "Your input must consist of only letters and numbers." >&2 + exit 1 +else + echo "Input is valid." +fi + +exit 0 diff --git a/003-normdate.sh b/003-normdate.sh new file mode 100755 index 0000000..556d8ea --- /dev/null +++ b/003-normdate.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# normdate - Normalizes month field in date specification +# to three letters, first letter capitalized. A helper +# function for hack #7, validdate. Exits w/ zero if no error. + +monthnoToName() +{ + # sets the variable 'month' to the appropriate value + case $1 in + 1 ) month="Jan" ;; 2 ) month="Feb" ;; + 3 ) month="Mar" ;; 4 ) month="Apr" ;; + 5 ) month="May" ;; 6 ) month="Jun" ;; + 7 ) month="Jul" ;; 8 ) month="Aug" ;; + 9 ) month="Sep" ;; 10) month="Oct" ;; + 11) month="Nov" ;; 12) month="Dec" ;; + * ) echo "$0: Unknown numeric month value $1" >&2; exit 1 + esac + return 0 +} + +## Begin main script + +if [ $# -eq 1 ] ; then # try to compensate for / or - formats + set -- $(echo $1 | sed 's/[\/\-]/ /g') +fi + +if [ $# -ne 3 ] ; then + echo "Usage: $0 month day year" >&2 + echo "Typical input formats are August 3 1962 and 8 3 2002" >&2 + exit 1 +fi + +if [ $3 -lt 99 ] ; then + echo "$0: expected four-digit year value." >&2; exit 1 +fi + +if [ -z $(echo $1|sed 's/[[:digit:]]//g') ]; then + monthnoToName $1 +else + # normalize to first three letters, first upper, rest lowercase + month="$(echo $1|cut -c1|tr '[:lower:]' '[:upper:]')" + month="$month$(echo $1|cut -c2-3 | tr '[:upper:]' '[:lower:]')" +fi + +echo $month $2 $3 + +exit 0 diff --git a/004-nicenumber.sh b/004-nicenumber.sh new file mode 100755 index 0000000..dc656c6 --- /dev/null +++ b/004-nicenumber.sh @@ -0,0 +1,63 @@ +#!/bin/sh + +# nicenumber - given a number, show it with comma separated values +# expects DD and TD to be instantiated. instantiates nicenum +# or, if a second arg is specified, the output is echoed to stdout + +nicenumber() +{ + # Note that we use the '.' as the decimal separator for parsing + # the INPUT value to this script. The output value is as specified + # by the user with the -d flag, if different from a '.' + + integer=$(echo $1 | cut -d. -f1) # left of the decimal + decimal=$(echo $1 | cut -d. -f2) # right of the decimal + + if [ $decimal != $1 ]; then + # there's a fractional part, let's include it. + result="${DD:="."}$decimal" + fi + + thousands=$integer + + while [ $thousands -gt 999 ]; do + remainder=$(($thousands % 1000)) # three least significant digits + + while [ ${#remainder} -lt 3 ] ; do # force leading zeroes as needed + remainder="0$remainder" + done + + thousands=$(($thousands / 1000)) # to left of remainder, if any + result="${TD:=","}${remainder}${result}" # builds right-to-left + done + + nicenum="${thousands}${result}" + if [ ! -z $2 ] ; then + echo $nicenum + fi +} + +DD="." # decimal point delimiter, between integer & fractional value +TD="," # thousands delimiter, separates every three digits + +while getopts "d:t:" opt; do + case $opt in + d ) DD="$OPTARG" ;; + t ) TD="$OPTARG" ;; + esac +done + +shift $(($OPTIND - 1)) + +if [ $# -eq 0 ] ; then + cat << "EOF" >&2 +Usage: $(basename $0) [-d c] [-t c] numeric value + -d specifies the decimal point delimiter (default '.') + -t specifies the thousands delimiter (default ',') +EOF + exit 1 +fi + +nicenumber $1 1 # second arg forces this to 'echo' output + +exit 0 diff --git a/005-validint.sh b/005-validint.sh new file mode 100755 index 0000000..ff386d3 --- /dev/null +++ b/005-validint.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# validint - validate integer input, allow negative ints too + +validint() +{ + # validate first field. Optionally test against min value $2 and/or + # max value $3: if you'd rather skip these tests, send "" as values. + # returns 1 for error, 0 for success. + + number="$1"; min="$2"; max="$3" + + if [ -z $number ] ; then + echo "You didn't enter anything. Unacceptable." >&2 ; return 1 + fi + + if [ "${number%${number#?}}" = "-" ] ; then # first char '-' ? + testvalue="${number#?}" # all but first character + else + testvalue="$number" + fi + + nodigits="$(echo $testvalue | sed 's/[[:digit:]]//g')" + + if [ ! -z $nodigits ] ; then + echo "Invalid number format! Only digits, no commas, spaces, etc." >&2 + return 1 + fi + + if [ ! -z $min ] ; then + if [ "$number" -lt "$min" ] ; then + echo "Your value is too small: smallest acceptable value is $min" >&2 + return 1 + fi + fi + if [ ! -z $max ] ; then + if [ "$number" -gt "$max" ] ; then + echo "Your value is too big: largest acceptable value is $max" >&2 + return 1 + fi + fi + return 0 +} + +# uncomment these lines to test, but beware that it'll break Hack #6 +# because Hack #6 wants to source this file to get the validint() +# function. :-) + +# if validint "$1" "$2" "$3" ; then +# echo "That input is a valid integer value within your constraints" +# fi diff --git a/006-validfloat.sh b/006-validfloat.sh new file mode 100755 index 0000000..c978439 --- /dev/null +++ b/006-validfloat.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +# validfloat - test whether a number is a valid floating point value. +# Note that this cannot accept scientific (1.304e5) notation. + +# To test whether an entered value is a valid floating point number, we +# need to split the value at the decimal point, then test the first part +# to see if it's a valid integer, then the second part to see if it's a +# valid >=0 integer, so -30.5 is valid, but -30.-8 isn't. + +. 005-validint.sh # source the validint function + +validfloat() +{ + fvalue="$1" + + if [ ! -z $(echo $fvalue | sed 's/[^.]//g') ] ; then + + decimalPart="$(echo $fvalue | cut -d. -f1)" + fractionalPart="$(echo $fvalue | cut -d. -f2)" + + if [ ! -z $decimalPart ] ; then + if ! validint "$decimalPart" "" "" ; then + return 1 + fi + fi + + if [ "${fractionalPart%${fractionalPart#?}}" = "-" ] ; then + echo "Invalid floating point number: '-' not allowed \ + after decimal point" >&2 + return 1 + fi + if [ "$fractionalPart" != "" ] ; then + if ! validint "$fractionalPart" "0" "" ; then + return 1 + fi + fi + + if [ "$decimalPart" = "-" -o -z $decimalPart ] ; then + if [ -z $fractionalPart ] ; then + echo "Invalid floating point format." >&2 ; return 1 + fi + fi + + else + if [ "$fvalue" = "-" ] ; then + echo "Invalid floating point format." >&2 ; return 1 + fi + + if ! validint "$fvalue" "" "" ; then + return 1 + fi + fi + + return 0 +} + +if validfloat $1 ; then + echo "$1 is a valid floating point value" +fi + +exit 0 diff --git a/007-valid-date.sh b/007-valid-date.sh new file mode 100755 index 0000000..55f5311 --- /dev/null +++ b/007-valid-date.sh @@ -0,0 +1,85 @@ +#!/bin/sh +# valid-date - validate date, taking into account leap year rules + +normdate="./003-normdate.sh" # hack #3 for normalizing month name + +exceedsDaysInMonth() +{ + # given a month name, return 0 if the specified day value is + # less than or equal to the max days in the month, 1 otherwise + + case $(echo $1|tr '[:upper:]' '[:lower:]') in + jan* ) days=31 ;; feb* ) days=28 ;; + mar* ) days=31 ;; apr* ) days=30 ;; + may* ) days=31 ;; jun* ) days=30 ;; + jul* ) days=31 ;; aug* ) days=31 ;; + sep* ) days=30 ;; oct* ) days=31 ;; + nov* ) days=30 ;; dec* ) days=31 ;; + * ) echo "$0: Unknown month name $1" >&2; exit 1 + esac + + if [ $2 -lt 1 -o $2 -gt $days ] ; then + return 1 + else + return 0 # all is well + fi +} + +isLeapYear() +{ + # this function returns 0 if a leap year, 1 otherwise + # The formula for checking whether a year is a leap year is: + # 1. years divisible by four are leap years, unless.. + # 2. years also divisible by 100 are not leap years, except... + # 3. years divisible by 400 are leap years + + year=$1 + if [ "$((year % 4))" -ne 0 ] ; then + return 1 # nope, not a leap year + elif [ "$((year % 400))" -eq 0 ] ; then + return 0 # yes, it's a leap year + elif [ "$((year % 100))" -eq 0 ] ; then + return 1 + else + return 0 + fi +} + +## Begin main script + +if [ $# -ne 3 ] ; then + echo "Usage: $0 month day year" >&2 + echo "Typical input formats are August 3 1962 and 8 3 2002" >&2 + exit 1 +fi + +# normalize date and split back out returned values + +newdate="$($normdate "$@")" + +if [ $? -eq 1 ] ; then + exit 1 # error condition already reported by normdate +fi + +month="$(echo $newdate | cut -d\ -f1)" + day="$(echo $newdate | cut -d\ -f2)" + year="$(echo $newdate | cut -d\ -f3)" + +# Now that we have a normalized date, let's check to see if the +# day value is logical + +if ! exceedsDaysInMonth $month "$2" ; then + if [ "$month" = "Feb" -a $2 -eq 29 ] ; then + if ! isLeapYear $3 ; then + echo "$0: $3 is not a leap year, so Feb doesn't have 29 days" >&2 + exit 1 + fi + else + echo "$0: bad day value: $month doesn't have $2 days" >&2 + exit 1 + fi +fi + +echo "Valid date: $newdate" + +exit 0 diff --git a/008-echon.sh b/008-echon.sh new file mode 100755 index 0000000..093ae42 --- /dev/null +++ b/008-echon.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# echon - a script to emulate the -n flag functionality with 'echo' +# for Unix systems that don't have that available. + +echon() +{ + echo "$*" | tr -d '\n' +} + +echon "this is a test: " +read answer + +echon this is a test too " " +read answer2 diff --git a/009-scriptbc.sh b/009-scriptbc.sh new file mode 100755 index 0000000..4d355e7 --- /dev/null +++ b/009-scriptbc.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# scriptbc - wrapper for 'bc' that returns the value of a formula + +if [ $1 = "-p" ] ; then + precision=$2 + shift 2 +else + precision=2 # default +fi + +bc -q -l << EOF +scale=$precision +$* +quit +EOF + +exit 0 diff --git a/010-filelock.sh b/010-filelock.sh new file mode 100755 index 0000000..c2a2e34 --- /dev/null +++ b/010-filelock.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +# filelock - a flexible file locking mechanism + +retries="10" # default number of retries: 10 +action="lock" # default action +nullcmd="/bin/true" # null command for lockf + +while getopts "lur:" opt; do + case $opt in + l ) action="lock" ;; + u ) action="unlock" ;; + r ) retries="$OPTARG" ;; + esac +done +shift $(($OPTIND - 1)) + +if [ $# -eq 0 ] ; then + cat << EOF >&2 +Usage: $0 [-l|-u] [-r retries] lockfilename +Where -l requests a lock (the default), -u requests an unlock, -r X +specifies a maximum number of retries before it fails (default = $retries). +EOF + exit 1 +fi + +# ascertain whether we have lockf or lockfile system apps + +if [ -z "$(which lockfile | grep -v '^no ')" ] ; then + echo "$0 failed: 'lockfile' utility not found in PATH." >&2 + exit 1 +fi + +if [ "$action" = "lock" ] ; then + if ! lockfile -1 -r $retries "$1" 2> /dev/null; then + echo "$0: Failed: Couldn't create lockfile in time" >&2 + exit 1 + fi +else # action = unlock + if [ ! -f "$1" ] ; then + echo "$0: Warning: lockfile $1 doesn't exist to unlock" >&2 + exit 1 + fi + rm -f "$1" +fi + +exit 0 diff --git a/011-colors.sh b/011-colors.sh new file mode 100755 index 0000000..eba6ed8 --- /dev/null +++ b/011-colors.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# ANSI Color -- use these variables to easily have different color +# and format output. Make sure to output the reset sequence after +# colors (f = foreground, b = background), and use the 'off' +# feature for anything you turn on. + +initializeANSI() +{ + esc="" + + blackf="${esc}[30m"; redf="${esc}[31m"; greenf="${esc}[32m" + yellowf="${esc}[33m" bluef="${esc}[34m"; purplef="${esc}[35m" + cyanf="${esc}[36m"; whitef="${esc}[37m" + + blackb="${esc}[40m"; redb="${esc}[41m"; greenb="${esc}[42m" + yellowb="${esc}[43m" blueb="${esc}[44m"; purpleb="${esc}[45m" + cyanb="${esc}[46m"; whiteb="${esc}[47m" + + boldon="${esc}[1m"; boldoff="${esc}[22m" + italicson="${esc}[3m"; italicsoff="${esc}[23m" + ulon="${esc}[4m"; uloff="${esc}[24m" + invon="${esc}[7m"; invoff="${esc}[27m" + + reset="${esc}[0m" +} + +# note in this first use that switching colors doesn't require a reset +# first - the new color overrides the old one. + +initializeANSI + +cat << EOF +${yellowf}This is a phrase in yellow${redb} and red${reset} +${boldon}This is bold${ulon} this is italics${reset} bye bye +${italicson}This is italics${italicsoff} and this is not +${ulon}This is ul${uloff} and this is not +${invon}This is inv${invoff} and this is not +${yellowf}${redb}Warning I${yellowb}${redf}Warning II${reset} +EOF diff --git a/012-library-test.sh b/012-library-test.sh new file mode 100644 index 0000000..163e5a0 --- /dev/null +++ b/012-library-test.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# Script to demonstrate use of the shell function library + +. 012-library.sh + +initializeANSI + +echon "First off, do you have echo in your path? (1=yes, 2=no) " +read answer +while ! validint $answer 1 2 ; do + echon "${boldon}Try again${boldoff}. Do you have echo " + echon "in your path? (1=yes, 2=no) " + read answer +done + +if ! checkForCmdInPath "echo" ; then + echo "Nope, can't find the echo command." +else + echo "The echo command is in the PATH." +fi + +echo "" +echon "Enter a year you think might be a leap year: " +read year + +while ! validint $year 1 9999 ; do + echon "Please enter a year in the ${boldon}correct${boldoff} format: " + read year +done + +if isLeapYear $year ; then + echo "${greenf}You're right! $year was a leap year.${reset}" +else + echo "${redf}Nope, that's not a leap year.${reset}" +fi + +exit 0 diff --git a/012-library.sh b/012-library.sh new file mode 100644 index 0000000..b102299 --- /dev/null +++ b/012-library.sh @@ -0,0 +1,313 @@ +#!/bin/sh + +# inpath - verify that a specified program is either valid as-is, +# or can be found in the PATH directory list. + +in_path() +{ + # given a command and the PATH, try to find the command. Returns + # 0 if found and executable, 1 if not. Note that this temporarily modifies + # the the IFS (input field seperator), but restores it upon completion. + # return variable 'directory' contains the directory where the + # command was found. + + cmd=$1 path=$2 retval=1 + oldIFS=$IFS IFS=":" + + for directory in $path + do + if [ -x $directory/$cmd ] ; then + retval=0 # if we're here, we found $cmd in $directory + fi + done + IFS=$oldIFS + return $retval +} + +checkForCmdInPath() +{ + var=$1 + + # The variable slicing notation in the following conditional + # needs some explanation: ${var#expr} returns everything after + # the match for 'expr' in the variable value (if any), and + # ${var%expr} returns everything that doesn't match (in this + # case just the very first character. You can also do this in + # Bash with ${var:0:1} and you could use cut too: cut -c1 + + if [ "$var" != "" ] ; then + if [ "${var%${var#?}}" = "/" ] ; then + if [ ! -x $var ] ; then + return 1 + fi + elif ! in_path $var $PATH ; then + return 2 + fi + fi + return 0 +} + +# cnvalidate - Ensures that input only consists of alphabetical +# and numeric characters. + +cnvalidate() +{ + # validate arg: returns 0 if all upper+lower+digits, 1 otherwise + + # Remove all unacceptable chars + compressed="$(echo $1 | sed -e 's/[^[:alnum:]]//g')" + + if [ "$compressed" != "$input" ] ; then + return 1 + else + return 0 + fi +} + +monthnoToName() +{ + # sets the variable 'month' to the appropriate value + case $1 in + 1 ) month="Jan" ;; 2 ) month="Feb" ;; + 3 ) month="Mar" ;; 4 ) month="Apr" ;; + 5 ) month="May" ;; 6 ) month="Jun" ;; + 7 ) month="Jul" ;; 8 ) month="Aug" ;; + 9 ) month="Sep" ;; 10) month="Oct" ;; + 11) month="Nov" ;; 12) month="Dec" ;; + * ) echo "$0: Unknown numeric month value $1" >&2; exit 1 + esac + return 0 +} + +# nicenumber - given a number, show it with comma separated values +# expects DD and TD to be instantiated. instantiates nicenum +# if arg2 is specified, this function echoes output, rather than +# sending it back as a variable + +nicenumber() +{ + # Note that we use the '.' as the decimal separator for parsing + # the INPUT value to this script. The output value is as specified + # by the user with the -d flag, if different from a '.' + + integer=$(echo $1 | cut -d. -f1) # left of the decimal + decimal=$(echo $1 | cut -d. -f2) # right of the decimal + + if [ $decimal != $1 ]; then + # there's a fractional part, let's include it. + result="${DD:="."}$decimal" + fi + + thousands=$integer + + while [ $thousands -gt 999 ]; do + remainder=$(($thousands % 1000)) # three least significant digits + + while [ ${#remainder} -lt 3 ] ; do # force leading zeroes as needed + remainder="0$remainder" + done + + thousands=$(($thousands / 1000)) # to left of remainder, if any + result="${TD:=","}${remainder}${result}" # builds right-to-left + done + + nicenum="${thousands}${result}" + + if [ ! -z $2 ] ; then + echo $nicenum + fi +} + +# validint - validate integer input, allow negative ints too + +validint() +{ + # validate first field. Optionally test against min value $2 and/or + # max value $3: if you'd rather skip these tests, send "" as values. + # returns 1 for error, 0 for success + + number="$1"; min="$2"; max="$3" + + if [ -z "$number" ] ; then + echo "You didn't enter anything. Unacceptable." >&2 ; return 1 + fi + + if [ "${number%${number#?}}" = "-" ] ; then # first char '-' ? + testvalue="${number#?}" # all but first character + else + testvalue="$number" + fi + + nodigits="$(echo $testvalue | sed 's/[[:digit:]]//g')" + + if [ ! -z "$nodigits" ] ; then + echo "Invalid number format! Only digits, no commas, spaces, etc." >&2 + return 1 + fi + + if [ ! -z "$min" ] ; then + if [ "$number" -lt "$min" ] ; then + echo "Your value is too small: smallest acceptable value is $min" >&2 + return 1 + fi + fi + if [ ! -z "$max" ] ; then + if [ "$number" -gt "$max" ] ; then + echo "Your value is too big: largest acceptable value is $max" >&2 + return 1 + fi + fi + return 0 +} + +# validfloat - test whether a number is a valid floating point value. +# Note that this cannot accept scientific (1.304e5) notation. + +# To test whether an entered value is a valid floating point number, we +# need to split the value at the decimal point, then test the first part +# to see if it's a valid integer, then the second part to see if it's a +# valid >=0 integer, so -30.5 is valid, but -30.-8 isn't. Returns 0 on +# success, 1 on failure. + +validfloat() +{ + fvalue="$1" + + if [ ! -z "$(echo $fvalue | sed 's/[^.]//g')" ] ; then + + decimalPart="$(echo $fvalue | cut -d. -f1)" + fractionalPart="$(echo $fvalue | cut -d. -f2)" + + if [ ! -z "$decimalPart" ] ; then + if ! validint "$decimalPart" "" "" ; then + return 1 + fi + fi + + if [ "${fractionalPart%${fractionalPart#?}}" = "-" ] ; then + echo "Invalid floating point number: '-' not allowed \ + after decimal point" >&2 + return 1 + fi + if [ "$fractionalPart" != "" ] ; then + if ! validint "$fractionalPart" "0" "" ; then + return 1 + fi + fi + + if [ "$decimalPart" = "-" -o -z "$decimalPart" ] ; then + if [ -z "$fractionalPart" ] ; then + echo "Invalid floating point format." >&2 ; return 1 + fi + fi + + else + if [ "$fvalue" = "-" ] ; then + echo "Invalid floating point format." >&2 ; return 1 + fi + + if ! validint "$fvalue" "" "" ; then + return 1 + fi + fi + + return 0 +} + +exceedsDaysInMonth() +{ + # given a month name, return 0 if the specified day value is + # less than or equal to the max days in the month, 1 otherwise + + case $(echo $1|tr '[:upper:]' '[:lower:]') in + jan* ) days=31 ;; feb* ) days=28 ;; + mar* ) days=31 ;; apr* ) days=30 ;; + may* ) days=31 ;; jun* ) days=30 ;; + jul* ) days=31 ;; aug* ) days=31 ;; + sep* ) days=30 ;; oct* ) days=31 ;; + nov* ) days=30 ;; dec* ) days=31 ;; + * ) echo "$0: Unknown month name $1" >&2; exit 1 + esac + + if [ $2 -lt 1 -o $2 -gt $days ] ; then + return 1 + else + return 0 # all is well + fi +} + +isLeapYear() +{ + # this function returns 0 if a leap year, 1 otherwise + # The formula for checking whether a year is a leap year is: + # 1. years divisible by four are leap years, unless.. + # 2. years also divisible by 100 are not leap years, except... + # 3. years divisible by 400 are leap years + + year=$1 + if [ "$((year % 4))" -ne 0 ] ; then + return 1 # nope, not a leap year + elif [ "$((year % 400))" -eq 0 ] ; then + return 0 # yes, it's a leap year + elif [ "$((year % 100))" -eq 0 ] ; then + return 1 + else + return 0 + fi +} + +validdate() +{ + # expects three values, month, day and year. Returns 0 if success. + + newdate="$(normdate "$@")" + + if [ $? -eq 1 ] ; then + exit 1 # error condition already reported by normdate + fi + + month="$(echo $newdate | cut -d\ -f1)" + day="$(echo $newdate | cut -d\ -f2)" + year="$(echo $newdate | cut -d\ -f3)" + + # Now that we have a normalized date, let's check to see if the + # day value is logical + + if ! exceedsDaysInMonth $month "$2" ; then + if [ "$month" = "Feb" -a $2 -eq 29 ] ; then + if ! isLeapYear $3 ; then + echo "$0: $3 is not a leap year, so Feb doesn't have 29 days" >&2 + exit 1 + fi + else + echo "$0: bad day value: $month doesn't have $2 days" >&2 + exit 1 + fi + fi + return 0 +} + +echon() +{ + echo "$*" | tr -d '\n' +} + +initializeANSI() +{ + esc="" + + blackf="${esc}[30m"; redf="${esc}[31m"; greenf="${esc}[32m" + yellowf="${esc}[33m" bluef="${esc}[34m"; purplef="${esc}[35m" + cyanf="${esc}[36m"; whitef="${esc}[37m" + + blackb="${esc}[40m"; redb="${esc}[41m"; greenb="${esc}[42m" + yellowb="${esc}[43m" blueb="${esc}[44m"; purpleb="${esc}[45m" + cyanb="${esc}[46m"; whiteb="${esc}[47m" + + boldon="${esc}[1m"; boldoff="${esc}[22m" + italicson="${esc}[3m"; italicsoff="${esc}[23m" + ulon="${esc}[4m"; uloff="${esc}[24m" + invon="${esc}[7m"; invoff="${esc}[27m" + + reset="${esc}[0m" +} diff --git a/013-guessword.sh b/013-guessword.sh new file mode 100755 index 0000000..a331e90 --- /dev/null +++ b/013-guessword.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# guessword - a simple word guessing game a la hangman +blank=".................." # must be longer than longest word + +getword() +{ + case $(( $$ % 8 )) in + 0 ) echo "pizzazz" ;; 1 ) echo "delicious" ;; + 2 ) echo "gargantuan" ;; 3 ) echo "minaret" ;; + 4 ) echo "paparazzi" ;; 5 ) echo "delinquent" ;; + 6 ) echo "zither" ;; 7 ) echo "cuisine" ;; + esac +} + +addLetterToWord() +{ + # This function replaces all '.' in template with guess + # then updates remaining to be the number of empty slots left + + letter=1 + while [ $letter -le $letters ] ; do + if [ "$(echo $word | cut -c$letter)" = "$guess" ] ; then + before="$(( $letter - 1 ))"; after="$(( $letter + 1 ))" + if [ $before -gt 0 ] ; then + tbefore="$(echo $template | cut -c1-$before)" + else + tbefore="" + fi + if [ $after -gt $letters ] ; then + template="$tbefore$guess" + else + template="$tbefore$guess$(echo $template | cut -c$after-$letters)" + fi + fi + letter=$(( $letter + 1 )) + done + + remaining=$(echo $template|sed 's/[^\.]//g'|wc -c|sed 's/[[:space:]]//g') + remaining=$(( $remaining - 1 )) # fix to ignore '\n' +} + +word=$(getword) +letters=$(echo $word | wc -c | sed 's/[[:space:]]//g') +letters=$(( $letters - 1 )) # fix character count to ignore \n +template="$(echo $blank | cut -c1-$letters)" +remaining=$letters ; guessed="" ; guesses=0; badguesses=0 + +echo "** You're trying to guess a word with $letters letters **" + +while [ $remaining -gt 0 ] ; do + echo -n "Word is: $template Try what letter next? " ; read guess + guesses=$(( $guesses + 1 )) + if echo $guessed | grep -i $guess > /dev/null ; then + echo "You've already guessed that letter. Try again!" + elif ! echo $word | grep -i $guess > /dev/null ; then + echo "Sorry, the letter \"$guess\" is not in the word." + guessed="$guessed$guess" + badguesses=$(( $badguesses + 1 )) + else + echo "Good going! The letter $guess is in the word!" + addLetterToWord $guess + fi +done + +echo -n "Congratulations! You guessed $word in $guesses guesses" +echo " with $badguesses bad guesses" + +exit 0 diff --git a/013-hilow.sh b/013-hilow.sh new file mode 100755 index 0000000..6346b32 --- /dev/null +++ b/013-hilow.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# hilow - a simple number guessing game + +biggest=100 # maximum number possible +guess=0 # guessed by player +guesses=0 # number of guesses made +number=$(( $$ % $biggest )) # random number, 1 .. $biggest + +while [ $guess -ne $number ] ; do + echo -n "Guess? " ; read guess + if [ "$guess" -lt $number ] ; then + echo "... bigger!" + elif [ "$guess" -gt $number ] ; then + echo "... smaller!" + fi + guesses=$(( $guesses + 1 )) +done + +echo "Right!! Guessed $number in $guesses guesses." + +exit 0 diff --git a/014-nfmt.sh b/014-nfmt.sh new file mode 100755 index 0000000..714e871 --- /dev/null +++ b/014-nfmt.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +# nfmt - A version of fmt, using nroff. Adds two useful flags: -w X for +# line width and -h to enable hyphenation for better fills. + +while getopts "hw:" opt; do + case $opt in + h ) hyph=1 ;; + w ) width="$OPTARG" ;; + esac +done +shift $(($OPTIND - 1)) + +nroff << EOF +.ll ${width:-72} +.na +.hy ${hyph:-0} +.pl 1 +$(cat "$@") +EOF + +exit 0 diff --git a/014-ragged.txt b/014-ragged.txt new file mode 100644 index 0000000..ee6510e --- /dev/null +++ b/014-ragged.txt @@ -0,0 +1,10 @@ +So she sat on, with closed eyes, and half believed herrself in +Wonderland, though she knew she had but to open them again, and +all would change to dull reality--the grass would be only rustling in the wind, and the pool rippling to the waving of the reeds--the +rattling teacups would change to tinkling sheep- bells, and the +Queen's shrill cries to the voice of the shepherd boy--and the +sneeze +of the baby, the shriek of the Gryphon, and all thy other queer noises, would change (she knew) +to the confused clamour of the busy farm-yard--while the lowing of +the cattle in +the distance would take the place of the Mock Turtle's heavy sobs. diff --git a/014-ragged.txt.shp b/014-ragged.txt.shp new file mode 100644 index 0000000..4f246af --- /dev/null +++ b/014-ragged.txt.shp @@ -0,0 +1,10 @@ +So she sat on, with closed eyes, and half believed herrself in +Wonderland, though she knew she had but to open them again, and +all would change to dull reality--the grass would be only rustling in the wind, and the pool reippling to the waving of the reeds--the +rattling teacups would change to tinkling sheep- bells, and the +Queen's shrill cries to the voice of the shepherd boy--and the +sneeze +of the baby, the shriek of the Gryphon, and all thy other queer noises, would change (she knew) +to the confused clamour of the busy farm-yard--while the lowing of +the cattle in +the distance would take the place of the Mock Turtle's heavy sobs. diff --git a/015-newrm.sh b/015-newrm.sh new file mode 100755 index 0000000..a6b7a4c --- /dev/null +++ b/015-newrm.sh @@ -0,0 +1,55 @@ +#!/bin/sh + +# newrm - a replacement for the existing rm command that allows a +# rudimentary unremove capability through utilizing a newly created +# directory in the user's home directory. It can handle directories +# of content as well as individual files, and if the user specifies +# the -f flag, files are NOT archived, but removed. + +# Big Important Warning: you'll want a cron job or similar to keep the +# individual trash directories tamed, otherwise nothing will ever +# actually be deleted on the system and you'll run out of disk space! + + mydir="$HOME/.deleted-files" +realrm="/bin/rm " + copy="/bin/cp -R" + +if [ $# -eq 0 ] ; then # let 'rm' ouptut the usage error + exec $realrm # our shell dies and is replaced by /bin/rm +fi + +# parse all options looking for '-f' + +flags="" + +while getopts "dfiPRrvW" opt +do + case $opt in + f ) exec $realrm "$@" ;; # exec lets us exit this script directly. + * ) flags="$flags -$opt" ;; # other flags are for 'rm', not us + esac +done +shift $(( $OPTIND - 1 )) + +# make sure that the $mydir exists + +if [ ! -d $mydir ] ; then + if [ ! -w $HOME ] ; then + echo "$0 failed: can't create $mydir in $HOME" >&2 + exit 1 + fi + mkdir $mydir + chmod 700 $mydir # a little bit of privacy, please +fi + +for arg +do + newname="$mydir/$(date "+%S.%M.%H.%d.%m").$(basename "$arg")" + if [ -f "$arg" ] ; then + $copy "$arg" "$newname" + elif [ -d "$arg" ] ; then + $copy "$arg" "$newname" + fi +done + +exec $realrm $flags "$@" # our shell is replaced by realrm diff --git a/016-unrm.sh b/016-unrm.sh new file mode 100755 index 0000000..11d6d0c --- /dev/null +++ b/016-unrm.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +# unrm - search the deleted files archive for the specified file. If +# there is more than one match, show a list ordered by timestamp, and +# let the user specify which they want restored. + +# Big Important Warning: you'll want a cron job or similar to keep the +# individual trash directories tamed, otherwise nothing will ever +# actually be deleted on the system and you'll run out of disk space! + +mydir="$HOME/.deleted-files" +realrm="/bin/rm" +move="/bin/mv" + +dest=$(pwd) + +if [ ! -d $mydir ] ; then + echo "$0: No deleted files directory: nothing to unrm" >&2 ; exit 1 +fi + +cd $mydir + +if [ $# -eq 0 ] ; then # no args, just show listing + echo "Contents of your deleted files archive (sorted by date):" +# ls -FC | sed -e 's/[[:digit:]][[:digit:]]\.//g' -e 's/^/ /' + ls -FC | sed -e 's/\([[:digit:]][[:digit:]]\.\)\{5\}//g' \ + -e 's/^/ /' + exit 0 +fi + +# Otherwise we must have a pattern to work with. Let's see if the +# user-specified pattern matches more than one file or directory +# in the archive. + +matches="$(ls *"$1" 2> /dev/null | wc -l)" + +if [ $matches -eq 0 ] ; then + echo "No match for \"$1\" in the deleted file archive." >&2 + exit 1 +fi + +if [ $matches -gt 1 ] ; then + echo "More than one file or directory match in the archive:" + index=1 + for name in $(ls -td *"$1") + do + datetime="$(echo $name | cut -c1-14| \ + awk -F. '{ print $5"/"$4" at "$3":"$2":"$1 }')" + if [ -d $name ] ; then + size="$(ls $name | wc -l | sed 's/[^0-9]//g')" + echo " $index) $1 (contents = ${size} items, deleted = $datetime)" + else + size="$(ls -sdk1 $name | awk '{print $1}')" + echo " $index) $1 (size = ${size}Kb, deleted = $datetime)" + fi + index=$(( $index + 1)) + done + + echo "" + echo -n "Which version of $1 do you want to restore ('0' to quit)? [1] : " + read desired + + if [ ${desired:=1} -ge $index ] ; then + echo "$0: Restore cancelled by user: index value too big." >&2 + exit 1 + fi + + if [ $desired -lt 1 ] ; then + echo "$0: restore cancelled by user." >&2 ; exit 1 + fi + + restore="$(ls -td1 *"$1" | sed -n "${desired}p")" + + if [ -e "$dest/$1" ] ; then + echo "\"$1\" already exists in this directory. Cannot overwrite." >&2 + exit 1 + fi + + echo -n "Restoring file \"$1\" ..." + $move "$restore" "$dest/$1" + echo "done." + + echo -n "Delete the additional copies of this file? [y] " + read answer + + if [ ${answer:=y} = "y" ] ; then + $realrm -rf *"$1" + echo "deleted." + else + echo "additional copies retained." + fi +else + if [ -e "$dest/$1" ] ; then + echo "\"$1\" already exists in this directory. Cannot overwrite." >&2 + exit 1 + fi + + restore="$(ls -d *"$1")" + + echo -n "Restoring file \"$1\" ... " + $move "$restore" "$dest/$1" + echo "done." +fi + +exit 0 diff --git a/017-logrm.sh b/017-logrm.sh new file mode 100755 index 0000000..55e311a --- /dev/null +++ b/017-logrm.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# logrm - log all file deletion requests unless "-s" flag is used + +removelog="/tmp/removelog.log" + +if [ $# -eq 0 ] ; then + echo "Usage: $0 [-s] list of files or directories" >&2 + exit 1 +fi + +if [ "$1" = "-s" ] ; then + # silent operation requested... don't log + shift +else + echo "$(date): ${USER}: $@" >> $removelog +fi + +/bin/rm "$@" + +exit 0 diff --git a/018-formatdir.sh b/018-formatdir.sh new file mode 100755 index 0000000..e30923d --- /dev/null +++ b/018-formatdir.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# formatdir - output a directory listing in a friendly and useful format + +gmk() +{ + # given input in Kb, output in Kb, Mb or Gb for best output format + if [ $1 -ge 1000000 ] ; then + echo "$(scriptbc -p 2 $1 / 1000000)Gb" + elif [ $1 -ge 1000 ] ; then + echo "$(scriptbc -p 2 $1 / 1000)Mb" + else + echo "${1}Kb" + fi +} + +if [ $# -gt 1 ] ; then + echo "Usage: $0 [dirname]" >&2; exit 1 +elif [ $# -eq 1 ] ; then + cd "$@" +fi + +for file in * +do + if [ -d "$file" ] ; then + size=$(ls "$file" | wc -l | sed 's/[^[:digit:]]//g') + if [ $size -eq 1 ] ; then + echo "$file ($size entry)|" + else + echo "$file ($size entries)|" + fi + else + size="$(ls -sk "$file" | awk '{print $1}')" + echo "$file ($(gmk $size))|" + fi +done | \ + sed 's/ /^^^/g' | \ + xargs -n 2 | \ + sed 's/\^\^\^/ /g' | \ + awk -F\| '{ printf "%-39s %-39s\n", $1, $2 }' + +exit 0 diff --git a/019-locate.sh b/019-locate.sh new file mode 100755 index 0000000..a841e4f --- /dev/null +++ b/019-locate.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +# locate - search the locate database for the specified pattern + +locatedb="/var/locate.db" + +exec grep -i "$@" $locatedb diff --git a/019-mklocatedb.sh b/019-mklocatedb.sh new file mode 100755 index 0000000..d159df2 --- /dev/null +++ b/019-mklocatedb.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# mklocatedb - build the locate database using find. Must be root to run this + +locatedb="/var/locate.db" + +if [ "$(whoami)" != "root" ] ; then + echo "Must be root to run this command." >&2 + exit 1 +fi + +find / -print > $locatedb + +exit 0 diff --git a/020-DIR.sh b/020-DIR.sh new file mode 100755 index 0000000..ba664d7 --- /dev/null +++ b/020-DIR.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# DIR - pretend we're the DIR command in DOS and display the contents +# of the specified file, accepting some of the standard DIR flags + +usage() +{ +cat << EOF >&2 + Usage: $0 [DOS flags] directory or directories + Where: + /D sort by columns + /H show help for this shell script + /N show long listing format with filenames on right + /OD sort by oldest to newest + /O-D sort by newest to oldest + /P pause after each screenful of information + /Q show owner of the file + /S recursive listing + /W use wide listing format +EOF + exit 0 +} + +postcmd="" +flags="" + +while [ $# -gt 0 ] +do + case $1 in + /D ) flags="$flags -x" ;; + /H ) usage ;; + /[NQW] ) flags="$flags -l" ;; + /OD ) flags="$flags -rt" ;; + /O-D ) flags="$flags -t" ;; + /P ) postcmd="more" ;; + /S ) flags="$flags -s" ;; + * ) # unknown flag: probably a dir specifier + break; # so let's get outta the while loop + esac + shift # processed flag, let's see if there's another +done + +# done processing flags, now the command itself: + +if [ ! -z "$postcmd" ] ; then + ls $flags "$@" | $postcmd +else + ls $flags "$@" +fi + +exit 0 diff --git a/021-findman.sh b/021-findman.sh new file mode 100755 index 0000000..8d84a5b --- /dev/null +++ b/021-findman.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# findman -- given a pattern and a man section, show all the matches +# for that pattern from within all relevant man pages. + +match1="/tmp/$0.1.$$" +matches="/tmp/$0.$$" +manpagelist="" + +trap "rm -f $match1 $matches" EXIT + +case $# +in + 3 ) section="$1" cmdpat="$2" manpagepat="$3" ;; + 2 ) section="" cmdpat="$1" manpagepat="$2" ;; + * ) echo "Usage: $0 [section] cmdpattern manpagepattern" >&2 + exit 1 +esac + +if ! man -k "$cmdpat" | grep "($section" > $match1 ; then + echo "No matches to pattern \"$cmdpat\". Try something broader?"; exit 1 +fi + +cut -d\( -f1 < $match1 > $matches # command names only +cat /dev/null > $match1 # clear the file... + +for manpage in $(cat $matches) +do + manpagelist="$manpagelist $manpage" + man $manpage | col -b | grep -i $manpagepat | \ + sed "s/^/${manpage}: /" | tee -a $match1 +done + +if [ ! -s $match1 ] ; then +cat << EOF +Command pattern "$cmdpat" had matches, but within those there were no +matches to your man page pattern "$manpagepat" found in that set. +Man pages checked:$manpagelist +EOF +fi + +exit 0 diff --git a/022-timein.sh b/022-timein.sh new file mode 100755 index 0000000..0f8fdf2 --- /dev/null +++ b/022-timein.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +# timein - show the current time in the specified timezone or +# geographic zone. Without any argument, show UTC/GMT. Use +# the word "list" to see a list of known geographic regions +# Note that it's possible to match a zone directory (a region) +# but that only timezone files are valid specifications. + +# Timezone database ref: http://www.twinsun.com/tz/tz-link.htm + +zonedir="/usr/share/zoneinfo" + +if [ ! -d $zonedir ] ; then + echo "No timezone database at $zonedir." >&2 ; exit 1 +fi + +if [ -d "$zonedir/posix" ] ; then + zonedir=$zonedir/posix # modern Linux systems +fi + +if [ $# -eq 0 ] ; then + timezone="UTC" + mixedzone="UTC" +elif [ "$1" = "list" ] ; then + ( echo "All known timezones and regions defined on this system:" + cd $zonedir + find * -type f -print | xargs -n 2 | \ + awk '{ printf " %-38s %-38s\n", $1, $2 }' + ) | more + exit 0 +else + + region="$(dirname $1)" + zone="$(basename $1)" + + # Is it a direct match? If so, we're good to go. Otherwise we need + # to dig around a bit to find things. Start by just counting matches + + matchcnt="$(find $zonedir -name $zone -type f -print | + wc -l | sed 's/[^[:digit:]]//g' )" + + if [ "$matchcnt" -gt 0 ] ; then # at least one file matches + if [ $matchcnt -gt 1 ] ; then # more than one file match + echo "\"$zone\" matches more than one possible time zone record." >&2 + echo "Please use 'list' to see all known regions and timezones" >&2 + exit 1 + fi + match="$(find $zonedir -name $zone -type f -print)" + mixedzone="$zone" + else + # Normalize to first upper, rest of word lowercase for region + zone + mixedregion="$(echo ${region%${region#?}} | tr '[[:lower:]]' '[[:upper:]]')\ +$(echo ${region#?} | tr '[[:upper:]]' '[[:lower:]]')" + mixedzone="$(echo ${zone%${zone#?}} | tr '[[:lower:]]' '[[:upper:]]')\ +$(echo ${zone#?} | tr '[[:upper:]]' '[[:lower:]]')" + + if [ "$mixedregion" != "." ] ; then + # only look for specified zone in specified region + # to let users specify unique matches when there's more than one + # possibility (e.g., "Atlantic") + match="$(find $zonedir/$mixedregion -type f -name $mixedzone -print)" + else + match="$(find $zonedir -name $mixedzone -type f -print)" + fi + + if [ -z "$match" ] ; then # no file matches specified pattern + if [ ! -z $(find $zonedir -name $mixedzone -type d -print) ] ; then + echo \ + "The region \"$1\" has more than one timezone. Please use 'list'" >&2 + else # just not a match at all + echo "Can't find an exact match for \"$1\". Please use 'list'" >&2 + fi + echo "to see all known regions and timezones." >&2 + exit 1 + fi + fi + timezone="$match" +fi + +nicetz=$(echo $timezone | sed "s|$zonedir/||g") # pretty up the output + +echo It\'s $(TZ=$timezone date '+%A, %B %e, %Y, at %l:%M %p') in $nicetz + +exit 0 diff --git a/023-remember.sh b/023-remember.sh new file mode 100755 index 0000000..60014ce --- /dev/null +++ b/023-remember.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# remember - an easy command-line based memory pad +# search the results with 'remindme' + +rememberfile="$HOME/.remember" + +if [ $# -eq 0 ] ; then + echo "Enter note, end with ^D: " + cat - >> $rememberfile +else + echo "$@" >> $rememberfile +fi + +exit 0 diff --git a/023-remindme.sh b/023-remindme.sh new file mode 100755 index 0000000..1252334 --- /dev/null +++ b/023-remindme.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# remindme - search a datafile for matching lines, or show the contents +# of the datafile if no arg is specified + +rememberfile="$HOME/.remember" + +if [ $# -eq 0 ] ; then + more $rememberfile +else + grep -i "$@" $rememberfile | ${PAGER:-more} +fi + +exit 0 diff --git a/024-calc.sh b/024-calc.sh new file mode 100755 index 0000000..8004213 --- /dev/null +++ b/024-calc.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +# calc - a command-line calculator that acts as a front-end to bc + +scale=2 + +show_help() +{ +cat << EOF + In addition to standard math functions, calc also supports + + a % b remainder of a/b + a ^ b exponential: a raised to the b power + s(x) sine of x, x in radians + c(x) cosine of x, x in radians + a(x) arctangent of x, returns radians + l(x) natural log of x + e(x) exponential log of raising e to the x + j(n,x) bessel function of integer order n of x + scale N show N fractional digits (default = 2) +EOF +} + +if [ $# -gt 0 ] ; then + exec scriptbc "$@" +fi + +echo "Calc - a simple calculator. Use 'help' for help, 'quit' to quit." + +echo -n "calc> " + +while read command args +do + case $command + in + quit|exit) exit 0 ;; + help|\?) show_help ;; + scale) scale=$args ;; + *) scriptbc -p $scale "$command" "$args" ;; + esac + + echo -n "calc> " +done + +echo "" + +exit 0 diff --git a/025-checkspelling.sh b/025-checkspelling.sh new file mode 100755 index 0000000..4c955b3 --- /dev/null +++ b/025-checkspelling.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# checkspelling - check the spelling of a word + +spell="ispell -l" # if you have ispell installed instead + # if not, just define spell=spell or + # equivalent. + +if [ $# -lt 1 ] ; then + echo "Usage: $0 word or words" >&2 + exit 1 +fi + +for word +do + if [ -z $(echo $word | $spell) ] ; then + echo "$word: spelled correctly." + else + echo "$word: misspelled." + fi +done + +exit 0 diff --git a/026-shpell.sh b/026-shpell.sh new file mode 100755 index 0000000..e879da3 --- /dev/null +++ b/026-shpell.sh @@ -0,0 +1,100 @@ +#!/bin/sh + +# shpell - An interactive spell checking program that lets you step +# through all known spelling errors in a document, indicate which +# ones you'd like to fix (and the correction), then applies them +# all to the file. The original version of the file is saved with a +# .shp suffix and the new version replaces the old. +# +# Note that you need a standard 'spell' command for this to work, which +# might involve you installing aspell, ispell, or pspell on your system. + +tempfile="/tmp/$0.$$" +changerequests="/tmp/$0.$$.sed" +spell="ispell -l" # modify as neede for your own spell + +trap "rm -f $tempfile $changerequests" EXIT HUP INT QUIT TERM + +# include the ansi color sequence definitions + +. 012-library.sh +initializeANSI + +getfix() +{ + # asks for a correction. Keeps track of nesting, + # and only level 1 can output "replacing" message. + + word=$1 + filename=$2 + misspelled=1 + + while [ $misspelled -eq 1 ] + do + echo ""; echo "${boldon}Misspelled word ${word}:${boldoff}" + grep -n $word $filename | + sed -e 's/^/ /' -e "s/$word/$boldon$word$boldoff/g" + echo -n "i)gnore, q)uit, or type replacement: " + read fix + if [ "$fix" = "q" -o "$fix" = "quit" ] ; then + echo "Exiting without applying any fixes."; exit 0 + elif [ "${fix%${fix#?}}" = "!" ] ; then + misspelled=0 # once we see spaces, we stop checking + echo "s/$word/${fix#?}/g" >> $changerequests + elif [ "$fix" = "i" -o -z "$fix" ] ; then + misspelled=0 + else + if [ ! -z "$(echo $fix | sed 's/[^ ]//g')" ] ; then + misspelled=0 # once we see spaces, we stop checking + echo "s/$word/$fix/g" >> $changerequests + else + # it's a single word replacement, let's spell check that too + if [ ! -z "$(echo $fix | $spell)" ] ; then + echo "" + echo "*** Your suggested replacement $fix is misspelled." + echo "*** Prefix the word with '!' to force acceptance." + else + misspelled=0 # suggested replacement word is acceptable + echo "s/$word/$fix/g" >> $changerequests + fi + fi + fi + done +} + +### beginning of actual script body + +if [ $# -lt 1 ] ; then + echo "Usage: $0 filename" >&2 ; exit 1 +fi + +if [ ! -r $1 ] ; then + echo "$0: Cannot read file $1 to check spelling" >&2 ; exit 1 +fi + +# note that the following invocation fills $tempfile along the way + +errors="$($spell < $1 | tee $tempfile | wc -l | sed 's/[^[:digit:]]//g')" + +if [ $errors -eq 0 ] ; then + echo "There are no spelling errors in $1."; exit 0 +fi + +echo "We need to fix $errors misspellings in the document. Remember that the" +echo "default answer to the spelling prompt is 'ignore', if you're lazy." + +touch $changerequests + +for word in $(cat $tempfile) +do + getfix $word $1 1 +done + +if [ $(wc -l < $changerequests) -gt 0 ] ; then + sed -f $changerequests $1 > $1.new + mv $1 $1.shp + mv $1.new $1 + echo Done. Made $(wc -l < $changerequests) changes. +fi + +exit 0 diff --git a/027-spelldict.sh b/027-spelldict.sh new file mode 100755 index 0000000..60e659e --- /dev/null +++ b/027-spelldict.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# spelldict - use the 'aspell' feature and some filtering to allow easy +# command-line spell checking of a given input (file) + +okaywords="$HOME/.okaywords" +tempout="/tmp/spell.tmp.$$" +spell="virtual aspell" # tweak as needed + +trap "/bin/rm -f $tempout" EXIT + +if [ -z "$1" ] ; then + echo "Usage: spell file|URL" >&2; exit 1 +elif [ ! -f $okaywords ] ; then + echo "No personal dictionary found. Create one and rerun this command" >&2 + echo "Your dictionary file: $okaywords" >&2 + exit 1 +fi + +for filename +do + $spell -a < $filename | \ + grep -v '@(#)' | sed "s/\'//g" | \ + awk '{ if (length($0) > 15 && length($2) > 2) print $2 }' | \ + grep -vif $okaywords | \ + grep '[[:lower:]]' | grep -v '[[:digit:]]' | sort -u | \ + sed 's/^/ /' > $tempout + + if [ -s $tempout ] ; then + sed "s/^/${filename}: /" $tempout + fi +done + +exit 0 diff --git a/028-convertatemp.sh b/028-convertatemp.sh new file mode 100755 index 0000000..fe5df55 --- /dev/null +++ b/028-convertatemp.sh @@ -0,0 +1,50 @@ +#!/bin/sh + +# CONVERTATEMP - Temperature conversion script that lets the user enter +# a temperature in any of Fahrenheit, Celsius or Kelvin and receive the +# equivalent temperature in the other two units as the output. + +if uname | grep 'SunOS'>/dev/null ; then + echo "Yep, SunOS, let\'s fix this baby" + PATH="/usr/xpg4/bin:$PATH" +fi + +if [ $# -eq 0 ] ; then + cat << EOF >&2 +Usage: $0 temperature[F|C|K] +where the suffix: + F indicates input is in Fahrenheit (default) + C indicates input is in Celsius + K indicates input is in Kelvin +EOF + exit 1 +fi + +unit="$(echo $1|sed -e 's/[-[[:digit:]]*//g' | tr '[:lower:]' '[:upper:]' )" +temp="$(echo $1|sed -e 's/[^-[[:digit:]]*//g')" + +case ${unit:=F} +in + F ) # Fahrenheit to Celsius formula: Tc = (F -32 ) / 1.8 + farn="$temp" + cels="$(echo "scale=2;($farn - 32) / 1.8" | bc)" + kelv="$(echo "scale=2;$cels + 273.15" | bc)" + ;; + + C ) # Celsius to Fahrenheit formula: Tf = (9/5)*Tc+32 + cels=$temp + kelv="$(echo "scale=2;$cels + 273.15" | bc)" + farn="$(echo "scale=2;((9/5) * $cels) + 32" | bc)" + ;; + + K ) # Celsius = Kelvin + 273.15, then use Cels -> Fahr formula + kelv=$temp + cels="$(echo "scale=2; $kelv - 273.15" | bc)" + farn="$(echo "scale=2; ((9/5) * $cels) + 32" | bc)" +esac + +echo "Fahrenheit = $farn" +echo "Celsius = $cels" +echo "Kelvin = $kelv" + +exit 0 diff --git a/029-loancalc.sh b/029-loancalc.sh new file mode 100755 index 0000000..0182376 --- /dev/null +++ b/029-loancalc.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# mortgage - given a principal loan amount, interest rate, and +# duration of loan (years), calculate the per-payment amount. + +# formula is: M = P * ( J / (1 - (1 + J) ** -N)) +# where P = principal, J = monthly interest rate, N = duration (months) +# +# users typically enter P, I (annual interest rate) and L (length, years) + +. 012-library.sh + +if [ $# -ne 3 ] ; then + echo "Usage: $0 principal interest loan-duration-years" >&2 + exit 1 +fi + +P=$1 I=$2 L=$3 +J="$(scriptbc -p 8 $I / \( 12 \* 100 \) )" +N="$(( $L * 12 ))" +M="$(scriptbc -p 8 $P \* \( $J / \(1 - \(1 + $J\) \^ -$N\) \) )" + +# now a little prettying up of the value: + +dollars="$(echo $M | cut -d. -f1)" +cents="$(echo $M | cut -d. -f2 | cut -c1-2)" + +cat << EOF +A $L year loan at $I% interest with a principal amount of $(nicenumber $P 1 ) +results in a payment of \$$dollars.$cents each month for the duration of +the loan ($N payments). +EOF + +exit 0 diff --git a/030-addagenda.sh b/030-addagenda.sh new file mode 100755 index 0000000..bc13a5f --- /dev/null +++ b/030-addagenda.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +# addagenda - prompt the user to add a new event for the Agenda script + +agendafile="$HOME/.agenda" + +isDayName() +{ + # return = 0 if all is well, 1 on error + + case $(echo $1 | tr '[[:upper:]]' '[[:lower:]]') in + sun*|mon*|tue*|wed*|thu*|fri*|sat*) retval=0 ;; + * ) retval=1 ;; + esac + return $retval +} + +isMonthName() +{ + case $(echo $1 | tr '[[:upper:]]' '[[:lower:]]') in + jan*|feb*|mar*|apr*|may*|jun*) return 0 ;; + jul*|aug*|sep*|oct*|nov*|dec*) return 0 ;; + * ) return 1 ;; + esac +} + +normalize() +{ + # return string with first char uppercase, next two lowercase + echo -n $1 | cut -c1 | tr '[[:lower:]]' '[[:upper:]]' + echo $1 | cut -c2-3| tr '[[:upper:]]' '[[:lower:]]' +} + +if [ ! -w $HOME ] ; then + echo "$0: cannot write in your home directory ($HOME)" >&2 + exit 1 +fi + +echo "Agenda: The Unix Reminder Service" +echo -n "Date of event (day mon, day month year, or dayname): " +read word1 word2 word3 junk + +if isDayName $word1 ; then + if [ ! -z "$word2" ] ; then + echo "Bad dayname format: just specify the day name by itself." >&2 + exit 1 + fi + date="$(normalize $word1)" + +else + + if [ -z "$word2" ] ; then + echo "Bad dayname format: unknown day name specified" >&2 + exit 1 + fi + + if [ ! -z "$(echo $word1|sed 's/[[:digit:]]//g')" ] ; then + echo "Bad date format: please specify day first, by day number" >&2 + exit 1 + fi + + if [ "$word1" -lt 1 -o "$word1" -gt 31 ] ; then + echo "Bad date format: day number can only be in range 1-31" >&2 + exit 1 + fi + + if ! isMonthName $word2 ; then + echo "Bad date format: unknown month name specified." >&2 + exit 1 + fi + + word2="$(normalize $word2)" + + if [ -z "$word3" ] ; then + date="$word1$word2" + else + if [ ! -z "$(echo $word3|sed 's/[[:digit:]]//g')" ] ; then + echo "Bad date format: third field should be year." >&2 + exit 1 + elif [ $word3 -lt 2000 -o $word3 -gt 2500 ] ; then + echo "Bad date format: year value should be 2000-2500" >&2 + exit 1 + fi + date="$word1$word2$word3" + fi +fi + +echo -n "One line description: " +read description + +# ready to write to datafile + +echo "$(echo $date|sed 's/ //g')|$description" >> $agendafile + +exit 0 diff --git a/030-agenda.sh b/030-agenda.sh new file mode 100755 index 0000000..b31fd50 --- /dev/null +++ b/030-agenda.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# agenda - scan through the user's .agenda file to see if there +# are any matches for the current or next day + +agendafile="$HOME/.agenda" + +checkDate() +{ + # create the possible default values that'll match today + weekday=$1 day=$2 month=$3 year=$4 + format1="$weekday" format2="$day$month" format3="$day$month$year" + + # and step through the file comparing dates... + + IFS="|" # The reads will naturally split at the IFS + + echo "On the Agenda for today:" + + while read date description ; do + if [ "$date" = "$format1" -o "$date" = "$format2" -o "$date" = "$format3" ] + then + echo " $description" + fi + done < $agendafile +} + +if [ ! -e $agendafile ] ; then + echo "$0: You don't seem to have an .agenda file. " >&2 + echo "To remedy this, please use 'addagenda' to add events" >&2 + exit 1 +fi + +# now let's get today's date... + +eval $(date "+weekday=\"%a\" month=\"%b\" day=\"%e\" year=\"%G\"") + +day="$(echo $day|sed 's/ //g')" # remove possible leading space + +checkDate $weekday $day $month $year + +exit 0 diff --git a/031-numberlines.sh b/031-numberlines.sh new file mode 100755 index 0000000..fce601a --- /dev/null +++ b/031-numberlines.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# numberlines - a simple alternative to cat -n, etc + +for filename +do + linecount="1" + (while read line + do + echo "${linecount}: $line" + linecount="$(( $linecount + 1 ))" + done) < $filename +done + +exit 0 diff --git a/032-showfile.sh b/032-showfile.sh new file mode 100755 index 0000000..cf04f81 --- /dev/null +++ b/032-showfile.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# showfile - show the contents of a file, including additional useful info + +width=72 + +for input +do + lines="$(wc -l < $input | sed 's/ //g')" + chars="$(wc -c < $input | sed 's/ //g')" + owner="$(ls -ld $input | awk '{print $3}')" + echo "-----------------------------------------------------------------" + echo "File $input ($lines lines, $chars characters, owned by $owner):" + echo "-----------------------------------------------------------------" + while read line + do + if [ ${#line} -gt $width ] ; then + echo "$line" | fmt | sed -e '1s/^/ /' -e '2,$s/^/+ /' + else + echo " $line" + fi + done < $input + + echo "-----------------------------------------------------------------" + +done | more + +exit 0 diff --git a/033-toolong.sh b/033-toolong.sh new file mode 100755 index 0000000..fa82006 --- /dev/null +++ b/033-toolong.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# toolong - only feed the fmt command those lines in the input stream +# that are longer than the specified length + +width=72 + +if [ ! -r "$1" ] ; then + echo "Usage: $0 filename" >&2; exit 1 +fi + +while read input + do + if [ ${#input} -gt $width ] ; then + echo "$input" | fmt + else + echo "$input" + fi + done < $1 + +exit 0 + diff --git a/034-quota.sh b/034-quota.sh new file mode 100755 index 0000000..26d8ed6 --- /dev/null +++ b/034-quota.sh @@ -0,0 +1,25 @@ +#!/bin/sh +# newquota - a front-end to quota that works with fullword flags a la GNU + +# quota has three possible flags: -g, -v and -q and in this script +# we allow them to be '--group' '--verbose' and '--quiet' too: + +flags="" +realquota="/usr/bin/quota" + +while [ $# -gt 0 ] +do + echo checking flag $1 + case $1 + in + --help ) echo "Usage: $0 [--group --verbose --quiet -gvq]" >&2 + exit 1 ;; + --group | -group) flags="$flags -g"; shift ;; + --verbose | -verbose) flags="$flags -v"; shift ;; + --quiet | -quiet) flags="$flags -q"; shift ;; + -- ) shift; break ;; + * ) break; # done with 'while' loop! + esac +done + +exec $realquota $flags "$@" diff --git a/035-mysftp.sh b/035-mysftp.sh new file mode 100755 index 0000000..5be1db8 --- /dev/null +++ b/035-mysftp.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# mysftp - make sftp start up more like ftp + +echo -n "User account: " +read account + +if [ -z "$account" ] ; then + exit 0; # changed their mind, presumably +fi + +if [ -z "$1" ] ; then + echo -n "Remote host: " + read host + if [ -z $host ] ; then + exit 0 + fi +else + host=$1 +fi + +# echo sftp -C $account@$host + +exec /usr/bin/sftp -C $account@$host diff --git a/036-cgrep.sh b/036-cgrep.sh new file mode 100755 index 0000000..8336971 --- /dev/null +++ b/036-cgrep.sh @@ -0,0 +1,68 @@ +#!/bin/sh + +# cgrep - grep with context display and highlighted pattern matches + +context=0 +esc="" +bOn="${esc}[1m" bOff="${esc}[22m" +sedscript="/tmp/cgrep.sed.$$" +tempout="/tmp/cgrep.$$" + +showMatches() +{ + matches=0 + + echo "s/$pattern/${bOn}$pattern${bOff}/g" > $sedscript + + for lineno in $(grep -n "$pattern" $1 | cut -d: -f1) + do + if [ $context -gt 0 ] ; then + prev="$(( $lineno - $context ))" + if [ "$(echo $prev | cut -c1)" = "-" ] ; then + prev="0" + fi + next="$(( $lineno + $context ))" + + if [ $matches -gt 0 ] ; then + echo "${prev}i\\" >> $sedscript + echo "----" >> $sedscript + fi + echo "${prev},${next}p" >> $sedscript + else + echo "${lineno}p" >> $sedscript + fi + matches="$(( $matches + 1 ))" + done + + if [ $matches -gt 0 ] ; then + sed -n -f $sedscript $1 | uniq | more + fi +} + +trap "/bin/rm -f $tempout $sedscript" EXIT + +if [ -z "$1" ] ; then + echo "Usage: $0 [-c X] pattern {filename}" >&2; exit 0 +fi + +if [ "$1" = "-c" ] ; then + context="$2" + shift; shift +elif [ "$(echo $1|cut -c1-2)" = "-c" ] ; then + context="$(echo $1 | cut -c3-)" + shift +fi + +pattern="$1"; shift + +if [ $# -gt 0 ] ; then + for filename ; do + echo "----- $filename -----" + showMatches $filename + done +else + cat - > $tempout # save stream to a temp file + showMatches $tempout +fi + +exit 0 diff --git a/037-zcat.sh b/037-zcat.sh new file mode 100755 index 0000000..372dda3 --- /dev/null +++ b/037-zcat.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +# zcat, zmore, and zgrep - this script should be either symbolically +# linked or hard linked to all three names - it allows users to work +# with compressed files transparently. + + Z="compress"; unZ="uncompress" ; Zlist="" +gz="gzip" ; ungz="gunzip" ; gzlist="" +bz="bzip2" ; unbz="bunzip2" ; bzlist="" + +# First step is to try and isolate the filenames in the command line +# we'll do this lazily by stepping through each argument testing to +# see if it's a filename or not. If it is, and it has a compression +# suffix, we'll uncompress the file, rewrite the filename, and proceed. +# When done, we'll recompress everything that was uncompressed. + +for arg +do + if [ -f "$arg" ] ; then + case $arg in + *.Z) $unZ "$arg" + arg="$(echo $arg | sed 's/\.Z$//')" + Zlist="$Zlist \"$arg\"" + ;; + + *.gz) $ungz "$arg" + arg="$(echo $arg | sed 's/\.gz$//')" + gzlist="$gzlist \"$arg\"" + ;; + + *.bz2) $unbz "$arg" + arg="$(echo $arg | sed 's/\.bz2$//')" + bzlist="$bzlist \"$arg\"" + ;; + + esac + fi + newargs="${newargs:-""} \"$arg\"" +done + +case $0 in + *zcat* ) eval cat $newargs ;; + *zmore* ) eval more $newargs ;; + *zgrep* ) eval grep $newargs ;; + * ) echo "$0: unknown base name. Can't proceed." >&2; exit 1 +esac + +# now recompress everything + +if [ ! -z "$Zlist" ] ; then + eval $Z $Zlist +fi +if [ ! -z "$gzlist" ] ; then + eval $gz $gzlist +fi +if [ ! -z "$bzlist" ] ; then + eval $bz $bzlist +fi + +# and done + +exit 0 diff --git a/038-bestcompress.sh b/038-bestcompress.sh new file mode 100755 index 0000000..0d8416a --- /dev/null +++ b/038-bestcompress.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +# bestcompress - given a file, try compressing it with all the available +# compression tools and keep the compressed file that's smallest, reporting +# the result to the user. If '-a' isn't specified, it skips compressed +# files in the input stream. + +Z="compress" +gz="gzip" +bz="bzip2" +Zout="/tmp/bestcompress.$$.Z" +gzout="/tmp/bestcompress.$$.gz" +bzout="/tmp/bestcompress.$$.bz" +skipcompressed=1 + +if [ "$1" = "-a" ] ; then + skipcompressed=0 ; shift +fi + +if [ $# -eq 0 ]; then + echo "Usage: $0 [-a] file or files to optimally compress" >&2; exit 1 +fi + +trap "/bin/rm -f $Zout $gzout $bzout" EXIT + +for name +do + if [ ! -f "$name" ] ; then + echo "$0: file $name not found. Skipped." >&2 + continue + fi + + if [ "$(echo $name | egrep '(\.Z$|\.gz$|\.bz2$)')" != "" ] ; then + if [ $skipcompressed -eq 1 ] ; then + echo "Skipped file ${name}: it's already compressed." + continue + else + echo "Warning: Trying to double-compress $name" + fi + fi + + $Z < "$name" > $Zout & + $gz < "$name" > $gzout & + $bz < "$name" > $bzout & + + wait # run compressions in parallel for speed. Wait until all are done + + smallest="$(ls -l "$name" $Zout $gzout $bzout | \ + awk '{print $5"="NR}' | sort -n | cut -d= -f2 | head -1)" + + case "$smallest" in + 1 ) echo "No space savings by compressing $name. Left as-is." + ;; + 2 ) echo Best compression is with compress. File renamed ${name}.Z + mv $Zout "${name}.Z" ; rm -f "$name" + ;; + 3 ) echo Best compression is with gzip. File renamed ${name}.gz + mv $gzout "${name}.gz" ; rm -f "$name" + ;; + 4 ) echo Best compression is with bzip2. File renamed ${name}.bz2 + mv $bzout "${name}.bz2" ; rm -f "$name" + esac + +done + +exit 0 diff --git a/039-fquota.sh b/039-fquota.sh new file mode 100755 index 0000000..4184157 --- /dev/null +++ b/039-fquota.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# FQUOTA - Disk quota analysis tool for Unix. +# Assumes that all user accounts are >= UID 100. + +MAXDISKUSAGE=20 + +for name in $(cut -d: -f1,3 /etc/passwd | awk -F: '$2 > 99 { print $1 }') +do + echo -n "User $name exceeds disk quota. Disk usage is: " + + find / /usr /var /Users -user $name -xdev -type f -ls | \ + awk '{ sum += $7 } END { print sum / (1024*1024) " Mbytes" }' + +done | awk "\$9 > $MAXDISKUSAGE { print \$0 }" + +exit 0 + diff --git a/040-diskhogs.sh b/040-diskhogs.sh new file mode 100755 index 0000000..bfed253 --- /dev/null +++ b/040-diskhogs.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +# DISKHOGS - Disk quota analysis tool for Unix, assumes all user +# accounts are >= UID 100. Emails message to each violating user +# and reports a summary to the screen + +MAXDISKUSAGE=20 +violators="/tmp/diskhogs0.$$" + +trap "/bin/rm -f $violators" 0 + +for name in $(cut -d: -f1,3 /etc/passwd | awk -F: '$2 > 99 { print $1 }') +do + echo -n "$name " + + find / /usr /var /Users -user $name -xdev -type f -ls | \ + awk '{ sum += $7 } END { print sum / (1024*1024) }' + +done | awk "\$2 > $MAXDISKUSAGE { print \$0 }" > $violators + +if [ ! -s $violators ] ; then + echo "No users exceed the disk quota of ${MAXDISKUSAGE}MB" + cat $violators + exit 0 +fi + +while read account usage ; do + + cat << EOF | fmt | mail -s "Warning: $account Exceeds Quota" $account +Your disk usage is ${usage}MB but you have only been allocated +${MAXDISKUSAGE}MB. This means that either you need to delete some of +your files, compress your files (see 'gzip' or 'bzip2' for powerful and +easy-to-use compression programs), or talk with us about increasing +your disk allocation. + +Thanks for your cooperation on this matter. + +Dave Taylor @ x554 +EOF + + echo "Account $account has $usage MB of disk space. User notified." + +done < $violators + +exit 0 diff --git a/041-diskspace.sh b/041-diskspace.sh new file mode 100755 index 0000000..3a9997e --- /dev/null +++ b/041-diskspace.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# diskspace - summarize available disk space and present in a logical +# and readable fashion + +tempfile="/tmp/available.$$" + +trap "rm -f $tempfile" EXIT + +cat << 'EOF' > $tempfile + { sum += $4 } +END { mb = sum / 1024 + gb = mb / 1024 + printf "%.0f MB (%.2fGB) of available disk space\n", mb, gb + } +EOF + +df -k | awk -f $tempfile + +exit 0 diff --git a/042-newdf.sh b/042-newdf.sh new file mode 100755 index 0000000..2ba66f2 --- /dev/null +++ b/042-newdf.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +# newdf - a friendlier version of df + +sedscript="/tmp/newdf.$$" + +trap "rm -f $sedscript" EXIT + +cat << 'EOF' > $sedscript +function showunit(size) +{ mb = size / 1024; prettymb=(int(mb * 100)) / 100; + gb = mb / 1024; prettygb=(int(gb * 100)) / 100; + + if ( substr(size,1,1) !~ "[0-9]" || + substr(size,2,1) !~ "[0-9]" ) { return size } + else if ( mb < 1) { return size "K" } + else if ( gb < 1) { return prettymb "M" } + else { return prettygb "G" } +} + +BEGIN { + printf "%-27s %7s %7s %7s %8s %-s\n", + "Filesystem", "Size", "Used", "Avail", "Capacity", "Mounted" +} + +!/Filesystem/ { + + size=showunit($2); + used=showunit($3); + avail=showunit($4); + + printf "%-27s %7s %7s %7s %8s %-s\n", + $1, size, used, avail, $5, $6 +} +EOF + +df -k | awk -f $sedscript + +exit 0 diff --git a/043-mkslocate.sh b/043-mkslocate.sh new file mode 100755 index 0000000..0662982 --- /dev/null +++ b/043-mkslocate.sh @@ -0,0 +1,48 @@ +#!/bin/sh + +# mkslocatedb - build the central, public locate database as user nobody, +# and simultaneously step through each home directory to find those +# that contain a .slocatedb file. If found, an additional, private +# version of the locate database will be created for that user. + +locatedb="/var/locate.db" +slocatedb=".slocatedb" + +if [ "$(whoami)" != "root" ] ; then + echo "$0: Error: You must be root to run this command." >&2 + exit 1 +fi + +if [ "$(grep '^nobody:' /etc/passwd)" = "" ] ; then + echo "$0: Error: you must have an account for user 'nobody'" >&2 + echo "to create the default slocate database." >&2; exit 1 +fi + +cd / # sidestep post-su pwd permission problems + +# first, create or update the public database +su -fm nobody -c "find / -print" > $locatedb 2>/dev/null +echo "building default slocate database (user = nobody)" +echo ... result is $(wc -l < $locatedb) lines long. + + +# now step through the user accounts on the system to see who has +# a $slocatedb file in their home directory.... + +for account in $(cut -d: -f1 /etc/passwd) +do + homedir="$(grep "^${account}:" /etc/passwd | cut -d: -f6)" + + if [ "$homedir" = "/" ] ; then + continue # refuse to build one for root dir + elif [ -e $homedir/$slocatedb ] ; then + echo "building slocate database for user $account" + su -fm $account -c "find / -print" > $homedir/$slocatedb \ + 2>/dev/null + chmod 600 $homedir/$slocatedb + chown $account $homedir/$slocatedb + echo ... result is $(wc -l < $homedir/$slocatedb) lines long. + fi +done + +exit 0 diff --git a/043-slocate.sh b/043-slocate.sh new file mode 100755 index 0000000..8d9e10c --- /dev/null +++ b/043-slocate.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# slocate - Try to search the user's secure locate database for the +# specified pattern. If none exists, output a warning and create +# one. If secure locate db is empty, use system one instead. + +locatedb="/var/locate.db" +slocatedb="$HOME/.slocatedb" + +if [ "$1" = "--explain" ] ; then + cat << "EOF" >&2 +Warning: Secure locate keeps a private database for each user, and your +database hasn't yet been created. Until it is (probably late tonight) +I'll just use the public locate database, which will show you all +publicly accessible matches, rather than those explicitly available to +account ${USER:-$LOGNAME}. +EOF + if [ "$1" = "--explain" ] ; then + exit 0 + fi + + # before we go, create a .slocatedb so that cron will fill it + # the next time the mkslocatedb script is run + + touch $slocatedb # mkslocatedb will build it next time through + chmod 600 $slocatedb # start on the right foot with permissions + +elif [ -s $slocatedb ] ; then + locatedb=$slocatedb +else + echo "Warning: using public database. Use \"$0 --explain\" for details." >&2 +fi + +if [ -z "$1" ] ; then + echo "Usage: $0 pattern" >&2; exit 1 +fi + +exec grep -i "$1" $locatedb diff --git a/044-adduser.sh b/044-adduser.sh new file mode 100755 index 0000000..42ed4b5 --- /dev/null +++ b/044-adduser.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +# ADDUSER - add a new user to the system, including building their +# home directory, copying in default config data, etc. +# For a standard Unix/Linux system, not Mac OS X + +pwfile="/etc/passwd" shadowfile="/etc/shadow" +gfile="/etc/group" +hdir="/home" + +if [ "$(whoami)" != "root" ] ; then + echo "Error: You must be root to run this command." >&2 + exit 1 +fi + +echo "Add new user account to $(hostname)" +echo -n "login: " ; read login + +# adjust '5000' to match the top end of your user account namespace +# because some system accounts have uid's like 65535 and similar. + +uid="$(awk -F: '{ if (big < $3 && $3 < 5000) big=$3 } END { print big + 1 }' $pwfile)" +homedir=$hdir/$login + +# we are giving each user their own group, so gid=uid +gid=$uid + +echo -n "full name: " ; read fullname +echo -n "shell: " ; read shell + +echo "Setting up account $login for $fullname..." + +echo ${login}:x:${uid}:${gid}:${fullname}:${homedir}:$shell >> $pwfile +echo ${login}:*:11647:0:99999:7::: >> $shadowfile + +echo "${login}:x:${gid}:$login" >> $gfile + +mkdir $homedir +cp -R /etc/skel/.[a-zA-Z]* $homedir +chmod 755 $homedir +find $homedir -print | xargs chown ${login}:$login + +# setting an initial password +passwd $login + +exit 0 diff --git a/045-suspenduser.sh b/045-suspenduser.sh new file mode 100755 index 0000000..6bbc311 --- /dev/null +++ b/045-suspenduser.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +## Suspend - suspend a user account for the indefinite future + +homedir="/home" # home directory for users +secs=10 # seconds before user is logged out + +if [ -z $1 ] ; then + echo "Usage: $0 account" >&2 ; exit 1 +elif [ "$(whoami)" != "root" ] ; then + echo "Error. You must be 'root' to run this command." >&2; exit 1 +fi + +echo "Please change account $1 password to something new." +passwd $1 + +# Now, let's see if they're logged in, and if so, boot 'em + +if [ ! -z $(who | grep $1) ] ; then + + tty="$(who | grep $1 | tail -1 | awk '{print $2}')" + + cat << "EOF" > /dev/$tty + +************************************************************* +URGENT NOTICE FROM THE ADMINISTRATOR: + +This account is being suspended at the request of management. +You are going to be logged out in $secs seconds. Please immediately +shut down any processes you have running and log out. + +If you have any questions, please contact your supervisor or +John Doe, Director of Information Technology. +************************************************************* +EOF + + echo "(Warned $1, now sleeping $secs seconds)" + + sleep $secs + + killall -s HUP -u $1 # send hangup sig to their processes + sleep 1 # give it a second... + killall -s KILL -u $1 # and kill anything left + + echo "$(date): $1 was logged in. Just logged them out." +fi + +# Finally, let's close off their home directory from prying eyes: + +chmod 000 $homedir/$1 + +echo "Account $1 has been suspended." + +exit 0 diff --git a/046-deleteuser.sh b/046-deleteuser.sh new file mode 100755 index 0000000..af41352 --- /dev/null +++ b/046-deleteuser.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +## Delete - delete a user account without a trace... +# Not for use with Mac OS X + +homedir="/home" +pwfile="/etc/passwd" shadow="/etc/shadow" +newpwfile="/etc/passwd.new" newshadow="/etc/shadow.new" +suspend="echo suspending " +locker="/etc/passwd.lock" + +if [ -z $1 ] ; then + echo "Usage: $0 account" >&2; exit 1 +elif [ "$(whoami)" != "root" ] ; then + echo "Error: you must be 'root' to run this command.">&2; exit 1 +fi + +# $suspend $1 # suspend their account while we do the dirty work + +uid="$(grep -E "^${1}:" $pwfile | cut -d: -f3)" + +if [ -z $uid ] ; then + echo "Error: no account $1 found in $pwfile" >&2; exit 1 +fi + +# remove from the password and shadow files +grep -vE "^${1}:" $pwfile > $newpwfile +grep -vE "^${1}:" $shadow > $newshadow + +lockcmd="$(which lockfile)" # find it in the path +if [ ! -z $lockcmd ] ; then # let's use the system lockfile + eval $lockcmd -r 15 $locker +else # ulp, let's do it ourselves + while [ -e $locker ] ; do + echo "waiting for the password file" ; sleep 1 + done + touch $locker # created a file-based lock +fi + +mv $newpwfile $pwfile +mv $newshadow $shadow +rm -f $locker # click! unlocked again + +chmod 644 $pwfile +chmod 400 $shadow + +# now remove home directory and list anything left... +rm -rf $homedir/$1 + +echo "Files still left to remove (if any):" +find / -uid $uid -print 2>/dev/null | sed 's/^/ /' + +echo "" +echo "Account $1 (uid $uid) has been deleted, and their home directory " +echo "($homedir/$1) has been removed." + +exit 0 diff --git a/047-validator.sh b/047-validator.sh new file mode 100755 index 0000000..d0026e2 --- /dev/null +++ b/047-validator.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# VALIDATOR - Checks to ensure that all environment variables are valid +# looks at SHELL, HOME, PATH, EDITOR, MAIL, and PAGER + +errors=0 + +in_path() +{ + # given a command and the PATH, try to find the command. Returns + # 1 if found, 0 if not. Note that this temporarily modifies the + # the IFS input field seperator, but restores it upon completion. + cmd=$1 path=$2 retval=0 + + oldIFS=$IFS; IFS=":" + + for directory in $path + do + if [ -x $directory/$cmd ] ; then + retval=1 # if we're here, we found $cmd in $directory + fi + done + IFS=$oldIFS + return $retval +} + +validate() +{ + varname=$1 varvalue=$2 + + if [ ! -z $varvalue ] ; then + if [ "${varvalue%${varvalue#?}}" = "/" ] ; then + if [ ! -x $varvalue ] ; then + echo "** $varname set to $varvalue, but I cannot find executable." + errors=$(( $errors + 1 )) + fi + else + if in_path $varvalue $PATH ; then + echo "** $varname set to $varvalue, but I cannot find it in PATH." + errors=$(( $errors + 1 )) + fi + fi + fi +} + +####### Beginning of actual shell script ####### + +if [ ! -x ${SHELL:?"Cannot proceed without SHELL being defined."} ] ; then + echo "** SHELL set to $SHELL, but I cannot find that executable." + errors=$(( $errors + 1 )) +fi + +if [ ! -d ${HOME:?"You need to have your HOME set to your home directory"} ] +then + echo "** HOME set to $HOME, but it's not a directory." + errors=$(( $errors + 1 )) +fi + +# Our first interesting test: are all the paths in PATH valid? + +oldIFS=$IFS; IFS=":" # IFS is the field separator. We'll change to ':' + +for directory in $PATH +do + if [ ! -d $directory ] ; then + echo "** PATH contains invalid directory $directory" + errors=$(( $errors + 1 )) + fi +done + +IFS=$oldIFS # restore value for rest of script + +# The following can be undefined, and they can also be a progname, rather +# than a fully qualified path. Add additional variables as necessary for +# your site and user community. + +validate "EDITOR" $EDITOR +validate "MAILER" $MAILER +validate "PAGER" $PAGER + +# and, finally, a different ending depending on whether errors > 0 + +if [ $errors -gt 0 ] ; then + echo "Errors encountered. Please notify sysadmin for help." +else + echo "Your environment checks out fine." +fi + +exit 0 diff --git a/048-fixguest.sh b/048-fixguest.sh new file mode 100755 index 0000000..b11c57f --- /dev/null +++ b/048-fixguest.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +# fixguest - Clean up the guest account during the logout process + +# don't trust environment variables: reference read-only sources + +iam="$(whoami)" +myhome="$(grep "^${iam}:" /etc/passwd | cut -d: -f6)" + +# *** Do NOT run this script on a regular user account! + +if [ "$iam" != "guest" ] ; then + echo "Error: you really don't want to run fixguest on this account." >&2 + exit 1 +fi + +if [ ! -d $myhome/..template ] ; then + echo "$0: no template directory found for rebuilding." >&2 + exit 1 +fi + +# remove all files and directories in the home account + +cd $myhome + +rm -rf * $(find . -name ".[a-zA-Z0-9]*" -print) + +# now the only thing present should be the ..template directory + +cp -Rp ..template/* . + +exit 0 diff --git a/049-findsuid.sh b/049-findsuid.sh new file mode 100755 index 0000000..b5e93be --- /dev/null +++ b/049-findsuid.sh @@ -0,0 +1,33 @@ +#!/bin/sh + +# findsuid - find all SUID files or programs on the system other +# than those that live in /bin and /usr/bin, and +# output the matches in a friendly and useful format. + +mtime="7" # how far back (in days) to check for modified cmds +verbose=0 # by default, let's be quiet about things + +if [ "$1" = "-v" ] ; then + verbose=1 +fi + +for match in $(find /bin /usr/bin -type f -perm +4000 -print) +do + if [ -x $match ] ; then + + owner="$(ls -ld $match | awk '{print $3}')" + perms="$(ls -ld $match | cut -c5-10 | grep 'w')" + + if [ ! -z $perms ] ; then + echo "**** $match (writeable and setuid $owner)" + elif [ ! -z $(find $match -mtime -$mtime -print) ] ; then + echo "**** $match (modified within $mtime days and setuid $owner)" + elif [ $verbose -eq 1 ] ; then + lastmod="$(ls -ld $match | awk '{print $6, $7, $8}')" + echo " $match (setuid $owner, last modified $lastmod)" + fi + fi +done + +exit 0 + diff --git a/050-set-date.sh b/050-set-date.sh new file mode 100755 index 0000000..8efa65d --- /dev/null +++ b/050-set-date.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# setdate - friendly front-end to the date command + +# Date wants: [[[[[cc]yy]mm]dd]hh]mm[.ss] + +askvalue() +{ + # $1 = field name, $2 = default value, $3 = max value, + # $4 = required char/digit length + + echo -n "$1 [$2] : " + read answer + if [ ${answer:=$2} -gt $3 ] ; then + echo "$0: $1 $answer is invalid"; exit 0 + elif [ "$(( $(echo $answer | wc -c) - 1 ))" -lt $4 ] ; then + echo "$0: $1 $answer is too short: please specify $4 digits"; exit 0 + fi + eval $1=$answer +} + +eval $(date "+nyear=%Y nmon=%m nday=%d nhr=%H nmin=%M") + +askvalue year $nyear 3000 4 +askvalue month $nmon 12 2 +askvalue day $nday 31 2 +askvalue hour $nhr 24 2 +askvalue minute $nmin 59 2 + +squished="$year$month$day$hour$minute" +# or, if you're running a Linux system: +# squished="$month$day$hour$minute$year" + +echo "Setting date to $squished. You might need to enter your sudo password:" +sudo date $squished + +exit 0 diff --git a/051-enabled.sh b/051-enabled.sh new file mode 100755 index 0000000..700119e --- /dev/null +++ b/051-enabled.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +# enabled - show what services are enabled with inetd and xinetd, +# if they're available on the system. + +iconf="/etc/inetd.conf" +xconf="/etc/xinetd.conf" +xdir="/etc/xinetd.d" + +if [ -r $iconf ] ; then + echo "Services enabled in $iconf are:" + grep -v '^#' $iconf | awk '{print " " $1}' + echo "" + if [ "$(ps -aux | grep inetd | egrep -vE '(xinet|grep)')" = "" ] ; then + echo "** warning: inetd does not appear to be running" + fi +fi + +if [ -r $xconf ] ; then + # don't need to look in xinietd.conf, just know it exists + echo "Services enabled in $xdir are:" + + for service in $xdir/* + do + if ! $(grep disable $service | grep 'yes' > /dev/null) ; then + echo -n " " + basename $service + fi + done + + if ! $(ps -aux | grep xinetd | grep -v 'grep' > /dev/null) ; then + echo "** warning: xinetd does not appear to be running" + fi +fi + +exit 0 diff --git a/052-killall.sh b/052-killall.sh new file mode 100755 index 0000000..d3c1a96 --- /dev/null +++ b/052-killall.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +# killall - send the specified signal to all processes that match a +# specific process name + +# By default it only kills processes owned by the same user, unless +# you're root. Use -s SIGNAL to specify a signal to send, -u user to +# specify user, -t tty to specify a tty, and -n to only show what'd +# be done rather than doing it + +signal="-INT" # default signal +user="" tty="" donothing=0 + +while getopts "s:u:t:n" opt; do + case "$opt" in + # note the trick below: kill wants -SIGNAL but we're asking + # for SIGNAL, so we slip the '-' in as part of the assignment + s ) signal="-$OPTARG"; ;; + u ) if [ ! -z "$tty" ] ; then + echo "$0: error: -u and -t are mutually exclusive." >&2 + exit 1 + fi + user=$OPTARG; ;; + t ) if [ ! -z "$user" ] ; then + echo "$0: error: -u and -t are mutually exclusive." >&2 + exit 1 + fi + tty=$2; ;; + n ) donothing=1; ;; + ? ) echo "Usage: $0 [-s signal] [-u user|-t tty] [-n] pattern" >&2 + exit 1 + esac +done + +shift $(( $OPTIND - 1 )) + +if [ $# -eq 0 ] ; then + echo "Usage: $0 [-s signal] [-u user|-t tty] [-n] pattern" >&2 + exit 1 +fi + +if [ ! -z "$tty" ] ; then + pids=$(ps cu -t $tty | awk "/ $1$/ { print \$2 }") +elif [ ! -z "$user" ] ; then + pids=$(ps cu -U $user | awk "/ $1$/ { print \$2 }") +else + pids=$(ps cu -U ${USER:-LOGNAME} | awk "/ $1$/ { print \$2 }") +fi + +if [ -z "$pids" ] ; then + echo "$0: no processes match pattern $1" >&2; exit 1 +fi + +for pid in $pids +do + # Sending signal $signal to process id $pid: kill might + # still complain if the process has finished, user doesn't + # have permission, etc, but that's okay. + if [ $donothing -eq 1 ] ; then + echo "kill $signal $pid" + else + kill $signal $pid + fi +done + +exit 0 diff --git a/053-verifycron.sh b/053-verifycron.sh new file mode 100755 index 0000000..298c746 --- /dev/null +++ b/053-verifycron.sh @@ -0,0 +1,144 @@ +#!/bin/sh + +# verifycron - script checks a crontab file to ensure that it's +# formatted properly. Expects standard cron notation of +# min hr dom mon dow CMD +# where min is 0-59, hr 0-23, dom is 1-31, mon is 1-12 (or names) +# and dow is 0-7 (or names). Fields can have ranges (a-e), lists +# separated by commas (a,c,z), or an asterisk. Note that the step +# value notation of Vixie cron is not supported (e.g., 2-6/2). + + +validNum() +{ + # return 0 if valid, 1 if not. Specify number and maxvalue as args + num=$1 max=$2 + + if [ "$num" = "X" ] ; then + return 0 + elif [ ! -z $(echo $num | sed 's/[[:digit:]]//g') ] ; then + return 1 + elif [ $num -lt 0 -o $num -gt $max ] ; then + return 1 + else + return 0 + fi +} + +validDay() +{ + # return 0 if a valid dayname, 1 otherwise + + case $(echo $1 | tr '[:upper:]' '[:lower:]') in + sun*|mon*|tue*|wed*|thu*|fri*|sat*) return 0 ;; + X) return 0 ;; # special case - it's an "*" + *) return 1 + esac +} + +validMon() +{ + # return 0 if a valid month name, 1 otherwise + + case $(echo $1 | tr '[:upper:]' '[:lower:]') in + jan*|feb*|mar*|apr*|may|jun*|jul*|aug*) return 0 ;; + sep*|oct*|nov*|dec*) return 0 ;; + X) return 0 ;; # special case, it's an "*" + *) return 1 ;; + esac +} + +fixvars() +{ + # translate all '*' into 'X' to bypass shell expansion hassles + # save original as "sourceline" for error messages + + sourceline="$min $hour $dom $mon $dow $command" + min=$(echo "$min" | tr '*' 'X') + hour=$(echo "$hour" | tr '*' 'X') + dom=$(echo "$dom" | tr '*' 'X') + mon=$(echo "$mon" | tr '*' 'X') + dow=$(echo "$dow" | tr '*' 'X') +} + +if [ $# -ne 1 ] || [ ! -r $1 ] ; then + echo "Usage: $0 usercrontabfile" >&2; exit 1 +fi + +lines=0 entries=0 totalerrors=0 + +while read min hour dom mon dow command +do + lines="$(( $lines + 1 ))" + errors=0 + + if [ -z "$min" -o "${min%${min#?}}" = "#" ] ; then + continue # nothing to check + elif [ ! -z $(echo ${min%${min#?}} | sed 's/[[:digit:]]//') ] ; then + continue # first char not digit: skip! + fi + + entries="$(($entries + 1))" + + fixvars + + #### Broken into fields, all '*' replaced with 'X' + # minute check + + for minslice in $(echo "$min" | sed 's/[,-]/ /g') ; do + if ! validNum $minslice 60 ; then + echo "Line ${lines}: Invalid minute value \"$minslice\"" + errors=1 + fi + done + + # hour check + + for hrslice in $(echo "$hour" | sed 's/[,-]/ /g') ; do + if ! validNum $hrslice 24 ; then + echo "Line ${lines}: Invalid hour value \"$hrslice\"" + errors=1 + fi + done + + # day of month check + + for domslice in $(echo $dom | sed 's/[,-]/ /g') ; do + if ! validNum $domslice 31 ; then + echo "Line ${lines}: Invalid day of month value \"$domslice\"" + errors=1 + fi + done + + # month check + + for monslice in $(echo "$mon" | sed 's/[,-]/ /g') ; do + if ! validNum $monslice 12 ; then + if ! validMon "$monslice" ; then + echo "Line ${lines}: Invalid month value \"$monslice\"" + errors=1 + fi + fi + done + + # day of week check + + for dowslice in $(echo "$dow" | sed 's/[,-]/ /g') ; do + if ! validNum $dowslice 7 ; then + if ! validDay $dowslice ; then + echo "Line ${lines}: Invalid day of week value \"$dowslice\"" + errors=1 + fi + fi + done + + if [ $errors -gt 0 ] ; then + echo ">>>> ${lines}: $sourceline" + echo "" + totalerrors="$(( $totalerrors + 1 ))" + fi +done < $1 + +echo "Done. Found $totalerrors errors in $entries crontab entries." + +exit 0 diff --git a/054-docron.sh b/054-docron.sh new file mode 100755 index 0000000..4c4e633 --- /dev/null +++ b/054-docron.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# DOCRON - simple script to run the daily, weekly and monthly +# system cron jobs on a system where it's likely that +# it'll be shut down at the usual time of day when +# this would occur. + +rootcron="/etc/crontab" + +if [ $# -ne 1 ] ; then + echo "Usage: $0 [daily|weekly|monthly]" >&2 + exit 1 +fi + +if [ "$(id -u)" -ne 0 ] ; then + echo "$0: Command must be run as 'root'" >&2 + exit 1 +fi + +job="$(awk "NR > 6 && /$1/ { for (i=7;i<=NF;i++) print \$i }" $rootcron)" + +if [ -z $job ] ; then + echo "$0: Error: no $1 job found in $rootcron" >&2 + exit 1 +fi + +SHELL=/bin/sh # to be consistent with cron's default + +eval $job diff --git a/055-rotatelogs.sh b/055-rotatelogs.sh new file mode 100755 index 0000000..d036981 --- /dev/null +++ b/055-rotatelogs.sh @@ -0,0 +1,86 @@ +#!/bin/sh + +# rotatelogs - roll logfiles in /var/log for archival purposes. +# uses a config file to allow customization of how frequently +# each log should be rolled. That file is in +# logfilename=duration +# format, where duration is in days. If nothing is configured, +# rotatelogs won't rotate more frequently than every seven days. + +logdir="/var/log" +config="/var/log/rotatelogs.conf" +mv="/bin/mv" +default_duration=7 +count=0 + +duration=$default_duration + +if [ ! -f $config ] ; then + echo "$0: no config file found. Can't proceed." >&2; exit 1 +fi + +if [ ! -w $logdir -o ! -x $logdir ] ; then + echo "$0: you don't have the appropriate permissions in $logdir" >&2 + exit 1 +fi + +cd $logdir + +# While we'd like to use ':digit:' with the find, many versions of +# find don't support Posix character class identifiers, hence [0-9] + +for name in $(find . -type f -size +0c ! -name '*[0-9]*' \ + ! -name '\.*' ! -name '*conf' -maxdepth 1 -print | sed 's/^\.\///') +do + + count=$(( $count + 1 )) + + # grab this entry from the config file + + duration="$(grep "^${name}=" $config|cut -d= -f2)" + + if [ -z $duration ] ; then + duration=$default_duration + elif [ "$duration" = "0" ] ; then + echo "Duration set to zero: skipping $name" + continue + fi + + back1="${name}.1"; back2="${name}.2"; + back3="${name}.3"; back4="${name}.4"; + + # If the most recently rolled log file (back1) has been modified within + # the specific quantum then it's not time to rotate it. + + if [ -f "$back1" ] ; then + if [ -z $(find "$back1" -mtime +$duration -print 2>/dev/null) ] + then + echo "$name's most recent backup is more recent than $duration days: skipping" + continue + fi + fi + + echo "Rotating log $name (using a $duration day schedule)" + + # rotate, starting with the oldest log + if [ -f "$back3" ] ; then + echo "... $back3 -> $back4" ; $mv -f "$back3" "$back4" + fi + if [ -f "$back2" ] ; then + echo "... $back2 -> $back3" ; $mv -f "$back2" "$back3" + fi + if [ -f "$back1" ] ; then + echo "... $back1 -> $back2" ; $mv -f "$back1" "$back2" + fi + if [ -f "$name" ] ; then + echo "... $name -> $back1" ; $mv -f "$name" "$back1" + fi + touch "$name" + chmod 0600 "$name" +done + +if [ $count -eq 0 ] ; then + echo "Nothing to do: no log files big enough or old enough to rotate" +fi + +exit 0 diff --git a/056-backup.sh b/056-backup.sh new file mode 100755 index 0000000..ef30fc9 --- /dev/null +++ b/056-backup.sh @@ -0,0 +1,62 @@ +#!/bin/sh + +# Backup - create either a full or incremental backup of a set of +# defined directories on the system. By default, the output +# file is saved in /tmp with a timestamped filename, compressed. +# Otherwise, specify an output device (another disk, a removable). + +usageQuit() +{ + cat << "EOF" >&2 +Usage: $0 [-o output] [-i|-f] [-n] + -o lets you specify an alternative backup file/device + -i is an incremental or -f is a full backup, and -n prevents + updating the timestamp if an incremental backup is done. +EOF + exit 1 +} + +compress="bzip2" # change for your favorite compression app +inclist="/tmp/backup.inclist.$(date +%d%m%y)" + output="/tmp/backup.$(date +%d%m%y).bz2" + tsfile="$HOME/.backup.timestamp" + btype="incremental" # default to an incremental backup + noinc=0 # and an update of the timestamp + +trap "/bin/rm -f $inclist" EXIT + +while getopts "o:ifn" opt; do + case "$arg" in + o ) output="$OPTARG"; ;; + i ) btype="incremental"; ;; + f ) btype="full"; ;; + n ) noinc=1; ;; + ? ) usageQuit ;; + esac +done + +shift $(( $OPTIND - 1 )) + +echo "Doing $btype backup, saving output to $output" + +timestamp="$(date +'%m%d%I%M')" + +if [ "$btype" = "incremental" ] ; then + if [ ! -f $tsfile ] ; then + echo "Error: can't do an incremental backup: no timestamp file" >&2 + exit 1 + fi + find $HOME -depth -type f -newer $tsfile -user ${USER:-LOGNAME} | \ + pax -w -x tar | $compress > $output + failure="$?" +else + find $HOME -depth -type f -user ${USER:-LOGNAME} | \ + pax -w -x tar | $compress > $output + failure="$?" +fi + +if [ "$noinc" = "0" -a "$failure" = "0" ] ; then + touch -t $timestamp $tsfile +fi + +exit 0 diff --git a/057-archivedir.sh b/057-archivedir.sh new file mode 100755 index 0000000..1c8961e --- /dev/null +++ b/057-archivedir.sh @@ -0,0 +1,47 @@ +#!/bin/sh + +# archivedir - create a compressed archive of the specified directory + +maxarchivedir=10 # size, in blocks, of 'big' directory, to confirm +compress=gzip # change to your favorite compress app +progname=$(basename $0) + +if [ $# -eq 0 ] ; then + echo "Usage: $progname directory" >&2 ;exit 1 +fi + +if [ ! -d $1 ] ; then + echo "${progname}: can't find directory $1 to archive." >&2; exit 1 +fi + +if [ "$(basename $1)" != "$1" -o "$1" = "." ] ; then + echo "${progname}: You must specify a subdirectory" >&2 + exit 1 +fi + +if [ ! -w . ] ; then + echo "${progname}: cannot write archive file to current directory." >&2 + exit 1 +fi + +dirsize="$(du -s $1 | awk '{print $1}')" + +if [ $dirsize -gt $maxarchivedir ] ; then + echo -n "Warning: directory $1 is $dirsize blocks. Proceed? [n] " + read answer + answer="$(echo $answer | tr '[:upper:]' '[:lower:]' | cut -c1)" + if [ "$answer" != "y" ] ; then + echo "${progname}: archive of directory $1 cancelled." >&2 + exit 0 + fi +fi + +archivename="$(echo $1 | sed 's/$/.tgz/')" + +if tar cf - $1 | $compress > $archivename ; then + echo "Directory $1 archived as $archivename" +else + echo "Warning: tar encountered errors archiving $1" +fi + +exit 0 diff --git a/058-connecttime.sh b/058-connecttime.sh new file mode 100755 index 0000000..7e7a472 --- /dev/null +++ b/058-connecttime.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# connecttime - reports cumulative connection time for month/year entries +# found in the system log file. + +log="/var/log/system.log" +tempfile="/tmp/$0.$$" + +trap "rm $tempfile" 0 + +cat << 'EOF' > $tempfile +BEGIN { + lastmonth=""; sum = 0 +} +{ + if ( $1 != lastmonth && lastmonth != "" ) { + if (sum > 60) { total = sum/60 " hours" } + else { total = sum " minutes" } + print lastmonth ": " total + sum=0 + } + lastmonth=$1 + sum += $8 +} +END { + if (sum > 60) { total = sum/60 " hours" } + else { total = sum " minutes" } + print lastmonth ": " total +} +EOF + +grep "Connect time" $log | awk -f $tempfile + +exit 0 diff --git a/059-ftpget.sh b/059-ftpget.sh new file mode 100755 index 0000000..afb08aa --- /dev/null +++ b/059-ftpget.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +# ftpget - given an ftp: style URL, unwrap it, and try to obtain the file +# using anonymous ftp. + +anonpass="$LOGNAME@$(hostname)" + +if [ $# -ne 1 ] ; then + echo "Usage: $0 ftp://..." >&2 + exit 1 +fi + +# Typical URL: ftp://ftp.ncftp.com/2.7.1/ncftpd-2.7.1.tar.gz + +if [ "$(echo $1 | cut -c1-6)" != "ftp://" ] ; then + echo "$0: Malformed url. I need it to start with ftp://" >&2; + exit 1 +fi + +server="$(echo $1 | cut -d/ -f3)" +filename="$(echo $1 | cut -d/ -f4-)" +basefile="$(basename $filename)" + +echo ${0}: Downloading $basefile from server $server + +ftp -n << EOF +open $server +user ftp $anonpass +get $filename $basefile +quit +EOF + +if [ $? -eq 0 ] ; then + ls -l $basefile +fi + +exit 0 diff --git a/060-bbcnews.sh b/060-bbcnews.sh new file mode 100755 index 0000000..1dada30 --- /dev/null +++ b/060-bbcnews.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# bbcnews - report the top stories on the BBC World Service + +url="http://news.bbc.co.uk/2/low/technology/default.stm" + +lynx -source $url | \ + sed -n '/Last Updated:/,/newssearch.bbc.co.uk/p' | \ + sed 's/\ +/>\ +/g' | \ + grep -v -E '(<|>)' | \ + fmt | \ + uniq diff --git a/061-getlinks.sh b/061-getlinks.sh new file mode 100755 index 0000000..a33bb66 --- /dev/null +++ b/061-getlinks.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# getlinks - given a URL, return all of its internal and +# external links + +if [ $# -eq 0 ] ; then + echo "Usage: $0 [-d|-i|-x] url" >&2 + echo "-d=domains only, -i=internal refs only, -x=external only" >&2 + exit 1 +fi + +if [ $# -gt 1 ] ; then + case "$1" in + -d) lastcmd="cut -d/ -f3 | sort | uniq" + shift + ;; + -i) basedomain="http://$(echo $2 | cut -d/ -f3)/" + lastcmd="grep \"^$basedomain\" | sed \"s|$basedomain||g\" | sort | uniq" + shift + ;; + -x) basedomain="http://$(echo $2 | cut -d/ -f3)/" + lastcmd="grep -v \"^$basedomain\" | sort | uniq" + shift + ;; + *) echo "$0: unknown option specified: $1" >&2; exit 1 + esac +else + lastcmd="sort | uniq" +fi + +lynx -dump "$1" | \ + sed -n '/^References$/,$p' | \ + grep -E '[[:digit:]]+\.' | \ + awk '{print $2}' | \ + cut -d\? -f1 | \ + eval $lastcmd + +exit 0 diff --git a/062-define.sh b/062-define.sh new file mode 100755 index 0000000..ae1f80f --- /dev/null +++ b/062-define.sh @@ -0,0 +1,27 @@ +#!/bin/sh + +# define - given a word, return its definition from dictionary.com + +url="http://www.cogsci.princeton.edu/cgi-bin/webwn2.0?stage=1&word=" + +if [ $# -ne 1 ] ; then + echo "Usage: $0 word" >&2 + exit 1 +fi + +lynx -source "$url$1" | \ + grep -E '(^[[:digit:]]+\.| has [[:digit:]]+$)' | \ + sed 's/<[^>]*>//g' | +( while read line + do + if [ "${line:0:3}" = "The" ] ; then + part="$(echo $line | awk '{print $2}')" + echo "" + echo "The $part $1:" + else + echo "$line" | fmt | sed 's/^/ /g' + fi + done +) + +exit 0 diff --git a/063-weather.sh b/063-weather.sh new file mode 100755 index 0000000..073a1dd --- /dev/null +++ b/063-weather.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +# weather - report weather forecast, including lat/long, for zip + +llurl="http://www.census.gov/cgi-bin/gazetteer?city=&state=&zip=" +wxurl="http://wwwa.accuweather.com" +wxurl="$wxurl/adcbin/public/local_index_print.asp?zipcode=" + +if [ "$1" = "-a" ] ; then + size=999; shift +else + size=5 +fi + +if [ $# -eq 0 ] ; then + echo "Usage: $0 [-a] zipcode" >&2 + exit 1 +fi + +if [ $size -eq 5 ] ; then + echo "" + + # get some information on the zipcode from the Census Bureau + + lynx -source "${llurl}$1" | \ + sed -n '/^
" +env || printenv +echo "" +echo "
" +cat - +echo "(end of input stream)" + +exit 0 diff --git a/070-logsearch.cgi b/070-logsearch.cgi new file mode 100755 index 0000000..2366202 --- /dev/null +++ b/070-logsearch.cgi @@ -0,0 +1,23 @@ +#!/bin/sh + +# log Yahoo! search - given a search request, log the pattern, then +# feed the entire sequence to the real Yahoo search system. + +logfile="/home/taylor/scripts/searchlog.txt" + +if [ ! -f $logfile ] ; then + touch $logfile + chmod a+rw $logfile +fi + +if [ -w $logfile ] ; then + echo "$(date): $QUERY_STRING" | sed 's/p=//g;s/+/ /g' >> $logfile +fi + +# echo "Content-type: text/html" +# echo "" + +echo "Location: http://search.yahoo.com/bin/search?$QUERY_STRING" +echo "" + +exit 0 diff --git a/070-yahoo-search.html b/070-yahoo-search.html new file mode 100644 index 0000000..c8f0034 --- /dev/null +++ b/070-yahoo-search.html @@ -0,0 +1,5 @@ + diff --git a/071-getdope.sh b/071-getdope.sh new file mode 100755 index 0000000..0dd8671 --- /dev/null +++ b/071-getdope.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# Within cron set it up so that every Friday, grab the latest column +# of 'The Straight Dope' and mail it out to the specified recipient + +now="$(date +%y%m%d)" +url="http://www.straightdope.com/columns/${now}.html" +to="taylor" + +( cat << EOF +Subject: The Straight Dope for $(date "+%A, %d %B, %Y") +From: Cecil Adams
Bill Holbrook's Kevin & Kell |
---|
" +echo "© Bill Holbrook. Please see " +echo "kevinandkell.com" +echo "for more strips, books, etc." +echo " |
" + count=1 + else + echo " | "
+ count=$(( $count + 1 ))
+ fi
+
+ nicename="$(echo $name | sed 's/.jpg//;s/-/ /g')"
+
+ echo " " + echo "$nicename" +done + +echo " |
$name signed thusly: |
$comment |
Added $date"
+ echo " |
+This page was last modified on + +according to the SSI LAST_MODIFIED variable. +
+Finally, the random tagline for this load is: +
Password Manager Actions | |||
---|---|---|---|
+ + | + + | + + | + + |