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