Basically we have two scripts, one in Bash and the other in Python.
The script in Bash will do the following:
- Read a MD file
- Get check-boxes and convert in Tasks
- Case the checkbox is a sub checkbox then it is a child or subtask.
- In the end I need to know which task was added then I put a logic for insert the UUID from the Task into the markdown line of the task
#!/usr/bin/env bash
set -euo pipefail
if ! command -v awk >/dev/null; then
echo "awk is required"
exit 1
fi
if [ $# -lt 1 ]; then
echo "Usage: $0 <markdown-file> <project-name>"
exit 1
fi
FILE="$1"
if [ ! -f "$FILE" ]; then
echo "File not found: $FILE"
exit 1
fi
PROJECT="${2:-}"
echo "=== Taskwarrior Markdown Importer ==="
if [ -z "$PROJECT" ]; then
while true; do
read -rp "Project name: " INPUT_PROJECT
if [ ! -z "$INPUT_PROJECT" ]; then
PROJECT="$INPUT_PROJECT"
break
fi
done
fi
echo "Project: $PROJECT, File: $FILE"
get_task() {
awk -v line="$1" '
BEGIN {
gsub(/\r/, "", line)
gsub(/\t/, " ", line)
match(line, /^ */)
indent = int(RLENGTH / 4)
trimmed = substr(line, RLENGTH + 1)
checked = "false"
if (match(trimmed, /^[-*+][[:space:]]*\[([xX ])\][[:space:]]*/)) {
if (substr(trimmed, RSTART+2, 3) ~ /[xX]/) {
checked = "true"
}
trimmed = substr(trimmed, RLENGTH + 1)
while (match(trimmed, /\[\[[^]]+\]\]/)) {
link = substr(trimmed, RSTART + 2, RLENGTH - 4) # content inside [[ ]]
n = split(link, tmp, /\|/)
replacement = tmp[n] # last part (after | if exists)
trimmed = substr(trimmed, 1, RSTART - 1) replacement substr(trimmed, RSTART + RLENGTH)
}
gsub(/ +/, " ", trimmed)
n = split(trimmed, parts, /;;/)
description = parts[1]
priority = (n >= 2 && parts[2] != "" ? parts[2] : "L")
due = (n >= 3 && parts[3] != "" ? parts[3] : "today")
printf "%d\x1f%s\x1f%s\x1f%s\x1f%s\n",
indent, checked, description, priority, due
}
}'
}
insert_uuid_on_file() {
local file="$1"
local lineno="$2"
local uuid="$3"
local short="${uuid:0:8}"
if sed -n "${lineno}p" "$file" | grep -q "task:"; then
return
fi
sed -i "${lineno}s|\$| <!-- task:${short} -->|" "$file"
}
IGNORED=()
PCHECKED="false"
PID=""
CID=""
UUID=""
tmp=$(mktemp)
cp "$FILE" "$tmp"
i=0
while IFS= read -r line; do
((i += 1))
task=$(get_task "$line")
IFS=$'\x1f' read -r indent checked description priority due <<<"$task"
if [[ -z "${description// /}" ]]; then
continue
fi
child="false"
if [[ "$indent" -gt 0 ]]; then
child="true"
else
PCHECKED="$checked"
fi
if [[ $checked = "true" || $child = "true" && $PCHECKED = "true" || "$line" =~ task: ]]; then
IGNORED+=("$description|$priority|$due")
continue
fi
ID=$(task add "$description" project:"$PROJECT" priority:"$priority" due:"$due" mdfile:"$FILE" |
grep -oP 'Created task \K[0-9]+')
if [[ "$child" = "false" ]]; then
PID="$ID"
else
CID="$ID"
task "$CID" modify +P"$PID" >/dev/null
task "$PID" modify depends:"$CID" >/dev/null
fi
UUID=$(task _get "$ID".uuid)
insert_uuid_on_file "$FILE" "$i" "$UUID"
# echo "$indent|$checked|$description|$priority|$due"
done <"$tmp"
rm "$tmp"
echo "=== IGNORED ==="
printf "%s\n" "${IGNORED[@]}"
echo "=== === === ==="
You need awk installed for use it, and I this only work in Linux and MacOS.
And you can run with ./taskmd.sh <path/to/file.md> <project_name (optional)>
In my case I created a function in my fish shell for execute this script, then I don't need to put the path to the script, maybe it can be a alias in the terminal too.
And the Python is not really necessary in this logic, cause it only do a simple logic of when I finish some task in the Task Warrior this script will be executed as a hook, and It will check the checkbox in the Markdown file.
#!/usr/bin/env python3
import json
import re
import sys
from pathlib import Path
def update_line(line: str, uuid: str, status: str):
if f"task:{uuid}" not in line:
return line, False
# line = re.sub(r"\s*<!--\s*task:" + re.escape(uuid) + r"\s*-->", "", line)
if status in ("completed", "deleted"):
line = re.sub(r"\[\s\]", "[x]", line)
else:
line = re.sub(r"\[[xX]\]", "[ ]", line)
return line, True
def main():
_ = json.loads(sys.stdin.readline())
new_task = json.loads(sys.stdin.readline())
status = new_task.get("status", "")
if status not in ("completed", "deleted"):
print(json.dumps(new_task))
return
uuid = new_task.get("uuid", "")
if not uuid:
print(json.dumps(new_task))
return
uuid_short = uuid[:8]
mdfile = new_task.get("mdfile")
if not mdfile:
print(json.dumps(new_task))
return
path = Path(mdfile)
if not path.exists():
print(json.dumps(new_task))
return
lines = path.read_text().splitlines()
changed = False
for i, line in enumerate(lines):
new_line, matched = update_line(line, uuid_short, new_task.get("status"))
if matched:
lines[i] = new_line
changed = True
break
if changed:
path.write_text("\n".join(lines) + "\n")
print(json.dumps(new_task))
if __name__ == "__main__":
main()
This python script need to stay in ~/.task/hooks with name like on-modify.<anyname>.pyon Linux case, you need to verify in Windows where this paste need to be.
And you need to add those lines in the .taskrc file, that on Linux, will stay in the ~ directory.
uda.mdfile.type=string
uda.mdfile.label=File
In that way I can convert markdown files, that is very simple to create in tasks on my Task Warrior.
The task warrior you can download here and I recommend to use the Task Warrior TUI for have a better visualization in the terminal.
Is that, have a nice day, and see you soon :)




