1#!/usr/local/bin/bash 2 3# === HEAVY LIFTING === 4shopt -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" 10version() { 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. 23TODO_SH=$(basename "$0") 24TODO_FULL_SH="$0" 25export TODO_SH TODO_FULL_SH 26 27oneline_usage="$TODO_SH [-fhpantvV] [-d todo_config] action [task_number] [task_description]" 28 29usage() 30{ 31 cat <<-EndUsage 32 Usage: $oneline_usage 33 Try '$TODO_SH -h' for more information. 34 EndUsage 35 exit 1 36} 37 38shorthelp() 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 84help() 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 162actionsHelp() 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 294addonHelp() 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 321actionUsage() 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 342dieWithHelp() 343{ 344 case "$1" in 345 help) help;; 346 shorthelp) shorthelp;; 347 esac 348 shift 349 350 die "$@" 351} 352die() 353{ 354 echo "$*" 355 exit 1 356} 357 358cleaninput() 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 379getPrefix() 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 389getTodo() 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} 403getNewtodo() 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 418replaceOrPrepend() 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 472fixMissingEndOfLine() 473{ 474 # Parameters: $1: todo file; empty means $TODO_FILE. 475 sed -i.bak -e '$a\' "${1:-$TODO_FILE}" 476} 477 478uppercasePriority() 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 492OVR_TODOTXT_AUTO_ARCHIVE="$TODOTXT_AUTO_ARCHIVE" 493OVR_TODOTXT_FORCE="$TODOTXT_FORCE" 494OVR_TODOTXT_PRESERVE_LINE_NUMBERS="$TODOTXT_PRESERVE_LINE_NUMBERS" 495OVR_TODOTXT_PLAIN="$TODOTXT_PLAIN" 496OVR_TODOTXT_DATE_ON_ADD="$TODOTXT_DATE_ON_ADD" 497OVR_TODOTXT_PRIORITY_ON_ADD="$TODOTXT_PRIORITY_ON_ADD" 498OVR_TODOTXT_DISABLE_FILTER="$TODOTXT_DISABLE_FILTER" 499OVR_TODOTXT_VERBOSE="$TODOTXT_VERBOSE" 500OVR_TODOTXT_DEFAULT_ACTION="$TODOTXT_DEFAULT_ACTION" 501OVR_TODOTXT_SORT_COMMAND="$TODOTXT_SORT_COMMAND" 502OVR_TODOTXT_FINAL_FILTER="$TODOTXT_FINAL_FILTER" 503 504# Prevent GREP_OPTIONS from malforming grep's output 505export GREP_OPTIONS="" 506 507# == PROCESS OPTIONS == 508while getopts ":fhpcnNaAtTvVx+@Pd:" Option 509do 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 609done 610shift $((OPTIND - 1)) 611 612# defaults if not yet defined 613TODOTXT_VERBOSE=${TODOTXT_VERBOSE:-1} 614TODOTXT_PLAIN=${TODOTXT_PLAIN:-0} 615TODOTXT_CFG_FILE=${TODOTXT_CFG_FILE:-$HOME/.todo/config} 616TODOTXT_FORCE=${TODOTXT_FORCE:-0} 617TODOTXT_PRESERVE_LINE_NUMBERS=${TODOTXT_PRESERVE_LINE_NUMBERS:-1} 618TODOTXT_AUTO_ARCHIVE=${TODOTXT_AUTO_ARCHIVE:-1} 619TODOTXT_DATE_ON_ADD=${TODOTXT_DATE_ON_ADD:-0} 620TODOTXT_PRIORITY_ON_ADD=${TODOTXT_PRIORITY_ON_ADD:-} 621TODOTXT_DEFAULT_ACTION=${TODOTXT_DEFAULT_ACTION:-} 622TODOTXT_SORT_COMMAND=${TODOTXT_SORT_COMMAND:-env LC_COLLATE=C sort -f -k2} 623TODOTXT_DISABLE_FILTER=${TODOTXT_DISABLE_FILTER:-} 624TODOTXT_FINAL_FILTER=${TODOTXT_FINAL_FILTER:-cat} 625TODOTXT_GLOBAL_CFG_FILE=${TODOTXT_GLOBAL_CFG_FILE:-/usr/local/etc/todo.cfg} 626TODOTXT_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. 627TODOTXT_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. 628TODOTXT_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 631export "${!TODOTXT_@}" 632 633# Default color map 634export NONE='' 635export BLACK='\\033[0;30m' 636export RED='\\033[0;31m' 637export GREEN='\\033[0;32m' 638export BROWN='\\033[0;33m' 639export BLUE='\\033[0;34m' 640export PURPLE='\\033[0;35m' 641export CYAN='\\033[0;36m' 642export LIGHT_GREY='\\033[0;37m' 643export DARK_GREY='\\033[1;30m' 644export LIGHT_RED='\\033[1;31m' 645export LIGHT_GREEN='\\033[1;32m' 646export YELLOW='\\033[1;33m' 647export LIGHT_BLUE='\\033[1;34m' 648export LIGHT_PURPLE='\\033[1;35m' 649export LIGHT_CYAN='\\033[1;36m' 650export WHITE='\\033[1;37m' 651export DEFAULT='\\033[0m' 652 653# Default priority->color map. 654export PRI_A=$YELLOW # color for A priority 655export PRI_B=$GREEN # color for B priority 656export PRI_C=$LIGHT_BLUE # color for C priority 657export PRI_X=$WHITE # color unless explicitly defined 658 659# Default project, context, date, item number, and metadata key:value pairs colors. 660export COLOR_PROJECT=$NONE 661export COLOR_CONTEXT=$NONE 662export COLOR_DATE=$NONE 663export COLOR_NUMBER=$NONE 664export COLOR_META=$NONE 665 666# Default highlight colors. 667export 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. 673export 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 721if [ -z "$TODO_ACTIONS_DIR" ] || [ ! -d "$TODO_ACTIONS_DIR" ] 722then 723 TODO_ACTIONS_DIR="$HOME/.todo/actions" 724 export TODO_ACTIONS_DIR 725fi 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 751if [ -n "$OVR_TODOTXT_AUTO_ARCHIVE" ] ; then 752 TODOTXT_AUTO_ARCHIVE="$OVR_TODOTXT_AUTO_ARCHIVE" 753fi 754if [ -n "$OVR_TODOTXT_FORCE" ] ; then 755 TODOTXT_FORCE="$OVR_TODOTXT_FORCE" 756fi 757if [ -n "$OVR_TODOTXT_PRESERVE_LINE_NUMBERS" ] ; then 758 TODOTXT_PRESERVE_LINE_NUMBERS="$OVR_TODOTXT_PRESERVE_LINE_NUMBERS" 759fi 760if [ -n "$OVR_TODOTXT_PLAIN" ] ; then 761 TODOTXT_PLAIN="$OVR_TODOTXT_PLAIN" 762fi 763if [ -n "$OVR_TODOTXT_DATE_ON_ADD" ] ; then 764 TODOTXT_DATE_ON_ADD="$OVR_TODOTXT_DATE_ON_ADD" 765fi 766if [ -n "$OVR_TODOTXT_PRIORITY_ON_ADD" ] ; then 767 TODOTXT_PRIORITY_ON_ADD="$OVR_TODOTXT_PRIORITY_ON_ADD" 768fi 769if [ -n "$OVR_TODOTXT_DISABLE_FILTER" ] ; then 770 TODOTXT_DISABLE_FILTER="$OVR_TODOTXT_DISABLE_FILTER" 771fi 772if [ -n "$OVR_TODOTXT_VERBOSE" ] ; then 773 TODOTXT_VERBOSE="$OVR_TODOTXT_VERBOSE" 774fi 775if [ -n "$OVR_TODOTXT_DEFAULT_ACTION" ] ; then 776 TODOTXT_DEFAULT_ACTION="$OVR_TODOTXT_DEFAULT_ACTION" 777fi 778if [ -n "$OVR_TODOTXT_SORT_COMMAND" ] ; then 779 TODOTXT_SORT_COMMAND="$OVR_TODOTXT_SORT_COMMAND" 780fi 781if [ -n "$OVR_TODOTXT_FINAL_FILTER" ] ; then 782 TODOTXT_FINAL_FILTER="$OVR_TODOTXT_FINAL_FILTER" 783fi 784 785ACTION=${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 802if [ $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 814fi 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 843shellquote() 844{ 845 typeset -r qq=\'; printf %s\\n "'${1//\'/${qq}\\${qq}${qq}}'"; 846} 847 848filtercommand() 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} 909getPadding() 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 1039listWordsWithSigil() 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 1055export -f cleaninput getPrefix getTodo getNewtodo shellquote filtercommand _list listWordsWithSigil getPadding _format die 1056 1057# == HANDLE ACTION == 1058action=$( 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 1064if [ "$action" == command ] 1065then 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:]' ) 1070elif [ -d "$TODO_ACTIONS_DIR/$action" ] && [ -x "$TODO_ACTIONS_DIR/$action/$action" ] 1071then 1072 "$TODO_ACTIONS_DIR/$action/$action" "$@" 1073 exit $? 1074elif [ -d "$TODO_ACTIONS_DIR" ] && [ -x "$TODO_ACTIONS_DIR/$action" ] 1075then 1076 "$TODO_ACTIONS_DIR/$action" "$@" 1077 exit $? 1078fi 1079 1080## Only run if $action isn't found in .todo.actions.d 1081case $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 1408note: 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;; 1525esac 1526