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