Browse Source

rename apps, add man page for dirnotes-cli

Pat Beirne 1 year ago
parent
commit
cf650c44ee
6 changed files with 408 additions and 206 deletions
  1. 0 82
      cleanup.py
  2. 2 0
      dirnotes
  3. 205 0
      dirnotes-cli
  4. 186 0
      dirnotes-cli.md
  5. 15 21
      dirnotes-tui
  6. 0 103
      dncli

+ 0 - 82
cleanup.py

@@ -1,82 +0,0 @@
-#!/usr/bin/python3
-
-""" Maintenance up the database used by dirnotes and c_dirnotes
-- view the history of file comments
-- delete the history of file comments
-"""
-
-import sqlite3, os, sys
-import logging
-VERSION = "0.1"
-
-dbname = "~/.dirnotes.db"
-dbname = os.path.expanduser(dbname)
-
-def main(summary, view, delete):
-  try:
-    d = sqlite3.connect(dbname)
-  except sqlite3.OperationalError:
-    print("database not found, or permission-locked; cleanup aborted")
-    return
-  c = d.cursor()
-  if summary:
-    c = d.cursor()
-    total,distinct = c.execute("select count(name),count(distinct name) from dirnotes;").fetchone()
-    print(f"this database has entries for {distinct} files, and {total-distinct} history entries")
-
-  if view or delete:
-    c.execute("select distinct name from dirnotes;")
-    for f in c:
-      # note, f is a tuple of (name,)
-      c1 = d.cursor()
-      if view:
-        history = c1.execute("select comment_date,author,comment from dirnotes where name=? order by comment_date desc",f).fetchall()
-        if len(history)>1:
-          print(f"filename: {f[0]} has a history of comments:")
-          first = True
-          for lin in history:
-            print(f"{'->' if first else '  '}  date: {lin[0]} author: {lin[1]}\n    comment: {lin[2]}")
-            first = False
-
-      if delete:
-        most_recent = c1.execute("select comment_date from dirnotes where name=? order by comment_date desc",f).fetchone()[0]
-        logging.info(f"working on file: {f} with most-recent-date of {most_recent}")
-        c1.execute("delete from dirnotes where name=? and comment_date <> ?",(f[0],most_recent))
-
-  if delete:
-    total,distinct = c.execute("select count(name),count(distinct name) from dirnotes;").fetchone()
-    print(f"this database has been cleaned to {distinct} files, and {total-distinct} history entries")
-    d.commit()
-  d.close()
-
-
-view = False
-delete = False
-summary = True  # default
-
-if __name__ == "__main__":
-  logging.basicConfig(filename="/tmp/dirnotes_clear.log", level=logging.DEBUG)
-  logging.info("starting the db mainenance tool")
-  if len(sys.argv)>1:
-    if "-h" in sys.argv or "--help" in sys.argv:
-      print(f"Usage: {sys.argv[0]}    maintenance for the database used by dirnotes & c_dirnotes")
-      print("      -d     delete comment history")
-      print("      -s     view comment summary")
-      print("      -v     view comment history")
-      sys.exit(1)
-    if "-v" in sys.argv:
-      view = True
-    if "-s" in sys.argv:
-      summary = True
-    if "-d" in sys.argv:
-      delete = True
-  if delete:
-    print("Cleanup of the dirnotes database located at ~/.dirnotes.db")
-    c = input("Proceed? (Y/N)")
-    if c in ("yY"):
-      main(summary, view, delete)
-    else:
-      print("nothing done")
-  else:
-    main(summary, view, delete)
-

+ 2 - 0
dirnotes

@@ -1,4 +1,6 @@
 #!/usr/bin/python3
+# TODO: add author to access
+# TODO: convert dates to strings
 """ a simple gui or command line app
 to view and create/edit file comments
 

+ 205 - 0
dirnotes-cli

@@ -0,0 +1,205 @@
+#!/usr/bin/python3
+# TODO: get rid of the -n option; multiple files are prefixed by 'filename:', single files aren't
+
+VERSION = "0.3"
+
+import os, sys, argparse, xattr, json, sqlite3
+
+answer_json = []
+verbose = 0
+db = None
+xattr_comment = "user.xdg.comment"
+xattr_author  = "user.xdg.comment.author"
+xattr_date    = "user.xdg.comment.date"
+
+#======= debugging/verbose ===========
+def print_d(*a):
+  if verbose > 1:
+    print('>>',*a)
+def print_v(*a):
+  if verbose:
+    print('>',*a)
+
+#============= the functions that are called from the main.loop ===============
+
+def file_copy(f,target,target_is_dir,force):
+  print_d(f"call file_copy with args={target},{target_is_dir} and {force}")
+  dest = target if not target_is_dir else target+'/'+os.path.basename(f)
+  if os.path.exists(dest) and not force:
+    go = input("The copy target <<" + dest + ">> exists. Overwrite? (y or n) ")
+    if go != 'y' and go != 'Y':
+      return
+  print_d(f"copy from {f} to {dest}")
+
+def file_zap(f,all_flag):
+  print_d(f"zapping the comment history of {f}")
+  if all_flag:
+    print_d("zapping the entire database")
+
+def file_modify_comment(f, create, append, erase):
+  print_d(f"modify the comment on file {f} with extra={(create,append,erase)}")
+  if not os.path.exists(f):
+    print(f"the target file does not exist; please check the spelling of the file: {f}")
+    # sys.exit() here?
+
+def file_display(f, listall, history, json, minimal):
+  print_d(f"list file details {f}")
+  x_comment = None
+  try:
+    x_comment = xattr.getxattr(f,xattr_comment).decode()
+    x_author  = xattr.getxattr(f,xattr_author).decode()
+    x_date    = xattr.getxattr(f,xattr_date).decode()
+  except:
+    pass
+  full_f = os.path.realpath(f)
+
+  d_comment = getDbComment(full_f)
+  if d_comment:
+    d_comment, d_author, d_date = d_comment
+  print_d(f"for file {f}, database comment is <{d_comment}>, xattr comment is <{x_comment}>")
+
+  if os.path.isdir(f):
+    f = f+'/'
+  if x_comment or listall:
+    if x_comment and (d_comment != x_comment):
+      x_comment += '*'
+    if not json:
+      if minimal:
+        print(f"{x_comment}")
+      else:
+        print(f"{f}: {x_comment}")
+    else:
+      if verbose:
+        answer_json.append( {"file":f,"comment":x_comment,"author":x_author,"date":x_date } )
+      else:
+        answer_json.append( {"file":f,"comment":x_comment} )
+
+def getDbComment(full_filename):
+  global db
+  print_d(f"db access for {full_filename}")
+  c = db.cursor()
+  c.execute("select comment,author,comment_date from dirnotes where name=? and comment<>'' order by comment_date desc",(full_filename,))
+  a = c.fetchone()
+  if a:
+    return a[0:3]
+
+def openDb():
+  global db
+  dbName = "~/.dirnotes.db"
+  dbName = os.path.expanduser(dbName)
+  db = sqlite3.connect(dbName)
+  try:
+    c = db.cursor()
+    c.execute("select * from dirnotes")
+  except sqlite3.OperationalError:
+    c.execute("create table dirnotes (name TEXT, date DATETIME, size INTEGER, comment TEXT, comment_date DATETIME, author TEXT)")
+  return db 
+
+def main(args):
+  parser = argparse.ArgumentParser(description="Display or add comments to files",
+    epilog="Some options conflict. Use only one of: -l -c -a -H -e -z -Z and one of -d -x")
+  parser.add_argument('-V',"--version", action="version",   version=f"dncli ver:{VERSION}")
+  parser.add_argument('-v',"--verbose", action='count',     help="verbose, almost debugging; do not use in scripts",default=0)
+  parser.add_argument('-j',"--json",    action="store_true",help="output in JSON format")
+  pars_m = parser.add_mutually_exclusive_group()
+  pars_m.add_argument('-l',"--listall", action="store_true",help="list all files, including those without comments")
+  parser.add_argument('-d',"--db",      action="store_true",help="list comments from database")
+  parser.add_argument('-x',"--xattr",   action="store_true",help="list comments from xattr")
+  parser.add_argument('-n',"--minimal", action="store_true",help="output only comments; useful in scripting")
+  parser.add_argument('-H',"--history", action="store_true",help="output the history of comments for a file")
+  pars_m.add_argument('-c',"--create",  metavar="comment",  help="add a comment to a file")
+  pars_m.add_argument('-a',"--append",  metavar="comment",  help="append to a comment on a file, separator=';'")
+  pars_m.add_argument('-C',"--copy",    action="store_true",help="copy a file with its comments")
+  parser.add_argument('-y',"--cp_force",action="store_true",help="copy over existing files")
+  pars_m.add_argument('-e',"--erase",   action="store_true",help="erase the comment on a file")
+  pars_m.add_argument('-z',"--zap",     action="store_true",help="clear the comment history on a file")
+  pars_m.add_argument('-Z',"--zapall",   action="store_true",help="clear the comment history in the entire database")
+  parser.add_argument('file_list',nargs='*',help="file(s); list commands may omit this")
+  args = parser.parse_args()
+
+  # default is to display all files that have comments
+
+  # major modes are: display (<none> -l -H), add-comment (-a -c -e), clear-history(-z -Z), copy (-C)
+  # determine the major mode, then apply an appropriate function over the file_list
+  
+  args.display = not (args.create or args.append or args.copy or args.erase or args.zap or args.zapall)
+  if args.cp_force and not args.copy:
+    print("the -y/--cp_force options can only be used with the -C/--copy command")
+    sys.exit(3)   
+  if args.json and not args.display:
+    print("the -j/--json option can only be used with the display modes")
+    sys.exit(4)
+  if args.minimal and not args.display:
+    print("the -n/--minimal option only applies to the display modes")
+    sys.exit(5)
+  if args.history and not args.display:
+    print("the -H/--history option only applies to the display modes")
+    sys.exit(5)
+  if args.xattr and (args.zap or args.zapall):
+    print("the -x/--xattr option doesn't apply to the -z/--zap and -Z/--zapall commands")
+    sys.exit(7)
+
+  global verbose
+  verbose = args.verbose
+  
+  #====== 1) build the file list =============
+
+  files = args.file_list
+  # for the list commands, auto-fill the file list with the current directory
+  if not files and args.display:
+    files = os.listdir(".")
+    files.sort()
+  # other command require explicity file lists, but 'dncli -c "new comment" *' will work
+  if not files:
+    print("please specify a file or files to use")
+    sys.exit(10)
+  print_d("got the files:", files)
+
+  #======= 2) build the function
+  if args.create or args.append or args.erase:
+    print_d(f"create/append/erase: {args.create} . {args.append} . {args.erase}")
+    func = file_modify_comment
+    loop_args = (args.create, args.append, args.erase)
+  elif args.zap or args.zapall:
+    print_d(f"got a zap command {args.zap} . {args.zapall}")
+    func = file_zap
+    loop_args = (args.zapall,)
+    if args.zapall:
+      files = (1,)
+  elif args.copy:
+    print_d(f"got a copy command to {args.copy}")
+    # the last item on the file list is the target
+    n_files = len(files)
+    if n_files < 2:
+      print_d(f"the copy command requires at least two arguments, the last one is the destination")
+      sys.exit(1)
+    target = files[-1]
+    target_is_directory = os.path.isdir(target)
+    files = files[:-1]
+    print_d(f"copy from {files} to {target}")
+    if n_files > 2 and not target_is_directory:
+      print_d(f"multiple copy files must go to a target directory")
+      sys.exit(3)
+    func = file_copy
+    loop_args = (target, target_is_directory, args.cp_force)
+  else:
+    assert(args.display)
+    print_v(f"list files using {'database' if args.db else 'xattr'} priority")
+    print_d(f"display command with option: {args.listall} and {args.history} and {args.json} and {args.minimal}")
+    loop_args = (args.listall, args.history, args.json, args.minimal)
+    func = file_display
+
+  #====== 3) loop on the list, execute the function =============
+  global db
+  db = openDb()
+
+  for f in files:
+    func(f,*loop_args)
+
+  if answer_json:
+    print(json.dumps(answer_json))
+
+if __name__ == "__main__":
+  main(sys.argv)
+
+

+ 186 - 0
dirnotes-cli.md

@@ -0,0 +1,186 @@
+---
+title: dirnotes-cli
+section: 1
+header: User Manual
+footer: dirnotes-cli 1.0
+date: Sept 26, 2022
+---
+
+# NAME
+
+dirnotes-cli - view and manage Dirnotes file annotations
+
+# SYNOPSIS
+
+<!--- the indent below is using unicode hard space u00a0, and space at EOL  --->   
+    **dirnotes-cli** [-h] [--help] [-V] [--version]   
+
+  List:   
+    **dirnotes-cli** [-v] [-j] [-l] [-d|-x]  
+    **dirnotes-cli** [-v] [-j] [-n] [-d|-x] [filename]...   
+    **dirnotes-cli** [-v] [-j] -H [filename]...  
+  
+  Create:  
+    **dirnotes-cli** -c "comment" filename...  
+    **dirnotes-cli** -a "comment" filename...  
+    **dirnotes-cli** -e filename...
+
+  FileCopy:  
+    **dirnotes-cli** -C src-file... <dest-file | dest-dir>  
+    **dirnotes-cli** -M src-file... <dest-file | dest-dir>
+
+  Cleanup:  
+    **dirnotes-cli** -z [filename]...   
+    **dirnotes-cli** -Z
+
+# DESCRIPTION
+
+The **dirnotes** family of apps allows you to add a descriptive comment to 
+a file. The descriptions are stored in two places:
+
+  * in the xattr properties of the file
+  * in a database located in the user's home directory
+
+
+The *list* commands will display the comment from the database or xattr (as
+determined by the config file, below, or as specified with **-d**/**-x** options). If the database and xattr comments differ, the
+comment will be terminated by a '\*' character. The _-H_ option displays the history 
+of comments from the database.
+
+The output of the *list* commands can be in .json format (_-j_) , and can optionally 
+display the comment creator and the comment date (_-v_)
+
+The *create* commands will attempt to store the file comments in both the xattr of the file,
+and in the database. If either of these fail, they fail silently. Use the _-c_ to create a comment, use _-a_ to append to an existing comment, and _-e_ to erase a comment.
+
+The *filecopy* commands will copy the file to a destination, and preserve the file
+comments. [See notes below about LIMITATIONS]
+
+The *cleanup* commands can clean up the history of comments in the database.
+
+# LIMITATIONS
+
+The file comments are located in two locations: a database, and in the xattr properties
+of the file. Each of these storage locations has its own benefits and limitations:
+
+## xattr
+
+Comments stored in the xattr properties can be copied/moved with the file, if you
+use the correct options for _cp_. The _mv_ utility automatically preserves _xattr_.
+Other programs can also be coerced into perserving _xattr_ properties:
+
+* rsync
+* tar
+* mksquashfs
+
+Not all file systems support xattr properties (vfat/exfat does not). 
+
+The current implementation of _sshfs_ and _scp_ do not support the copy of _xattr_ properties. 
+If you are using _sshfs_, you can use the 'dirnotes' programs to copy a file, and the database comments will work properly.
+If you want to copy files to a remote machine and include the _xattr_ comments, use _rsync_ with the _-X_ option. Or _tar_ of course.
+
+Some editing apps (like _vim_) will create a new file when saving the data, which orphans the _xattr_ comments. For these apps, use the _database_ system.
+
+
+## database
+
+Comments stored in the database work for all filesystem types (including vfat/exfat/sshfs)
+Comments are personalized to the _current user_. 
+Another user on the same system will not see these comments.
+
+Files are indexed by their complete path name. Removable filesystems should be
+mounted in a consistent way, so that the complete path name is reproducable.
+
+Comments stored in the database do not travel with the files when
+ they are moved or copied, unless using the dirnotes-family of tools.
+
+
+# OPTIONS
+  
+**-c**
+: add a comment to a file
+
+**-C**
+: attempt to copy the file(s) and comments to a destination; if multiple files are copied, the destination must be a directory
+
+**-d**
+: use database comments as the primary source; cannot be used with **-x**
+
+**-e**
+: erase the comments on a file
+
+**-h** **--help**
+: display help and usage
+
+**-H**
+: output the history of comments for this file(s)
+
+**-i**  
+: add a comment to a file; if the comment is not in the command, accept it from stdin
+
+**-j**
+: output (to stdio) the file comment in .json format 
+
+**-l**
+: list all files, including those without _dirnotes_ comments
+
+**-M**
+: move the file(s) and comments to the destination; if multiple files are moved, the destination must be a directory
+
+**-n**
+: output only the comment; this may be useful for scripting
+
+**-v**
+: also include the comment author and comment date in the output
+
+**-V** **--version**
+: display version number
+
+**-x**
+: use xattr comments as the primary source; connot be used with **-d**
+
+**-z**
+: erase history comments associated with this file; keep the current comment; if no file is specified, erase the history comments for all files in the current directory
+
+**-Z**
+: erase all the historic comments in the user's database
+
+# EXAMPLES
+
+To display the comment for a file:
+
+> $ dirnotes-cli filename.txt  
+> filename.txt: notes on the car repair
+
+To extract _only_ the comment from a file, use the _-n_ option (useful for scripts):
+
+> $ dirnotes-cli -n filename.txt  
+> notes on the car repair
+
+Or, in json format:
+
+> $ dirnotes-cli -j filename.txt  
+> [{"file": "filename.txt", "comment": "notes on the car repair"}]
+
+
+
+# SEE ALSO
+
+dirnotes, dirnotes-tui, dirnotes-cli
+
+The **dirnotes** program provides a GUI for accessing the files descriptions.
+
+The **dirnotes-tui** program provides a **curses** based (terminal) access to the descriptions.
+
+The **dirnotes-cli** program provides command line access, and can be scripted.
+
+# CONFIGURATION FILE
+
+By default, the file **~/.dirnotes.conf** will be used to load the user's config. There are
+three attributes described in that file that are important:
+
+> * xattr_tag  
+> * database  
+> * start_mode
+
+

+ 15 - 21
c_dirnotes.py → dirnotes-tui

@@ -3,9 +3,8 @@
 # TOTO: when changing directories, keep the sort order?
 # TODO: fix the 'reload' function....need 'this_dir' in Files class
 # TODO: write color scheme
-# TODO: add file copy/move/del
-# TODO: add database access & preference
 # TODO: re-read date/author to xattr after an edit
+# TODO: consider adding h,j,k,l movement
 
 # scroll
 # up/down - change focus, at limit: move 1 line,
@@ -24,12 +23,12 @@ import curses, sqlite3, curses.textpad
 import logging, getpass, argparse
 import json
 
-VERSION = "1.5"
+VERSION = "1.7"
 # these may be different on MacOS
 xattr_comment = "user.xdg.comment"
-xattr_author = "user.xdg.comment.author"
-xattr_date = "user.xdg.comment.date"
-DEFAULT_CONFIG_FILE = "~/.dirnotes.conf"
+xattr_author  = "user.xdg.comment.author"
+xattr_date    = "user.xdg.comment.date"
+DATE_FORMAT   = "%Y-%m-%d %H:%M:%S"
 
 # convert the ~/ form to a fully qualified path
 # database_name = "~/.dirnotes.db"
@@ -43,15 +42,18 @@ CMD_COPY   = ord('c')  # open dialog to copy-with-comment
 CMD_DETAIL = ord('d')  # open dialog
 CMD_EDIT   = ord('e')  # open dialog for typing & <esc> or <enter>
 CMD_HELP   = ord('h')  # open dialog
+CMD_MODE   = ord('M')  # switch between xattr and database mode
 CMD_MOVE   = ord('m')  # open dialog to move-with-comment
 CMD_QUIT   = ord('q')
 CMD_RELOAD = ord('r')  # reload
 CMD_SORT   = ord('s')  # open dialog for N,S,D,C
 CMD_CMNT_CP= ord('C')  # open dialog to copy comments accept 1 or a or <esc>
-CMD_MODE   = ord('M')  # switch between xattr and database mode
 CMD_ESC    = 27
 CMD_CD     = ord('\n')
 
+# file comments will ALWAYS be written to both xattrs & database
+#   access failure is shown once per directory
+
 # other options will be stored in database at ~/.dirnotes.db or /etc/dirnotes.db
 #   - option to use MacOSX xattr labels
 #  
@@ -60,13 +62,7 @@ CMD_CD     = ord('\n')
 # ~/.dirnotes.db or /var/lib/dirnotes.db
 # at usage time, check for ~/.dirnotes.db first
 
-# there should be a copy db -> xattr (one file or all-in-dir)
-#   and copy xattr -> db (one file or all-in-dir)
-
-# file copy/move will copy the comments IN BOTH DB AND XATTR
-
-# file comments will ALWAYS be written to both xattrs & database
-#   access failure is shown once per directory
+DEFAULT_CONFIG_FILE = "~/.dirnotes.conf"
 
 # config
 #    we could store the config in the database, in a second table
@@ -411,8 +407,8 @@ class FileObj():
     self.commentsDiffer = False
     try:
       self.xattrComment = os.getxattr(fileName, xattr_comment, follow_symlinks=False).decode()
-      self.xattrAuthor = os.getxattr(fileName, xattr_author, follow_symlinks=False).decode()
-      self.xattrDate = float(os.getxattr(fileName, xattr_date, follow_symlinks=False).decode())
+      self.xattrAuthor  = os.getxattr(fileName, xattr_author, follow_symlinks=False).decode()
+      self.xattrDate    = os.getxattr(fileName, xattr_date, follow_symlinks=False).decode()
       self.commentsDiffer = True if self.xattrComment == self.dbComment else False
     except:  # no xattr comment
       pass
@@ -468,9 +464,9 @@ class FileObj():
     try:
       os.setxattr(self.fileName,xattr_comment,bytes(newComment,'utf8'),follow_symlinks=False)
       os.setxattr(self.fileName,xattr_author,bytes(getpass.getuser(),'utf8'),follow_symlinks=False)
-      os.setxattr(self.fileName,xattr_date,bytes(str(time.time()),'utf8'),follow_symlinks=False)
+      os.setxattr(self.fileName,xattr_date,bytes(time.strftime(DATE_FORMAT),'utf8'),follow_symlinks=False)
       self.xattrAuthor = getpass.getuser()
-      self.xattrDate = time.time()      # alternatively, re-instantiate this FileObj
+      self.xattrDate = time.strftime(DATE_FORMAT)      # alternatively, re-instantiate this FileObj
       self.xattrComment = newComment
       self.commentsDiffer = True if self.xattrComment == self.dbComment else False
       return True
@@ -935,9 +931,6 @@ def pre_main():
   # print(repr(config))
   # print("start_mode",config["start_mode"])
   
-  # change ~ tilde to username
-  config["database"] = os.path.expanduser(config["database"])
-  # load globals from the config
   return args, config
 
 args, config = pre_main()
@@ -946,6 +939,7 @@ mode = config["start_mode"]
 xattr_comment = config["xattr_tag"]
 xattr_author  = config["xattr_tag"] + ".author"
 xattr_date    = config["xattr_tag"] + ".date"
+# change ~ tilde to username
 database_name = os.path.expanduser(config["database"])
 cwd = args.directory
 

+ 0 - 103
dncli

@@ -1,103 +0,0 @@
-#!/usr/bin/python3
-# TODO: change the default file list to only apply to listing, not copying/erasing/creating/appending/zapping
-
-VERSION = "0.1"
-
-import os, sys, argparse
-
-def file_copy(f,target,target_is_dir,force):
-  print(f"call file_copy with args={target},{target_is_dir} and {force}")
-  dest = target if not target_is_dir else target+'/'+os.path.basename(f)
-  if os.path.exists(dest) and not force:
-    go = input("The copy target <<" + dest + ">> exists. Overwrite? (y or n) ")
-    if go != 'y' and go != 'Y':
-      return
-  print(f"copy from {f} to {dest}")
-
-def file_zap(f,all_flag):
-  print(f"zapping the comment history of {f}")
-  if all_flag:
-    print("zapping the entire database")
-
-def file_erase(f, extra):
-  print(f"erase the comment on file {f}")
-
-
-def main(args):
-  parser = argparse.ArgumentParser(description="Display or add comments to files",
-    epilog="Some options conflict. Use only one of: -l -c -a -H -e -z -Z and one of -d -x")
-  parser.add_argument('-V',"--version", action="version",version=f"dncli ver:{VERSION}")
-  parser.add_argument('-v',"--verbose", action='store_true',help="verbose, almost debugging; do not use in scripts")
-  parser.add_argument('-j',"--json",    action="store_true",help="output in JSON format")
-  parser.add_argument('-l',"--listall", action="store_true",help="list all files, including those without comments")
-  parser.add_argument('-d',"--db",      action="store_true",help="list comments from database")
-  parser.add_argument('-x',"--xattr",   action="store_true",help="list comments from xattr")
-  parser.add_argument('-n',"--minimal", action="store_true",help="output only comments; useful in scripting")
-  parser.add_argument('-H',"--history", action="store_true",help="output the history of comments for a file")
-  parser.add_argument('-c',"--create",  metavar="comment",  help="add a comment to a file")
-  parser.add_argument('-a',"--append",  metavar="comment",  help="append to a comment on a file, separator=';'")
-  parser.add_argument('-C',"--copy",    action="store_true",help="copy a file with its comments")
-  parser.add_argument('-y',"--cp_force",action="store_true",help="copy over existing files")
-  parser.add_argument('-e',"--erase",   action="store_true",help="erase the comment on a file")
-  parser.add_argument('-z',"--zap",     action="store_true",help="clear the comment history on a file")
-  parser.add_argument('-Z',"--zapall",   action="store_true",help="clear the comment history in the entire database")
-  parser.add_argument('file_list',nargs='*',help="file(s) for comments; default is all files in ./")
-  args = parser.parse_args()
-
-  # default is to display all files that have comments
-
-  # major modes are: display (-l -H), add-comment (-a -c -e), clear-history(-z -Z), copy (-C)
-  # determine the major mode, then apply an appropriate function over the file_list
-
-  #====== 1) build the file list =============
-
-  files = args.file_list
-  if not files:
-    files = os.listdir(".")
-  # if the user gave me a single dir...expand it
-  elif len(files)==1 and os.path.isdir(files[0]):
-    files = os.listdir(files[0])
-  print("got the files:", files)
-
-  #======= 2) build the function
-  if args.create or args.append:
-    print(f"create/append: {args.create} . {args.append}")
-  elif args.erase:
-    print(f"erase command")
-    func = file_erase
-    loop_args = (0,)
-  elif args.zap or args.zapall:
-    print(f"got a zap command {args.zap} . {args.zapall}")
-    func = file_zap
-    loop_args = (args.zapall,)
-    if args.zapall:
-      files = (1,)
-  elif args.copy:
-    print(f"got a copy command to {args.copy}")
-    # the last item on the file list is the target
-    n_files = len(files)
-    if n_files < 2:
-      print(f"the copy command requires at least two arguments, the last one is the destination")
-      sys.exit(1)
-    target = files[-1]
-    target_is_directory = os.path.isdir(target)
-    files = files[:-1]
-    print(f"copy from {files} to {target}")
-    if n_files > 2 and not target_is_directory:
-      print(f"multiple copy files must go to a target directory")
-      sys.exit(3)
-    func = file_copy
-    loop_args = (target, target_is_directory, args.cp_force)
-  else:
-    print(f"display command with option: {args.listall}")
-
-
-  #====== 3) loop on the list, execute the function =============
-  for f in files:
-    func(f,*loop_args)
-
-
-if __name__ == "__main__":
-  main(sys.argv)
-
-