"Fossies" - the Fresh Open Source Software Archive

Member "todo.txt_cli-2.12.0/todo.sh" (12 Aug 2020, 48678 Bytes) of package /linux/privat/todo.txt_cli-2.12.0.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Bash source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the latest Fossies "Diffs" side-by-side code changes report for "todo.sh": 2.11.0_vs_2.12.0.

A hint: This file contains one or more very long lines, so maybe it is better readable using the pure text view mode that shows the contents as wrapped lines within the browser window.


    1 #!/usr/bin/env bash
    2 
    3 # === HEAVY LIFTING ===
    4 shopt -s extglob extquote
    5 
    6 # NOTE:  Todo.sh requires the .todo/config configuration file to run.
    7 # Place the .todo/config file in your home directory or use the -d option for a custom location.
    8 
    9 [ -f VERSION-FILE ] && . VERSION-FILE || VERSION="2.12.0"
   10 version() {
   11     cat <<-EndVersion
   12         TODO.TXT Command Line Interface v$VERSION
   13 
   14         Homepage: http://todotxt.org
   15         Code repository: https://github.com/todotxt/todo.txt-cli/
   16         Contributors: https://github.com/todotxt/todo.txt-cli/graphs/contributors
   17         License: https://github.com/todotxt/todo.txt-cli/blob/master/LICENSE
   18     EndVersion
   19     exit 1
   20 }
   21 
   22 # Set script name and full path early.
   23 TODO_SH=$(basename "$0")
   24 TODO_FULL_SH="$0"
   25 export TODO_SH TODO_FULL_SH
   26 
   27 oneline_usage="$TODO_SH [-fhpantvV] [-d todo_config] action [task_number] [task_description]"
   28 
   29 usage()
   30 {
   31     cat <<-EndUsage
   32         Usage: $oneline_usage
   33         Try '$TODO_SH -h' for more information.
   34     EndUsage
   35     exit 1
   36 }
   37 
   38 shorthelp()
   39 {
   40     cat <<-EndHelp
   41           Usage: $oneline_usage
   42 
   43           Actions:
   44             add|a "THING I NEED TO DO +project @context"
   45             addm "THINGS I NEED TO DO
   46                   MORE THINGS I NEED TO DO"
   47             addto DEST "TEXT TO ADD"
   48             append|app ITEM# "TEXT TO APPEND"
   49             archive
   50             command [ACTIONS]
   51             deduplicate
   52             del|rm ITEM# [TERM]
   53             depri|dp ITEM#[, ITEM#, ITEM#, ...]
   54             done|do ITEM#[, ITEM#, ITEM#, ...]
   55             help [ACTION...]
   56             list|ls [TERM...]
   57             listall|lsa [TERM...]
   58             listaddons
   59             listcon|lsc [TERM...]
   60             listfile|lf [SRC [TERM...]]
   61             listpri|lsp [PRIORITIES] [TERM...]
   62             listproj|lsprj [TERM...]
   63             move|mv ITEM# DEST [SRC]
   64             prepend|prep ITEM# "TEXT TO PREPEND"
   65             pri|p ITEM# PRIORITY
   66             replace ITEM# "UPDATED TODO"
   67             report
   68             shorthelp
   69 
   70           Actions can be added and overridden using scripts in the actions
   71           directory.
   72     EndHelp
   73 
   74     # Only list the one-line usage from the add-on actions. This assumes that
   75     # add-ons use the same usage indentation structure as todo.sh.
   76     addonHelp | grep -e '^  Add-on Actions:' -e '^    [[:alpha:]]'
   77 
   78     cat <<-EndHelpFooter
   79 
   80           See "help" for more details.
   81     EndHelpFooter
   82 }
   83 
   84 help()
   85 {
   86     cat <<-EndOptionsHelp
   87           Usage: $oneline_usage
   88 
   89           Options:
   90             -@
   91                 Hide context names in list output.  Use twice to show context
   92                 names (default).
   93             -+
   94                 Hide project names in list output.  Use twice to show project
   95                 names (default).
   96             -c
   97                 Color mode
   98             -d CONFIG_FILE
   99                 Use a configuration file other than the default ~/.todo/config
  100             -f
  101                 Forces actions without confirmation or interactive input
  102             -h
  103                 Display a short help message; same as action "shorthelp"
  104             -p
  105                 Plain mode turns off colors
  106             -P
  107                 Hide priority labels in list output.  Use twice to show
  108                 priority labels (default).
  109             -a
  110                 Don't auto-archive tasks automatically on completion
  111             -A
  112                 Auto-archive tasks automatically on completion
  113             -n
  114                 Don't preserve line numbers; automatically remove blank lines
  115                 on task deletion
  116             -N
  117                 Preserve line numbers
  118             -t
  119                 Prepend the current date to a task automatically
  120                 when it's added.
  121             -T
  122                 Do not prepend the current date to a task automatically
  123                 when it's added.
  124             -v
  125                 Verbose mode turns on confirmation messages
  126             -vv
  127                 Extra verbose mode prints some debugging information and
  128                 additional help text
  129             -V
  130                 Displays version, license and credits
  131             -x
  132                 Disables TODOTXT_FINAL_FILTER
  133 
  134 
  135     EndOptionsHelp
  136 
  137     [ "$TODOTXT_VERBOSE" -gt 1 ] && cat <<-'EndVerboseHelp'
  138           Environment variables:
  139             TODOTXT_AUTO_ARCHIVE            is same as option -a (0)/-A (1)
  140             TODOTXT_CFG_FILE=CONFIG_FILE    is same as option -d CONFIG_FILE
  141             TODOTXT_FORCE=1                 is same as option -f
  142             TODOTXT_PRESERVE_LINE_NUMBERS   is same as option -n (0)/-N (1)
  143             TODOTXT_PLAIN                   is same as option -p (1)/-c (0)
  144             TODOTXT_DATE_ON_ADD             is same as option -t (1)/-T (0)
  145             TODOTXT_PRIORITY_ON_ADD=pri     default priority A-Z
  146             TODOTXT_VERBOSE=1               is same as option -v
  147             TODOTXT_DISABLE_FILTER=1        is same as option -x
  148             TODOTXT_DEFAULT_ACTION=""       run this when called with no arguments
  149             TODOTXT_SORT_COMMAND="sort ..." customize list output
  150             TODOTXT_FINAL_FILTER="sed ..."  customize list after color, P@+ hiding
  151             TODOTXT_SOURCEVAR=\$DONE_FILE   use another source for listcon, listproj
  152             TODOTXT_SIGIL_BEFORE_PATTERN="" optionally allow chars preceding +p / @c
  153             TODOTXT_SIGIL_VALID_PATTERN=.*  tweak the allowed chars for +p and @c
  154             TODOTXT_SIGIL_AFTER_PATTERN=""  optionally allow chars after +p / @c
  155 
  156 
  157     EndVerboseHelp
  158         actionsHelp
  159         addonHelp
  160 }
  161 
  162 actionsHelp()
  163 {
  164     cat <<-EndActionsHelp
  165           Built-in Actions:
  166             add "THING I NEED TO DO +project @context"
  167             a "THING I NEED TO DO +project @context"
  168               Adds THING I NEED TO DO to your todo.txt file on its own line.
  169               Project and context notation optional.
  170               Quotes optional.
  171 
  172             addm "FIRST THING I NEED TO DO +project1 @context
  173             SECOND THING I NEED TO DO +project2 @context"
  174               Adds FIRST THING I NEED TO DO to your todo.txt on its own line and
  175               Adds SECOND THING I NEED TO DO to you todo.txt on its own line.
  176               Project and context notation optional.
  177 
  178             addto DEST "TEXT TO ADD"
  179               Adds a line of text to any file located in the todo.txt directory.
  180               For example, addto inbox.txt "decide about vacation"
  181 
  182             append ITEM# "TEXT TO APPEND"
  183             app ITEM# "TEXT TO APPEND"
  184               Adds TEXT TO APPEND to the end of the task on line ITEM#.
  185               Quotes optional.
  186 
  187             archive
  188               Moves all done tasks from todo.txt to done.txt and removes blank lines.
  189 
  190             command [ACTIONS]
  191               Runs the remaining arguments using only todo.sh builtins.
  192               Will not call any .todo.actions.d scripts.
  193 
  194             deduplicate
  195               Removes duplicate lines from todo.txt.
  196 
  197             del ITEM# [TERM]
  198             rm ITEM# [TERM]
  199               Deletes the task on line ITEM# in todo.txt.
  200               If TERM specified, deletes only TERM from the task.
  201 
  202             depri ITEM#[, ITEM#, ITEM#, ...]
  203             dp ITEM#[, ITEM#, ITEM#, ...]
  204               Deprioritizes (removes the priority) from the task(s)
  205               on line ITEM# in todo.txt.
  206 
  207             done ITEM#[, ITEM#, ITEM#, ...]
  208             do ITEM#[, ITEM#, ITEM#, ...]
  209               Marks task(s) on line ITEM# as done in todo.txt.
  210 
  211             help [ACTION...]
  212               Display help about usage, options, built-in and add-on actions,
  213               or just the usage help for the passed ACTION(s).
  214 
  215             list [TERM...]
  216             ls [TERM...]
  217               Displays all tasks that contain TERM(s) sorted by priority with line
  218               numbers.  Each task must match all TERM(s) (logical AND); to display
  219               tasks that contain any TERM (logical OR), use
  220               "TERM1\|TERM2\|..." (with quotes), or TERM1\\\|TERM2 (unquoted).
  221               Hides all tasks that contain TERM(s) preceded by a
  222               minus sign (i.e. -TERM). If no TERM specified, lists entire todo.txt.
  223 
  224             listall [TERM...]
  225             lsa [TERM...]
  226               Displays all the lines in todo.txt AND done.txt that contain TERM(s)
  227               sorted by priority with line  numbers.  Hides all tasks that
  228               contain TERM(s) preceded by a minus sign (i.e. -TERM).  If no
  229               TERM specified, lists entire todo.txt AND done.txt
  230               concatenated and sorted.
  231 
  232             listaddons
  233               Lists all added and overridden actions in the actions directory.
  234 
  235             listcon [TERM...]
  236             lsc [TERM...]
  237               Lists all the task contexts that start with the @ sign in todo.txt.
  238               If TERM specified, considers only tasks that contain TERM(s).
  239 
  240             listfile [SRC [TERM...]]
  241             lf [SRC [TERM...]]
  242               Displays all the lines in SRC file located in the todo.txt directory,
  243               sorted by priority with line  numbers.  If TERM specified, lists
  244               all lines that contain TERM(s) in SRC file.  Hides all tasks that
  245               contain TERM(s) preceded by a minus sign (i.e. -TERM).
  246               Without any arguments, the names of all text files in the todo.txt
  247               directory are listed.
  248 
  249             listpri [PRIORITIES] [TERM...]
  250             lsp [PRIORITIES] [TERM...]
  251               Displays all tasks prioritized PRIORITIES.
  252               PRIORITIES can be a single one (A) or a range (A-C).
  253               If no PRIORITIES specified, lists all prioritized tasks.
  254               If TERM specified, lists only prioritized tasks that contain TERM(s).
  255               Hides all tasks that contain TERM(s) preceded by a minus sign
  256               (i.e. -TERM).
  257 
  258             listproj [TERM...]
  259             lsprj [TERM...]
  260               Lists all the projects (terms that start with a + sign) in
  261               todo.txt.
  262               If TERM specified, considers only tasks that contain TERM(s).
  263 
  264             move ITEM# DEST [SRC]
  265             mv ITEM# DEST [SRC]
  266               Moves a line from source text file (SRC) to destination text file (DEST).
  267               Both source and destination file must be located in the directory defined
  268               in the configuration directory.  When SRC is not defined
  269               it's by default todo.txt.
  270 
  271             prepend ITEM# "TEXT TO PREPEND"
  272             prep ITEM# "TEXT TO PREPEND"
  273               Adds TEXT TO PREPEND to the beginning of the task on line ITEM#.
  274               Quotes optional.
  275 
  276             pri ITEM# PRIORITY
  277             p ITEM# PRIORITY
  278               Adds PRIORITY to task on line ITEM#.  If the task is already
  279               prioritized, replaces current priority with new PRIORITY.
  280               PRIORITY must be a letter between A and Z.
  281 
  282             replace ITEM# "UPDATED TODO"
  283               Replaces task on line ITEM# with UPDATED TODO.
  284 
  285             report
  286               Adds the number of open tasks and done tasks to report.txt.
  287 
  288             shorthelp
  289               List the one-line usage of all built-in and add-on actions.
  290 
  291     EndActionsHelp
  292 }
  293 
  294 addonHelp()
  295 {
  296     if [ -d "$TODO_ACTIONS_DIR" ]; then
  297         didPrintAddonActionsHeader=
  298         for action in "$TODO_ACTIONS_DIR"/*
  299         do
  300             if [ -f "$action" ] && [ -x "$action" ]; then
  301                 if [ ! "$didPrintAddonActionsHeader" ]; then
  302                     cat <<-EndAddonActionsHeader
  303           Add-on Actions:
  304     EndAddonActionsHeader
  305                     didPrintAddonActionsHeader=1
  306                 fi
  307                 "$action" usage
  308             elif [ -d "$action" ] && [ -x "$action"/"$(basename "$action")" ]; then
  309                 if [ ! "$didPrintAddonActionsHeader" ]; then
  310                     cat <<-EndAddonActionsHeader
  311           Add-on Actions:
  312     EndAddonActionsHeader
  313                     didPrintAddonActionsHeader=1
  314                 fi
  315                 "$action"/"$(basename "$action")" usage
  316             fi
  317         done
  318     fi
  319 }
  320 
  321 actionUsage()
  322 {
  323     for actionName
  324     do
  325         action="${TODO_ACTIONS_DIR}/${actionName}"
  326         if [ -f "$action" ] && [ -x "$action" ]; then
  327             "$action" usage
  328         elif [ -d "$action" ] && [ -x "$action"/"$(basename "$action")" ]; then
  329             "$action"/"$(basename "$action")" usage
  330         else
  331             builtinActionUsage=$(actionsHelp | sed -n -e "/^    ${actionName//\//\\/} /,/^\$/p" -e "/^    ${actionName//\//\\/}$/,/^\$/p")
  332             if [ "$builtinActionUsage" ]; then
  333                 echo "$builtinActionUsage"
  334                 echo
  335             else
  336                 die "TODO: No action \"${actionName}\" exists."
  337             fi
  338         fi
  339     done
  340 }
  341 
  342 dieWithHelp()
  343 {
  344     case "$1" in
  345         help)       help;;
  346         shorthelp)  shorthelp;;
  347     esac
  348     shift
  349 
  350     die "$@"
  351 }
  352 die()
  353 {
  354     echo "$*"
  355     exit 1
  356 }
  357 
  358 cleaninput()
  359 {
  360     # Parameters:    When $1 = "for sed", performs additional escaping for use
  361     #                in sed substitution with "|" separators.
  362     # Precondition:  $input contains text to be cleaned.
  363     # Postcondition: Modifies $input.
  364 
  365     # Replace CR and LF with space; tasks always comprise a single line.
  366     input=${input//$'\r'/ }
  367     input=${input//$'\n'/ }
  368 
  369     if [ "$1" = "for sed" ]; then
  370         # This action uses sed with "|" as the substitution separator, and & as
  371         # the matched string; these must be escaped.
  372         # Backslashes must be escaped, too, and before the other stuff.
  373         input=${input//\\/\\\\}
  374         input=${input//|/\\|}
  375         input=${input//&/\\&}
  376     fi
  377 }
  378 
  379 getPrefix()
  380 {
  381     # Parameters:    $1: todo file; empty means $TODO_FILE.
  382     # Returns:       Uppercase FILE prefix to be used in place of "TODO:" where
  383     #                a different todo file can be specified.
  384     local base
  385     base=$(basename "${1:-$TODO_FILE}")
  386     echo "${base%%.[^.]*}" | tr '[:lower:]' '[:upper:]'
  387 }
  388 
  389 getTodo()
  390 {
  391     # Parameters:    $1: task number
  392     #                $2: Optional todo file
  393     # Precondition:  $errmsg contains usage message.
  394     # Postcondition: $todo contains task text.
  395 
  396     local item=$1
  397     [ -z "$item" ] && die "$errmsg"
  398     [ "${item//[0-9]/}" ] && die "$errmsg"
  399 
  400     todo=$(sed "$item!d" "${2:-$TODO_FILE}")
  401     [ -z "$todo" ] && die "$(getPrefix "$2"): No task $item."
  402 }
  403 getNewtodo()
  404 {
  405     # Parameters:    $1: task number
  406     #                $2: Optional todo file
  407     # Precondition:  None.
  408     # Postcondition: $newtodo contains task text.
  409 
  410     local item=$1
  411     [ -z "$item" ] && die "Programming error: $item should exist."
  412     [ "${item//[0-9]/}" ] && die "Programming error: $item should be numeric."
  413 
  414     newtodo=$(sed "$item!d" "${2:-$TODO_FILE}")
  415     [ -z "$newtodo" ] && die "$(getPrefix "$2"): No updated task $item."
  416 }
  417 
  418 replaceOrPrepend()
  419 {
  420   action=$1; shift
  421   case "$action" in
  422     replace)
  423       backref=
  424       querytext="Replacement: "
  425       ;;
  426     prepend)
  427       backref=' &'
  428       querytext="Prepend: "
  429       ;;
  430   esac
  431   shift; item=$1; shift
  432   getTodo "$item"
  433 
  434   if [[ -z "$1" && $TODOTXT_FORCE = 0 ]]; then
  435     echo -n "$querytext"
  436     read -r -i "$todo" -e input
  437   else
  438     input=$*
  439   fi
  440 
  441   # Retrieve existing priority and prepended date
  442   local -r priAndDateExpr='^\((.) \)\{0,1\}\([0-9]\{2,4\}-[0-9]\{2\}-[0-9]\{2\} \)\{0,1\}'
  443   priority=$(sed -e "$item!d" -e "${item}s/${priAndDateExpr}.*/\\1/" "$TODO_FILE")
  444   prepdate=$(sed -e "$item!d" -e "${item}s/${priAndDateExpr}.*/\\2/" "$TODO_FILE")
  445 
  446   if [ "$prepdate" ] && [ "$action" = "replace" ] && [ "$(echo "$input"|sed -e "s/${priAndDateExpr}.*/\\1\\2/")" ]; then
  447       # If the replaced text starts with a [priority +] date, it will replace
  448       # the existing date, too.
  449     prepdate=
  450   fi
  451 
  452   # Temporarily remove any existing priority and prepended date, perform the
  453   # change (replace/prepend) and re-insert the existing priority and prepended
  454   # date again.
  455   cleaninput "for sed"
  456   sed -i.bak -e "$item s/^${priority}${prepdate}//" -e "$item s|^.*|${priority}${prepdate}${input}${backref}|" "$TODO_FILE"
  457   if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
  458     getNewtodo "$item"
  459     case "$action" in
  460       replace)
  461         echo "$item $todo"
  462         echo "TODO: Replaced task with:"
  463         echo "$item $newtodo"
  464         ;;
  465       prepend)
  466         echo "$item $newtodo"
  467         ;;
  468     esac
  469   fi
  470 }
  471 
  472 fixMissingEndOfLine()
  473 {
  474     # Parameters:    $1: todo file; empty means $TODO_FILE.
  475     sed -i.bak -e '$a\' "${1:-$TODO_FILE}"
  476 }
  477 
  478 uppercasePriority()
  479 {
  480     # Precondition:  $input contains task text for which to uppercase priority.
  481     # Postcondition: Modifies $input.
  482     lower=( {a..z} )
  483     upper=( {A..Z} )
  484     for ((i=0; i<26; i++))
  485     do
  486         upperPriority="${upperPriority};s/^[(]${lower[i]}[)]/(${upper[i]})/"
  487     done
  488     input=$(echo "$input" | sed "$upperPriority")
  489 }
  490 
  491 #Preserving environment variables so they don't get clobbered by the config file
  492 OVR_TODOTXT_AUTO_ARCHIVE="$TODOTXT_AUTO_ARCHIVE"
  493 OVR_TODOTXT_FORCE="$TODOTXT_FORCE"
  494 OVR_TODOTXT_PRESERVE_LINE_NUMBERS="$TODOTXT_PRESERVE_LINE_NUMBERS"
  495 OVR_TODOTXT_PLAIN="$TODOTXT_PLAIN"
  496 OVR_TODOTXT_DATE_ON_ADD="$TODOTXT_DATE_ON_ADD"
  497 OVR_TODOTXT_PRIORITY_ON_ADD="$TODOTXT_PRIORITY_ON_ADD"
  498 OVR_TODOTXT_DISABLE_FILTER="$TODOTXT_DISABLE_FILTER"
  499 OVR_TODOTXT_VERBOSE="$TODOTXT_VERBOSE"
  500 OVR_TODOTXT_DEFAULT_ACTION="$TODOTXT_DEFAULT_ACTION"
  501 OVR_TODOTXT_SORT_COMMAND="$TODOTXT_SORT_COMMAND"
  502 OVR_TODOTXT_FINAL_FILTER="$TODOTXT_FINAL_FILTER"
  503 
  504 # Prevent GREP_OPTIONS from malforming grep's output
  505 export GREP_OPTIONS=""
  506 
  507 # == PROCESS OPTIONS ==
  508 while getopts ":fhpcnNaAtTvVx+@Pd:" Option
  509 do
  510   case $Option in
  511     '@')
  512         ## HIDE_CONTEXT_NAMES starts at zero (false); increment it to one
  513         ##   (true) the first time this flag is seen. Each time the flag
  514         ##   is seen after that, increment it again so that an even
  515         ##   number shows context names and an odd number hides context
  516         ##   names.
  517         : $(( HIDE_CONTEXT_NAMES++ ))
  518         if [ $(( HIDE_CONTEXT_NAMES % 2 )) -eq 0 ]
  519         then
  520             ## Zero or even value -- show context names
  521             unset HIDE_CONTEXTS_SUBSTITUTION
  522         else
  523             ## One or odd value -- hide context names
  524             export HIDE_CONTEXTS_SUBSTITUTION='[[:space:]]@[[:graph:]]\{1,\}'
  525         fi
  526         ;;
  527     '+')
  528         ## HIDE_PROJECT_NAMES starts at zero (false); increment it to one
  529         ##   (true) the first time this flag is seen. Each time the flag
  530         ##   is seen after that, increment it again so that an even
  531         ##   number shows project names and an odd number hides project
  532         ##   names.
  533         : $(( HIDE_PROJECT_NAMES++ ))
  534         if [ $(( HIDE_PROJECT_NAMES % 2 )) -eq 0 ]
  535         then
  536             ## Zero or even value -- show project names
  537             unset HIDE_PROJECTS_SUBSTITUTION
  538         else
  539             ## One or odd value -- hide project names
  540             export HIDE_PROJECTS_SUBSTITUTION='[[:space:]][+][[:graph:]]\{1,\}'
  541         fi
  542         ;;
  543     a)
  544         OVR_TODOTXT_AUTO_ARCHIVE=0
  545         ;;
  546     A)
  547         OVR_TODOTXT_AUTO_ARCHIVE=1
  548         ;;
  549     c)
  550         OVR_TODOTXT_PLAIN=0
  551         ;;
  552     d)
  553         TODOTXT_CFG_FILE=$OPTARG
  554         ;;
  555     f)
  556         OVR_TODOTXT_FORCE=1
  557         ;;
  558     h)
  559         # Short-circuit option parsing and forward to the action.
  560         # Cannot just invoke shorthelp() because we need the configuration
  561         # processed to locate the add-on actions directory.
  562         set -- '-h' 'shorthelp'
  563         OPTIND=2
  564         ;;
  565     n)
  566         OVR_TODOTXT_PRESERVE_LINE_NUMBERS=0
  567         ;;
  568     N)
  569         OVR_TODOTXT_PRESERVE_LINE_NUMBERS=1
  570         ;;
  571     p)
  572         OVR_TODOTXT_PLAIN=1
  573         ;;
  574     P)
  575         ## HIDE_PRIORITY_LABELS starts at zero (false); increment it to one
  576         ##   (true) the first time this flag is seen. Each time the flag
  577         ##   is seen after that, increment it again so that an even
  578         ##   number shows priority labels and an odd number hides priority
  579         ##   labels.
  580         : $(( HIDE_PRIORITY_LABELS++ ))
  581         if [ $(( HIDE_PRIORITY_LABELS % 2 )) -eq 0 ]
  582         then
  583             ## Zero or even value -- show priority labels
  584             unset HIDE_PRIORITY_SUBSTITUTION
  585         else
  586             ## One or odd value -- hide priority labels
  587             export HIDE_PRIORITY_SUBSTITUTION="([A-Z])[[:space:]]"
  588         fi
  589         ;;
  590     t)
  591         OVR_TODOTXT_DATE_ON_ADD=1
  592         ;;
  593     T)
  594         OVR_TODOTXT_DATE_ON_ADD=0
  595         ;;
  596     v)
  597         : $(( TODOTXT_VERBOSE++ ))
  598         ;;
  599     V)
  600         version
  601         ;;
  602     x)
  603         OVR_TODOTXT_DISABLE_FILTER=1
  604         ;;
  605     *)
  606         usage
  607         ;;
  608   esac
  609 done
  610 shift $((OPTIND - 1))
  611 
  612 # defaults if not yet defined
  613 TODOTXT_VERBOSE=${TODOTXT_VERBOSE:-1}
  614 TODOTXT_PLAIN=${TODOTXT_PLAIN:-0}
  615 TODOTXT_CFG_FILE=${TODOTXT_CFG_FILE:-$HOME/.todo/config}
  616 TODOTXT_FORCE=${TODOTXT_FORCE:-0}
  617 TODOTXT_PRESERVE_LINE_NUMBERS=${TODOTXT_PRESERVE_LINE_NUMBERS:-1}
  618 TODOTXT_AUTO_ARCHIVE=${TODOTXT_AUTO_ARCHIVE:-1}
  619 TODOTXT_DATE_ON_ADD=${TODOTXT_DATE_ON_ADD:-0}
  620 TODOTXT_PRIORITY_ON_ADD=${TODOTXT_PRIORITY_ON_ADD:-}
  621 TODOTXT_DEFAULT_ACTION=${TODOTXT_DEFAULT_ACTION:-}
  622 TODOTXT_SORT_COMMAND=${TODOTXT_SORT_COMMAND:-env LC_COLLATE=C sort -f -k2}
  623 TODOTXT_DISABLE_FILTER=${TODOTXT_DISABLE_FILTER:-}
  624 TODOTXT_FINAL_FILTER=${TODOTXT_FINAL_FILTER:-cat}
  625 TODOTXT_GLOBAL_CFG_FILE=${TODOTXT_GLOBAL_CFG_FILE:-/etc/todo/config}
  626 TODOTXT_SIGIL_BEFORE_PATTERN=${TODOTXT_SIGIL_BEFORE_PATTERN:-}  # Allow any other non-whitespace entity before +project and @context; should be an optional match; example: \(w:\)\{0,1\} to allow w:@context.
  627 TODOTXT_SIGIL_VALID_PATTERN=${TODOTXT_SIGIL_VALID_PATTERN:-.*}  # Limit the valid characters (from the default any non-whitespace sequence) for +project and @context; example: [a-zA-Z]\{3,\} to only allow alphabetic ones that are at least three characters long.
  628 TODOTXT_SIGIL_AFTER_PATTERN=${TODOTXT_SIGIL_AFTER_PATTERN:-}    # Allow any other non-whitespace entity after +project and @context; should be an optional match; example: )\{0,1\} to allow (with the corresponding TODOTXT_SIGIL_BEFORE_PATTERN) enclosing in parentheses.
  629 
  630 # Export all TODOTXT_* variables
  631 export "${!TODOTXT_@}"
  632 
  633 # Default color map
  634 export NONE=''
  635 export BLACK='\\033[0;30m'
  636 export RED='\\033[0;31m'
  637 export GREEN='\\033[0;32m'
  638 export BROWN='\\033[0;33m'
  639 export BLUE='\\033[0;34m'
  640 export PURPLE='\\033[0;35m'
  641 export CYAN='\\033[0;36m'
  642 export LIGHT_GREY='\\033[0;37m'
  643 export DARK_GREY='\\033[1;30m'
  644 export LIGHT_RED='\\033[1;31m'
  645 export LIGHT_GREEN='\\033[1;32m'
  646 export YELLOW='\\033[1;33m'
  647 export LIGHT_BLUE='\\033[1;34m'
  648 export LIGHT_PURPLE='\\033[1;35m'
  649 export LIGHT_CYAN='\\033[1;36m'
  650 export WHITE='\\033[1;37m'
  651 export DEFAULT='\\033[0m'
  652 
  653 # Default priority->color map.
  654 export PRI_A=$YELLOW        # color for A priority
  655 export PRI_B=$GREEN         # color for B priority
  656 export PRI_C=$LIGHT_BLUE    # color for C priority
  657 export PRI_X=$WHITE         # color unless explicitly defined
  658 
  659 # Default project, context, date, item number, and metadata key:value pairs colors.
  660 export COLOR_PROJECT=$NONE
  661 export COLOR_CONTEXT=$NONE
  662 export COLOR_DATE=$NONE
  663 export COLOR_NUMBER=$NONE
  664 export COLOR_META=$NONE
  665 
  666 # Default highlight colors.
  667 export COLOR_DONE=$LIGHT_GREY   # color for done (but not yet archived) tasks
  668 
  669 # Default sentence delimiters for todo.sh append.
  670 # If the text to be appended to the task begins with one of these characters, no
  671 # whitespace is inserted in between. This makes appending to an enumeration
  672 # (todo.sh add 42 ", foo") syntactically correct.
  673 export SENTENCE_DELIMITERS=',.:;'
  674 
  675 [ -e "$TODOTXT_CFG_FILE" ] || {
  676     CFG_FILE_ALT="$HOME/todo.cfg"
  677 
  678     if [ -e "$CFG_FILE_ALT" ]
  679     then
  680         TODOTXT_CFG_FILE="$CFG_FILE_ALT"
  681     fi
  682 }
  683 
  684 [ -e "$TODOTXT_CFG_FILE" ] || {
  685     CFG_FILE_ALT="$HOME/.todo.cfg"
  686 
  687     if [ -e "$CFG_FILE_ALT" ]
  688     then
  689         TODOTXT_CFG_FILE="$CFG_FILE_ALT"
  690     fi
  691 }
  692 
  693 [ -e "$TODOTXT_CFG_FILE" ] || {
  694     CFG_FILE_ALT="${XDG_CONFIG_HOME:-$HOME/.config}/todo/config"
  695 
  696     if [ -e "$CFG_FILE_ALT" ]
  697     then
  698         TODOTXT_CFG_FILE="$CFG_FILE_ALT"
  699     fi
  700 }
  701 
  702 [ -e "$TODOTXT_CFG_FILE" ] || {
  703     CFG_FILE_ALT=$(dirname "$0")"/todo.cfg"
  704 
  705     if [ -e "$CFG_FILE_ALT" ]
  706     then
  707         TODOTXT_CFG_FILE="$CFG_FILE_ALT"
  708     fi
  709 }
  710 
  711 [ -e "$TODOTXT_CFG_FILE" ] || {
  712     CFG_FILE_ALT="$TODOTXT_GLOBAL_CFG_FILE"
  713 
  714     if [ -e "$CFG_FILE_ALT" ]
  715     then
  716         TODOTXT_CFG_FILE="$CFG_FILE_ALT"
  717     fi
  718 }
  719 
  720 
  721 if [ -z "$TODO_ACTIONS_DIR" ] || [ ! -d "$TODO_ACTIONS_DIR" ]
  722 then
  723     TODO_ACTIONS_DIR="$HOME/.todo/actions"
  724     export TODO_ACTIONS_DIR
  725 fi
  726 
  727 [ -d "$TODO_ACTIONS_DIR" ] || {
  728     TODO_ACTIONS_DIR_ALT="$HOME/.todo.actions.d"
  729 
  730     if [ -d "$TODO_ACTIONS_DIR_ALT" ]
  731     then
  732         TODO_ACTIONS_DIR="$TODO_ACTIONS_DIR_ALT"
  733     fi
  734 }
  735 
  736 [ -d "$TODO_ACTIONS_DIR" ] || {
  737     TODO_ACTIONS_DIR_ALT="${XDG_CONFIG_HOME:-$HOME/.config}/todo/actions"
  738 
  739     if [ -d "$TODO_ACTIONS_DIR_ALT" ]
  740     then
  741         TODO_ACTIONS_DIR="$TODO_ACTIONS_DIR_ALT"
  742     fi
  743 }
  744 
  745 # === SANITY CHECKS (thanks Karl!) ===
  746 [ -r "$TODOTXT_CFG_FILE" ] || dieWithHelp "$1" "Fatal Error: Cannot read configuration file $TODOTXT_CFG_FILE"
  747 
  748 . "$TODOTXT_CFG_FILE"
  749 
  750 # === APPLY OVERRIDES
  751 if [ -n "$OVR_TODOTXT_AUTO_ARCHIVE" ] ; then
  752   TODOTXT_AUTO_ARCHIVE="$OVR_TODOTXT_AUTO_ARCHIVE"
  753 fi
  754 if [ -n "$OVR_TODOTXT_FORCE" ] ; then
  755   TODOTXT_FORCE="$OVR_TODOTXT_FORCE"
  756 fi
  757 if [ -n "$OVR_TODOTXT_PRESERVE_LINE_NUMBERS" ] ; then
  758   TODOTXT_PRESERVE_LINE_NUMBERS="$OVR_TODOTXT_PRESERVE_LINE_NUMBERS"
  759 fi
  760 if [ -n "$OVR_TODOTXT_PLAIN" ] ; then
  761   TODOTXT_PLAIN="$OVR_TODOTXT_PLAIN"
  762 fi
  763 if [ -n "$OVR_TODOTXT_DATE_ON_ADD" ] ; then
  764   TODOTXT_DATE_ON_ADD="$OVR_TODOTXT_DATE_ON_ADD"
  765 fi
  766 if [ -n "$OVR_TODOTXT_PRIORITY_ON_ADD" ] ; then
  767     TODOTXT_PRIORITY_ON_ADD="$OVR_TODOTXT_PRIORITY_ON_ADD"
  768 fi
  769 if [ -n "$OVR_TODOTXT_DISABLE_FILTER" ] ; then
  770   TODOTXT_DISABLE_FILTER="$OVR_TODOTXT_DISABLE_FILTER"
  771 fi
  772 if [ -n "$OVR_TODOTXT_VERBOSE" ] ; then
  773   TODOTXT_VERBOSE="$OVR_TODOTXT_VERBOSE"
  774 fi
  775 if [ -n "$OVR_TODOTXT_DEFAULT_ACTION" ] ; then
  776   TODOTXT_DEFAULT_ACTION="$OVR_TODOTXT_DEFAULT_ACTION"
  777 fi
  778 if [ -n "$OVR_TODOTXT_SORT_COMMAND" ] ; then
  779   TODOTXT_SORT_COMMAND="$OVR_TODOTXT_SORT_COMMAND"
  780 fi
  781 if [ -n "$OVR_TODOTXT_FINAL_FILTER" ] ; then
  782   TODOTXT_FINAL_FILTER="$OVR_TODOTXT_FINAL_FILTER"
  783 fi
  784 
  785 ACTION=${1:-$TODOTXT_DEFAULT_ACTION}
  786 
  787 [ -z "$ACTION" ]    && usage
  788 [ -d "$TODO_DIR" ]  || mkdir -p "$TODO_DIR" 2> /dev/null || dieWithHelp "$1" "Fatal Error: $TODO_DIR is not a directory"
  789 ( cd "$TODO_DIR" )  || dieWithHelp "$1" "Fatal Error: Unable to cd to $TODO_DIR"
  790 [ -z "$TODOTXT_PRIORITY_ON_ADD" ] \
  791     || echo "$TODOTXT_PRIORITY_ON_ADD" | grep -q "^[A-Z]$" \
  792     || die "TODOTXT_PRIORITY_ON_ADD should be a capital letter from A to Z (it is now \"$TODOTXT_PRIORITY_ON_ADD\")."
  793 
  794 [ -z "$TODO_FILE" ] && TODO_FILE="$TODO_DIR/todo.txt"
  795 [ -z "$DONE_FILE" ] && DONE_FILE="$TODO_DIR/done.txt"
  796 [ -z "$REPORT_FILE" ] && REPORT_FILE="$TODO_DIR/report.txt"
  797 
  798 [ -f "$TODO_FILE" ] || [ -c "$TODO_FILE" ] || > "$TODO_FILE"
  799 [ -f "$DONE_FILE" ] || [ -c "$DONE_FILE" ] || > "$DONE_FILE"
  800 [ -f "$REPORT_FILE" ] || [ -c "$REPORT_FILE" ] || > "$REPORT_FILE"
  801 
  802 if [ $TODOTXT_PLAIN = 1 ]; then
  803     for clr in ${!PRI_@}; do
  804         export "$clr"=$NONE
  805     done
  806     PRI_X=$NONE
  807     DEFAULT=$NONE
  808     COLOR_DONE=$NONE
  809     COLOR_PROJECT=$NONE
  810     COLOR_CONTEXT=$NONE
  811     COLOR_DATE=$NONE
  812     COLOR_NUMBER=$NONE
  813     COLOR_META=$NONE
  814 fi
  815 
  816 [[ "$HIDE_PROJECTS_SUBSTITUTION" ]] && COLOR_PROJECT="$NONE"
  817 [[ "$HIDE_CONTEXTS_SUBSTITUTION" ]] && COLOR_CONTEXT="$NONE"
  818 
  819 _addto() {
  820     file="$1"
  821     input="$2"
  822     cleaninput
  823     uppercasePriority
  824 
  825     if [[ "$TODOTXT_DATE_ON_ADD" -eq 1 ]]; then
  826         now=$(date '+%Y-%m-%d')
  827         input=$(echo "$input" | sed -e 's/^\(([A-Z]) \)\{0,1\}/\1'"$now /")
  828     fi
  829     if [[ -n "$TODOTXT_PRIORITY_ON_ADD" ]]; then
  830         if ! echo "$input" | grep -q '^([A-Z])'; then
  831             input=$(echo -n "($TODOTXT_PRIORITY_ON_ADD) " ; echo "$input")
  832         fi
  833     fi
  834     fixMissingEndOfLine "$file"
  835     echo "$input" >> "$file"
  836     if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
  837         TASKNUM=$(sed -n '$ =' "$file")
  838         echo "$TASKNUM $input"
  839         echo "$(getPrefix "$file"): $TASKNUM added."
  840     fi
  841 }
  842 
  843 shellquote()
  844 {
  845     typeset -r qq=\'; printf %s\\n "'${1//\'/${qq}\\${qq}${qq}}'";
  846 }
  847 
  848 filtercommand()
  849 {
  850     filter=${1:-}
  851     shift
  852     post_filter=${1:-}
  853     shift
  854 
  855     for search_term
  856     do
  857         ## See if the first character of $search_term is a dash
  858         if [ "${search_term:0:1}" != '-' ]
  859         then
  860             ## First character isn't a dash: hide lines that don't match
  861             ## this $search_term
  862             filter="${filter:-}${filter:+ | }grep -i $(shellquote "$search_term")"
  863         else
  864             ## First character is a dash: hide lines that match this
  865             ## $search_term
  866             #
  867             ## Remove the first character (-) before adding to our filter command
  868             filter="${filter:-}${filter:+ | }grep -v -i $(shellquote "${search_term:1}")"
  869         fi
  870     done
  871 
  872     [ -n "$post_filter" ] && {
  873         filter="${filter:-}${filter:+ | }${post_filter:-}"
  874     }
  875 
  876     printf %s "$filter"
  877 }
  878 
  879 _list() {
  880     local FILE="$1"
  881     ## If the file starts with a "/" use absolute path. Otherwise,
  882     ## try to find it in either $TODO_DIR or using a relative path
  883     if [ "${1:0:1}" == / ]; then
  884         ## Absolute path
  885         src="$FILE"
  886     elif [ -f "$TODO_DIR/$FILE" ]; then
  887         ## Path relative to todo.sh directory
  888         src="$TODO_DIR/$FILE"
  889     elif [ -f "$FILE" ]; then
  890         ## Path relative to current working directory
  891         src="$FILE"
  892     elif [ -f "$TODO_DIR/${FILE}.txt" ]; then
  893         ## Path relative to todo.sh directory, missing file extension
  894         src="$TODO_DIR/${FILE}.txt"
  895     else
  896         die "TODO: File $FILE does not exist."
  897     fi
  898 
  899     ## Get our search arguments, if any
  900     shift ## was file name, new $1 is first search term
  901 
  902     _format "$src" '' "$@"
  903 
  904     if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
  905         echo "--"
  906         echo "$(getPrefix "$src"): ${NUMTASKS:-0} of ${TOTALTASKS:-0} tasks shown"
  907     fi
  908 }
  909 getPadding()
  910 {
  911     ## We need one level of padding for each power of 10 $LINES uses.
  912     LINES=$(sed -n '$ =' "${1:-$TODO_FILE}")
  913     printf %s ${#LINES}
  914 }
  915 _format()
  916 {
  917     # Parameters:    $1: todo input file; when empty formats stdin
  918     #                $2: ITEM# number width; if empty auto-detects from $1 / $TODO_FILE.
  919     # Precondition:  None
  920     # Postcondition: $NUMTASKS and $TOTALTASKS contain statistics (unless $TODOTXT_VERBOSE=0).
  921 
  922     FILE=$1
  923     shift
  924 
  925     ## Figure out how much padding we need to use, unless this was passed to us.
  926     PADDING=${1:-$(getPadding "$FILE")}
  927     shift
  928 
  929     ## Number the file, then run the filter command,
  930     ## then sort and mangle output some more
  931     if [[ $TODOTXT_DISABLE_FILTER = 1 ]]; then
  932         TODOTXT_FINAL_FILTER="cat"
  933     fi
  934     items=$(
  935         if [ "$FILE" ]; then
  936             sed = "$FILE"
  937         else
  938             sed =
  939         fi                                                      \
  940         | sed -e '''
  941             N
  942             s/^/     /
  943             s/ *\([ 0-9]\{'"$PADDING"',\}\)\n/\1 /
  944             /^[ 0-9]\{1,\} *$/d
  945          '''
  946     )
  947 
  948     ## Build and apply the filter.
  949     filter_command=$(filtercommand "${pre_filter_command:-}" "${post_filter_command:-}" "$@")
  950     if [ "${filter_command}" ]; then
  951         filtered_items=$(echo -n "$items" | eval "${filter_command}")
  952     else
  953         filtered_items=$items
  954     fi
  955     filtered_items=$(
  956         echo -n "$filtered_items"                              \
  957         | sed '''
  958             s/^     /00000/;
  959             s/^    /0000/;
  960             s/^   /000/;
  961             s/^  /00/;
  962             s/^ /0/;
  963           ''' \
  964         | eval "${TODOTXT_SORT_COMMAND}" \
  965         | awk '''
  966             function highlight(colorVar,      color) {
  967                 color = ENVIRON[colorVar]
  968                 gsub(/\\+033/, "\033", color)
  969                 return color
  970             }
  971             {
  972                 clr = ""
  973                 if (match($0, /^[0-9]+ x /)) {
  974                     clr = highlight("COLOR_DONE")
  975                 } else if (match($0, /^[0-9]+ \([A-Z]\) /)) {
  976                     clr = highlight("PRI_" substr($0, RSTART + RLENGTH - 3, 1))
  977                     clr = (clr ? clr : highlight("PRI_X"))
  978                     if (ENVIRON["HIDE_PRIORITY_SUBSTITUTION"] != "") {
  979                         $0 = substr($0, 1, RLENGTH - 4) substr($0, RSTART + RLENGTH)
  980                     }
  981                 }
  982                 end_clr = (clr ? highlight("DEFAULT") : "")
  983 
  984                 prj_beg = highlight("COLOR_PROJECT")
  985                 prj_end = (prj_beg ? (highlight("DEFAULT") clr) : "")
  986 
  987                 ctx_beg = highlight("COLOR_CONTEXT")
  988                 ctx_end = (ctx_beg ? (highlight("DEFAULT") clr) : "")
  989 
  990                 dat_beg = highlight("COLOR_DATE")
  991                 dat_end = (dat_beg ? (highlight("DEFAULT") clr) : "")
  992 
  993                 num_beg = highlight("COLOR_NUMBER")
  994                 num_end = (num_beg ? (highlight("DEFAULT") clr) : "")
  995 
  996                 met_beg = highlight("COLOR_META")
  997                 met_end = (met_beg ? (highlight("DEFAULT") clr) : "")
  998 
  999                 gsub(/[ \t][ \t]*/, "\n&\n")
 1000                 len = split($0, words, /\n/)
 1001 
 1002                 printf "%s", clr
 1003                 for (i = 1; i <= len; ++i) {
 1004                     if (i == 1 && words[i] ~ /^[0-9]+$/ ) {
 1005                         printf "%s", num_beg words[i] num_end
 1006                     } else if (words[i] ~ /^[+].*[A-Za-z0-9_]$/) {
 1007                         printf "%s", prj_beg words[i] prj_end
 1008                     } else if (words[i] ~ /^[@].*[A-Za-z0-9_]$/) {
 1009                         printf "%s", ctx_beg words[i] ctx_end
 1010                     } else if (words[i] ~ /^(19|20)[0-9]{2}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/) {
 1011                         printf "%s", dat_beg words[i] dat_end
 1012                     } else if (words[i] ~ /^[[:alnum:]]+:[^ ]+$/) {
 1013                         printf "%s", met_beg words[i] met_end
 1014                     } else {
 1015                         printf "%s", words[i]
 1016                     }
 1017                 }
 1018                 printf "%s\n", end_clr
 1019             }
 1020           '''  \
 1021         | sed '''
 1022             s/'"${HIDE_PROJECTS_SUBSTITUTION:-^}"'//g
 1023             s/'"${HIDE_CONTEXTS_SUBSTITUTION:-^}"'//g
 1024             s/'"${HIDE_CUSTOM_SUBSTITUTION:-^}"'//g
 1025           '''                                                   \
 1026         | eval ${TODOTXT_FINAL_FILTER}                          \
 1027     )
 1028     [ "$filtered_items" ] && echo "$filtered_items"
 1029 
 1030     if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1031         NUMTASKS=$( echo -n "$filtered_items" | sed -n '$ =' )
 1032         TOTALTASKS=$( echo -n "$items" | sed -n '$ =' )
 1033     fi
 1034     if [ "$TODOTXT_VERBOSE" -gt 1 ]; then
 1035         echo "TODO DEBUG: Filter Command was: ${filter_command:-cat}"
 1036     fi
 1037 }
 1038 
 1039 listWordsWithSigil()
 1040 {
 1041     sigil=$1
 1042     shift
 1043 
 1044     FILE=$TODO_FILE
 1045     [ "$TODOTXT_SOURCEVAR" ] && eval "FILE=$TODOTXT_SOURCEVAR"
 1046     eval "$(filtercommand 'cat "${FILE[@]}"' '' "$@")" \
 1047         | grep -o "[^ ]*${sigil}[^ ]\\+" \
 1048         | sed -n \
 1049             -e "s#^${TODOTXT_SIGIL_BEFORE_PATTERN//#/\\#}##" \
 1050             -e "s#${TODOTXT_SIGIL_AFTER_PATTERN//#/\\#}\$##" \
 1051             -e "/^${sigil}${TODOTXT_SIGIL_VALID_PATTERN//\//\\/}$/p" \
 1052         | sort -u
 1053 }
 1054 
 1055 export -f cleaninput getPrefix getTodo getNewtodo shellquote filtercommand _list listWordsWithSigil getPadding _format die
 1056 
 1057 # == HANDLE ACTION ==
 1058 action=$( printf "%s\n" "$ACTION" | tr '[:upper:]' '[:lower:]' )
 1059 
 1060 ## If the first argument is "command", run the rest of the arguments
 1061 ## using todo.sh builtins.
 1062 ## Else, run a actions script with the name of the command if it exists
 1063 ## or fallback to using a builtin
 1064 if [ "$action" == command ]
 1065 then
 1066     ## Get rid of "command" from arguments list
 1067     shift
 1068     ## Reset action to new first argument
 1069     action=$( printf "%s\n" "$1" | tr '[:upper:]' '[:lower:]' )
 1070 elif [ -d "$TODO_ACTIONS_DIR/$action" ] && [ -x "$TODO_ACTIONS_DIR/$action/$action" ]
 1071 then
 1072     "$TODO_ACTIONS_DIR/$action/$action" "$@"
 1073     exit $?
 1074 elif [ -d "$TODO_ACTIONS_DIR" ] && [ -x "$TODO_ACTIONS_DIR/$action" ]
 1075 then
 1076     "$TODO_ACTIONS_DIR/$action" "$@"
 1077     exit $?
 1078 fi
 1079 
 1080 ## Only run if $action isn't found in .todo.actions.d
 1081 case $action in
 1082 "add" | "a")
 1083     if [[ -z "$2" && $TODOTXT_FORCE = 0 ]]; then
 1084         echo -n "Add: "
 1085         read -e -r input
 1086     else
 1087         [ -z "$2" ] && die "usage: $TODO_SH add \"TODO ITEM\""
 1088         shift
 1089         input=$*
 1090     fi
 1091     _addto "$TODO_FILE" "$input"
 1092     ;;
 1093 
 1094 "addm")
 1095     if [[ -z "$2" && $TODOTXT_FORCE = 0 ]]; then
 1096         echo -n "Add: "
 1097         read -e -r input
 1098     else
 1099         [ -z "$2" ] && die "usage: $TODO_SH addm \"TODO ITEM\""
 1100         shift
 1101         input=$*
 1102     fi
 1103 
 1104     # Set Internal Field Seperator as newline so we can
 1105     # loop across multiple lines
 1106     SAVEIFS=$IFS
 1107     IFS=$'\n'
 1108 
 1109     # Treat each line seperately
 1110     for line in $input ; do
 1111         _addto "$TODO_FILE" "$line"
 1112     done
 1113     IFS=$SAVEIFS
 1114     ;;
 1115 
 1116 "addto" )
 1117     [ -z "$2" ] && die "usage: $TODO_SH addto DEST \"TODO ITEM\""
 1118     dest="$TODO_DIR/$2"
 1119     [ -z "$3" ] && die "usage: $TODO_SH addto DEST \"TODO ITEM\""
 1120     shift
 1121     shift
 1122     input=$*
 1123 
 1124     if [ -f "$dest" ]; then
 1125         _addto "$dest" "$input"
 1126     else
 1127         die "TODO: Destination file $dest does not exist."
 1128     fi
 1129     ;;
 1130 
 1131 "append" | "app" )
 1132     errmsg="usage: $TODO_SH append ITEM# \"TEXT TO APPEND\""
 1133     shift; item=$1; shift
 1134     getTodo "$item"
 1135 
 1136     if [[ -z "$1" && $TODOTXT_FORCE = 0 ]]; then
 1137         echo -n "Append: "
 1138         read -e -r input
 1139     else
 1140         input=$*
 1141     fi
 1142     case "$input" in
 1143       [$SENTENCE_DELIMITERS]*)  appendspace=;;
 1144       *)                        appendspace=" ";;
 1145     esac
 1146     cleaninput "for sed"
 1147 
 1148     if sed -i.bak "${item} s|^.*|&${appendspace}${input}|" "$TODO_FILE"; then
 1149         if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1150             getNewtodo "$item"
 1151             echo "$item $newtodo"
 1152     fi
 1153     else
 1154         die "TODO: Error appending task $item."
 1155     fi
 1156     ;;
 1157 
 1158 "archive" )
 1159     # defragment blank lines
 1160     sed -i.bak -e '/./!d' "$TODO_FILE"
 1161     [ "$TODOTXT_VERBOSE" -gt 0 ] && grep "^x " "$TODO_FILE"
 1162     grep "^x " "$TODO_FILE" >> "$DONE_FILE"
 1163     sed -i.bak '/^x /d' "$TODO_FILE"
 1164     if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1165     echo "TODO: $TODO_FILE archived."
 1166     fi
 1167     ;;
 1168 
 1169 "del" | "rm" )
 1170     # replace deleted line with a blank line when TODOTXT_PRESERVE_LINE_NUMBERS is 1
 1171     errmsg="usage: $TODO_SH del ITEM# [TERM]"
 1172     item=$2
 1173     getTodo "$item"
 1174 
 1175     if [ -z "$3" ]; then
 1176         if  [ $TODOTXT_FORCE = 0 ]; then
 1177             echo "Delete '$todo'?  (y/n)"
 1178             read -e -r ANSWER
 1179         else
 1180             ANSWER="y"
 1181         fi
 1182         if [ "$ANSWER" = "y" ]; then
 1183             if [ $TODOTXT_PRESERVE_LINE_NUMBERS = 0 ]; then
 1184                 # delete line (changes line numbers)
 1185                 sed -i.bak -e "${item}s/^.*//" -e '/./!d' "$TODO_FILE"
 1186             else
 1187                 # leave blank line behind (preserves line numbers)
 1188                 sed -i.bak -e "${item}s/^.*//" "$TODO_FILE"
 1189             fi
 1190             if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1191                 echo "$item $todo"
 1192                 echo "TODO: $item deleted."
 1193             fi
 1194         else
 1195             echo "TODO: No tasks were deleted."
 1196         fi
 1197     else
 1198         sed -i.bak \
 1199             -e "${item}s/^\((.) \)\{0,1\} *$3 */\1/g" \
 1200             -e "${item}s/ *$3 *\$//g" \
 1201             -e "${item}s/  *$3 */ /g" \
 1202             -e "${item}s/ *$3  */ /g" \
 1203             -e "${item}s/$3//g" \
 1204             "$TODO_FILE"
 1205         getNewtodo "$item"
 1206         if [ "$todo" = "$newtodo" ]; then
 1207             [ "$TODOTXT_VERBOSE" -gt 0 ] && echo "$item $todo"
 1208             die "TODO: '$3' not found; no removal done."
 1209         fi
 1210         if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1211             echo "$item $todo"
 1212             echo "TODO: Removed '$3' from task."
 1213             echo "$item $newtodo"
 1214         fi
 1215     fi
 1216     ;;
 1217 
 1218 "depri" | "dp" )
 1219     errmsg="usage: $TODO_SH depri ITEM#[, ITEM#, ITEM#, ...]"
 1220     shift;
 1221     [ $# -eq 0 ] && die "$errmsg"
 1222 
 1223     # Split multiple depri's, if comma separated change to whitespace separated
 1224     # Loop the 'depri' function for each item
 1225     for item in ${*//,/ }; do
 1226         getTodo "$item"
 1227 
 1228     if [[ "$todo" = \(?\)\ * ]]; then
 1229         sed -i.bak -e "${item}s/^(.) //" "$TODO_FILE"
 1230         if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1231         getNewtodo "$item"
 1232         echo "$item $newtodo"
 1233         echo "TODO: $item deprioritized."
 1234         fi
 1235     else
 1236         echo "TODO: $item is not prioritized."
 1237     fi
 1238     done
 1239     ;;
 1240 
 1241 "do" | "done" )
 1242     errmsg="usage: $TODO_SH do ITEM#[, ITEM#, ITEM#, ...]"
 1243     # shift so we get arguments to the do request
 1244     shift;
 1245     [ "$#" -eq 0 ] && die "$errmsg"
 1246 
 1247     # Split multiple do's, if comma separated change to whitespace separated
 1248     # Loop the 'do' function for each item
 1249     for item in ${*//,/ }; do
 1250         getTodo "$item"
 1251 
 1252         # Check if this item has already been done
 1253         if [ "${todo:0:2}" != "x " ]; then
 1254             now=$(date '+%Y-%m-%d')
 1255             # remove priority once item is done
 1256             sed -i.bak "${item}s/^(.) //" "$TODO_FILE"
 1257             sed -i.bak "${item}s|^|x $now |" "$TODO_FILE"
 1258             if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1259                 getNewtodo "$item"
 1260                 echo "$item $newtodo"
 1261                 echo "TODO: $item marked as done."
 1262         fi
 1263         else
 1264             echo "TODO: $item is already marked done."
 1265         fi
 1266     done
 1267 
 1268     if [ $TODOTXT_AUTO_ARCHIVE = 1 ]; then
 1269         # Recursively invoke the script to allow overriding of the archive
 1270         # action.
 1271         "$TODO_FULL_SH" archive
 1272     fi
 1273     ;;
 1274 
 1275 "help" )
 1276     shift  ## Was help; new $1 is first help topic / action name
 1277     if [ $# -gt 0 ]; then
 1278         # Don't use PAGER here; we don't expect much usage output from one / few actions.
 1279         actionUsage "$@"
 1280     else
 1281         if [ -t 1 ] ; then # STDOUT is a TTY
 1282             if which "${PAGER:-less}" >/dev/null 2>&1; then
 1283                 # we have a working PAGER (or less as a default)
 1284                 help | "${PAGER:-less}" && exit 0
 1285             fi
 1286         fi
 1287         help # just in case something failed above, we go ahead and just spew to STDOUT
 1288     fi
 1289     ;;
 1290 
 1291 "shorthelp" )
 1292     if [ -t 1 ] ; then # STDOUT is a TTY
 1293         if which "${PAGER:-less}" >/dev/null 2>&1; then
 1294             # we have a working PAGER (or less as a default)
 1295             shorthelp | "${PAGER:-less}" && exit 0
 1296         fi
 1297     fi
 1298     shorthelp # just in case something failed above, we go ahead and just spew to STDOUT
 1299     ;;
 1300 
 1301 "list" | "ls" )
 1302     shift  ## Was ls; new $1 is first search term
 1303     _list "$TODO_FILE" "$@"
 1304     ;;
 1305 
 1306 "listall" | "lsa" )
 1307     shift  ## Was lsa; new $1 is first search term
 1308 
 1309     TOTAL=$( sed -n '$ =' "$TODO_FILE" )
 1310     PADDING=${#TOTAL}
 1311 
 1312     post_filter_command="${post_filter_command:-}${post_filter_command:+ | }awk -v TOTAL=$TOTAL -v PADDING=$PADDING '{ \$1 = sprintf(\"%\" PADDING \"d\", (\$1 > TOTAL ? 0 : \$1)); print }' "
 1313     cat "$TODO_FILE" "$DONE_FILE" | TODOTXT_VERBOSE=0 _format '' "$PADDING" "$@"
 1314 
 1315     if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1316         TDONE=$( sed -n '$ =' "$DONE_FILE" )
 1317         TASKNUM=$(TODOTXT_PLAIN=1 TODOTXT_VERBOSE=0 _format "$TODO_FILE" 1 "$@" | sed -n '$ =')
 1318         DONENUM=$(TODOTXT_PLAIN=1 TODOTXT_VERBOSE=0 _format "$DONE_FILE" 1 "$@" | sed -n '$ =')
 1319         echo "--"
 1320         echo "$(getPrefix "$TODO_FILE"): ${TASKNUM:-0} of ${TOTAL:-0} tasks shown"
 1321         echo "$(getPrefix "$DONE_FILE"): ${DONENUM:-0} of ${TDONE:-0} tasks shown"
 1322         echo "total $((TASKNUM + DONENUM)) of $((TOTAL + TDONE)) tasks shown"
 1323     fi
 1324     ;;
 1325 
 1326 "listfile" | "lf" )
 1327     shift  ## Was listfile, next $1 is file name
 1328     if [ $# -eq 0 ]; then
 1329         [ "$TODOTXT_VERBOSE" -gt 0 ] && echo "Files in the todo.txt directory:"
 1330         cd "$TODO_DIR" && ls -1 -- *.txt
 1331     else
 1332         FILE="$1"
 1333         shift  ## Was filename; next $1 is first search term
 1334 
 1335         _list "$FILE" "$@"
 1336     fi
 1337     ;;
 1338 
 1339 "listcon" | "lsc" )
 1340     shift
 1341     listWordsWithSigil '@' "$@"
 1342     ;;
 1343 
 1344 "listproj" | "lsprj" )
 1345     shift
 1346     listWordsWithSigil '+' "$@"
 1347     ;;
 1348 
 1349 "listpri" | "lsp" )
 1350     shift ## was "listpri", new $1 is priority to list or first TERM
 1351 
 1352     pri=$(printf "%s\n" "$1" | tr '[:lower:]' '[:upper:]' | grep -e '^[A-Z]$' -e '^[A-Z]-[A-Z]$') && shift || pri="A-Z"
 1353     post_filter_command="${post_filter_command:-}${post_filter_command:+ | }grep '^ *[0-9]\+ ([${pri}]) '"
 1354     _list "$TODO_FILE" "$@"
 1355     ;;
 1356 
 1357 "move" | "mv" )
 1358     # replace moved line with a blank line when TODOTXT_PRESERVE_LINE_NUMBERS is 1
 1359     errmsg="usage: $TODO_SH mv ITEM# DEST [SRC]"
 1360     item=$2
 1361     dest="$TODO_DIR/$3"
 1362     src="$TODO_DIR/$4"
 1363 
 1364     [ -z "$4" ] && src="$TODO_FILE"
 1365     [ -z "$dest" ] && die "$errmsg"
 1366 
 1367     [ -f "$src" ] || die "TODO: Source file $src does not exist."
 1368     [ -f "$dest" ] || die "TODO: Destination file $dest does not exist."
 1369 
 1370     getTodo "$item" "$src"
 1371     [ -z "$todo" ] && die "$item: No such item in $src."
 1372     if  [ $TODOTXT_FORCE = 0 ]; then
 1373         echo "Move '$todo' from $src to $dest? (y/n)"
 1374         read -e -r ANSWER
 1375     else
 1376         ANSWER="y"
 1377     fi
 1378     if [ "$ANSWER" = "y" ]; then
 1379         if [ $TODOTXT_PRESERVE_LINE_NUMBERS = 0 ]; then
 1380             # delete line (changes line numbers)
 1381             sed -i.bak -e "${item}s/^.*//" -e '/./!d' "$src"
 1382         else
 1383             # leave blank line behind (preserves line numbers)
 1384             sed -i.bak -e "${item}s/^.*//" "$src"
 1385         fi
 1386         fixMissingEndOfLine "$dest"
 1387         echo "$todo" >> "$dest"
 1388 
 1389         if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1390             echo "$item $todo"
 1391             echo "TODO: $item moved from '$src' to '$dest'."
 1392         fi
 1393     else
 1394         echo "TODO: No tasks moved."
 1395     fi
 1396     ;;
 1397 
 1398 "prepend" | "prep" )
 1399     errmsg="usage: $TODO_SH prepend ITEM# \"TEXT TO PREPEND\""
 1400     replaceOrPrepend 'prepend' "$@"
 1401     ;;
 1402 
 1403 "pri" | "p" )
 1404     item=$2
 1405     newpri=$( printf "%s\n" "$3" | tr '[:lower:]' '[:upper:]' )
 1406 
 1407     errmsg="usage: $TODO_SH pri ITEM# PRIORITY
 1408 note: PRIORITY must be anywhere from A to Z."
 1409 
 1410     [ "$#" -ne 3 ] && die "$errmsg"
 1411     [[ "$newpri" = @([A-Z]) ]] || die "$errmsg"
 1412     getTodo "$item"
 1413 
 1414     oldpri=
 1415     if [[ "$todo" = \(?\)\ * ]]; then
 1416         oldpri=${todo:1:1}
 1417     fi
 1418 
 1419     if [ "$oldpri" != "$newpri" ]; then
 1420         sed -i.bak -e "${item}s/^(.) //" -e "${item}s/^/($newpri) /" "$TODO_FILE"
 1421     fi
 1422     if [ "$TODOTXT_VERBOSE" -gt 0 ]; then
 1423         getNewtodo "$item"
 1424         echo "$item $newtodo"
 1425         if [ "$oldpri" != "$newpri" ]; then
 1426             if [ "$oldpri" ]; then
 1427                 echo "TODO: $item re-prioritized from ($oldpri) to ($newpri)."
 1428             else
 1429                 echo "TODO: $item prioritized ($newpri)."
 1430             fi
 1431         fi
 1432     fi
 1433     if [ "$oldpri" = "$newpri" ]; then
 1434         echo "TODO: $item already prioritized ($newpri)."
 1435     fi
 1436     ;;
 1437 
 1438 "replace" )
 1439     errmsg="usage: $TODO_SH replace ITEM# \"UPDATED ITEM\""
 1440     replaceOrPrepend 'replace' "$@"
 1441     ;;
 1442 
 1443 "report" )
 1444     # archive first
 1445     # Recursively invoke the script to allow overriding of the archive
 1446     # action.
 1447     "$TODO_FULL_SH" archive
 1448 
 1449     TOTAL=$( sed -n '$ =' "$TODO_FILE" )
 1450     TDONE=$( sed -n '$ =' "$DONE_FILE" )
 1451     NEWDATA="${TOTAL:-0} ${TDONE:-0}"
 1452     LASTREPORT=$(sed -ne '$p' "$REPORT_FILE")
 1453     LASTDATA=${LASTREPORT#* }   # Strip timestamp.
 1454     if [ "$LASTDATA" = "$NEWDATA" ]; then
 1455         echo "$LASTREPORT"
 1456         [ "$TODOTXT_VERBOSE" -gt 0 ] && echo "TODO: Report file is up-to-date."
 1457     else
 1458         NEWREPORT="$(date +%Y-%m-%dT%T) ${NEWDATA}"
 1459         echo "${NEWREPORT}" >> "$REPORT_FILE"
 1460         echo "${NEWREPORT}"
 1461         [ "$TODOTXT_VERBOSE" -gt 0 ] && echo "TODO: Report file updated."
 1462     fi
 1463     ;;
 1464 
 1465 "deduplicate" )
 1466     if [ $TODOTXT_PRESERVE_LINE_NUMBERS = 0 ]; then
 1467         deduplicateSedCommand='d'
 1468     else
 1469         deduplicateSedCommand='s/^.*//; p'
 1470     fi
 1471 
 1472     # To determine the difference when deduplicated lines are preserved, only
 1473     # non-empty lines must be counted.
 1474     originalTaskNum=$( sed -e '/./!d' "$TODO_FILE" | sed -n '$ =' )
 1475 
 1476     # Look for duplicate lines and discard the second occurrence.
 1477     # We start with an empty hold space on the first line.  For each line:
 1478     #   G - appends newline + hold space to the pattern space
 1479     #   s/\n/&&/; - double up the first new line so we catch adjacent dups
 1480     #   /^\([^\n]*\n\).*\n\1/b dedup
 1481     #       If the first line of the hold space shows up again later as an
 1482     #       entire line, it's a duplicate. Jump to the "dedup" label, where
 1483     #       either of the following is executed, depending on whether empty
 1484     #       lines should be preserved:
 1485     #       d           - Delete the current pattern space, quit this line and
 1486     #                     move on to the next, or:
 1487     #       s/^.*//; p  - Clear the task text, print this line and move on to
 1488     #                     the next.
 1489     #   s/\n//;   - else (no duplicate), drop the doubled newline
 1490     #   h;        - replace the hold space with the expanded pattern space
 1491     #   P;        - print up to the first newline (that is, the input line)
 1492     #   b         - end processing of the current line
 1493     sed -i.bak -n \
 1494         -e 'G; s/\n/&&/; /^\([^\n]*\n\).*\n\1/b dedup' \
 1495         -e 's/\n//; h; P; b' \
 1496         -e ':dedup' \
 1497         -e "$deduplicateSedCommand" \
 1498         "$TODO_FILE"
 1499 
 1500     newTaskNum=$( sed -e '/./!d' "$TODO_FILE" | sed -n '$ =' )
 1501     deduplicateNum=$(( originalTaskNum - newTaskNum ))
 1502     if [ $deduplicateNum -eq 0 ]; then
 1503         echo "TODO: No duplicate tasks found"
 1504     else
 1505         echo "TODO: $deduplicateNum duplicate task(s) removed"
 1506     fi
 1507     ;;
 1508 
 1509 "listaddons" )
 1510     if [ -d "$TODO_ACTIONS_DIR" ]; then
 1511         cd "$TODO_ACTIONS_DIR" || exit $?
 1512         for action in *
 1513         do
 1514             if [ -f "$action" ] && [ -x "$action" ]; then
 1515                 echo "$action"
 1516             elif [ -d "$action" ] && [ -x "$action/$action" ]; then
 1517                 echo "$action"
 1518             fi
 1519         done
 1520     fi
 1521     ;;
 1522 
 1523 * )
 1524     usage;;
 1525 esac