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