vault-cli: extract to standalone repo, add flake.nix, move one-off scripts to scripts/one-off/
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add a short-name alias (ID prefix stripped) to every ID-prefixed note's
|
||||
frontmatter, so Obsidian's alias-autocomplete can produce clean [[ID|Name]]
|
||||
links without the user typing the ID. Idempotent: skips notes that already
|
||||
have that alias or non-ID-prefixed notes.
|
||||
"""
|
||||
import os, re, sys
|
||||
|
||||
VAULT = "/vault"
|
||||
# Matches "F.50.12 Jen", "R.31.10 Hindi", "F.22.11 Places We've Slept" etc.
|
||||
ID_TITLE_RE = re.compile(r'^([RJFI](?:\.\d+){1,3})\s+(.+)$')
|
||||
|
||||
FRONTMATTER_RE = re.compile(r'^---\n(.*?)\n---\n', re.DOTALL)
|
||||
|
||||
def split_frontmatter(text):
|
||||
m = FRONTMATTER_RE.match(text)
|
||||
if m:
|
||||
return m.group(1), text[m.end():]
|
||||
return None, text
|
||||
|
||||
def add_alias(fm_text, alias):
|
||||
lines = fm_text.split("\n") if fm_text else []
|
||||
# find aliases: line (list style) or aliases: [a, b]
|
||||
alias_line_idx = None
|
||||
for i, l in enumerate(lines):
|
||||
if re.match(r'^aliases\s*:', l):
|
||||
alias_line_idx = i
|
||||
break
|
||||
if alias_line_idx is None:
|
||||
lines.append(f"aliases:\n - {alias}")
|
||||
return "\n".join(lines)
|
||||
line = lines[alias_line_idx]
|
||||
# inline list form: aliases: [a, b]
|
||||
m = re.match(r'^aliases\s*:\s*\[(.*)\]\s*$', line)
|
||||
if m:
|
||||
existing = [x.strip().strip('"\'') for x in m.group(1).split(",") if x.strip()]
|
||||
if alias in existing:
|
||||
return None # already present
|
||||
existing.append(alias)
|
||||
lines[alias_line_idx] = "aliases: [" + ", ".join(existing) + "]"
|
||||
return "\n".join(lines)
|
||||
# block list form: aliases:\n - a\n - b
|
||||
if line.strip() == "aliases:" or re.match(r'^aliases\s*:\s*$', line):
|
||||
j = alias_line_idx + 1
|
||||
existing = []
|
||||
while j < len(lines) and re.match(r'^\s*-\s*', lines[j]):
|
||||
existing.append(lines[j].strip()[2:].strip().strip('"\''))
|
||||
j += 1
|
||||
if alias in existing:
|
||||
return None
|
||||
lines.insert(j, f" - {alias}")
|
||||
return "\n".join(lines)
|
||||
# single scalar alias: aliases: Foo
|
||||
scalar = line.split(":", 1)[1].strip().strip('"\'')
|
||||
if scalar == alias:
|
||||
return None
|
||||
lines[alias_line_idx] = "aliases:\n - " + scalar + f"\n - {alias}"
|
||||
return "\n".join(lines)
|
||||
|
||||
def main():
|
||||
execute = "--execute" in sys.argv
|
||||
changed = 0
|
||||
skipped_no_id = 0
|
||||
skipped_has_alias = 0
|
||||
errors = []
|
||||
for dp, _, fns in os.walk(VAULT):
|
||||
if ".git" in dp.split(os.sep):
|
||||
continue
|
||||
for fn in fns:
|
||||
if not fn.lower().endswith(".md"):
|
||||
continue
|
||||
base = fn[:-3]
|
||||
m = ID_TITLE_RE.match(base)
|
||||
if not m:
|
||||
skipped_no_id += 1
|
||||
continue
|
||||
alias = m.group(2)
|
||||
p = os.path.join(dp, fn)
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8", errors="ignore") as f:
|
||||
text = f.read()
|
||||
except Exception as e:
|
||||
errors.append((p, str(e)))
|
||||
continue
|
||||
fm, rest = split_frontmatter(text)
|
||||
new_fm = add_alias(fm or "", alias)
|
||||
if new_fm is None:
|
||||
skipped_has_alias += 1
|
||||
continue
|
||||
new_text = f"---\n{new_fm}\n---\n\n{rest.lstrip(chr(10))}" if fm is None else f"---\n{new_fm}\n---\n{rest}"
|
||||
changed += 1
|
||||
if execute:
|
||||
with open(p, "w", encoding="utf-8") as f:
|
||||
f.write(new_text)
|
||||
|
||||
print(f"Notes with ID-prefixed title: {changed + skipped_has_alias}")
|
||||
print(f" Would add alias: {changed}")
|
||||
print(f" Already had alias: {skipped_has_alias}")
|
||||
print(f"Notes without ID prefix (skipped, not applicable): {skipped_no_id}")
|
||||
if errors:
|
||||
print(f"Errors: {len(errors)}")
|
||||
for p, e in errors[:10]:
|
||||
print(f" {p}: {e}")
|
||||
if not execute:
|
||||
print("\n(dry run -- pass --execute to write changes)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scan /vault for [[wikilinks]] and report which targets don't resolve
|
||||
to an actual note. Obsidian resolves links by basename match anywhere
|
||||
in the vault (unless the link includes a path), so we build an index
|
||||
of all note basenames (and their full relative paths) first.
|
||||
"""
|
||||
import os, re, json
|
||||
from collections import defaultdict
|
||||
|
||||
VAULT = "/vault"
|
||||
LINK_RE = re.compile(r'\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]*)?\]\]')
|
||||
|
||||
def all_notes():
|
||||
idx = defaultdict(list) # basename (no ext, lowercase) -> [full paths]
|
||||
all_paths = []
|
||||
for dp, dirnames, fns in os.walk(VAULT):
|
||||
if ".git" in dp.split(os.sep):
|
||||
continue
|
||||
for fn in fns:
|
||||
if fn.lower().endswith(".md"):
|
||||
p = os.path.join(dp, fn)
|
||||
base = fn[:-3]
|
||||
idx[base.lower()].append(p)
|
||||
all_paths.append(p)
|
||||
return idx, all_paths
|
||||
|
||||
def resolve(link_target, index, current_dir):
|
||||
# Obsidian: link may be a bare name, or a relative/vault path (with /)
|
||||
target = link_target.strip()
|
||||
if "/" in target:
|
||||
# path-style link; try resolving relative to vault root or current dir
|
||||
candidates = [
|
||||
os.path.join(VAULT, target + ".md"),
|
||||
os.path.join(VAULT, target),
|
||||
os.path.join(current_dir, target + ".md"),
|
||||
os.path.join(current_dir, target),
|
||||
]
|
||||
for c in candidates:
|
||||
if os.path.isfile(c):
|
||||
return True
|
||||
# also try basename match as fallback
|
||||
base = os.path.basename(target)
|
||||
return base.lower() in index
|
||||
else:
|
||||
return target.lower() in index
|
||||
|
||||
def main():
|
||||
index, all_paths = all_notes()
|
||||
unresolved = defaultdict(list) # target -> [source files]
|
||||
total_links = 0
|
||||
for p in all_paths:
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8", errors="ignore") as f:
|
||||
text = f.read()
|
||||
except Exception:
|
||||
continue
|
||||
for m in LINK_RE.finditer(text):
|
||||
target = m.group(1)
|
||||
total_links += 1
|
||||
if not resolve(target, index, os.path.dirname(p)):
|
||||
unresolved[target].append(os.path.relpath(p, VAULT))
|
||||
|
||||
print(f"Total wikilinks scanned: {total_links}")
|
||||
print(f"Unique unresolved targets: {len(unresolved)}")
|
||||
print(f"Total unresolved link occurrences: {sum(len(v) for v in unresolved.values())}")
|
||||
|
||||
# dump full detail to json for further processing
|
||||
out = {t: srcs for t, srcs in unresolved.items()}
|
||||
with open("/tmp/unresolved_links.json", "w") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
print("Full detail written to /tmp/unresolved_links.json")
|
||||
|
||||
# sorted by occurrence count desc
|
||||
by_count = sorted(unresolved.items(), key=lambda kv: -len(kv[1]))
|
||||
print("\n=== TOP 40 unresolved targets by occurrence count ===")
|
||||
for target, srcs in by_count[:40]:
|
||||
print(f" {len(srcs):4d}x [[{target}]]")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply the two link-fix passes across /vault:
|
||||
1. fix_map: [[OldTarget]] -> [[NewNote|OldTarget]] (preserves display text)
|
||||
Also handles piped/heading variants: [[OldTarget|Display]] -> [[NewNote|Display]]
|
||||
[[OldTarget#Heading]] -> [[NewNote#Heading]]
|
||||
2. unlink_targets: [[Target]] -> Target (plain text, drop brackets)
|
||||
Also handles piped: [[Target|Display]] -> Display
|
||||
"""
|
||||
import os, re, json, sys
|
||||
|
||||
VAULT = "/vault"
|
||||
|
||||
with open("/tmp/link_fix_map.json") as f:
|
||||
FIX_MAP = json.load(f)
|
||||
with open("/tmp/unlink_targets.json") as f:
|
||||
UNLINK = set(json.load(f))
|
||||
|
||||
# Full link regex capturing target, optional #heading, optional |display
|
||||
LINK_RE = re.compile(r'\[\[([^\]|#]+)(#[^\]|]*)?(\|[^\]]*)?\]\]')
|
||||
|
||||
def process_text(text):
|
||||
changed = False
|
||||
def repl(m):
|
||||
nonlocal changed
|
||||
target = m.group(1)
|
||||
heading = m.group(2) or ""
|
||||
pipe_display = m.group(3) # includes leading |
|
||||
target_stripped = target.strip()
|
||||
|
||||
if target_stripped in FIX_MAP:
|
||||
new_target = FIX_MAP[target_stripped]
|
||||
changed = True
|
||||
if pipe_display:
|
||||
# keep existing custom display text
|
||||
return f"[[{new_target}{heading}{pipe_display}]]"
|
||||
else:
|
||||
# preserve original text as display so prose doesn't change visually
|
||||
return f"[[{new_target}{heading}|{target_stripped}]]"
|
||||
|
||||
if target_stripped in UNLINK:
|
||||
changed = True
|
||||
if pipe_display:
|
||||
display = pipe_display[1:] # strip leading |
|
||||
return display
|
||||
else:
|
||||
return target_stripped
|
||||
|
||||
return m.group(0) # unchanged
|
||||
|
||||
new_text = LINK_RE.sub(repl, text)
|
||||
return new_text, changed
|
||||
|
||||
def main():
|
||||
execute = "--execute" in sys.argv
|
||||
files_changed = 0
|
||||
total_fix_applied = 0
|
||||
total_unlink_applied = 0
|
||||
|
||||
for dp, _, fns in os.walk(VAULT):
|
||||
if ".git" in dp.split(os.sep):
|
||||
continue
|
||||
for fn in fns:
|
||||
if not fn.lower().endswith(".md"):
|
||||
continue
|
||||
p = os.path.join(dp, fn)
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8", errors="ignore") as f:
|
||||
text = f.read()
|
||||
except Exception:
|
||||
continue
|
||||
new_text, changed = process_text(text)
|
||||
if changed:
|
||||
files_changed += 1
|
||||
if execute:
|
||||
with open(p, "w", encoding="utf-8") as f:
|
||||
f.write(new_text)
|
||||
|
||||
print(f"Files changed: {files_changed}")
|
||||
if not execute:
|
||||
print("(dry run -- pass --execute to write)")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Sort /data/_unsorted (old pre-RJFI JD tree) into /data/R,J,F,I storage tree.
|
||||
|
||||
Dry-run by default; pass --execute to actually move files.
|
||||
"""
|
||||
import os, shutil, sys, csv
|
||||
|
||||
UNSORTED = "/data/_unsorted"
|
||||
DATA = "/data"
|
||||
|
||||
# old_prefix -> new_prefix (folder-level rename/move rules)
|
||||
# Order matters: longer/more specific prefixes first.
|
||||
MAPPING = [
|
||||
# --- WhatsApp exports: wholesale move, old->new folder rename ---
|
||||
("1 Life admin/11 Me & other living things/11.54 Whatsapp exports",
|
||||
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.54 Whatsapp exports"),
|
||||
|
||||
# --- 12 Home, house, and transport -> split F.11 (home) / F.12 (vehicles) ---
|
||||
("1 Life admin/12 Home, house, and transport/12.12 Home insurance & claims",
|
||||
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.12 Home insurance & claims"),
|
||||
("1 Life admin/12 Home, house, and transport/12.21 Electricity, gas, & water",
|
||||
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.21 Electricity, gas, & water"),
|
||||
("1 Life admin/12 Home, house, and transport/12.23 All other utilities & services",
|
||||
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.23 All other utilities & services"),
|
||||
("1 Life admin/12 Home, house, and transport/12.24 Renovations & improvements",
|
||||
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.24 Renovations & improvements"),
|
||||
("1 Life admin/12 Home, house, and transport/12.25 House repairs, maintenance, & upkeep",
|
||||
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.24 Renovations & improvements"),
|
||||
("1 Life admin/12 Home, house, and transport/12.61 Move out of 6 Dorothy",
|
||||
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.61 Move out of 6 Dorothy"),
|
||||
("1 Life admin/12 Home, house, and transport/12.30 Vehicles",
|
||||
"F Family/F.1 Home, household & finances/F.12 Vehicles/F.12.30 Vehicles"),
|
||||
("1 Life admin/12 Home, house, and transport/12.32 Motor vehicle auto insurance & claims",
|
||||
"F Family/F.1 Home, household & finances/F.12 Vehicles/F.12.32 Motor vehicle auto insurance & claims"),
|
||||
("1 Life admin/12 Home, house, and transport/12.33 Car repairs, maintenance, & upkeep",
|
||||
"F Family/F.1 Home, household & finances/F.12 Vehicles/F.12.33 Car repairs, maintenance, & upkeep"),
|
||||
|
||||
# --- 13 Money -> split R.13 (solo) / F.13 (joint); tax + financial insurance are joint per design doc ---
|
||||
("1 Life admin/13 Money earned, spent, saved, & owed/13.54 Tax returns & accounting",
|
||||
"F Family/F.1 Home, household & finances/F.13 Household finances/F.13.05 Tax returns & accounting"),
|
||||
("1 Life admin/13 Money earned, spent, saved, & owed/13.12 Financial insurance & claims",
|
||||
"F Family/F.1 Home, household & finances/F.13 Household finances/F.13.12 Financial insurance & claims"),
|
||||
("1 Life admin/13 Money earned, spent, saved, & owed/13.44 Cryptocurrency",
|
||||
"R Ryan/R.1 Life admin/R.13 Personal finance/R.13.44 Cryptocurrency"),
|
||||
|
||||
# --- 11 Me & other living things -> mostly R (Ryan personal) ---
|
||||
("1 Life admin/11 Me & other living things/11.11 Birth certificate & proof of name",
|
||||
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.11 Birth certificate & proof of name"),
|
||||
("1 Life admin/11 Me & other living things/11.14 Licenses",
|
||||
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.14 Licenses"),
|
||||
("1 Life admin/11 Me & other living things/11.16 Legal documents & certificates",
|
||||
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.16 Legal documents & certificates"),
|
||||
("1 Life admin/11 Me & other living things/11.28 Reproductive health",
|
||||
"R Ryan/R.1 Life admin/R.12 Health & body/R.12.28 Reproductive health"),
|
||||
("1 Life admin/11 Me & other living things/11.83 My library",
|
||||
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.83 Ryan's library"),
|
||||
|
||||
# --- 15 Travel -> R.15 Travel (solo trips; matches exactly) ---
|
||||
("1 Life admin/15 Travel, events, & entertainment/15.41 All short trips",
|
||||
"R Ryan/R.1 Life admin/R.15 Travel/R.15.41 All short trips"),
|
||||
|
||||
# --- 2 Career -> R.2 Career, ~1:1 ---
|
||||
("2 Career/21 Perfect Day", "R Ryan/R.2 Career/R.21 Perfect Day"),
|
||||
("2 Career/22 Sejour", "R Ryan/R.2 Career/R.22 Sejour"),
|
||||
("2 Career/23 Consulting", "R Ryan/R.2 Career/R.23 Consulting"),
|
||||
("2 Career/24 Stanford", "R Ryan/R.2 Career/R.24 Stanford"),
|
||||
("2 Career/29 Old applications & archives", "R Ryan/R.2 Career/R.29 Old applications & archives"),
|
||||
|
||||
# --- 3 Hobbies -> R.3 Hobbies, ~1:1 ---
|
||||
("3 Hobbies & interests/31 Language learning", "R Ryan/R.3 Hobbies & interests/R.31 Language learning"),
|
||||
("3 Hobbies & interests/36 Projects", "R Ryan/R.3 Hobbies & interests/R.36 Projects"),
|
||||
|
||||
# --- 4 Flying -> R.4 Flying, ~1:1 ---
|
||||
("4 Flying/41 Certification", "R Ryan/R.4 Flying/R.41 Certification, legal, & insurance"),
|
||||
("4 Flying/43 Resources", "R Ryan/R.4 Flying/R.43 Resources"),
|
||||
]
|
||||
|
||||
# Items needing human judgment - do NOT auto-move, just report.
|
||||
FLAG_FOR_REVIEW = [
|
||||
"1 Life admin/10 Management of area 10-19", # SMS backups - unclear whose/where
|
||||
"1 Life admin/11 Me & other living things/11.43 My family", # Mom's book proposal
|
||||
"1 Life admin/11 Me & other living things/11.09 Archive for category 11", # AI deck for Jen
|
||||
"1 Life admin/11 Me & other living things/11.71 My career sales pitch",
|
||||
"1 Life admin/13 Money earned, spent, saved, & owed/13.41 Bank accounts", # solo or joint?
|
||||
"1 Life admin/13 Money earned, spent, saved, & owed/13.01 Inbox for category 13",
|
||||
"1 Life admin/12 Home, house, and transport/12.01 Inbox for category 12",
|
||||
"1 Life admin/14 My digital life/14.14 My data storage & backups",
|
||||
"3 Hobbies & interests/30 Management for Hobbies",
|
||||
]
|
||||
|
||||
|
||||
def find_files(root):
|
||||
for dirpath, _, filenames in os.walk(root):
|
||||
for fn in filenames:
|
||||
yield os.path.join(dirpath, fn)
|
||||
|
||||
|
||||
def plan_moves():
|
||||
moves = [] # (src, dst)
|
||||
unmatched = []
|
||||
for src in find_files(UNSORTED):
|
||||
rel = os.path.relpath(src, UNSORTED)
|
||||
matched = False
|
||||
for old_prefix, new_prefix in MAPPING:
|
||||
if rel == old_prefix or rel.startswith(old_prefix + os.sep):
|
||||
suffix = rel[len(old_prefix):]
|
||||
dst = os.path.join(DATA, new_prefix.replace("/", os.sep) + suffix)
|
||||
moves.append((src, dst))
|
||||
matched = True
|
||||
break
|
||||
if not matched:
|
||||
is_flagged = any(rel.startswith(f) for f in FLAG_FOR_REVIEW)
|
||||
unmatched.append((rel, is_flagged))
|
||||
return moves, unmatched
|
||||
|
||||
|
||||
def main():
|
||||
execute = "--execute" in sys.argv
|
||||
moves, unmatched = plan_moves()
|
||||
|
||||
print(f"=== PLANNED MOVES: {len(moves)} files ===")
|
||||
by_target = {}
|
||||
for src, dst in moves:
|
||||
key = os.path.dirname(dst)
|
||||
by_target[key] = by_target.get(key, 0) + 1
|
||||
for k, v in sorted(by_target.items()):
|
||||
print(f" {v:5d} -> {k}")
|
||||
|
||||
print(f"\n=== FLAGGED FOR REVIEW (not moved): {sum(1 for _, f in unmatched if f)} files ===")
|
||||
flagged_dirs = {}
|
||||
for rel, is_flagged in unmatched:
|
||||
if is_flagged:
|
||||
d = os.path.dirname(rel)
|
||||
flagged_dirs[d] = flagged_dirs.get(d, 0) + 1
|
||||
for k, v in sorted(flagged_dirs.items()):
|
||||
print(f" {v:5d} :: {k}")
|
||||
|
||||
print(f"\n=== TRULY UNMATCHED (no mapping rule, not flagged): {sum(1 for _, f in unmatched if not f)} files ===")
|
||||
other_dirs = {}
|
||||
for rel, is_flagged in unmatched:
|
||||
if not is_flagged:
|
||||
d = os.path.dirname(rel)
|
||||
other_dirs[d] = other_dirs.get(d, 0) + 1
|
||||
for k, v in sorted(other_dirs.items()):
|
||||
print(f" {v:5d} :: {k}")
|
||||
|
||||
if execute:
|
||||
print("\n=== EXECUTING MOVES ===")
|
||||
moved = 0
|
||||
for src, dst in moves:
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
if os.path.exists(dst):
|
||||
print(f" SKIP (dst exists): {dst}")
|
||||
continue
|
||||
shutil.move(src, dst)
|
||||
moved += 1
|
||||
print(f"Moved {moved} files.")
|
||||
else:
|
||||
print("\n(dry run only -- pass --execute to actually move files)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Final placement pass for the 8 flagged/reviewed _unsorted items."""
|
||||
import os, shutil, sys
|
||||
|
||||
U = "/data/_unsorted"
|
||||
D = "/data"
|
||||
|
||||
MOVES = [
|
||||
# (src relative to _unsorted, dst absolute under /data)
|
||||
("1 Life admin/10 Management of area 10-19/10.09 Archive for area 10-19/SMS Backups as of 2026-04-07/data",
|
||||
"R Ryan/R.1 Life admin/R.11 Identity & vital records/R.11.55 SMS backups"),
|
||||
("1 Life admin/11 Me & other living things/11.09 Archive for category 11/2025-08-07 AI Deck for Jen.pdf",
|
||||
"J Jen/J.0 Meta/J.00 Meta/J.00.01 Inbox for Jen/2025-08-07 AI Deck for Jen.pdf"),
|
||||
("1 Life admin/11 Me & other living things/11.43 My family/Moms Anandi Book & Screenplay/America's Niece - Proposal.pdf",
|
||||
"F Family/F.5 People/F.50 Family/F.50.20 Pandyas/F.50.21 Nandini Pandya/America's Niece - Proposal.pdf"),
|
||||
("1 Life admin/11 Me & other living things/11.71 My career sales pitch/Ryan_Pandya_Resume___2024_10-1.pdf",
|
||||
"R Ryan/R.2 Career/R.20 Curiosity/R.20.02 Resume/Ryan_Pandya_Resume___2024_10-1.pdf"),
|
||||
("1 Life admin/11 Me & other living things/11.71 My career sales pitch/Ryan_Pandya_Resume.pdf",
|
||||
"R Ryan/R.2 Career/R.20 Curiosity/R.20.02 Resume/Ryan_Pandya_Resume.pdf"),
|
||||
("1 Life admin/12 Home, house, and transport/12.01 Inbox for category 12/2021-03-08 - 6 Dorothy sheet from Jason.pdf",
|
||||
"F Family/F.1 Home, household & finances/F.11 Home & property/F.11.09 Old Homes/2021-2026 Dorothy/2021-03-08 - 6 Dorothy sheet from Jason.pdf"),
|
||||
("1 Life admin/13 Money earned, spent, saved, & owed/13.01 Inbox for category 13/Voided check.jpg",
|
||||
"R Ryan/R.1 Life admin/R.13 Personal finance/R.13.01 Inbox/Voided check.jpg"),
|
||||
("1 Life admin/13 Money earned, spent, saved, & owed/13.41 Bank accounts/Ryan Wells Fargo direct deposit instructions.pdf",
|
||||
"R Ryan/R.1 Life admin/R.13 Personal finance/R.13.01 Inbox/Ryan Wells Fargo direct deposit instructions.pdf"),
|
||||
("1 Life admin/14 My digital life/14.14 My data storage & backups/Google backup key.pdf",
|
||||
"R Ryan/R.1 Life admin/R.14 My digital life/R.14.01 Inbox/Google backup key.pdf"),
|
||||
("3 Hobbies & interests/30 Management for Hobbies/30.01 Inbox for Hobbies/elixirbook.pdf",
|
||||
"R Ryan/R.3 Hobbies & interests/R.35 Computers/R.35.21 Computer & programming language textbooks/elixirbook.pdf"),
|
||||
]
|
||||
|
||||
execute = "--execute" in sys.argv
|
||||
for rel_src, rel_dst in MOVES:
|
||||
src = os.path.join(U, rel_src)
|
||||
dst = os.path.join(D, rel_dst)
|
||||
exists = os.path.exists(src)
|
||||
print(f"{'OK ' if exists else 'MISS'} {src} -> {dst}")
|
||||
if execute and exists:
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.move(src, dst)
|
||||
|
||||
if execute:
|
||||
print("Done.")
|
||||
else:
|
||||
print("\n(dry run -- pass --execute)")
|
||||
Reference in New Issue
Block a user