cleaned up prep process
[feisty_meow.git] / scripts / core / functions.sh
1 #!/usr/bin/env 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     local to_find="$1"; shift
42     local WHICHER="$(/usr/bin/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_cmd="awk -- '{ print \$4; }'"
297       local field_number=4
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_cmd="sed -n -e \\'s/^[[:space:]]*\([0-9][0-9]*\).*$/\\\\1/p\\'"
305 #echo pidfinder: $pid_finder_cmd
306 #      local pid_finder_cmd="awk -- '{ print \$1; }'"
307       local field_number=1
308     fi
309
310     /bin/ps $EXTRA_DOZER_FLAGS $EXTRA_UNIX_FLAGS $user_flag | tail -n +2 >$PID_DUMP
311 #echo ====
312 #echo got all this stuff in the pid dump file:
313 #cat $PID_DUMP
314 #echo ====
315
316     # search for the pattern the user wants to find, and just pluck the process
317     # ids out of the results.
318     local i
319     for i in "${patterns[@]}"; do
320 #echo "pattern curr is '$i'"
321       PIDS_SOUGHT+=($(cat $PID_DUMP \
322         | grep -i "$i" \
323         | grep -v "$excluder" \
324         | awk -- "{ print \$${field_number}; }" ))
325     done
326 #echo ====
327 #echo pids sought list became:
328 #echo "${PIDS_SOUGHT[@]}"
329 #echo ====
330
331     if [ ${#PIDS_SOUGHT[*]} -ne 0 ]; then
332       local PIDS_SOUGHT2=$(printf -- '%s\n' ${PIDS_SOUGHT[@]} | sort | uniq)
333       PIDS_SOUGHT=()
334       PIDS_SOUGHT=${PIDS_SOUGHT2[*]}
335       echo ${PIDS_SOUGHT[*]}
336     fi
337     /bin/rm $PID_DUMP
338   }
339   
340   # finds all processes matching the pattern specified and shows their full
341   # process listing (whereas psfind just lists process ids).
342   function psa() {
343     if [ -z "$1" ]; then
344       echo "psa finds processes by pattern, but there was no pattern on the command line."
345       return 1
346     fi
347     local -a patterns=("${@}")
348     p=$(psfind "${patterns[@]}")
349     if [ -z "$p" ]; then
350       # no matches.
351       return 0
352     fi
353
354     if [ "${patterns[0]}" == "-u" ]; then
355       # void the two elements with that user flag so we don't use them as patterns.
356       unset patterns[0] patterns[1]=
357     fi
358
359     echo ""
360     echo "Processes matching ${patterns[@]}..."
361     echo ""
362     if [ -n "$IS_DARWIN" ]; then
363       unset fuzil_sentinel
364       for i in $p; do
365         # only print the header the first time.
366         if [ -z "$fuzil_sentinel" ]; then
367           ps $i -w -u
368         else
369           ps $i -w -u | sed -e '1d'
370         fi
371         fuzil_sentinel=true
372       done
373     else 
374       # cases besides mac os x's darwin.
375       if [ "$OS" == "Windows_NT" ]; then
376         # special case for windows.
377         ps | head -1
378         for curr in $p; do
379 #hmmm: currently not working right for windows cygwin.  we're getting proper
380 #      winpids out of the list now, but not able to use them in ps?
381 #      should i be keeping the weirdo pid that we were getting in column 1 and
382 #      use that, except when talking to taskkill?
383 #      need further research.
384           ps -W -p $curr | tail -n +2
385         done
386       else
387         # normal OSes can handle a nice simple query.
388         ps wu $p
389       fi
390     fi
391   }
392   
393   ##############
394
395 #hmmm: holy crowbars, this is an old one.  do we ever still have any need of it?
396   # an unfortunately similarly named function to the above 'ps' as in process
397   # methods, but this 'ps' stands for postscript.  this takes a postscript file
398   # and converts it into pcl3 printer language and then ships it to the printer.
399   # this mostly makes sense for an environment where one's default printer is
400   # pcl.  if the input postscript causes ghostscript to bomb out, there has been
401   # some good success running ps2ps on the input file and using the cleaned
402   # postscript file for printing.
403   function ps2pcl2lpr() {
404     for $i in $*; do
405       gs -sDEVICE=pcl3 -sOutputFile=- -sPAPERSIZE=letter "$i" | lpr -l 
406     done
407   }
408   
409   function screen() {
410     save_terminal_title
411 #hmmm: ugly absolute path here.
412     /usr/bin/screen $*
413     restore_terminal_title
414   }
415   
416   # switches from a /X/path form to an X:/ form.  this also processes cygwin paths.
417   function unix_to_dos_path() {
418     # we usually remove dos slashes in favor of forward slashes.
419     local DOSSYHOME
420     if [[ ! "$OS" =~ ^[Ww][iI][nN] ]]; then
421       # fake this value for non-windows (non-cygwin) platforms.
422       DOSSYHOME="$HOME"
423     else
424       # for cygwin, we must replace the /home/X path with an absolute one, since cygwin
425       # insists on the /home form instead of /c/cygwin/home being possible.  this is
426       # super frustrating and nightmarish.
427       DOSSYHOME="$(cygpath -am "$HOME")"
428     fi
429
430 #    if [ ! -z "$SERIOUS_SLASH_TREATMENT" ]; then
431 #      # unless this flag is set, in which case we force dos slashes.
432 #      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'
433 #    else
434       echo "$1" | sed -e "s?^$HOME?$DOSSYHOME?g" | sed -e 's/\\/\//g' | sed -e 's/\/cygdrive//' | sed -e 's/\/\([a-zA-Z]\)\/\(.*\)/\1:\/\2/'
435 #    fi
436   }
437   
438 #  # switches from an X:/ form to a /cygdrive/X/path form.  this is only useful
439 #  # for the cygwin environment currently.
440 #  function dos_to_unix_path() {
441 #    # we always remove dos slashes in favor of forward slashes.
442 ##old:    echo "$1" | sed -e 's/\\/\//g' | sed -e 's/\([a-zA-Z]\):\/\(.*\)/\/\1\/\2/'
443 #         echo "$1" | sed -e 's/\\/\//g' | sed -e 's/\([a-zA-Z]\):\/\(.*\)/\/cygdrive\/\1\/\2/'
444 #  }
445
446   # returns a successful value (0) if this system is debian or ubuntu.
447   function debian_like() {
448     # decide if we think this is debian or ubuntu or a variant.
449     DEBIAN_LIKE=$( \
450       if [ \
451         ! -z "$(grep -i debian /etc/issue)" -o \
452         ! -z "$(grep -i ubuntu /etc/issue)" -o \
453         ! -z "$(grep -i 'Pop._OS' /etc/issue)" \
454       ]; then echo 1; else echo 0; fi)
455     if [ $DEBIAN_LIKE -eq 1 ]; then
456       # success; this is debianish.
457       return 0
458     else
459       # this seems like some other OS.
460       return 1
461     fi
462   }
463   
464   # this function wraps the normal sudo by ensuring we replace the terminal
465   # label before we launch what they're passing to sudo.  we also preserve
466   # specific variables that enable the main user's ssh credentials to still
467   # be relied on for ssh forwarding, even if the '-i' flag is passed to cause
468   # a fresh shell (which normally doesn't get the launching user's environment
469   # variables).
470   function sudo() {
471     save_terminal_title
472
473     # hoist our X authorization info in case environment is passed along;
474     # this can allow root to use our display to show X.org windows.
475     if [ -z "$IMPORTED_XAUTH" -a ! -z "$DISPLAY" ]; then
476       export IMPORTED_XAUTH="$(xauth list $DISPLAY | head -n 1 | awk '{print $3}')"
477       local REMOVE_IMP_XAUTH=true
478     fi
479
480     # launch sudo with just the variables we want to reach the other side.
481     local varmods=
482     varmods+="OLD_HOME=$HOME "
483     if [ ! -z "$IMPORTED_XAUTH" ]; then varmods+="IMPORTED_XAUTH=$IMPORTED_XAUTH "; fi
484     if [ ! -z "$SSH_AUTH_SOCK" ]; then varmods+="SSH_AUTH_SOCK=$SSH_AUTH_SOCK "; fi
485     /usr/bin/sudo $varmods "$@"
486     retval=$?
487
488     # take the xauth info away again if it wasn't set already.
489     if [ ! -z "$REMOVE_IMP_XAUTH" ]; then
490       unset IMPORTED_XAUTH
491     fi
492     restore_terminal_title
493     return $retval
494   }
495   
496   # trashes the .#blah files that cvs and subversion leave behind when finding conflicts.
497   # this kind of assumes you've already checked them for any salient facts.
498   function clean_cvs_junk() {
499     for i in $*; do
500       find $i -follow -type f -iname ".#*" -exec perl $FEISTY_MEOW_SCRIPTS/files/safedel.pl {} ";" 
501     done
502   }
503
504   # overlay for nechung binary so that we can complain less grossly about it when it's missing.
505   function nechung() {
506     local wheres_nechung=$(whichable nechung)
507     if [ -z "$wheres_nechung" ]; then
508       echo "The nechung oracle program cannot be found.  You may want to consider"
509       echo "rebuilding the feisty meow applications with this command:"
510       echo "bash $FEISTY_MEOW_SCRIPTS/generator/produce_feisty_meow.sh"
511       echo
512     else
513       $wheres_nechung
514     fi
515   }
516   
517   # recreates all the generated files that the feisty meow scripts use.
518   function regenerate() {
519     # do the bootstrapping process again.
520     save_terminal_title
521     echo "regenerating feisty meow script environment."
522     bash $FEISTY_MEOW_SCRIPTS/core/reconfigure_feisty_meow.sh
523     echo
524     # force a full reload by turning off sentinel variables and methods.
525     unset -v CORE_VARIABLES_LOADED FEISTY_MEOW_LOADING_DOCK USER_CUSTOMIZATIONS_LOADED \
526         BUILD_VARS_LOADED
527     unalias CORE_ALIASES_LOADED &>/dev/null
528     unset -f function_sentinel 
529
530     # reuse the original path if we can.
531     if [ ! -z "$FEISTY_MEOW_ORIGINAL_PATH" ]; then
532       export PATH="$FEISTY_MEOW_ORIGINAL_PATH"
533     fi
534
535     # reload feisty meow environment in current shell.
536     log_feisty_meow_event "reloading the feisty meow scripts for $USER in current shell."
537     source "$FEISTY_MEOW_SCRIPTS/core/launch_feisty_meow.sh"
538     # run nechung oracle to give user a new fortune.
539     nechung
540     restore_terminal_title
541   }
542
543   # merges a set of custom scripts into the feisty meow environment.  can be
544   # passed a name to use as the custom scripts source folder (found on path
545   # $FEISTY_MEOW_SCRIPTS/customize/{name}), or it will try to guess the name
546   # by using the login name.
547   function recustomize()
548   {
549     local custom_user="$1"; shift
550     if [ -z "$custom_user" ]; then
551       # default to login name if there was no name provided.
552       custom_user="$(fm_username)"
553         # we do intend to use the login name here to get the login name and to ignore
554         # if the user has sudo root access; we don't want to provide a custom
555         # profile for root.
556     fi
557     # chop off any email address style formatting to leave just the name.
558     custom_user="$(echo "$custom_user" | cut -f1 -d'@')"
559
560     save_terminal_title
561
562     if [ ! -d "$FEISTY_MEOW_SCRIPTS/customize/$custom_user" ]; then
563       echo -e "the customization folder for '$custom_user' is missing:
564
565     $FEISTY_MEOW_SCRIPTS/customize/$custom_user
566
567 we will skip recustomization, but these other customizations are available:
568 "
569       # a little tr and sed magic to fix the carriage returns into commas.
570       local line="$(find $FEISTY_MEOW_SCRIPTS/customize -mindepth 1 -maxdepth 1 -type d -exec basename {} ';' | tr '\n' '&' | sed 's/&/, /g' | sed -e 's/, $//')"
571         # make the line feeds and carriage returns manageable with tr.
572         # convert the ampersand, our weird replacement for EOL, with a comma + space in sed.
573         # last touch with sed removes the last comma.
574       echo "    $line"
575       return 1
576     fi
577
578     # recreate the feisty meow loading dock.
579     regenerate >/dev/null
580
581     # jump into the loading dock and make our custom link.
582     pushd "$FEISTY_MEOW_LOADING_DOCK" &>/dev/null
583     if [ -h custom ]; then
584       # there's an existing link, so remove it.
585       \rm custom
586     fi
587     # make sure we cleaned up the area before we re-link.
588     if [ -h custom -o -d custom -o -f custom ]; then
589       echo "
590 Due to an over-abundance of caution, we are not going to remove an unexpected
591 'custom' object found in the file system.  This object is located in the
592 feisty meow loading dock here: $(pwd)
593 And here is a description of the rogue 'custom' object:
594 "
595       ls -al custom
596       echo "
597 If you are pretty sure that this is just a remnant of an older approach in
598 feisty meow, where we copied the custom directory rather than linking it
599 (and it most likely is just such a bit of cruft of that nature), then please
600 remove that old remnant 'custom' item, for example by saying:
601   /bin/rm -rf \"custom\" ; popd
602 Sorry for the interruption, but we want to make sure this removal wasn't
603 automatic if there is even a small amount of doubt about the issue."
604       return 1
605     fi
606
607     # create the custom folder as a link to the customizations.
608     ln -s "$FEISTY_MEOW_SCRIPTS/customize/$custom_user" custom
609
610     popd &>/dev/null
611
612     # now take into account all the customizations by regenerating the feisty meow environment.
613     regenerate
614
615     restore_terminal_title
616   }
617
618   # generates a random password where the first parameter is the number of characters
619   # in the password (default 20) and the second parameter specifies whether to use
620   # special characters (1) or not (0).
621   # found function at http://legroom.net/2010/05/06/bash-random-password-generator
622   function random_password()
623   {
624     [ "$2" == "0" ] && CHAR="[:alnum:]" || CHAR="[:graph:]"
625     cat /dev/urandom | tr -cd "$CHAR" | head -c ${1:-32}
626     echo
627   }
628
629   function add_cygwin_drive_mounts() {
630     for i in c d e f g h q z ; do
631 #hmmm: improve this by not adding the link if already there, or if the drive is not valid.
632       ln -s /cygdrive/$i $i
633     done
634   }
635
636   ############################
637
638   # takes a file to modify, and then it will replace any occurrences of the
639   # pattern provided as the second parameter with the text in the third
640   # parameter.
641   function replace_pattern_in_file()
642   {
643     local file="$1"; shift
644     local pattern="$1"; shift
645     local replacement="$1"; shift
646     if [ -z "$file" -o -z "$pattern" -o -z "$replacement" ]; then
647       echo "replace_pattern_in_file: needs a filename, a pattern to replace, and the"
648       echo "text to replace that pattern with."
649       return 1
650     fi
651     sed -i -e "s%$pattern%$replacement%g" "$file"
652   }
653
654   # similar to replace_pattern_in_file, but also will add the new value
655   # when the old one did not already exist in the file.
656   function replace_if_exists_or_add()
657   {
658     local file="$1"; shift
659     local phrase="$1"; shift
660     local replacement="$1"; shift
661     if [ -z "$file" -o ! -f "$file" -o -z "$phrase" -o -z "$replacement" ]; then
662       echo "replace_if_exists_or_add: needs a filename, a phrase to replace, and the"
663       echo "text to replace that phrase with."
664       return 1
665     fi
666     grep "$phrase" "$file" >/dev/null
667     # replace if the phrase is there, otherwise add it.
668     if [ $? -eq 0 ]; then
669       replace_pattern_in_file "$file" "$phrase" "$replacement"
670     else
671       # this had better be the complete line.
672       echo "$replacement" >>"$file"
673     fi
674   }
675
676   ############################
677
678   # finds a variable (first parameter) in a particular property file
679   # (second parameter).  the expected format for the file is:
680   # varX=valueX
681   function seek_variable()
682   {
683     local find_var="$1"; shift
684     local file="$1"; shift
685     if [ -z "$find_var" -o -z "$file" -o ! -f "$file" ]; then
686       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
687       return 1
688     fi
689   
690     while read line; do
691       if [ ${#line} -eq 0 ]; then continue; fi
692       # split the line into the variable name and value.
693       IFS='=' read -a assignment <<< "$line"
694       local var="${assignment[0]}"
695       local value="${assignment[1]}"
696       if [ "${value:0:1}" == '"' ]; then
697         # assume the entry was in quotes and remove them.
698         value="${value:1:$((${#value} - 2))}"
699       fi
700       if [ "$find_var" == "$var" ]; then
701         echo "$value"
702       fi
703     done < "$file"
704   }
705   
706   # finds a variable (first parameter) in a particular XML format file
707   # (second parameter).  the expected format for the file is:
708   # ... name="varX" value="valueX" ...
709   function seek_variable_in_xml()
710   {
711     local find_var="$1"; shift
712     local file="$1"; shift
713     if [ -z "$find_var" -o -z "$file" -o ! -f "$file" ]; then
714       echo "seek_variable_in_xml: needs two parameters, firstly a variable name, and"
715       echo "secondly an XML file where the variable's value will be sought."
716       return 1
717     fi
718   
719     while read line; do
720       if [ ${#line} -eq 0 ]; then continue; fi
721       # process the line to make it more conventional looking.
722       line="$(echo "$line" | sed -e 's/.*name="\([^"]*\)" value="\([^"]*\)"/\1=\2/')"
723       # split the line into the variable name and value.
724       IFS='=' read -a assignment <<< "$line"
725       local var="${assignment[0]}"
726       local value="${assignment[1]}"
727       if [ "${value:0:1}" == '"' ]; then
728         # assume the entry was in quotes and remove them.
729         value="${value:1:$((${#value} - 2))}"
730       fi
731       if [ "$find_var" == "$var" ]; then
732         echo "$value"
733       fi
734     done < "$file"
735   }
736   
737   ############################
738
739   # goes to a particular directory passed as parameter 1, and then removes all
740   # the parameters after that from that directory.
741   function push_whack_pop()
742   {
743     local dir="$1"; shift
744     pushd "$dir" &>/dev/null
745     if [ $? -ne 0 ]; then echo failed to enter dir--quitting.; fi
746     rm -rf $* &>/dev/null
747     if [ $? -ne 0 ]; then echo received a failure code when removing.; fi
748     popd &>/dev/null
749   }
750
751   ##############
752
753 # new breed of definer functions goes here.  still in progress.
754
755   # defines an alias and remembers that this is a new or modified definition.
756   # if the feisty meow codebase is unloaded, then so are all the aliases that
757   # were defined.
758   function define_yeti_alias()
759   {
760 # if alias exists already, save old value for restore,
761 # otherwise save null value for restore,
762 # have to handle unaliasing if there was no prior value of one
763 # we newly defined.
764 # add alias name to a list of feisty defined aliases.
765
766 #hmmm: first implem, just do the alias and get that working...
767 alias "${@}"
768
769
770 return 0
771   }
772
773   ##############
774
775 #hmmm: this points to an extended functions file being needed; not all of these are core.
776
777   # displays documentation in "md" formatted files.
778   function show_md()
779   {
780     local file="$1"; shift
781     pandoc "$file" | lynx -stdin
782   }
783
784   ##############
785
786   # just shows a separator line for an 80 column console, or uses the first
787   # parameter as the number of columns to expect.
788   function separator()
789   {
790     count=$1; shift
791     if [ -z "$count" ]; then
792       count=$(($COLUMNS - 1))
793     fi
794     echo
795     local i
796     for ((i=0; i < $count; i++)); do
797       echo -n "="
798     done
799     echo
800     echo
801   }
802   # alias for separator.
803   function sep()
804   {
805     separator $*
806   }
807
808   ##############
809
810   # count the number of sub-directories in a directory and echo the result.
811   function count_directories()
812   {
813     local subbydir="$1"; shift
814     numdirs="$(find "$subbydir" -mindepth 1 -maxdepth 1 -type d | wc -l)"
815     echo $numdirs
816   }
817
818   # takes a string and capitalizes just the first character.  any capital letters in the remainder of
819   # the string are made lower case.  the processed string is returned by an echo.
820   function capitalize_first_char()
821   {
822     local to_dromedary="$1"; shift
823     to_dromedary="$(tr '[:lower:]' '[:upper:]' <<< ${to_dromedary:0:1})$(tr '[:upper:]' '[:lower:]' <<< ${to_dromedary:1})"
824     echo "$to_dromedary"
825   }
826
827   # given a source path and a target path, this will make a symbolic link from
828   # the source to the destination, but only if the source actually exists.
829   function make_safe_link()
830   {
831     local src="$1"; shift
832     local target="$1"; shift
833   
834     if [ -d "$src" ]; then
835       ln -s "$src" "$target"
836       exit_on_error "Creating symlink from '$src' to '$target'"
837     fi
838     echo "Created symlink from '$src' to '$target'."
839   }
840
841   # pretty prints the json files provided as parameters.
842   function clean_json()
843   {
844     if [ -z "$*" ]; then return; fi
845     local show_list=()
846     while true; do
847       local file="$1"; shift
848       if [ -z "$file" ]; then break; fi
849       if [ ! -f "$file" ]; then "echo File '$file' does not exist."; continue; fi
850       temp_out="$TMP/$file.view"
851       cat "$file" | python -m json.tool > "$temp_out"
852       show_list+=($temp_out)
853       continue_on_error "pretty printing '$file'"
854     done
855     filedump "${show_list[@]}"
856     rm "${show_list[@]}"
857   }
858
859   function json_text()
860   {
861     # only print our special headers or text fields.
862     local CR=$'\r'
863     local LF=$'\n'
864     clean_json $* |
865         grep -i "\"text\":\|^=.*" | 
866         sed -e "s/\\\\r/$CR/g" -e "s/\\\\n/\\$LF/g"
867   }
868
869   ##############
870
871   # echoes the machine's hostname.  can be used like so:
872   #   local my_host=$(get_hostname)
873   function get_hostname()
874   {
875     # there used to be more variation in how to do this, but adopting mingw
876     # and cygwin tools really helped out.
877     local this_host=unknown
878     if [ "$OS" == "Windows_NT" ]; then
879       this_host=$(hostname)
880     elif [ ! -z "$(echo $MACHTYPE | grep apple)" ]; then
881       this_host=$(hostname)
882     elif [ ! -z "$(echo $MACHTYPE | grep suse)" ]; then
883       this_host=$(hostname --long)
884     elif [ -x "$(whichable hostname)" ]; then
885       this_host=$(hostname)
886     fi
887     echo "$this_host"
888   }
889
890   # makes sure that the provided "folder" is a directory and is writable.
891   function test_writeable()
892   {
893     local folder="$1"; shift
894     if [ ! -d "$folder" -o ! -w "$folder" ]; then return 1; fi
895     return 0
896   }
897
898   ##############
899
900   # given a filename and a string to seek and a number of lines, then this
901   # function will remove the first occurrence of a line in the file that
902   # matches the string, and it will also axe the next N lines as specified.
903   function create_chomped_copy_of_file()
904   {
905     local filename="$1"; shift
906     local seeker="$1"; shift
907     local numlines=$1; shift
908
909 #echo into create_chomped_copy...
910 #var filename seeker numlines 
911
912     # make a backup first, oy.
913     \cp -f "$filename" "/tmp/$(basename ${filename}).bkup-${RANDOM}" 
914     exit_on_error "backing up file: $filename"
915
916     # make a temp file to write to before we move file into place in bind.
917     local new_version="/tmp/$(basename ${filename}).bkup-${RANDOM}" 
918     \rm -f "$new_version"
919     exit_on_error "cleaning out new version of file from: $new_version"
920
921     local line
922     local skip_count=0
923     local found_any=
924     while read line; do
925       # don't bother looking at the lines if we're already in skip mode.
926       if [[ $skip_count == 0 ]]; then
927         # find the string they're seeking.
928         if [[ ! "$line" =~ .*${seeker}.* ]]; then
929           # no match.
930           echo "$line" >> "$new_version"
931         else
932           # a match!  start skipping.  we will delete this line and the next N lines.
933           ((skip_count++))
934 #echo first skip count is now $skip_count
935           found_any=yes
936         fi
937       else
938         # we're already skipping.  let's keep going until we hit the limit.
939         ((skip_count++))
940 #echo ongoing skip count is now $skip_count
941         if (( $skip_count > $numlines )); then
942           echo "Done skipping, and back to writing output file."
943           skip_count=0
944         fi
945       fi
946     done < "$filename"
947
948 #echo file we created looks like this:
949 #cat "$new_version"
950
951     if [ ! -z "$found_any" ]; then
952       # put the file back into place under the original name.
953       \mv "$new_version" "$filename"
954       exit_on_error "moving the new version into place in: $filename"
955     else
956       # cannot always be considered an error, but we can at least gripe.
957       echo "Did not find any matches for seeker '$seeker' in file: $filename"
958     fi
959   }
960
961   ##############
962
963   # space 'em all: fixes naming for all of the files of the appropriate types
964   # in the directories specified.  we skip any file with a dot in front, to
965   # respect their hidden nature.  currently the set of files we'll rename is
966   # very boutique; it's in this function, and just happens to be the types of
967   # files we work with a lot.
968   function spacemall() {
969     local -a dirs=("${@}")
970     if [ ${#dirs[@]} -eq 0 ]; then
971       dirs=(.)
972     fi
973
974     local charnfile="$(mktemp $TMP/zz_charn.XXXXXX)"
975 #hmmm: any way to do the below more nicely or reusably?
976 #hmmm: yes!  a variable with a list of files that are considered TEXT_FILE_EXTENSIONS or something like that.
977 #hmmm: yes continued!  also a variable for BINARY_FILE_EXTENSIONS to avoid those, where we need to in other scripts.
978 #hmmm: wait, we actually have a mix here, since this is a renaming function and not a searching function; get it straight!
979 #hmmm: would the composition of those two types of extensions cover all the files i want to rename?  they have to be "important".
980     find "${dirs[@]}" -follow -maxdepth 1 -mindepth 1 -type f -and -not -iname ".[a-zA-Z0-9]*" | \
981         grep -i \
982 "csv\|doc\|docx\|eml\|html\|ics\|jpeg\|jpg\|m4a\|mov\|mp3\|odp\|ods\|odt\|pdf\|png\|ppt\|pptx\|rtf\|txt\|vsd\|vsdx\|wav\|webp\|xls\|xlsx\|xml\|zip" | \
983         sed -e 's/^/"/' | sed -e 's/$/"/' | \
984         xargs bash "$FEISTY_MEOW_SCRIPTS/files/spacem.sh"
985     # drop the temp file now that we're done.
986     rm "$charnfile"
987   }
988
989   ##############
990
991   # tty relevant functions...
992
993   # keep_awake: sends a message to the screen from the background.
994   function keep_awake()
995   {
996     # just starts the keep_awake process in the background.
997     bash $FEISTY_MEOW_SCRIPTS/tty/keep_awake_process.sh &
998       # this should leave the job running as %1 or a higher number if there
999       # are pre-existing background jobs.
1000   }
1001
1002   ##############
1003
1004   # site avenger functions...
1005
1006   function switchto()
1007   {
1008     THISDIR="$FEISTY_MEOW_SCRIPTS/site_avenger"
1009     source "$FEISTY_MEOW_SCRIPTS/site_avenger/shared_site_mgr.sh"
1010     switch_to "$1"
1011   }
1012
1013   ##############
1014
1015   # you have hit the borderline functional zone...
1016
1017 #hmmm: not really doing anything yet; ubuntu seems to have changed from pulseaudio in 17.04?
1018   # restarts the sound driver.
1019   function fix_sound_driver() {
1020     # stop bash complaining about blank function body.
1021     local nothing=
1022 #if alsa something
1023 #    sudo service alsasound restart
1024 #elif pulse something
1025 #    sudo pulseaudio -k
1026 #    sudo pulseaudio -D
1027 #else
1028 #    something else...?
1029 #fi
1030
1031   }
1032
1033   # ...and here's the end of the borderline functional zone.
1034
1035   ##############
1036
1037   # NOTE: no more function definitions are allowed after this point.
1038
1039   function function_sentinel()
1040   {
1041     return 0; 
1042   }
1043   
1044   if [ ! -z "$DEBUG_FEISTY_MEOW" ]; then echo "feisty meow function definitions done."; fi
1045
1046   ##############
1047
1048   # test code for set_var_if_undefined.
1049   run_test=0
1050   if [ $run_test != 0 ]; then
1051     echo running tests on set_var_if_undefined.
1052     flagrant=petunia
1053     set_var_if_undefined flagrant forknordle
1054     exit_on_error "testing if defined variable would be whacked"
1055     if [ $flagrant != petunia ]; then
1056       echo set_var_if_undefined failed to leave the test variable alone
1057       exit 1
1058     fi
1059     unset bobblehead_stomper
1060     set_var_if_undefined bobblehead_stomper endurance
1061     if [ $bobblehead_stomper != endurance ]; then
1062       echo set_var_if_undefined failed to set a variable that was not defined yet
1063       exit 1
1064     fi
1065   fi
1066
1067 fi
1068