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