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