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