branch: elpa/pcmpl-args commit 36139ba64f43a3d3f4090ef0118bcebfef7e20c9 Author: Valeriy Litkovskyy <vlr.ltk...@protonmail.com> Commit: Troy Hinckley <t.mac...@gmail.com>
Added pass completion 2021-07-21 Valeriy Litkovskyy <vlr.ltk...@protonmail.com> * pcmpl-args.el (pcmpl-args-pass-subcommands): Added a completion table with annotations for password-store subcommands. (pcmpl-args-pass-prefix): Added a function that returns password-store directory. (pcmpl-args-pass-find): Added a function that returns a list of password-store entries. (pcmpl-args-pass-keys): Added a function that returns a list of gpg secret keys. (pcmpl-args-pass-subcommand-specs): Added a function that returns specs for password-store subcommands. (pcomplete/pass): Added an autoloaded password-store completion function. --- pcmpl-args.el | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/pcmpl-args.el b/pcmpl-args.el index 7f26edd996..a0cc5bae57 100644 --- a/pcmpl-args.el +++ b/pcmpl-args.el @@ -98,6 +98,7 @@ (require 'pcmpl-unix) (require 'pcmpl-linux) (require 'pcmpl-gnu) +(require 'cl-seq) (defgroup pcmpl-args nil "Refined argument completion for use with pcomplete." @@ -3400,6 +3401,153 @@ options found in its man page." (defalias 'pcomplete/wget 'pcmpl-args-pcomplete-on-man) +;; Pass completion + +(defalias 'pcmpl-args-pass-subcommands + (pcmpl-args-completion-table-with-annotations + '(("cp" "Copy password or directory") + ("edit" "Insert a new password or edit an existing password") + ("find" "List names of passwords inside the tree that match patterns") + ("generate" "Generate new password") + ("git" "Execute git commands") + ("grep" "Searches inside each decrypted password file") + ("help" "Show usage message") + ("init" "Initialize new password storage") + ("insert" "Insert a new password into the password store") + ("ls" "List names of passwords") + ("mv" "Move password or directory") + ("rm" "Remove password or directory") + ("show" "Decrypt and print a password") + ("version" "Show version information")))) + +(defun pcmpl-args-pass-prefix () + "Return password-store directory. +It is suffixed with a slash." + (let ((directory (or (getenv "PASSWORD_STORE_DIR") + (expand-file-name "~/.password-store")))) + (concat directory "/"))) + +(defun pcmpl-args-pass-find (&rest find-args) + "Return a list of password-store entries. +By default, return all directories and files in password-store. +These can be limited by `FIND-ARGS'. + +`FIND-ARGS' is a list of find (GNU/findutils) arguments. For +example, to get only directories: + +\(pcmpl-args-pass-find \"-type\" \"d\")" + (let* ((prefix (pcmpl-args-pass-prefix)) + (args `("find" "-L" ,prefix + "(" "-name" ".git*" "-o" "-name" ".gpg-id" ")" "-prune" + "-o" ,@find-args "-print")) + (rx (concat "^" (regexp-quote prefix) "\\(.+?\\)\\(?:\\.gpg\\)?$"))) + (with-temp-buffer + (apply #'pcmpl-args-process-file args) + (goto-char (point-min)) + (save-match-data + (while (search-forward-regexp rx nil t) + (replace-match "\\1"))) + (beginning-of-line) + (let (lines) + (while (progn (push (string-trim-right (thing-at-point 'line t)) lines) + (forward-line -1) + (not (bobp)))) + lines)))) + +(defun pcmpl-args-pass-keys (args) + "Return a list of gpg secret keys. +This list is filtered based on `ARGS', which is an alist with +inserted command line argument. If some gpg key was already +entered, it will be removed from returned list." + ;; Should we use epg-list-keys to get keys? + (let ((rx "^\\(?:[^:]*:\\)\\{9\\}\\([^:]*\\).*?$")) + (with-temp-buffer + (pcmpl-args-process-file "gpg2" "--list-secret-keys" "--with-colons") + (goto-char (point-min)) + (save-match-data + (while (search-forward-regexp rx nil t) + (replace-match "\\1"))) + (forward-line 1) + (let (lines) + (while (progn (forward-line -1) + (push (string-trim-right (thing-at-point 'line t)) lines) + (not (bobp)))) + (cl-set-difference lines (cadr (assq '* args)) :test #'string=))))) + +(defun pcmpl-args-pass-subcommand-specs (subcommand) + "Return specs for pass `SUBCOMMAND'." + (pcase subcommand + ("edit" + '((argument 0 (("PASSNAME" (:eval (pcmpl-args-pass-find "-type" "f"))))))) + + ("find" + '((argument * (("PATTERN" none))))) + + ("generate" + '((option "-n, --no-symbols" :help "Use only alphanumeric characters") + (option "-c, --clip" :help "Copy the password to the clipboard") + (option "-i, --in-place" :help "Only replace the first line of the password file") + (option "-f, --force" :help "Don't prompt before overwriting an existing password") + (argument 0 (("PASSNAME" (:eval (pcmpl-args-pass-find))))) + (argument 1 (("PASSLENGTH" none))))) + + ((or "git" "grep") + '((argument 0 (("CMDOPTS" none)) + :subparser + (lambda (args specs seen) + (push (plist-get (pop seen) :stub) args) + (pcmpl-args-command-subparser args specs seen))))) + + ("init" + '((option "-p, --path=SUBFOLDER" (("SUBFOLDER" (:eval (pcmpl-args-pass-find "-type" "d")))) + :help "GPGIDs are assigned for that specific SUBFOLDER of the store") + (argument * (("GPGID" (:lambda pcmpl-args-pass-keys)))))) + + ("insert" + '((option "-e, --echo" :help "Enable keyboard echo and don't confirm the password") + (option "-m, --multiline" :help "Read lines until EOF or Ctrl+D is reached") + (option "-f, --force" :help "Don't prompt before overwriting an existing password") + (argument 0 (("PASSNAME" (:eval (pcmpl-args-pass-find))))))) + + ("ls" + '((argument 0 (("SUBFOLDER" (:eval (pcmpl-args-pass-find "-type" "d"))))))) + + ("rm" + '((option "-r, --recursive" :help "Delete PASSNAME recursively if it is a directory") + (option "-f, --force" :help "Do not interactively prompt before removal") + (argument 0 (("PASSNAME" (:eval (pcmpl-args-pass-find))))))) + + ("show" + '((option "-c[LINENUMBER], --clip[=LINENUMBER]" (("LINENUMBER" none)) + :help "Copy the first (or specified) line to the clipboard") + (option "-q[LINENUMBER], --qrcode[=LINENUMBER]" (("LINENUMBER" none)) + :help "Display a QR code of the first (or specified) line") + (argument 0 (("PASSNAME" (:eval (pcmpl-args-pass-find "-type" "f"))))))) + + ((or "cp" "mv") + '((option "-f, --force" :help "Silently overwrite NEWPATH if it exists") + (argument 0 (("OLDPATH" (:eval (pcmpl-args-pass-find))))) + (argument 1 (("NEWPATH" (:eval (pcmpl-args-pass-find))))))))) + +(defun pcomplete/pass () + "Pass completion." + (pcmpl-args-pcomplete + (pcmpl-args-make-argspecs + '((argument 0 (("OPTIONS" nil)) + :subparser + (lambda (arguments argspecs seen) + (let ((command (pop arguments))) + (push (list :name 0 + :stub command + :values (list command) + :action '("COMMAND" pcmpl-args-pass-subcommands)) + seen) + (when arguments + (let ((specs (pcmpl-args-pass-subcommand-specs command))) + (setq argspecs (pcmpl-args-make-argspecs specs))))) + (list arguments argspecs seen))))))) + + ;;; Testing (defun pcmpl-args--debug-parse-help-buffer () @@ -3683,7 +3831,7 @@ will print completions for `ls -'." ;; (insert (format "\n\n;;;###autoload (dolist (func '(%s)) (autoload func \"pcmpl-args\"))\n" ;; (mapconcat 'identity accum " ")))) -;;;###autoload (dolist (func '(pcomplete/chgrp pcomplete/chmod pcomplete/chown pcomplete/chroot pcomplete/cp pcomplete/date pcomplete/dd pcomplete/dir pcomplete/echo pcomplete/env pcomplete/false pcomplete/groups pcomplete/id pcomplete/ln pcomplete/ls pcomplete/mv pcomplete/nice pcomplete/nohup pcomplete/printenv pcomplete/printf pcomplete/rm pcomplete/rmdir pcomplete/sort pcomplete/stat pcomplete/test pcomplete/true pcomplete/vdir pcomplete/basename pcomplete/cat pcomplete/cksum pcomple [...] +;;;###autoload (dolist (func '(pcomplete/chgrp pcomplete/chmod pcomplete/chown pcomplete/chroot pcomplete/cp pcomplete/date pcomplete/dd pcomplete/dir pcomplete/echo pcomplete/env pcomplete/false pcomplete/groups pcomplete/id pcomplete/ln pcomplete/ls pcomplete/mv pcomplete/nice pcomplete/nohup pcomplete/printenv pcomplete/printf pcomplete/rm pcomplete/rmdir pcomplete/sort pcomplete/stat pcomplete/test pcomplete/true pcomplete/vdir pcomplete/basename pcomplete/cat pcomplete/cksum pcomple [...] (provide 'pcmpl-args) ;;; pcmpl-args.el ends here