
# Source this file into sh, ksh, bash, or zsh
# See http://Yost.com/computers/d
#
# Needs the 'tac' command, which cats lines from end to beginning of file.
# Requires shell procedures.
#
# Bugs:
#  * The r command has problems with s. The sudo persists.
#  * Quoting arguments doesn't work properly - fix attempted, try again.
#  * The background job is shown in a weird way, not showing the command(s)
#    you're really running.
#  * If you have a bad old shell, there won't be a job number handle on the 
#    backgrounded task
#
# The timing of commands could be better.
#
# I have tested this only with zsh 4.2.3
#
# Author: Dave@Yost.com
#   2.0 1991-04-xx
#   4.0 2004-12-19
#   5.0 2006-07-09
#   5.1 2006-07-10
#   5.2 2006-07-14
#   5.3 2006-12-14 fix "d s" bug when out file doesn't exist
#   5.4 2006-12-27 Make it work with bash, ksh, and sh
#   6.0 2006-12-28 New options: f tail -f, x clear, + c++filt
#   6.1 2010-08-07 Cleaned up the --help
#   7.0 2012-02-08 Help improvements, new =d= line beginning, lots of cleanup
#-------------------------------------------------------------------------------

yostD_usage () {
  echo 1>&2 "
Usage: d [ -f <file> ] c[n][a][x][+][f|p] <cmd> [ <arg> ... ]
or:    d [ -f <file> ] m[n][a][x][+][f|p] [ <make-arg> ... ]
or:    d [ -f <file> ] s[n][a][x][+][f|p] [ <additional-arg> ... ]
or:    d [ -f <file> ] h
or:    d   -f <file>

Run a command in the background, with its stdout and stderr redirected to 
a file called '$yostD_OUTFILE' or to <file>. The last form above changes this 
name persistently in the \$yostD_OUTFILE shell variable, which defaults to 
'$yostD_OUTFILE_DEFAULT'.

The d command does
  mv ,$yostD_OUTFILE $yostD_OUTFILE
before creating a new '$yostD_OUTFILE' file unless told to append instead.

The first argument to the <code>d</code> command, the task argument, must consist of 
exactly one of these:
    c - Run the following command. If escaped, you can use ; | > etc.
    m - Run 'make'
    s - Run the same command again (extract the last command from '$yostD_OUTFILE',
        and run it again (without 'nice' unless you ask for 'nice' again).
    h - Print a history of what happened in '$yostD_OUTFILE'.
You can add one of these:
    p - Just print the command that would be executed; don't run it.
    f - Wait for it, and tail -f '$yostD_OUTFILE'.
    w - Wait for it.
and any of these:
    n - Do it 'nice'ly.
    a - Append to '$yostD_OUTFILE'.
    x - Clear the screen.
    r - Do it as root (easier if sudo is warmed up, as with 'sudo true').
    + - Filter the command through the program 'c++filt'.
    ! - Disown the process, so it will continue if you log out.

See http://Yost.com/computers/d
"
  return 2
}

#-------------------------------------------------------------------------------

d() {
  yostD_args "$@" \
  && \
  if [[ "$yostD_optPrint" = true ]] ; then
    yostD_printPrevious
  else
    yostD_run
    local bgProc=$!
    if [[ "$yostD_optForeground" = true ]] ; then
      tail -f $yostD_OUTFILE &
      local tailProc=$!
      # None of the killing seems to work, and I try twice!
      trap 'kill $bgProc $tailProc' INT
      wait $bgProc
      kill $tailProc
    elif [[ "$yostD_optWait" = true ]] ; then
      # None of the killing seems to work, and I try twice!
      trap 'kill $bgProc' INT
      wait $bgProc
    fi
  fi
}

#-------------------------------------------------------------------------------

yostD_args() {

  yostD_optAppend=false
  yostD_optNice=
  yostD_optNicePrefix=
  yostD_optPrint=false
  yostD_optPrintAll=false
  yostD_optSame=false
  yostD_optForeground=false
  yostD_optFilter=
  yostD_optWait=false
  yostD_optBang=

  # process option: -f file
  if [[ $# > 0 && "${1:-}" = -f ]] ; then
    if [[ $# == 1 ]] ; then
      yostD_OUTFILE=$yostD_OUTFILE_PERSISTENT
      return $(yostD_usage)
    fi
    if [[ $# == 2 ]] ; then
      yostD_OUTFILE_PERSISTENT=$2
      echo "d: will use '$yostD_OUTFILE_PERSISTENT' as log file hereafter"
      return 1
    fi
    yostD_OUTFILE=$2
    shift
    shift
  else
    yostD_OUTFILE=$yostD_OUTFILE_PERSISTENT
  fi

  if [[ $# = 0 ]] ; then return $(yostD_usage) ; fi

  yostD_task=$1
  shift

  # There must be exactly one command.

  case "$yostD_task" in
  ""|*[\`~@#$%^\&*\(\)_-={[}\]|\\:\;'"'"'"\<,\>.?/bdegijkloqtuvyzABCDEFGHIJKLMNOPQRTUVWXYZ]*)
    return $(yostD_usage)
    ;;
  h)
    yostD_optPrint=true
    yostD_optPrintAll=true
    ;;
  *c*m* | *m*c* | *c*[sS]* | *[sS]*c* | *m*[sS]* | *[sS]*m*)
    return $(yostD_usage)
    ;;
  *c*)
    if [[ $# = 0 ]] ; then
      return $(yostD_usage)
    else
      # This should probably do an array copy, eh?
      yostD_cmd="$@"
    fi
    ;;
  *m*)
    yostD_cmd=make
    if [[ -n "$@" ]] ; then
      yostD_cmd="$yostD_cmd $@"
    fi
    ;;
  *[sS]*)
    local outfile
    case "$yostD_task" in
    *s*)
      outfile=$yostD_OUTFILE
      ;;
    *S*) # undocumented feature: pull command from ,out
      outfile=$(yostD_mkBackupName "$yostD_OUTFILE")
      ;;
    esac
    yostD_optSame=true
    yostD_cmd="$(yostD_findSameCmd $outfile)"
    if [[ $? != 0 ]] ; then
      return 2
    fi
    if [[ -n "$@" ]] ; then
      # Shell syntax.  Hrmph.
      #if [[ $1 = ${yostD_cmd[1]} ]] ; then
      #  echo 1>&2 "d: extra args to s are probably wrong"
      #  return 2
      #fi
      yostD_cmd="$yostD_cmd $@"
    fi
    ;;
  *)
    return $(yostD_usage)
  esac

  # And there can be optional modifiers.
  
  case "$yostD_task" in
  *a*)
    yostD_optAppend=true
  esac
  
  case "$yostD_task" in
  *r*)
    yostD_optSudo="sudo "
  esac

  case "$yostD_task" in
  *n*)
    yostD_optNice=nice
    yostD_optNicePrefix="nice "
  esac

  case "$yostD_task" in
  *x*)
    clear
  esac
  
  case "$yostD_task" in
  *!*)
    yostD_optBang=true
  esac

  case "$yostD_task" in
  *+*)
    if [[ $USER = yost ]] ; then
      # Sorry for this hack, folks.  Ask me for this script.
      yostD_optFilter="runc++filt"
    else
      yostD_optFilter="c++filt"
    fi
  esac

  case "$yostD_task" in
  *p*f* | *f*p* | *f*w* | *w*f* | *p*w* | *w*p*)
    return $(yostD_usage)
    ;;
  *p*)
    yostD_optPrint=true
    ;;
  *f*)
    yostD_optForeground=true
    ;;
  *w*)
    yostD_optWait=true
  esac

  return $(yostD_ensureShellSet)
}

#-------------------------------------------------------------------------------

yostD_findSameCmd() {
  local outfile="$1"
  if [[ ! -r $outfile ]] ; then
    echo 1>&2 "d: can't run same command again; can't read file '$outfile'"
    return 2
  fi
  yostD_cmd=$(
    tac $outfile 2> /dev/null \
    | sed -n '
        /^=d=cmd *nice  *sudo  */{
          s///p
          q
        }
        /^=d=cmd *nice  */{
          s///p
          q
        }
        /^=d=cmd *sudo  */{
          s///p
          q
        }
        /^=d=cmd  */{
          s///p
          q
        }
      '
  )
  if [[ -z "$yostD_cmd" ]] ; then
    echo 1>&2 "d: can't run same command again; can't find usable command in file '$outfile'"
    return 2
  fi
  echo "$yostD_cmd"
}

#-------------------------------------------------------------------------------

yostD_ensureShellSet() {
  if [[ -n "$SHELL:-" ]] ; then
       SHELL=$(which zsh) \
    || SHELL=$(which bash) \
    || SHELL=$(which ksh) \
    || SHELL=$(which sh) \
    || (
      echo 1>&2 "d: Can't operate because you have no SHELL environment variable"
      return 2
    )
  fi
}

#-------------------------------------------------------------------------------

yostD_printPrevious() {
  if [[ "$yostD_optPrintAll" = true ]] ; then
    sed -n '
        /^=d=cmd/p
        /^=d=stat/p
        ' $yostD_OUTFILE
  else
    echo ${yostD_optNicePrefix}${yostD_optSudo}$yostD_cmd
  fi
  return 0
}

#-------------------------------------------------------------------------------

yostD_run() {
  if [[ "$yostD_optSame"    = true ]] ; then
    echo 1>&2 "[ $(yostD_printPrevious) ]"
  fi
  if [[ "$yostD_optAppend" != true ]] ; then
    yostD_mvOutFile
  fi \
  && \
  if [[ $yostD_optBang == true ]] ; then
    yostD_runCmd &!
  else
    yostD_runCmd &
  fi
}

#-------------------------------------------------------------------------------

yostD_mvOutFile() {
  mv -f $yostD_OUTFILE $(yostD_mkBackupName "$yostD_OUTFILE") 2> /dev/null
  echo -n '' >> $yostD_OUTFILE
}

#-------------------------------------------------------------------------------

yostD_runCmd() {
  {
    # We use sh -c so pipes and semcolons work.
    yostD_outputPrologue \
    && \
    if [[ -n "$yostD_optFilter" ]] ; then
      yostD_runInternal \
      | $SHELL -c "$yostD_optFilter"
    else
      yostD_runInternal
    fi
    yostD_outputEpilogue
    yostD_beep
  } \
  >> $yostD_OUTFILE 2>&1
}

yostD_runInternal() {
  local redirect
  if tty > /dev/null ; then
    redirect=" < /dev/null"
  fi
  if [[ -n $yostD_optSudo && $yostD_optBang == true ]] ; then
    # sudo has to succeed without asking for a password.
    yostD_optSudo+=" -n"
  fi
  if [[ -n "$yostD_optNice" ]] ; then
    time $SHELL -c "${yostD_optNicePrefix}${yostD_optSudo}$yostD_cmd$redirect"
  else
    time $SHELL -c "                      ${yostD_optSudo}$yostD_cmd$redirect"
  fi
}

#-------------------------------------------------------------------------------

yostD_outputPrologue() {
  cat << XXX
=d=tbegin $(yostD_formattedDate)
=d=see    http://yost.com/computers/d/
=d=host   ${HOST:-$(hostname)}
=d=uname  $(uname -a)
=d=wd     $(pwd)
=d=cmd    ${yostD_optNicePrefix}${yostD_optSudo}$yostD_cmd
=d= - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
XXX
}

#-------------------------------------------------------------------------------

yostD_outputEpilogue() {
  cat << XXX
=d= - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
=d=stat   $?
=d=tend   $(yostD_formattedDate)
========= =====================================================================
XXX
}

#-------------------------------------------------------------------------------

yostD_beep() {
  printf "\b" > /dev/tty
}

#-------------------------------------------------------------------------------

yostD_formattedDate() {
  date '+%Y-%m-%dT%H:%M:%S %Z %a'
}


#-------------------------------------------------------------------------------

yostD_mkBackupName() {
  echo ,"$1"
}

#===============================================================================

yostD_OUTFILE_DEFAULT=out
if [[ -z "${yostD_OUTFILE_PERSISTENT:-}" ]] ; then yostD_OUTFILE_PERSISTENT=$yostD_OUTFILE_DEFAULT ; fi


# Also useful:
# j() { jobs "$@" ; }
# k() { kill %%   ; }

if ! type q   \
&& ! type qq  \
&& ! type qt  \
&& ! type cqq \
&& ! type wq  \
&& ! type wqt \
&& ! type mvout ; then
  yostD_qOK=true
else
  yostD_qOK=
fi > /dev/null 2>&1

if [[ $yostD_qOK = true ]] ; then
  q    () { tail -f  ${yostD_yostD_OUTFILE:-$yostD_OUTFILE_DEFAULT} ; }
  qq   () { cat      ${yostD_yostD_OUTFILE:-$yostD_OUTFILE_DEFAULT} ; }
  qt   () { tail -20 ${yostD_yostD_OUTFILE:-$yostD_OUTFILE_DEFAULT} ; }
  cqq  () { clear                   ; qq ; }
  wq   () { wait "$@" ; printf "\b" ; qq ; }
  wqt  () { wait "$@" ; printf "\b" ; qt ; }
  mvout() { mv -f ${yostD_yostD_OUTFILE:-$yostD_OUTFILE_DEFAULT} $(yostD_mkBackupName "${yostD_yostD_OUTFILE:-$yostD_OUTFILE_DEFAULT}") ; }
else
  echo 1>&2 "d.sh can't create the 'd' helper commands because of conflicts"
fi

#-------------------------------------------------------------------------------

# Copyright 2006-2012 Dave Yost <Dave@Yost.com>
# This version is
#   d.sh 7.0 2012-02-08 Help improvements, new =d= line beginning, lots of cleanup
# which at time of this publication can be found at:
#   http://Yost.com/computers/d
# Redistribution and use in the form of source code or derivative data built 
# from the source code, with or without modification, are permitted provided 
# that the following conditions are met:
# 1. THE USER AGREES THAT THERE IS NO WARRANTY.
# 2. If and only if appropriate, the above phrase "This version is" must be 
#    followed by the phrase "a modified form of" or "extracted from" or 
#    "extracted and modified from".
# 3. Redistributions of source code must retain this notice intact.
# 4. Redistributions in the form of derivative data built from the source 
#    code must reproduce this notice intact in the documentation and/or other 
#    materials provided with the distribution, and each file in the derivative 
#    data must reproduce any Yost.com URI included in the original distribution.
# 5. Neither the name of Dave Yost nor the names of its contributors may be 
#    used to endorse or promote products derived from this software without 
#    specific prior written permission.
# This notice comprises all text from "Copyright" above through the end of 
# this sentence.

