c4ff3f65090c852c7501ef7766e35d7757d1fc1d
[feisty_meow.git] / scripts / core / functions.sh
1 #!/bin/bash
2
3 # This defines some general, useful functions.
4
5 #hmmm: starting to get a bit beefy in here.  perhaps there is a good way to refactor the functions into more specific folders, if they aren't really totally general purpose?
6
7 ##############
8
9 # test whether we've been here before or not.
10 skip_all=
11 type function_sentinel &>/dev/null
12 if [ $? -eq 0 ]; then
13   # there was no error, so we can skip the inits.
14   if [ ! -z "$DEBUG_FEISTY_MEOW" ]; then
15     echo "skipping function definitions, because already defined."
16   fi
17   skip_all=yes
18 else
19   skip_all=
20 fi
21
22 if [ -z "$skip_all" ]; then
23
24   if [ ! -z "$DEBUG_FEISTY_MEOW" ]; then
25     echo "feisty meow function definitions beginning now..."
26   fi
27
28   # a handy little method that can be used for date strings.  it was getting
29   # really tiresome how many different ways the script did the date formatting.
30   function date_stringer() {
31     local sep="$1"; shift
32     if [ -z "$sep" ]; then sep='_'; fi
33     date +"%Y$sep%m$sep%d$sep%H%M$sep%S" | tr -d '/\n/'
34   }
35   
36   # a wrapper for the which command that finds items on the path.  some OSes
37   # do not provide which, so we want to not be spewing errors when that
38   # happens.
39   function whichable()
40   {
41     to_find="$1"; shift
42     local WHICHER="$(\which which 2>/dev/null)"
43 #>&2 echo "got whicher as: $WHICHER"
44     if [ $? -ne 0 ]; then
45       # there is no which command here.  we produce nothing due to this.
46       echo
47       return 2
48     fi
49     local sporkenz  # must be defined local here, before call, or we don't get exit value?!
50     sporkenz=$($WHICHER "$to_find" 2>/dev/null)
51 #>&2 echo "broken with this line, but here is exit val: $?"
52     local err=$?
53 #>&2 echo "got whicher as: $WHICHER"
54     echo $sporkenz
55     return $err
56   }
57
58   # makes a directory of the name specified and then tries to change the
59   # current directory to that directory.
60   function mcd() {
61     if [ ! -d "$1" ]; then mkdir -p "$1"; fi
62     cd "$1"
63   }
64
65   # returns true if the variable is an array.
66   function is_array() {
67     [[ "$(declare -p $1)" =~ "declare -a" ]]
68   }
69
70   # returns true if the name provided is a defined alias.
71   function is_alias() {
72     alias $1 &>/dev/null
73     return $?
74   }
75
76   # makes the status of pipe number N (passed as first parameter) into the
77   # main return value (i.e., the value for $?).  this is super handy to avoid
78   # repeating the awkward looking code below in multiple places.
79   # the numbering starts at zero, for the first item at the head of the pipe.
80   function promote_pipe_return()
81   {
82     ( exit ${PIPESTATUS[$1]} )
83   }
84
85   ##############
86
87   function fm_username()
88   {
89     # see if we can get the user name from the login name.  oddly this sometimes doesn't work.
90     local custom_user="$(logname 2>/dev/null)"
91     if [ -z "$custom_user" ]; then
92       # try the normal unix user variable.
93       custom_user="$USER"
94     fi
95     if [ -z "$custom_user" ]; then
96       # try the windows user variable.
97       custom_user="$USERNAME"
98     fi
99     echo "$custom_user"
100   }
101
102   ##############
103
104   # displays the value of a variable in bash friendly format.
105   function var() {
106     HOLDIFS="$IFS"
107     IFS=""
108     while true; do
109       local varname="$1"; shift
110       if [ -z "$varname" ]; then
111         break
112       fi
113
114       if is_alias "$varname"; then
115 #echo found $varname is alias
116         local tmpfile="$(mktemp $TMP/aliasout.XXXXXX)"
117         alias $varname | sed -e 's/.*=//' >$tmpfile
118         echo "alias $varname=$(cat $tmpfile)"
119         \rm $tmpfile
120       elif [ -z "${!varname}" ]; then
121         echo "$varname undefined"
122       else
123         if is_array "$varname"; then
124 #echo found $varname is array var 
125           local temparray
126           eval temparray="(\${$varname[@]})"
127           echo "$varname=(${temparray[@]})"
128 #hmmm: would be nice to print above with elements enclosed in quotes, so that we can properly
129 # see ones that have spaces in them.
130         else
131 #echo found $varname is simple
132           echo "$varname=${!varname}"
133         fi
134       fi
135     done | sort
136     IFS="$HOLDIFS"
137   }
138
139   ##############
140
141   # when passed a list of things, this will return the unique items from that list as an echo.
142   function uniquify()
143   {
144     # do the uniquification: split the space separated items into separate lines, then
145     # sort the list, then run the uniq tool on the list.  results will be packed back onto
146     # one line when invoked like: local fredlist="$(uniquify a b c e d a e f a e d b)"
147     echo $* | tr ' ' '\n' | sort | uniq
148   }
149
150   # sets the variable in parameter 1 to the value in parameter 2, but only if
151   # that variable was undefined.
152   function set_var_if_undefined()
153   {
154     local var_name="$1"; shift
155     local var_value="$1"; shift
156     if [ -z "${!var_name}" ]; then
157       eval export $var_name="$var_value"
158     fi
159   }
160
161   ##############
162
163   function success_sound()
164   {
165     if [ ! -z "$CLAM_FINISH_SOUND" ]; then
166       bash $FEISTY_MEOW_SCRIPTS/multimedia/sound_play.sh "$CLAM_FINISH_SOUND"
167     fi
168   }
169
170   function error_sound()
171   {
172     if [ ! -z "$CLAM_ERROR_SOUND" ]; then
173       bash $FEISTY_MEOW_SCRIPTS/multimedia/sound_play.sh "$CLAM_ERROR_SOUND"
174     fi
175   }
176
177   ##############
178
179   # echoes the maximum number of columns that the terminal supports.  usually
180   # anything you print to the terminal with length less than (but not equal to)
181   # maxcols will never wrap.
182   function get_maxcols()
183   {
184     # calculate the number of columsn in the terminal.
185     local cols=$(stty size | awk '{print $2}')
186     echo $cols
187   }
188
189   ##############
190
191   # checks the result of the last command that was run, and if that failed,
192   # then this complains and exits from bash.  the function parameters are
193   # used as the message to print as a complaint.
194   function exit_on_error()
195   {
196     if [ $? -ne 0 ]; then
197       echo -e "\n\nan important action failed and this script will stop:\n\n$*\n\n*** Exiting script..."
198       error_sound
199       exit 1
200     fi
201   }
202
203   # like exit_on_error, but will keep going after complaining.
204   function continue_on_error()
205   {
206     if [ $? -ne 0 ]; then
207       echo -e "\n\na problem occurred, but we can continue:\n\n$*\n\n=> Continuing script..."
208       error_sound
209     fi
210   }
211
212   ##############
213
214   # accepts any number of arguments and outputs them to the feisty meow event log.
215   function log_feisty_meow_event()
216   {
217     echo -e "$(date_stringer) -- ${USER}@$(hostname): $*" >> "$FEISTY_MEOW_EVENT_LOG"
218   }
219
220   ##############
221
222   # wraps secure shell with some parameters we like, most importantly to enable X forwarding.
223   function ssh()
224   {
225     local args=($@)
226     save_terminal_title  # remember the current terminal title.
227     /usr/bin/ssh -C "${args[@]}"
228 #hmmm: removed -Y flag because considered dangerous to trust remote hosts to not abuse our X session.
229     restore_terminal_title
230   }
231
232   # this version of ssh preserves the use of the -Y flag for when X forwarding is needed.
233   function yssh()
234   {
235     local args=($@)
236     save_terminal_title  # remember the current terminal title.
237     /usr/bin/ssh -Y "${args[@]}"
238     restore_terminal_title
239   }
240
241   ##############
242
243   # locates a process given a search pattern to match in the process list.
244   #
245   # + the -u flag specifies a user name, e.g. "-u joe", which causes only
246   #   the processes of that user "joe" to be considered.
247   #
248   # + the -x flag specifies a pattern to exclude from the list, e.g. "-x pszap.sh"
249   #   would ignore any processes that mention the phrase "pszap.sh".
250   function psfind() {
251     local user_flag="-e"
252       # default user flag is for all users.
253     local excluder="ScrengeflebbitsAPhraseWeNeverExpecttomatchanythingYO298238"
254       # for our default, pick an exclusion string we would never match.
255
256     local found_flag=1
257     while [ $found_flag -eq 1 ]; do
258       # reset our sentinel now that we're safely in our loop.
259       found_flag=0
260
261       # save the first argument, since we're going to shift the args.
262       local arg1="$1"
263       if [ "$arg1" == "-u" ]; then
264         # handle the user flag.
265         user_flag="-u $2" 
266 #echo "found a -u parm and user=$2" 
267         found_flag=1  # signal that we found one.
268         # skip these two arguments, since we've consumed them.
269         shift
270         shift
271       elif [ "$arg1" == "-x" ]; then
272         # handle the exclusion flag.
273         excluder="$2" 
274 #echo "found a -x parm and excluder=$excluder" 
275         found_flag=1  # signal that we found one.
276         # skip these two arguments, since we've consumed them.
277         shift
278         shift
279       fi
280     done
281
282     # now that we've yanked any flags out, we can pull the rest of the
283     # arguments in as patterns to seek in the process list.
284     local -a patterns=("${@}")
285 #echo ====
286 #echo patterns list is: "${patterns[@]}"
287 #echo ====
288
289     local PID_DUMP="$(mktemp "$TMP/zz_pidlist.XXXXXX")"
290     local -a PIDS_SOUGHT
291
292     if [ "$OS" == "Windows_NT" ]; then
293       # gets cygwin's (god awful) ps to show windoze processes also.
294       local EXTRA_DOZER_FLAGS="-W"
295       # pattern to use for peeling off the process numbers.
296       local pid_finder_pattern='s/ *\([0-9][0-9]*\) *.*$/\1/p'
297
298     else
299       # flags which clean up the process listing output on unixes.
300       # apparently cygwin doesn't count as a type of unix, because their
301       # crummy specialized ps command doesn't support normal ps flags.
302       local EXTRA_UNIX_FLAGS="-o pid,args"
303       # pattern to use for peeling off the process numbers.
304       local pid_finder_pattern='s/^[[:space:]]*\([0-9][0-9]*\).*$/\1/p'
305     fi
306
307     /bin/ps $EXTRA_DOZER_FLAGS $EXTRA_UNIX_FLAGS $user_flag | tail -n +2 >$PID_DUMP
308 #echo ====
309 #echo got all this stuff in the pid dump file:
310 #cat $PID_DUMP
311 #echo ====
312
313     # search for the pattern the user wants to find, and just pluck the process
314     # ids out of the results.
315     local i
316     for i in "${patterns[@]}"; do
317 #echo "pattern curr is '$i'"
318       PIDS_SOUGHT+=($(cat $PID_DUMP \
319         | grep -i "$i" \
320         | grep -v "$excluder" \
321         | sed -n -e "$pid_finder_pattern"))
322     done
323 #echo ====
324 #echo pids sought list became:
325 #echo "${PIDS_SOUGHT[@]}"
326 #echo ====
327
328     if [ ${#PIDS_SOUGHT[*]} -ne 0 ]; then
329       local PIDS_SOUGHT2=$(printf -- '%s\n' ${PIDS_SOUGHT[@]} | sort | uniq)
330       PIDS_SOUGHT=()
331       PIDS_SOUGHT=${PIDS_SOUGHT2[*]}
332       echo ${PIDS_SOUGHT[*]}
333     fi
334     /bin/rm $PID_DUMP
335   }
336   
337   # finds all processes matching the pattern specified and shows their full
338   # process listing (whereas psfind just lists process ids).
339   function psa() {
340     if [ -z "$1" ]; then
341       echo "psa finds processes by pattern, but there was no pattern on the command line."
342       return 1
343     fi
344     local -a patterns=("${@}")
345     p=$(psfind "${patterns[@]}")
346     if [ -z "$p" ]; then
347       # no matches.
348       return 0
349     fi
350
351     if [ "${patterns[0]}" == "-u" ]; then
352       # void the two elements with that user flag so we don't use them as patterns.
353       unset patterns[0] patterns[1]=
354     fi
355
356     echo ""
357     echo "Processes matching ${patterns[@]}..."
358     echo ""
359     if [ -n "$IS_DARWIN" ]; then
360       unset fuzil_sentinel
361       for i in $p; do
362         # only print the header the first time.
363         if [ -z "$fuzil_sentinel" ]; then
364           ps $i -w -u
365         else
366           ps $i -w -u | sed -e '1d'
367         fi
368         fuzil_sentinel=true
369       done
370     else 
371       # cases besides mac os x's darwin.
372       if [ "$OS" == "Windows_NT" ]; then
373         # special case for windows.
374         ps | head -1
375         for curr in $p; do
376           ps -W -p $curr | tail -n +2
377         done
378       else
379         # normal OSes can handle a nice simple query.
380         ps wu $p
381       fi
382     fi
383   }
384   
385   ##############
386
387 #hmmm: holy crowbars, this is an old one.  do we ever still have any need of it?
388   # an unfortunately similarly named function to the above 'ps' as in process
389   # methods, but this 'ps' stands for postscript.  this takes a postscript file
390   # and converts it into pcl3 printer language and then ships it to the printer.
391   # this mostly makes sense for an environment where one's default printer is
392   # pcl.  if the input postscript causes ghostscript to bomb out, there has been
393   # some good success running ps2ps on the input file and using the cleaned
394   # postscript file for printing.
395   function ps2pcl2lpr() {
396     for $i in $*; do
397       gs -sDEVICE=pcl3 -sOutputFile=- -sPAPERSIZE=letter "$i" | lpr -l 
398     done
399   }
400   
401   function screen() {
402     save_terminal_title
403 #hmmm: ugly absolute path here.
404     /usr/bin/screen $*
405     restore_terminal_title
406   }
407   
408   # switches from a /X/path form to an X:/ form.  this also processes cygwin paths.
409   function unix_to_dos_path() {
410     # we usually remove dos slashes in favor of forward slashes.
411     local DOSSYHOME
412     if [[ ! "$OS" =~ ^[Ww][iI][nN] ]]; then
413       # fake this value for non-windows (non-cygwin) platforms.
414       DOSSYHOME="$HOME"
415     else
416       # for cygwin, we must replace the /home/X path with an absolute one, since cygwin
417       # insists on the /home form instead of /c/cygwin/home being possible.  this is
418       # super frustrating and nightmarish.
419       DOSSYHOME="$(cygpath -am "$HOME")"
420     fi
421
422     if [ ! -z "$SERIOUS_SLASH_TREATMENT" ]; then
423       # unless this flag is set, in which case we force dos slashes.
424       echo "$1" | sed -e "s?^$HOME?$DOSSYHOME?g" | sed -e 's/\\/\//g' | sed -e 's/\/cygdrive//' | sed -e 's/\/\([a-zA-Z]\)\/\(.*\)/\1:\/\2/' | sed -e 's/\//\\/g'
425     else
426       echo "$1" | sed -e "s?^$HOME?$DOSSYHOME?g" | sed -e 's/\\/\//g' | sed -e 's/\/cygdrive//' | sed -e 's/\/\([a-zA-Z]\)\/\(.*\)/\1:\/\2/'
427     fi
428   }
429   
430 #  # switches from an X:/ form to a /cygdrive/X/path form.  this is only useful
431 #  # for the cygwin environment currently.
432 #  function dos_to_unix_path() {
433 #    # we always remove dos slashes in favor of forward slashes.
434 ##old:    echo "$1" | sed -e 's/\\/\//g' | sed -e 's/\([a-zA-Z]\):\/\(.*\)/\/\1\/\2/'
435 #         echo "$1" | sed -e 's/\\/\//g' | sed -e 's/\([a-zA-Z]\):\/\(.*\)/\/cygdrive\/\1\/\2/'
436 #  }
437
438   # returns a successful value (0) if this system is debian or ubuntu.
439   function debian_like() {
440     # decide if we think this is debian or ubuntu or a variant.
441     DEBIAN_LIKE=$(if [ ! -z "$(grep -i debian /etc/issue)" \
442         -o ! -z "$(grep -i ubuntu /etc/issue)" ]; then echo 1; else echo 0; fi)
443     if [ $DEBIAN_LIKE -eq 1 ]; then
444       # success; this is debianish.
445       return 0
446     else
447       # this seems like some other OS.
448       return 1
449     fi
450   }
451   
452   # this function wraps the normal sudo by ensuring we replace the terminal
453   # label before we launch what they're passing to sudo.  we also preserve
454   # specific variables that enable the main user's ssh credentials to still
455   # be relied on for ssh forwarding, even if the '-i' flag is passed to cause
456   # a fresh shell (which normally doesn't get the launching user's environment
457   # variables).
458   function sudo() {
459     save_terminal_title
460
461     # hoist our X authorization info in case environment is passed along;
462     # this can allow root to use our display to show X.org windows.
463     if [ -z "$IMPORTED_XAUTH" -a ! -z "$DISPLAY" ]; then
464       export IMPORTED_XAUTH="$(xauth list $DISPLAY | head -n 1 | awk '{print $3}')"
465       local REMOVE_IMP_XAUTH=true
466     fi
467
468     # launch sudo with just the variables we want to reach the other side.
469     local varmods=
470     varmods+="OLD_HOME=$HOME "
471     if [ ! -z "$IMPORTED_XAUTH" ]; then varmods+="IMPORTED_XAUTH=$IMPORTED_XAUTH "; fi
472     if [ ! -z "$SSH_AUTH_SOCK" ]; then varmods+="SSH_AUTH_SOCK=$SSH_AUTH_SOCK "; fi
473     /usr/bin/sudo $varmods "$@"
474     retval=$?
475
476     # take the xauth info away again if it wasn't set already.
477     if [ ! -z "$REMOVE_IMP_XAUTH" ]; then
478       unset IMPORTED_XAUTH
479     fi
480     restore_terminal_title
481     return $retval
482   }
483   
484   # trashes the .#blah files that cvs and subversion leave behind when finding conflicts.
485   # this kind of assumes you've already checked them for any salient facts.
486   function clean_cvs_junk() {
487     for i in $*; do
488       find $i -follow -type f -iname ".#*" -exec perl $FEISTY_MEOW_SCRIPTS/files/safedel.pl {} ";" 
489     done
490   }
491
492   # overlay for nechung binary so that we can complain less grossly about it when it's missing.
493   function nechung() {
494     local wheres_nechung=$(whichable nechung)
495     if [ -z "$wheres_nechung" ]; then
496       echo "The nechung oracle program cannot be found.  You may want to consider"
497       echo "rebuilding the feisty meow applications with this command:"
498       echo "bash $FEISTY_MEOW_SCRIPTS/generator/produce_feisty_meow.sh"
499       echo
500     else
501       $wheres_nechung
502     fi
503   }
504   
505   # recreates all the generated files that the feisty meow scripts use.
506   function regenerate() {
507     # do the bootstrapping process again.
508     save_terminal_title
509     echo "regenerating feisty meow script environment."
510     bash $FEISTY_MEOW_SCRIPTS/core/reconfigure_feisty_meow.sh
511     echo
512     # force a full reload by turning off sentinel variables and methods.
513     unset -v CORE_VARIABLES_LOADED FEISTY_MEOW_LOADING_DOCK USER_CUSTOMIZATIONS_LOADED \
514         BUILD_VARS_LOADED
515     unalias CORE_ALIASES_LOADED &>/dev/null
516     unset -f function_sentinel 
517
518     # reuse the original path if we can.
519     if [ ! -z "$FEISTY_MEOW_ORIGINAL_PATH" ]; then
520       export PATH="$FEISTY_MEOW_ORIGINAL_PATH"
521     fi
522
523     # reload feisty meow environment in current shell.
524     log_feisty_meow_event "reloading the feisty meow scripts for $USER in current shell."
525     source "$FEISTY_MEOW_SCRIPTS/core/launch_feisty_meow.sh"
526     # run nechung oracle to give user a new fortune.
527     nechung
528     restore_terminal_title
529   }
530
531   # merges a set of custom scripts into the feisty meow environment.  can be
532   # passed a name to use as the custom scripts source folder (found on path
533   # $FEISTY_MEOW_SCRIPTS/customize/{name}), or it will try to guess the name
534   # by using the login name.
535   function recustomize()
536   {
537     local custom_user="$1"; shift
538     if [ -z "$custom_user" ]; then
539       # default to login name if there was no name provided.
540       custom_user="$(fm_username)"
541         # we do intend to use the login name here to get the login name and to ignore
542         # if the user has sudo root access; we don't want to provide a custom
543         # profile for root.
544     fi
545     # chop off any email address style formatting to leave just the name.
546     custom_user="$(echo "$custom_user" | cut -f1 -d'@')"
547
548     save_terminal_title
549
550     if [ ! -d "$FEISTY_MEOW_SCRIPTS/customize/$custom_user" ]; then
551       echo -e "the customization folder for '$custom_user' is missing:
552
553     $FEISTY_MEOW_SCRIPTS/customize/$custom_user
554
555 we will skip recustomization, but these other customizations are available:
556 "
557       # a little tr and sed magic to fix the carriage returns into commas.
558       local line="$(find $FEISTY_MEOW_SCRIPTS/customize -mindepth 1 -maxdepth 1 -type d -exec basename {} ';' | tr '\n' '&' | sed 's/&/, /g' | sed -e 's/, $//')"
559         # make the line feeds and carriage returns manageable with tr.
560         # convert the ampersand, our weird replacement for EOL, with a comma + space in sed.
561         # last touch with sed removes the last comma.
562       echo "    $line"
563       return 1
564     fi
565
566     # recreate the feisty meow loading dock.
567     regenerate >/dev/null
568
569     # jump into the loading dock and make our custom link.
570     pushd "$FEISTY_MEOW_LOADING_DOCK" &>/dev/null
571     if [ -h custom ]; then
572       # there's an existing link, so remove it.
573       \rm custom
574     fi
575     # make sure we cleaned up the area before we re-link.
576     if [ -h custom -o -d custom -o -f custom ]; then
577       echo "
578 Due to an over-abundance of caution, we are not going to remove an unexpected
579 'custom' object found in the file system.  This object is located in the
580 feisty meow loading dock here: $(pwd)
581 And here is a description of the rogue 'custom' object:
582 "
583       ls -al custom
584       echo "
585 If you are pretty sure that this is just a remnant of an older approach in
586 feisty meow, where we copied the custom directory rather than linking it
587 (and it most likely is just such a bit of cruft of that nature), then please
588 remove that old remnant 'custom' item, for example by saying:
589   /bin/rm -rf \"custom\" ; popd
590 Sorry for the interruption, but we want to make sure this removal wasn't
591 automatic if there is even a small amount of doubt about the issue."
592       return 1
593     fi
594
595     # create the custom folder as a link to the customizations.
596     ln -s "$FEISTY_MEOW_SCRIPTS/customize/$custom_user" custom
597
598     popd &>/dev/null
599
600     # now take into account all the customizations by regenerating the feisty meow environment.
601     regenerate
602
603     restore_terminal_title
604   }
605
606   # generates a random password where the first parameter is the number of characters
607   # in the password (default 20) and the second parameter specifies whether to use
608   # special characters (1) or not (0).
609   # found function at http://legroom.net/2010/05/06/bash-random-password-generator
610   function random_password()
611   {
612     [ "$2" == "0" ] && CHAR="[:alnum:]" || CHAR="[:graph:]"
613     cat /dev/urandom | tr -cd "$CHAR" | head -c ${1:-32}
614     echo
615   }
616
617   function add_cygwin_drive_mounts() {
618     for i in c d e f g h q z ; do
619 #hmmm: improve this by not adding the link if already there, or if the drive is not valid.
620       ln -s /cygdrive/$i $i
621     done
622   }
623
624   ############################
625
626   # takes a file to modify, and then it will replace any occurrences of the
627   # pattern provided as the second parameter with the text in the third
628   # parameter.
629   function replace_pattern_in_file()
630   {
631     local file="$1"; shift
632     local pattern="$1"; shift
633     local replacement="$1"; shift
634     if [ -z "$file" -o -z "$pattern" -o -z "$replacement" ]; then
635       echo "replace_pattern_in_file: needs a filename, a pattern to replace, and the"
636       echo "text to replace that pattern with."
637       return 1
638     fi
639     sed -i -e "s%$pattern%$replacement%g" "$file"
640   }
641
642   # similar to replace_pattern_in_file, but also will add the new value
643   # when the old one did not already exist in the file.
644   function replace_if_exists_or_add()
645   {
646     local file="$1"; shift
647     local phrase="$1"; shift
648     local replacement="$1"; shift
649     if [ -z "$file" -o ! -f "$file" -o -z "$phrase" -o -z "$replacement" ]; then
650       echo "replace_if_exists_or_add: needs a filename, a phrase to replace, and the"
651       echo "text to replace that phrase with."
652       return 1
653     fi
654     grep "$phrase" "$file" >/dev/null
655     # replace if the phrase is there, otherwise add it.
656     if [ $? -eq 0 ]; then
657       replace_pattern_in_file "$file" "$phrase" "$replacement"
658     else
659       # this had better be the complete line.
660       echo "$replacement" >>"$file"
661     fi
662   }
663
664   ############################
665
666   # finds a variable (first parameter) in a particular property file
667   # (second parameter).  the expected format for the file is:
668   # varX=valueX
669   function seek_variable()
670   {
671     local find_var="$1"; shift
672     local file="$1"; shift
673     if [ -z "$find_var" -o -z "$file" -o ! -f "$file" ]; then
674       echo -e "seek_variable: needs two parameters, firstly a variable name, and\nsecondly a file where the variable's value will be sought." 1>&2
675       return 1
676     fi
677   
678     while read line; do
679       if [ ${#line} -eq 0 ]; then continue; fi
680       # split the line into the variable name and value.
681       IFS='=' read -a assignment <<< "$line"
682       local var="${assignment[0]}"
683       local value="${assignment[1]}"
684       if [ "${value:0:1}" == '"' ]; then
685         # assume the entry was in quotes and remove them.
686         value="${value:1:$((${#value} - 2))}"
687       fi
688       if [ "$find_var" == "$var" ]; then
689         echo "$value"
690       fi
691     done < "$file"
692   }
693   
694   # finds a variable (first parameter) in a particular XML format file
695   # (second parameter).  the expected format for the file is:
696   # ... name="varX" value="valueX" ...
697   function seek_variable_in_xml()
698   {
699     local find_var="$1"; shift
700     local file="$1"; shift
701     if [ -z "$find_var" -o -z "$file" -o ! -f "$file" ]; then
702       echo "seek_variable_in_xml: needs two parameters, firstly a variable name, and"
703       echo "secondly an XML file where the variable's value will be sought."
704       return 1
705     fi
706   
707     while read line; do
708       if [ ${#line} -eq 0 ]; then continue; fi
709       # process the line to make it more conventional looking.
710       line="$(echo "$line" | sed -e 's/.*name="\([^"]*\)" value="\([^"]*\)"/\1=\2/')"
711       # split the line into the variable name and value.
712       IFS='=' read -a assignment <<< "$line"
713       local var="${assignment[0]}"
714       local value="${assignment[1]}"
715       if [ "${value:0:1}" == '"' ]; then
716         # assume the entry was in quotes and remove them.
717         value="${value:1:$((${#value} - 2))}"
718       fi
719       if [ "$find_var" == "$var" ]; then
720         echo "$value"
721       fi
722     done < "$file"
723   }
724   
725   ############################
726
727   # goes to a particular directory passed as parameter 1, and then removes all
728   # the parameters after that from that directory.
729   function push_whack_pop()
730   {
731     local dir="$1"; shift
732     pushd "$dir" &>/dev/null
733     if [ $? -ne 0 ]; then echo failed to enter dir--quitting.; fi
734     rm -rf $* &>/dev/null
735     if [ $? -ne 0 ]; then echo received a failure code when removing.; fi
736     popd &>/dev/null
737   }
738
739   ##############
740
741 # new breed of definer functions goes here.  still in progress.
742
743   # defines an alias and remembers that this is a new or modified definition.
744   # if the feisty meow codebase is unloaded, then so are all the aliases that
745   # were defined.
746   function define_yeti_alias()
747   {
748 # if alias exists already, save old value for restore,
749 # otherwise save null value for restore,
750 # have to handle unaliasing if there was no prior value of one
751 # we newly defined.
752 # add alias name to a list of feisty defined aliases.
753
754 #hmmm: first implem, just do the alias and get that working...
755 alias "${@}"
756
757
758 return 0
759   }
760
761   ##############
762
763 #hmmm: this points to an extended functions file being needed; not all of these are core.
764
765   # displays documentation in "md" formatted files.
766   function show_md()
767   {
768     local file="$1"; shift
769     pandoc "$file" | lynx -stdin
770   }
771
772   ##############
773
774   # just shows a separator line for an 80 column console, or uses the first
775   # parameter as the number of columns to expect.
776   function separator()
777   {
778     count=$1; shift
779     if [ -z "$count" ]; then
780       count=$(($COLUMNS - 1))
781     fi
782     echo
783     local i
784     for ((i=0; i < $count; i++)); do
785       echo -n "="
786     done
787     echo
788     echo
789   }
790   # alias for separator.
791   function sep()
792   {
793     separator $*
794   }
795
796   ##############
797
798   # count the number of sub-directories in a directory and echo the result.
799   function count_directories()
800   {
801     local subbydir="$1"; shift
802     numdirs="$(find "$subbydir" -mindepth 1 -maxdepth 1 -type d | wc -l)"
803     echo $numdirs
804   }
805
806   # takes a string and capitalizes just the first character.  any capital letters in the remainder of
807   # the string are made lower case.  the processed string is returned by an echo.
808   function capitalize_first_char()
809   {
810     local to_dromedary="$1"; shift
811     to_dromedary="$(tr '[:lower:]' '[:upper:]' <<< ${to_dromedary:0:1})$(tr '[:upper:]' '[:lower:]' <<< ${to_dromedary:1})"
812     echo "$to_dromedary"
813   }
814
815   # given a source path and a target path, this will make a symbolic link from
816   # the source to the destination, but only if the source actually exists.
817   function make_safe_link()
818   {
819     local src="$1"; shift
820     local target="$1"; shift
821   
822     if [ -d "$src" ]; then
823       ln -s "$src" "$target"
824       exit_on_error "Creating symlink from '$src' to '$target'"
825     fi
826     echo "Created symlink from '$src' to '$target'."
827   }
828
829   # pretty prints the json files provided as parameters.
830   function clean_json()
831   {
832     if [ -z "$*" ]; then return; fi
833     local show_list=()
834     while true; do
835       local file="$1"; shift
836       if [ -z "$file" ]; then break; fi
837       if [ ! -f "$file" ]; then "echo File '$file' does not exist."; continue; fi
838       temp_out="$TMP/$file.view"
839       cat "$file" | python -m json.tool > "$temp_out"
840       show_list+=($temp_out)
841       continue_on_error "pretty printing '$file'"
842     done
843     filedump "${show_list[@]}"
844     rm "${show_list[@]}"
845   }
846
847   function json_text()
848   {
849     # only print our special headers or text fields.
850     local CR=$'\r'
851     local LF=$'\n'
852     clean_json $* |
853         grep -i "\"text\":\|^=.*" | 
854         sed -e "s/\\\\r/$CR/g" -e "s/\\\\n/\\$LF/g"
855   }
856
857   ##############
858
859   # echoes the machine's hostname.  can be used like so:
860   #   local my_host=$(get_hostname)
861   function get_hostname()
862   {
863     # there used to be more variation in how to do this, but adopting mingw
864     # and cygwin tools really helped out.
865     local this_host=unknown
866     if [ "$OS" == "Windows_NT" ]; then
867       this_host=$(hostname)
868     elif [ ! -z "$(echo $MACHTYPE | grep apple)" ]; then
869       this_host=$(hostname)
870     elif [ ! -z "$(echo $MACHTYPE | grep suse)" ]; then
871       this_host=$(hostname --long)
872     elif [ -x "$(whichable hostname)" ]; then
873       this_host=$(hostname)
874     fi
875     echo "$this_host"
876   }
877
878   # makes sure that the provided "folder" is a directory and is writable.
879   function test_writeable()
880   {
881     local folder="$1"; shift
882     if [ ! -d "$folder" -o ! -w "$folder" ]; then return 1; fi
883     return 0
884   }
885
886   ##############
887
888   # given a filename and a string to seek and a number of lines, then this
889   # function will remove the first occurrence of a line in the file that
890   # matches the string, and it will also axe the next N lines as specified.
891   function create_chomped_copy_of_file()
892   {
893     local filename="$1"; shift
894     local seeker="$1"; shift
895     local numlines=$1; shift
896
897 #echo into create_chomped_copy...
898 #var filename seeker numlines 
899
900     # make a backup first, oy.
901     \cp -f "$filename" "/tmp/$(basename ${filename}).bkup-${RANDOM}" 
902     exit_on_error "backing up file: $filename"
903
904     # make a temp file to write to before we move file into place in bind.
905     local new_version="/tmp/$(basename ${filename}).bkup-${RANDOM}" 
906     \rm -f "$new_version"
907     exit_on_error "cleaning out new version of file from: $new_version"
908
909     local line
910     local skip_count=0
911     local found_any=
912     while read line; do
913       # don't bother looking at the lines if we're already in skip mode.
914       if [[ $skip_count == 0 ]]; then
915         # find the string they're seeking.
916         if [[ ! "$line" =~ .*${seeker}.* ]]; then
917           # no match.
918           echo "$line" >> "$new_version"
919         else
920           # a match!  start skipping.  we will delete this line and the next N lines.
921           ((skip_count++))
922 #echo first skip count is now $skip_count
923           found_any=yes
924         fi
925       else
926         # we're already skipping.  let's keep going until we hit the limit.
927         ((skip_count++))
928 #echo ongoing skip count is now $skip_count
929         if (( $skip_count > $numlines )); then
930           echo "Done skipping, and back to writing output file."
931           skip_count=0
932         fi
933       fi
934     done < "$filename"
935
936 #echo file we created looks like this:
937 #cat "$new_version"
938
939     if [ ! -z "$found_any" ]; then
940       # put the file back into place under the original name.
941       \mv "$new_version" "$filename"
942       exit_on_error "moving the new version into place in: $filename"
943     else
944       # cannot always be considered an error, but we can at least gripe.
945       echo "Did not find any matches for seeker '$seeker' in file: $filename"
946     fi
947   }
948
949   ##############
950
951   # space 'em all: fixes naming for all of the files of the appropriate types
952   # in the directories specified.  we skip any file with a dot in front, to
953   # respect their hidden nature.  currently the set of files we'll rename is
954   # very boutique; it's in this function, and just happens to be the types of
955   # files we work with a lot.
956   function spacemall() {
957     local -a dirs=("${@}")
958     if [ ${#dirs[@]} -eq 0 ]; then
959       dirs=(.)
960     fi
961
962     local charnfile="$(mktemp $TMP/zz_charn.XXXXXX)"
963 #hmmm: any way to do the below more nicely or reusably?
964 #hmmm: yes!  a variable with a list of files that are considered TEXT_FILE_EXTENSIONS or something like that.
965 #hmmm: yes continued!  also a variable for BINARY_FILE_EXTENSIONS to avoid those, where we need to in other scripts.
966 #hmmm: wait, we actually have a mix here, since this is a renaming function and not a searching function; get it straight!
967 #hmmm: would the composition of those two types of extensions cover all the files i want to rename?  they have to be "important".
968     find "${dirs[@]}" -follow -maxdepth 1 -mindepth 1 -type f -and -not -iname ".[a-zA-Z0-9]*" | \
969         grep -i \
970 "csv\|doc\|docx\|eml\|html\|ics\|jpeg\|jpg\|m4a\|mov\|mp3\|odp\|ods\|odt\|pdf\|png\|ppt\|pptx\|rtf\|txt\|vsd\|vsdx\|wav\|xls\|xlsx\|xml\|zip" | \
971         sed -e 's/^/"/' | sed -e 's/$/"/' | \
972         xargs bash "$FEISTY_MEOW_SCRIPTS/files/spacem.sh"
973     # drop the temp file now that we're done.
974     rm "$charnfile"
975   }
976
977   ##############
978
979   # tty relevant functions...
980
981   # keep_awake: sends a message to the screen from the background.
982   function keep_awake()
983   {
984     # just starts the keep_awake process in the background.
985     bash $FEISTY_MEOW_SCRIPTS/tty/keep_awake_process.sh &
986       # this should leave the job running as %1 or a higher number if there
987       # are pre-existing background jobs.
988   }
989
990   ##############
991
992   # site avenger functions...
993
994   function switchto()
995   {
996     THISDIR="$FEISTY_MEOW_SCRIPTS/site_avenger"
997     source "$FEISTY_MEOW_SCRIPTS/site_avenger/shared_site_mgr.sh"
998     switch_to "$1"
999   }
1000
1001   ##############
1002
1003   # you have hit the borderline functional zone...
1004
1005 #hmmm: not really doing anything yet; ubuntu seems to have changed from pulseaudio in 17.04?
1006   # restarts the sound driver.
1007   function fix_sound_driver() {
1008     # stop bash complaining about blank function body.
1009     local nothing=
1010 #if alsa something
1011 #    sudo service alsasound restart
1012 #elif pulse something
1013 #    sudo pulseaudio -k
1014 #    sudo pulseaudio -D
1015 #else
1016 #    something else...?
1017 #fi
1018
1019   }
1020
1021   # ...and here's the end of the borderline functional zone.
1022
1023   ##############
1024
1025   # NOTE: no more function definitions are allowed after this point.
1026
1027   function function_sentinel()
1028   {
1029     return 0; 
1030   }
1031   
1032   if [ ! -z "$DEBUG_FEISTY_MEOW" ]; then echo "feisty meow function definitions done."; fi
1033
1034   ##############
1035
1036   # test code for set_var_if_undefined.
1037   run_test=0
1038   if [ $run_test != 0 ]; then
1039     echo running tests on set_var_if_undefined.
1040     flagrant=petunia
1041     set_var_if_undefined flagrant forknordle
1042     exit_on_error "testing if defined variable would be whacked"
1043     if [ $flagrant != petunia ]; then
1044       echo set_var_if_undefined failed to leave the test variable alone
1045       exit 1
1046     fi
1047     unset bobblehead_stomper
1048     set_var_if_undefined bobblehead_stomper endurance
1049     if [ $bobblehead_stomper != endurance ]; then
1050       echo set_var_if_undefined failed to set a variable that was not defined yet
1051       exit 1
1052     fi
1053   fi
1054
1055 fi
1056