version 1.40.130 release
[feisty_meow.git] / scripts / core / functions.sh
1 #!/bin/bash
2
3 # This defines some general, useful functions.
4
5 #hmmm: starting to get a bit beefy in here.  perhaps there is a good way to refactor the functions into more specific folders, if they aren't really totally general purpose?
6
7 ##############
8
9 # test whether we've been here before or not.
10 skip_all=
11 type function_sentinel &>/dev/null
12 if [ $? -eq 0 ]; then
13   # there was no error, so we can skip the inits.
14   if [ ! -z "$DEBUG_FEISTY_MEOW" ]; then
15     echo "skipping function definitions, because already defined."
16   fi
17   skip_all=yes
18 else
19   skip_all=
20 fi
21
22 if [ -z "$skip_all" ]; then
23
24   if [ ! -z "$DEBUG_FEISTY_MEOW" ]; then
25     echo "feisty meow function definitions beginning now..."
26   fi
27
28   # a handy little method that can be used for date strings.  it was getting
29   # really tiresome how many different ways the script did the date formatting.
30   function date_stringer() {
31     local sep="$1"; shift
32     if [ -z "$sep" ]; then sep='_'; fi
33     date +"%Y$sep%m$sep%d$sep%H%M$sep%S" | tr -d '/\n/'
34   }
35   
36   # a wrapper for the which command that finds items on the path.  some OSes
37   # do not provide which, so we want to not be spewing errors when that
38   # happens.
39   function whichable()
40   {
41     to_find="$1"; shift
42     local WHICHER="$(\which which 2>/dev/null)"
43 #>&2 echo "got whicher as: $WHICHER"
44     if [ $? -ne 0 ]; then
45       # there is no which command here.  we produce nothing due to this.
46       echo
47       return 2
48     fi
49     local sporkenz  # must be defined local here, before call, or we don't get exit value?!
50     sporkenz=$($WHICHER "$to_find" 2>/dev/null)
51 #>&2 echo "broken with this line, but here is exit val: $?"
52     local err=$?
53 #>&2 echo "got whicher as: $WHICHER"
54     echo $sporkenz
55     return $err
56   }
57
58   # makes a directory of the name specified and then tries to change the
59   # current directory to that directory.
60   function mcd() {
61     if [ ! -d "$1" ]; then mkdir -p "$1"; fi
62     cd "$1"
63   }
64
65   # returns true if the variable is an array.
66   function is_array() {
67     [[ "$(declare -p $1)" =~ "declare -a" ]]
68   }
69
70   # returns true if the name provided is a defined alias.
71   function is_alias() {
72     alias $1 &>/dev/null
73     return $?
74   }
75
76   # makes the status of pipe number N (passed as first parameter) into the
77   # main return value (i.e., the value for $?).  this is super handy to avoid
78   # repeating the awkward looking code below in multiple places.
79   function promote_pipe_return()
80   {
81     ( exit ${PIPESTATUS[$1]} )
82   }
83
84   ##############
85
86   function fm_username()
87   {
88     # see if we can get the user name from the login name.  oddly this sometimes doesn't work.
89     local custom_user="$(logname 2>/dev/null)"
90     if [ -z "$custom_user" ]; then
91       # try the normal unix user variable.
92       custom_user="$USER"
93     fi
94     if [ -z "$custom_user" ]; then
95       # try the windows user variable.
96       custom_user="$USERNAME"
97     fi
98     echo "$custom_user"
99   }
100
101   ##############
102
103   # displays the value of a variable in bash friendly format.
104   function var() {
105     HOLDIFS="$IFS"
106     IFS=""
107     while true; do
108       local varname="$1"; shift
109       if [ -z "$varname" ]; then
110         break
111       fi
112
113       if is_alias "$varname"; then
114 #echo found $varname is alias
115         local tmpfile="$(mktemp $TMP/aliasout.XXXXXX)"
116         alias $varname | sed -e 's/.*=//' >$tmpfile
117         echo "alias $varname=$(cat $tmpfile)"
118         \rm $tmpfile
119       elif [ -z "${!varname}" ]; then
120         echo "$varname undefined"
121       else
122         if is_array "$varname"; then
123 #echo found $varname is array var 
124           local temparray
125           eval temparray="(\${$varname[@]})"
126           echo "$varname=(${temparray[@]})"
127 #hmmm: would be nice to print above with elements enclosed in quotes, so that we can properly
128 # see ones that have spaces in them.
129         else
130 #echo found $varname is simple
131           echo "$varname=${!varname}"
132         fi
133       fi
134     done | sort
135     IFS="$HOLDIFS"
136   }
137
138   ##############
139
140   # when passed a list of things, this will return the unique items from that list as an echo.
141   function uniquify()
142   {
143     # do the uniquification: split the space separated items into separate lines, then
144     # sort the list, then run the uniq tool on the list.  results will be packed back onto
145     # one line when invoked like: local fredlist="$(uniquify a b c e d a e f a e d b)"
146     echo $* | tr ' ' '\n' | sort | uniq
147   }
148
149   # sets the variable in parameter 1 to the value in parameter 2, but only if
150   # that variable was undefined.
151   function set_var_if_undefined()
152   {
153     local var_name="$1"; shift
154     local var_value="$1"; shift
155     if [ -z "${!var_name}" ]; then
156       eval export $var_name="$var_value"
157     fi
158   }
159
160   ##############
161
162   function success_sound()
163   {
164     if [ ! -z "$CLAM_FINISH_SOUND" ]; then
165       bash $FEISTY_MEOW_SCRIPTS/multimedia/sound_play.sh "$CLAM_FINISH_SOUND"
166     fi
167   }
168
169   function error_sound()
170   {
171     if [ ! -z "$CLAM_ERROR_SOUND" ]; then
172       bash $FEISTY_MEOW_SCRIPTS/multimedia/sound_play.sh "$CLAM_ERROR_SOUND"
173     fi
174   }
175
176   ##############
177
178   # echoes the maximum number of columns that the terminal supports.  usually
179   # anything you print to the terminal with length less than (but not equal to)
180   # maxcols will never wrap.
181   function get_maxcols()
182   {
183     # calculate the number of columsn in the terminal.
184     local cols=$(stty size | awk '{print $2}')
185     echo $cols
186   }
187
188   ##############
189
190   # checks the result of the last command that was run, and if that failed,
191   # then this complains and exits from bash.  the function parameters are
192   # used as the message to print as a complaint.
193   function exit_on_error()
194   {
195     if [ $? -ne 0 ]; then
196       echo -e "\n\nan important action failed and this script will stop:\n\n$*\n\n*** Exiting script..."
197       error_sound
198       exit 1
199     fi
200   }
201
202   # like exit_on_error, but will keep going after complaining.
203   function continue_on_error()
204   {
205     if [ $? -ne 0 ]; then
206       echo -e "\n\na problem occurred, but we can continue:\n\n$*\n\n=> Continuing script..."
207       error_sound
208     fi
209   }
210
211   ##############
212
213   # accepts any number of arguments and outputs them to the feisty meow event log.
214   function log_feisty_meow_event()
215   {
216     echo -e "$(date_stringer) -- ${USER}@$(hostname): $*" >> "$FEISTY_MEOW_EVENT_LOG"
217   }
218
219   ##############
220
221   # wraps secure shell with some parameters we like, most importantly to enable X forwarding.
222   function ssh()
223   {
224     local args=($@)
225     save_terminal_title  # remember the current terminal title.
226     /usr/bin/ssh -C "${args[@]}"
227 #hmmm: removed -Y flag because considered dangerous to trust remote hosts to not abuse our X session.
228     restore_terminal_title
229   }
230
231   # this version of ssh preserves the use of the -Y flag for when X forwarding is needed.
232   function yssh()
233   {
234     local args=($@)
235     save_terminal_title  # remember the current terminal title.
236     /usr/bin/ssh -Y "${args[@]}"
237     restore_terminal_title
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+="OLD_HOME=$HOME "
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         BUILD_VARS_LOADED
514     unalias CORE_ALIASES_LOADED &>/dev/null
515     unset -f function_sentinel 
516
517     # reuse the original path if we can.
518     if [ ! -z "$FEISTY_MEOW_ORIGINAL_PATH" ]; then
519       export PATH="$FEISTY_MEOW_ORIGINAL_PATH"
520     fi
521
522     # reload feisty meow environment in current shell.
523     log_feisty_meow_event "reloading the feisty meow scripts for $USER in current shell."
524     source "$FEISTY_MEOW_SCRIPTS/core/launch_feisty_meow.sh"
525     # run nechung oracle to give user a new fortune.
526     nechung
527     restore_terminal_title
528   }
529
530   # merges a set of custom scripts into the feisty meow environment.  can be
531   # passed a name to use as the custom scripts source folder (found on path
532   # $FEISTY_MEOW_SCRIPTS/customize/{name}), or it will try to guess the name
533   # by using the login name.
534   function recustomize()
535   {
536     local custom_user="$1"; shift
537     if [ -z "$custom_user" ]; then
538       # default to login name if there was no name provided.
539       custom_user="$(fm_username)"
540         # we do intend to use the login name here to get the login name and to ignore
541         # if the user has sudo root access; we don't want to provide a custom
542         # profile for root.
543     fi
544     # chop off any email address style formatting to leave just the name.
545     custom_user="$(echo "$custom_user" | cut -f1 -d'@')"
546
547     save_terminal_title
548
549     if [ ! -d "$FEISTY_MEOW_SCRIPTS/customize/$custom_user" ]; then
550       echo -e "the customization folder for '$custom_user' is missing:
551
552     $FEISTY_MEOW_SCRIPTS/customize/$custom_user
553
554 we will skip recustomization, but these other customizations are available:
555 "
556       # a little tr and sed magic to fix the carriage returns into commas.
557       local line="$(find $FEISTY_MEOW_SCRIPTS/customize -mindepth 1 -maxdepth 1 -type d -exec basename {} ';' | tr '\n' '&' | sed 's/&/, /g' | sed -e 's/, $//')"
558         # make the line feeds and carriage returns manageable with tr.
559         # convert the ampersand, our weird replacement for EOL, with a comma + space in sed.
560         # last touch with sed removes the last comma.
561       echo "    $line"
562       return 1
563     fi
564
565     # recreate the feisty meow loading dock.
566     regenerate >/dev/null
567
568     # jump into the loading dock and make our custom link.
569     pushd "$FEISTY_MEOW_LOADING_DOCK" &>/dev/null
570     if [ -h custom ]; then
571       # there's an existing link, so remove it.
572       \rm custom
573     fi
574     # make sure we cleaned up the area before we re-link.
575     if [ -h custom -o -d custom -o -f custom ]; then
576       echo "
577 Due to an over-abundance of caution, we are not going to remove an unexpected
578 'custom' object found in the file system.  This object is located in the
579 feisty meow loading dock here: $(pwd)
580 And here is a description of the rogue 'custom' object:
581 "
582       ls -al custom
583       echo "
584 If you are pretty sure that this is just a remnant of an older approach in
585 feisty meow, where we copied the custom directory rather than linking it
586 (and it most likely is just such a bit of cruft of that nature), then please
587 remove that old remnant 'custom' item, for example by saying:
588   /bin/rm -rf \"custom\" ; popd
589 Sorry for the interruption, but we want to make sure this removal wasn't
590 automatic if there is even a small amount of doubt about the issue."
591       return 1
592     fi
593
594     # create the custom folder as a link to the customizations.
595     ln -s "$FEISTY_MEOW_SCRIPTS/customize/$custom_user" custom
596
597     popd &>/dev/null
598
599     # now take into account all the customizations by regenerating the feisty meow environment.
600     regenerate
601
602     restore_terminal_title
603   }
604
605   # generates a random password where the first parameter is the number of characters
606   # in the password (default 20) and the second parameter specifies whether to use
607   # special characters (1) or not (0).
608   # found function at http://legroom.net/2010/05/06/bash-random-password-generator
609   function random_password()
610   {
611     [ "$2" == "0" ] && CHAR="[:alnum:]" || CHAR="[:graph:]"
612     cat /dev/urandom | tr -cd "$CHAR" | head -c ${1:-32}
613     echo
614   }
615
616   function add_cygwin_drive_mounts() {
617     for i in c d e f g h q z ; do
618 #hmmm: improve this by not adding the link if already there, or if the drive is not valid.
619       ln -s /cygdrive/$i $i
620     done
621   }
622
623   ############################
624
625   # takes a file to modify, and then it will replace any occurrences of the
626   # pattern provided as the second parameter with the text in the third
627   # parameter.
628   function replace_pattern_in_file()
629   {
630     local file="$1"; shift
631     local pattern="$1"; shift
632     local replacement="$1"; shift
633     if [ -z "$file" -o -z "$pattern" -o -z "$replacement" ]; then
634       echo "replace_pattern_in_file: needs a filename, a pattern to replace, and the"
635       echo "text to replace that pattern with."
636       return 1
637     fi
638     sed -i -e "s%$pattern%$replacement%g" "$file"
639   }
640
641   # similar to replace_pattern_in_file, but also will add the new value
642   # when the old one did not already exist in the file.
643   function replace_if_exists_or_add()
644   {
645     local file="$1"; shift
646     local phrase="$1"; shift
647     local replacement="$1"; shift
648     if [ -z "$file" -o ! -f "$file" -o -z "$phrase" -o -z "$replacement" ]; then
649       echo "replace_if_exists_or_add: needs a filename, a phrase to replace, and the"
650       echo "text to replace that phrase with."
651       return 1
652     fi
653     grep "$phrase" "$file" >/dev/null
654     # replace if the phrase is there, otherwise add it.
655     if [ $? -eq 0 ]; then
656       replace_pattern_in_file "$file" "$phrase" "$replacement"
657     else
658       # this had better be the complete line.
659       echo "$replacement" >>"$file"
660     fi
661   }
662
663   ############################
664
665   # finds a variable (first parameter) in a particular property file
666   # (second parameter).  the expected format for the file is:
667   # varX=valueX
668   function seek_variable()
669   {
670     local find_var="$1"; shift
671     local file="$1"; shift
672     if [ -z "$find_var" -o -z "$file" -o ! -f "$file" ]; then
673       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
674       return 1
675     fi
676   
677     while read line; do
678       if [ ${#line} -eq 0 ]; then continue; fi
679       # split the line into the variable name and value.
680       IFS='=' read -a assignment <<< "$line"
681       local var="${assignment[0]}"
682       local value="${assignment[1]}"
683       if [ "${value:0:1}" == '"' ]; then
684         # assume the entry was in quotes and remove them.
685         value="${value:1:$((${#value} - 2))}"
686       fi
687       if [ "$find_var" == "$var" ]; then
688         echo "$value"
689       fi
690     done < "$file"
691   }
692   
693   # finds a variable (first parameter) in a particular XML format file
694   # (second parameter).  the expected format for the file is:
695   # ... name="varX" value="valueX" ...
696   function seek_variable_in_xml()
697   {
698     local find_var="$1"; shift
699     local file="$1"; shift
700     if [ -z "$find_var" -o -z "$file" -o ! -f "$file" ]; then
701       echo "seek_variable_in_xml: needs two parameters, firstly a variable name, and"
702       echo "secondly an XML file where the variable's value will be sought."
703       return 1
704     fi
705   
706     while read line; do
707       if [ ${#line} -eq 0 ]; then continue; fi
708       # process the line to make it more conventional looking.
709       line="$(echo "$line" | sed -e 's/.*name="\([^"]*\)" value="\([^"]*\)"/\1=\2/')"
710       # split the line into the variable name and value.
711       IFS='=' read -a assignment <<< "$line"
712       local var="${assignment[0]}"
713       local value="${assignment[1]}"
714       if [ "${value:0:1}" == '"' ]; then
715         # assume the entry was in quotes and remove them.
716         value="${value:1:$((${#value} - 2))}"
717       fi
718       if [ "$find_var" == "$var" ]; then
719         echo "$value"
720       fi
721     done < "$file"
722   }
723   
724   ############################
725
726   # goes to a particular directory passed as parameter 1, and then removes all
727   # the parameters after that from that directory.
728   function push_whack_pop()
729   {
730     local dir="$1"; shift
731     pushd "$dir" &>/dev/null
732     if [ $? -ne 0 ]; then echo failed to enter dir--quitting.; fi
733     rm -rf $* &>/dev/null
734     if [ $? -ne 0 ]; then echo received a failure code when removing.; fi
735     popd &>/dev/null
736   }
737
738   ##############
739
740 # new breed of definer functions goes here.  still in progress.
741
742   # defines an alias and remembers that this is a new or modified definition.
743   # if the feisty meow codebase is unloaded, then so are all the aliases that
744   # were defined.
745   function define_yeti_alias()
746   {
747 # if alias exists already, save old value for restore,
748 # otherwise save null value for restore,
749 # have to handle unaliasing if there was no prior value of one
750 # we newly defined.
751 # add alias name to a list of feisty defined aliases.
752
753 #hmmm: first implem, just do the alias and get that working...
754 alias "${@}"
755
756
757 return 0
758   }
759
760   ##############
761
762 #hmmm: this points to an extended functions file being needed; not all of these are core.
763
764   # displays documentation in "md" formatted files.
765   function show_md()
766   {
767     local file="$1"; shift
768     pandoc "$file" | lynx -stdin
769   }
770
771   ##############
772
773   # just shows a separator line for an 80 column console, or uses the first
774   # parameter as the number of columns to expect.
775   function separator()
776   {
777     count=$1; shift
778     if [ -z "$count" ]; then
779       count=$(($COLUMNS - 1))
780     fi
781     echo
782     local i
783     for ((i=0; i < $count; i++)); do
784       echo -n "="
785     done
786     echo
787     echo
788   }
789   # alias for separator.
790   function sep()
791   {
792     separator $*
793   }
794
795   ##############
796
797   # count the number of sub-directories in a directory and echo the result.
798   function count_directories()
799   {
800     local subbydir="$1"; shift
801     numdirs="$(find "$subbydir" -mindepth 1 -maxdepth 1 -type d | wc -l)"
802     echo $numdirs
803   }
804
805   # takes a string and capitalizes just the first character.  any capital letters in the remainder of
806   # the string are made lower case.  the processed string is returned by an echo.
807   function capitalize_first_char()
808   {
809     local to_dromedary="$1"; shift
810     to_dromedary="$(tr '[:lower:]' '[:upper:]' <<< ${to_dromedary:0:1})$(tr '[:upper:]' '[:lower:]' <<< ${to_dromedary:1})"
811     echo "$to_dromedary"
812   }
813
814   # given a source path and a target path, this will make a symbolic link from
815   # the source to the destination, but only if the source actually exists.
816   function make_safe_link()
817   {
818     local src="$1"; shift
819     local target="$1"; shift
820   
821     if [ -d "$src" ]; then
822       ln -s "$src" "$target"
823       exit_on_error "Creating symlink from '$src' to '$target'"
824     fi
825     echo "Created symlink from '$src' to '$target'."
826   }
827
828   # pretty prints the json files provided as parameters.
829   function clean_json()
830   {
831     if [ -z "$*" ]; then return; fi
832     local show_list=()
833     while true; do
834       local file="$1"; shift
835       if [ -z "$file" ]; then break; fi
836       if [ ! -f "$file" ]; then "echo File '$file' does not exist."; continue; fi
837       temp_out="$TMP/$file.view"
838       cat "$file" | python -m json.tool > "$temp_out"
839       show_list+=($temp_out)
840       continue_on_error "pretty printing '$file'"
841     done
842     filedump "${show_list[@]}"
843     rm "${show_list[@]}"
844   }
845
846   function json_text()
847   {
848     # only print our special headers or text fields.
849     local CR=$'\r'
850     local LF=$'\n'
851     clean_json $* |
852         grep -i "\"text\":\|^=.*" | 
853         sed -e "s/\\\\r/$CR/g" -e "s/\\\\n/\\$LF/g"
854   }
855
856   ##############
857
858   # echoes the machine's hostname.  can be used like so:
859   #   local my_host=$(get_hostname)
860   function get_hostname()
861   {
862     # there used to be more variation in how to do this, but adopting mingw
863     # and cygwin tools really helped out.
864     local this_host=unknown
865     if [ "$OS" == "Windows_NT" ]; then
866       this_host=$(hostname)
867     elif [ ! -z "$(echo $MACHTYPE | grep apple)" ]; then
868       this_host=$(hostname)
869     elif [ ! -z "$(echo $MACHTYPE | grep suse)" ]; then
870       this_host=$(hostname --long)
871     elif [ -x "$(whichable hostname)" ]; then
872       this_host=$(hostname)
873     fi
874     echo "$this_host"
875   }
876
877   # makes sure that the provided "folder" is a directory and is writable.
878   function test_writeable()
879   {
880     local folder="$1"; shift
881     if [ ! -d "$folder" -o ! -w "$folder" ]; then return 1; fi
882     return 0
883   }
884
885   ##############
886
887   # given a filename and a string to seek and a number of lines, then this
888   # function will remove the first occurrence of a line in the file that
889   # matches the string, and it will also axe the next N lines as specified.
890   function create_chomped_copy_of_file()
891   {
892     local filename="$1"; shift
893     local seeker="$1"; shift
894     local numlines=$1; shift
895
896 #echo into create_chomped_copy...
897 #var filename seeker numlines 
898
899     # make a backup first, oy.
900     \cp -f "$filename" "/tmp/$(basename ${filename}).bkup-${RANDOM}" 
901     exit_on_error "backing up file: $filename"
902
903     # make a temp file to write to before we move file into place in bind.
904     local new_version="/tmp/$(basename ${filename}).bkup-${RANDOM}" 
905     \rm -f "$new_version"
906     exit_on_error "cleaning out new version of file from: $new_version"
907
908     local line
909     local skip_count=0
910     local found_any=
911     while read line; do
912       # don't bother looking at the lines if we're already in skip mode.
913       if [[ $skip_count == 0 ]]; then
914         # find the string they're seeking.
915         if [[ ! "$line" =~ .*${seeker}.* ]]; then
916           # no match.
917           echo "$line" >> "$new_version"
918         else
919           # a match!  start skipping.  we will delete this line and the next N lines.
920           ((skip_count++))
921 #echo first skip count is now $skip_count
922           found_any=yes
923         fi
924       else
925         # we're already skipping.  let's keep going until we hit the limit.
926         ((skip_count++))
927 #echo ongoing skip count is now $skip_count
928         if (( $skip_count > $numlines )); then
929           echo "Done skipping, and back to writing output file."
930           skip_count=0
931         fi
932       fi
933     done < "$filename"
934
935 #echo file we created looks like this:
936 #cat "$new_version"
937
938     if [ ! -z "$found_any" ]; then
939       # put the file back into place under the original name.
940       \mv "$new_version" "$filename"
941       exit_on_error "moving the new version into place in: $filename"
942     else
943       # cannot always be considered an error, but we can at least gripe.
944       echo "Did not find any matches for seeker '$seeker' in file: $filename"
945     fi
946   }
947
948   ##############
949
950   # space 'em all: fixes naming for all of the files of the appropriate types
951   # in the directories specified.  we skip any file with a dot in front, to
952   # respect their hidden nature.  currently the set of files we'll rename is
953   # very boutique; it's in this function, and just happens to be the types of
954   # files we work with a lot.
955   function spacemall() {
956     local -a dirs=("${@}")
957     if [ ${#dirs[@]} -eq 0 ]; then
958       dirs=(.)
959     fi
960
961     local charnfile="$(mktemp $TMP/zz_charn.XXXXXX)"
962 #hmmm: any way to do the below more nicely or reusably?
963 #hmmm: yes!  a variable with a list of files that are considered TEXT_FILE_EXTENSIONS or something like that.
964 #hmmm: yes continued!  also a variable for BINARY_FILE_EXTENSIONS to avoid those, where we need to in other scripts.
965 #hmmm: wait, we actually have a mix here, since this is a renaming function and not a searching function; get it straight!
966 #hmmm: would the composition of those two types of extensions cover all the files i want to rename?  they have to be "important".
967     find "${dirs[@]}" -follow -maxdepth 1 -mindepth 1 -type f -and -not -iname ".[a-zA-Z0-9]*" | \
968         grep -i \
969 "csv\|doc\|docx\|eml\|html\|jpeg\|jpg\|m4a\|mov\|mp3\|odp\|ods\|odt\|pdf\|png\|ppt\|pptx\|rtf\|txt\|vsd\|vsdx\|wav\|xls\|xlsx\|xml\|zip" | \
970         sed -e 's/^/"/' | sed -e 's/$/"/' | \
971         xargs bash "$FEISTY_MEOW_SCRIPTS/files/spacem.sh"
972     # drop the temp file now that we're done.
973     rm "$charnfile"
974   }
975
976   ##############
977
978   # tty relevant functions...
979
980   # keep_awake: sends a message to the screen from the background.
981   function keep_awake()
982   {
983     # just starts the keep_awake process in the background.
984     bash $FEISTY_MEOW_SCRIPTS/tty/keep_awake_process.sh &
985       # this should leave the job running as %1 or a higher number if there
986       # are pre-existing background jobs.
987   }
988
989   ##############
990
991   # site avenger functions...
992
993   function switchto()
994   {
995     THISDIR="$FEISTY_MEOW_SCRIPTS/site_avenger"
996     source "$FEISTY_MEOW_SCRIPTS/site_avenger/shared_site_mgr.sh"
997     switch_to "$1"
998   }
999
1000   ##############
1001
1002   # you have hit the borderline functional zone...
1003
1004 #hmmm: not really doing anything yet; ubuntu seems to have changed from pulseaudio in 17.04?
1005   # restarts the sound driver.
1006   function fix_sound_driver() {
1007     # stop bash complaining about blank function body.
1008     local nothing=
1009 #if alsa something
1010 #    sudo service alsasound restart
1011 #elif pulse something
1012 #    sudo pulseaudio -k
1013 #    sudo pulseaudio -D
1014 #else
1015 #    something else...?
1016 #fi
1017
1018   }
1019
1020   # ...and here's the end of the borderline functional zone.
1021
1022   ##############
1023
1024   # NOTE: no more function definitions are allowed after this point.
1025
1026   function function_sentinel()
1027   {
1028     return 0; 
1029   }
1030   
1031   if [ ! -z "$DEBUG_FEISTY_MEOW" ]; then echo "feisty meow function definitions done."; fi
1032
1033   ##############
1034
1035   # test code for set_var_if_undefined.
1036   run_test=0
1037   if [ $run_test != 0 ]; then
1038     echo running tests on set_var_if_undefined.
1039     flagrant=petunia
1040     set_var_if_undefined flagrant forknordle
1041     exit_on_error "testing if defined variable would be whacked"
1042     if [ $flagrant != petunia ]; then
1043       echo set_var_if_undefined failed to leave the test variable alone
1044       exit 1
1045     fi
1046     unset bobblehead_stomper
1047     set_var_if_undefined bobblehead_stomper endurance
1048     if [ $bobblehead_stomper != endurance ]; then
1049       echo set_var_if_undefined failed to set a variable that was not defined yet
1050       exit 1
1051     fi
1052   fi
1053
1054 fi
1055