unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#57673: [PATCH] Parse --help messages for pcomplete
@ 2022-09-08  9:34 Augusto Stoffel
  2022-09-08 12:39 ` Lars Ingebrigtsen
  2022-09-08 20:49 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 2 replies; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-08  9:34 UTC (permalink / raw)
  To: 57673

[-- Attachment #1: Type: text/plain, Size: 1142 bytes --]

Tags: patch

Find attached a “worse is better” approach for pcomplete, where we parse
help messages to generate a list of completions.

This is still a sketch.  I've added pcomplete functions for a random
selection of commands to see if this works reasonably, and I think it
probably does.  But in any case I don't think it would make sense to try
and add completions as detailed as the ones bash provides; there is an
awful lot of logic in the files under /usr/share/bash-completion and I
don't think anyone would want to redo that work.

Some further comments:

1. I'm a bit unsure whether `pcomplete-parse-help' should return a plain
   list of completions or a completion table.  The advantage of the
   former (which is the current approach) is that it's easier to
   manipulate the result of the parsing (cf. the need for that in the
   git case).  The advantage of returning a completion table is a better
   treatment of annotations.

2. I would rather not create a new pcmpl-git.el file (doing so for each
   new command doesn't seem very scalable), but I wouldn't know where
   else to put that stuff.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Add-pcomplete-parse-help.patch --]
[-- Type: text/patch, Size: 19781 bytes --]

From 5bcd9c3c84a95f1219411f91b03fcea0ad99beae Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <arstoffel@gmail.com>
Date: Thu, 8 Sep 2022 11:09:42 +0200
Subject: [PATCH] Add pcomplete-parse-help

---
 lisp/pcmpl-git.el            |  97 +++++++++++++++++++++++++++++++
 lisp/pcmpl-gnu.el            |  22 +++++++
 lisp/pcmpl-rpm.el            |  39 +++++++++++++
 lisp/pcmpl-x.el              |  16 ++++++
 lisp/pcomplete.el            | 107 +++++++++++++++++++++++++++++++++++
 test/lisp/pcomplete-tests.el | 100 ++++++++++++++++++++++++++++++++
 6 files changed, 381 insertions(+)
 create mode 100644 lisp/pcmpl-git.el
 create mode 100644 test/lisp/pcomplete-tests.el

diff --git a/lisp/pcmpl-git.el b/lisp/pcmpl-git.el
new file mode 100644
index 0000000000..369f66742c
--- /dev/null
+++ b/lisp/pcmpl-git.el
@@ -0,0 +1,97 @@
+;;; pcmpl-git.el --- completions for git -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; Package: pcomplete
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'pcomplete)
+(require 'vc-git)
+
+(defun pcmpl-git--expand-flags (args)
+  "In the list of ARGS, expand arguments of the form --[no-]flag."
+  (mapcan (lambda (arg) (if (string-search "[no-]" arg)
+                            (list (string-replace "[no-]" "" arg)
+                                  (string-replace "[no-]" "no-" arg))
+                          (list arg)))
+          args))
+
+(defun pcmpl-git--tracked-file-predicate ()
+  "Return a predicate function determining if a file is tracked by git."
+  (when-let ((files (ignore-errors
+                      (process-lines vc-git-program "ls-files")))
+             (table (make-hash-table :test #'equal)))
+    (dolist (file files) (puthash (expand-file-name file) t table))
+    (lambda (file) (or (gethash (expand-file-name file) table)
+                       (string-suffix-p "/" file)))))
+
+(defun pcmpl-git--remote-refs (remote)
+  "List the locally known git revisions from REMOTE.
+If REMOTE is nil, return the list of remotes."
+  (if (null remote)
+      (ignore-errors
+        (process-lines vc-git-program "remote"))
+    (delq nil
+          (mapcar
+           (let ((re (concat (regexp-quote remote) "/\\(.*\\)")))
+             (lambda (s) (when (string-match re s) (match-string 1 s))))
+           (vc-git-revision-table nil)))))
+
+;;;###autoload
+(defun pcomplete/git ()
+  "Completion for the `git' command."
+  (let ((subcmds (pcomplete-parse-help "git help -a"
+                                       :margin "^\\( +\\)[a-z]"
+                                       :argument "[-a-z]+")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (append subcmds
+                              (pcomplete-parse-help "git help"
+                                                    :margin "\\(\\[\\)-"
+                                                    :separator " | "
+                                                    :description "\\`"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (pcomplete-match "\\`-" 0))
+                (pcomplete-here
+                 (pcmpl-git--expand-flags
+                  (pcomplete-parse-help (format "git help %s" subcmd)
+                                        :argument "-+\\(?:\\[no-\\]\\)?[a-z-]+=?"))))
+               ;; Complete tracked files
+               ((or "mv" "rm" "restore" "grep" "status" "commit")
+                (pcomplete-here
+                 (pcomplete-entries nil (pcmpl-git--tracked-file-predicate))))
+               ;; Complete revisions
+               ((or "branch" "merge" "rebase" "switch")
+                (pcomplete-here (vc-git-revision-table nil)))
+               ;; Complete revisions and tracked files
+               ;; TODO: diff and log accept revision ranges
+               ((or "checkout" "reset" "show" "diff" "log")
+                (pcomplete-here
+                 (completion-table-in-turn
+                  (vc-git-revision-table nil)
+                  (pcomplete-entries nil (pcmpl-git--tracked-file-predicate)))))
+               ;; Complete remotes and their revisions
+               ((or "fetch" "pull" "push")
+                (pcomplete-here (pcmpl-git--remote-refs nil))
+                (pcomplete-here (pcmpl-git--remote-refs (pcomplete-arg 1)))))))))
+
+(provide 'pcmpl-git)
+;;; pcmpl-git.el ends here
diff --git a/lisp/pcmpl-gnu.el b/lisp/pcmpl-gnu.el
index 3c9bf1ec9d..639e0bac45 100644
--- a/lisp/pcmpl-gnu.el
+++ b/lisp/pcmpl-gnu.el
@@ -394,6 +394,28 @@ pcomplete/find
     (while (pcomplete-here (pcomplete-dirs) nil #'identity))))
 
 ;;;###autoload
+(defun pcomplete/awk ()
+  "Completion for the GNU Privacy Guard."
+  (while (pcomplete-match "^-" 0)
+    (pcomplete-here (pcomplete-parse-help "awk --help"
+                                          :margin "\t"
+                                          :separator "  +"
+                                          :description "\0"
+                                          :metavar "[=a-z]+"))))
+
+;;;###autoload
+(defun pcomplete/gpg ()
+  "Completion for the GNU Privacy Guard."
+  (while (pcomplete-match "^-" 0)
+    (pcomplete-here (pcomplete-parse-help "gpg --help" :narrow-end "^ -se"))))
+
+;;;###autoload
+(defun pcomplete/gdb ()
+  "Completion for the GNU debugger."
+  (while (pcomplete-match "^-" 0)
+    ;; FIXME: space is inserted after options ending in "=".
+    (pcomplete-here (pcomplete-parse-help "gdb --help"))))
+
 (defalias 'pcomplete/gdb 'pcomplete/xargs)
 
 ;;; pcmpl-gnu.el ends here
diff --git a/lisp/pcmpl-rpm.el b/lisp/pcmpl-rpm.el
index f7925d9d9e..1d833eaa91 100644
--- a/lisp/pcmpl-rpm.el
+++ b/lisp/pcmpl-rpm.el
@@ -378,6 +378,45 @@ pcomplete/rpm
        (t
 	(error "You must select a mode: -q, -i, -U, --verify, etc"))))))
 
+;;; DNF
+
+(defvar pcmpl-rpm-dnf-cache-file "/var/cache/dnf/packages.db"
+  "Location of the DNF cache.")
+
+(defun pcmpl-rpm--dnf-packages (status)
+  (when (and (file-exists-p pcmpl-rpm-dnf-cache-file)
+             (executable-find "sqlite3"))
+    (with-temp-message
+        "Getting list of packages..."
+      (process-lines "sqlite3" "-batch" "-init" "/dev/null"
+                     pcmpl-rpm-dnf-cache-file
+                     (pcase-exhaustive status
+                       ('available "select pkg from available")
+                       ('installed "select pkg from installed")
+                       ('not-installed "select pkg from available \
+where pkg not in (select pkg from installed)"))))))
+
+;;;###autoload
+(defun pcomplete/dnf ()
+  "Completion for the `dnf' command."
+  (let ((subcmds (pcomplete-parse-help "dnf help"
+                                       :margin "^\\(\\)[a-z-]+  "
+                                       :argument "[a-z-]+")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (append subcmds
+                              (pcomplete-parse-help "dnf help"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (pcomplete-match "\\`-" 0))
+                (pcomplete-here
+                 (pcomplete-parse-help (format "dnf help %s" subcmd))))
+               ((or "downgrade" "reinstall" "remove")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'installed)))
+               ((or "install" "mark" "reinstall" "upgrade")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'not-installed)))
+               ((or "builddep" "changelog" "info" "list" "repoquery" "updateinfo")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'available))))))))
+
 (provide 'pcmpl-rpm)
 
 ;;; pcmpl-rpm.el ends here
diff --git a/lisp/pcmpl-x.el b/lisp/pcmpl-x.el
index 261a3d4e27..479d549a3d 100644
--- a/lisp/pcmpl-x.el
+++ b/lisp/pcmpl-x.el
@@ -321,5 +321,21 @@ pcomplete/bcc32
 ;;;###autoload
 (defalias 'pcomplete/bcc 'pcomplete/bcc32)
 
+;;;###autoload
+(defun pcomplete/rclone ()
+  "Completion for the `rclone' command."
+  (let ((subcmds (pcomplete-parse-help "rclone help"
+                                       :margin "^  "
+                                       :argument "[a-z]+"
+                                       :narrow-start "\n\n")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (append subcmds
+                              (pcomplete-parse-help "rclone help flags"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (if (pcomplete-match "\\`-" 0)
+                 (pcomplete-here (pcomplete-parse-help
+                                  (format "rclone %s --help" subcmd)))
+               (pcomplete-here (pcomplete-entries)))))))
+
 (provide 'pcmpl-x)
 ;;; pcmpl-x.el ends here
diff --git a/lisp/pcomplete.el b/lisp/pcomplete.el
index 15b9880df8..c7b49c5596 100644
--- a/lisp/pcomplete.el
+++ b/lisp/pcomplete.el
@@ -119,6 +119,9 @@
 ;;; Code:
 
 (require 'comint)
+(eval-when-compile
+  (require 'cl-lib)
+  (require 'rx))
 
 (defgroup pcomplete nil
   "Programmable completion."
@@ -485,6 +488,14 @@ pcomplete-completions-at-point
           (when completion-ignore-case
             (setq table (completion-table-case-fold table)))
           (list beg (point) table
+                :annotation-function
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-annotation cand)))
+                :company-docsig
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-help cand)))
                 :predicate pred
                 :exit-function
 		;; If completion is finished, add a terminating space.
@@ -1332,6 +1343,102 @@ pcomplete-read-host-names
       (pcomplete-read-hosts pcomplete-hosts-file 'pcomplete--host-name-cache
                    'pcomplete--host-name-cache-timestamp)))
 
+;;; Parsing of help messages
+
+(defvar pcomplete-parse-help (make-hash-table :test #'equal)
+  "Hash table for memoization of function `pcomplete-parse-help'.")
+
+(cl-defun pcomplete-parse-help (command
+                                &rest args
+                                &key
+                                (margin (rx bol (+ " ")))
+                                (argument (rx "-" (+ (any "-" alnum)) (? "=")))
+                                (metavar (rx (? " ")
+                                             (or (+ (any alnum "_-"))
+                                                 (seq "[" (+? nonl) "]")
+                                                 (seq "<" (+? nonl) ">")
+                                                 (seq "{" (+? nonl) "}"))))
+                                (separator (rx ", " symbol-start))
+                                (description (rx (* nonl) (* "\n" (>= 8 " ") (* nonl))))
+                                narrow-start
+                                narrow-end
+                                &aux
+                                result)
+  "Parse output of COMMAND into a list of completion candidates.
+
+A list of arguments is expected after each match of MARGIN.  Each
+argument should match ARGUMENT, possibly followed by a match of
+METAVAR.  If a match of SEPARATOR follows, then more
+argument-metavar pairs are expected.  Finally, a match of
+DESCRIPTION is collected.
+
+Keyword ARGS:
+
+MARGIN: regular expression after which argument descriptions are
+  to be found.  Parsing continues at the end of the first match
+  group, or at the end of the match.
+
+ARGUMENT: regular expression matching an argument name.  The
+  first match group (or the entire match if missing) is collected
+  as the argument name.  Parsing continues at the end of the
+  second matching group (or first group, or entire match).
+
+METAVAR: regular expression matching an argument parameter name.
+  The first match group (or the entire match if missing) is
+  collected as the parameter name and used as completion
+  annotation.  Parsing continues at the end of the second
+  matching group (or first group, or entire match).
+
+SEPARATOR: regular expression matching the separator between
+  arguments.  Parsing continues at the end of the first match
+  group, or at the end of the match.
+
+DESCRIPTION: regular expression matching the description of an
+  argument.  The first match group (or the entire match if
+  missing) is collected as the parameter name and used as
+  completion help.  Parsing continues at the end of the first
+  matching group or entire match.
+
+NARROW-START, NARROW-END: if non-nil, parsing of help message is
+  narrowed to the region between the (end of) the first match of
+  these regular expressions."
+  (with-memoization (gethash (cons command args) pcomplete-parse-help)
+    (with-temp-buffer
+      (call-process-shell-command command nil t)
+      (goto-char (point-min))
+      (narrow-to-region (or (and narrow-start
+                                 (re-search-forward narrow-start nil t)
+                                 (or (match-beginning 1) (match-beginning 0)))
+                            (point-min))
+                        (or (and narrow-end
+                                 (re-search-forward narrow-end nil t)
+                                 (or (match-beginning 1) (match-beginning 0)))
+                            (point-max)))
+      (goto-char (point-min))
+      (while (re-search-forward margin nil t)
+        (goto-char (or (match-end 1) (match-end 0)))
+        (let ((i 0))
+          (while (and (or (zerop i)
+                          (prog1 (looking-at separator)
+                            (goto-char (or (match-end 1)
+                                           (match-end 0)))))
+                      (looking-at argument))
+            (cl-incf i)
+            (push (or (match-string 1) (match-string 0)) result)
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (when (looking-at metavar)
+              (put-text-property 0 1 'pcomplete-annotation
+                                 (or (match-string 1) (match-string 0))
+                                 (car result))
+              (goto-char (seq-some #'match-end '(2 1 0)))))
+          (when (looking-at description)
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (let ((help (string-clean-whitespace (or (match-string 1)
+                                                     (match-string 0)))))
+              (dotimes (j i)
+                (put-text-property 0 1 'pcomplete-help help (nth j result))))))))
+    (nreverse result)))
+
 (provide 'pcomplete)
 
 ;;; pcomplete.el ends here
diff --git a/test/lisp/pcomplete-tests.el b/test/lisp/pcomplete-tests.el
new file mode 100644
index 0000000000..c034f02c4f
--- /dev/null
+++ b/test/lisp/pcomplete-tests.el
@@ -0,0 +1,100 @@
+;;; pcomplete-tests.el --- Tests for pcomplete.el  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'ert)
+(require 'pcomplete)
+
+(ert-deftest pcomplete-test-parse-help-gpg ()
+  (cl-letf ((pcomplete-parse-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process-shell-command)
+             (lambda (&rest _) (insert "\
+gpg (GnuPG) 2.3.7
+
+Commands:
+
+ -s, --sign                         make a signature
+     --clear-sign                   make a clear text signature
+ -b, --detach-sign                  make a detached signature
+     --tofu-policy VALUE            set the TOFU policy for a key
+
+Options to specify keys:
+ -r, --recipient USER-ID            encrypt for USER-ID
+ -u, --local-user USER-ID           use USER-ID to sign or decrypt
+
+(See the man page for a complete listing of all commands and options)
+
+Examples:
+
+ -se -r Bob [file]          sign and encrypt for user Bob
+ --clear-sign [file]        make a clear text signature
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-parse-help "gpg --help" :narrow-end "^ -se")
+      '(#("-s" 0 1 (pcomplete-help "make a signature"))
+        #("--sign" 0 1 (pcomplete-help "make a signature"))
+        #("--clear-sign" 0 1 (pcomplete-help "make a clear text signature"))
+        #("-b" 0 1 (pcomplete-help "make a detached signature"))
+        #("--detach-sign" 0 1 (pcomplete-help "make a detached signature"))
+        #("--tofu-policy" 0 1
+          (pcomplete-help "set the TOFU policy for a key" pcomplete-annotation " VALUE"))
+        #("-r" 0 1 (pcomplete-help "encrypt for USER-ID"))
+        #("--recipient" 0 1
+          (pcomplete-help "encrypt for USER-ID" pcomplete-annotation " USER-ID"))
+        #("-u" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt"))
+        #("--local-user" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt" pcomplete-annotation " USER-ID")))))))
+
+(ert-deftest pcomplete-test-parse-help-git ()
+  (cl-letf ((pcomplete-parse-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process-shell-command)
+             (lambda (&rest _) (insert "\
+usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
+           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
+           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
+           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
+           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
+           <command> [<args>]
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-parse-help "git help"
+                             :margin "\\(\\[\\)-"
+                             :separator " | "
+                             :description "\\`")
+      '("-v" "--version" "-h" "--help"
+        #("-C" 0 1 (pcomplete-annotation " <path>"))
+        #("-c" 0 1 (pcomplete-annotation " <name>"))
+        #("--exec-path" 0 1 (pcomplete-annotation "[=<path>]"))
+        "--html-path" "--man-path" "--info-path"
+        "-p" "--paginate" "-P" "--no-pager"
+        "--no-replace-objects" "--bare"
+        #("--git-dir=" 0 1 (pcomplete-annotation "<path>"))
+        #("--work-tree=" 0 1 (pcomplete-annotation "<path>"))
+        #("--namespace=" 0 1 (pcomplete-annotation "<name>"))
+        #("--super-prefix=" 0 1 (pcomplete-annotation "<path>"))
+        #("--config-env=" 0 1 (pcomplete-annotation "<name>")))))))
+
+(provide 'pcomplete-tests)
+;;; pcomplete-tests.el ends here
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-08  9:34 bug#57673: [PATCH] Parse --help messages for pcomplete Augusto Stoffel
@ 2022-09-08 12:39 ` Lars Ingebrigtsen
  2022-09-08 13:05   ` Augusto Stoffel
  2022-09-08 20:49 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  1 sibling, 1 reply; 21+ messages in thread
From: Lars Ingebrigtsen @ 2022-09-08 12:39 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 57673, Stefan Monnier

Augusto Stoffel <arstoffel@gmail.com> writes:

> Find attached a “worse is better” approach for pcomplete, where we parse
> help messages to generate a list of completions.

Hm, interesting.  Perhaps Stefan has comments here; added to the CCs.

> This is still a sketch.  I've added pcomplete functions for a random
> selection of commands to see if this works reasonably, and I think it
> probably does.  But in any case I don't think it would make sense to try
> and add completions as detailed as the ones bash provides; there is an
> awful lot of logic in the files under /usr/share/bash-completion and I
> don't think anyone would want to redo that work.

I'm wholly unfamiliar with how bash does completions.  But is there any
reasonable way to reuse the bash completion framework here?






^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-08 12:39 ` Lars Ingebrigtsen
@ 2022-09-08 13:05   ` Augusto Stoffel
  2022-09-09 17:02     ` Lars Ingebrigtsen
  0 siblings, 1 reply; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-08 13:05 UTC (permalink / raw)
  To: Lars Ingebrigtsen; +Cc: 57673, Stefan Monnier

On Thu,  8 Sep 2022 at 14:39, Lars Ingebrigtsen <larsi@gnus.org> wrote:

> I'm wholly unfamiliar with how bash does completions.  But is there any
> reasonable way to reuse the bash completion framework here?

There is a MELPA package that does that:

    https://github.com/szermatt/emacs-bash-completion

But I it never worked very well for me, possibly because I often use
Tramp connections with high latency.  IIUC this package is like Python's
"native completion", i.e., it relies on sending "prefix\t\t" to the bash
process and hopes readline will do its thing.

Exploiting bash could work nicely if it had a
please_complete_this_partial_command function, but this doesn't seem
possible.  I might be wrong, though.





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-08  9:34 bug#57673: [PATCH] Parse --help messages for pcomplete Augusto Stoffel
  2022-09-08 12:39 ` Lars Ingebrigtsen
@ 2022-09-08 20:49 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-08 21:53   ` Augusto Stoffel
  1 sibling, 1 reply; 21+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2022-09-08 20:49 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 57673

> +;;; Commentary:
> +

I don't fully agree with you here.

> +(defun pcmpl-git--tracked-file-predicate ()
> +  "Return a predicate function determining if a file is tracked by git."

I'd capitalize "git".

> +  (when-let ((files (ignore-errors
> +                      (process-lines vc-git-program "ls-files")))

Is it normal&common for `ls-files` to return an error?  If not, then
maybe we should use `with-demoted-errors` so that the users are made
aware of an error if it occurs.

> +(defun pcmpl-git--remote-refs (remote)
> +  "List the locally known git revisions from REMOTE.
> +If REMOTE is nil, return the list of remotes."
> +  (if (null remote)

AFAICT you could just as well have two separate functions here since all
callers either always provide a nil arg or never provide a nil arg.

> +      (ignore-errors
> +        (process-lines vc-git-program "remote"))
> +    (delq nil
> +          (mapcar
> +           (let ((re (concat (regexp-quote remote) "/\\(.*\\)")))
> +             (lambda (s) (when (string-match re s) (match-string 1 s))))
> +           (vc-git-revision-table nil)))))

I think the `re` needs to be anchored to avoid false positives.

> +;;;###autoload
> +(defun pcomplete/git ()
> +  "Completion for the `git' command."
> +  (let ((subcmds (pcomplete-parse-help "git help -a"
> +                                       :margin "^\\( +\\)[a-z]"
> +                                       :argument "[-a-z]+")))
> +    (while (not (member (pcomplete-arg 1) subcmds))
> +      (pcomplete-here (append subcmds
> +                              (pcomplete-parse-help "git help"
> +                                                    :margin "\\(\\[\\)-"
> +                                                    :separator " | "
> +                                                    :description "\\`"))))

I don't quite understand this `while` loop.  IIUC this is to handle the
case where `--foo` args are passed before the `subcmd`, but if we want
to support that use case, don't we need to change the code so it doesn't
hardcode the `1` in (pcomplete-arg 1)?

Maybe we should follow a scheme similar to that used in `pcomplete-cvs`,
tho it'd probably require a new replacement for `pcomplete-opt`

> +    (let ((subcmd (pcomplete-arg 1)))
> +      (while (pcase subcmd
> +               ((guard (pcomplete-match "\\`-" 0))
> +                (pcomplete-here
> +                 (pcmpl-git--expand-flags
> +                  (pcomplete-parse-help (format "git help %s" subcmd)
> +                                        :argument "-+\\(?:\\[no-\\]\\)?[a-z-]+=?"))))

`subcmd` may contain funny chars like `&`, so we should
`shell-quote-argument` it (tho see further down).

> +               ;; Complete tracked files
> +               ((or "mv" "rm" "restore" "grep" "status" "commit")
> +                (pcomplete-here
> +                 (pcomplete-entries nil (pcmpl-git--tracked-file-predicate))))

Regarding `git commit`:
- I never specify files on `git commit` without using `--` first.
- I often appreciate the completion of `--amend`.
- It probably makes sense to only complete files that have been modified.

> +(cl-defun pcomplete-parse-help (command

AFAICT, this "command" never uses any functionality of the shell, so we
should be using `call-process` rather than `call-process-shell-command`,
since the use of a shell here only brings trouble (it's less efficient
and it forces us to `shell-quote-arguments` to try and avoid
pathological errors in corner cases).


        Stefan






^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-08 20:49 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2022-09-08 21:53   ` Augusto Stoffel
  2022-09-09  2:47     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 1 reply; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-08 21:53 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 57673

Hi Stefan,

thanks for the detailed comments.  I was also curious, on a more high
level, about your opinion on parsing the --help messages.  It's a bit of
a heuristic thing and it will probably lead to some false positives here
and there.

On Thu,  8 Sep 2022 at 16:49, Stefan Monnier <monnier@iro.umontreal.ca> wrote:

>> +;;; Commentary:
>> +
>
> I don't fully agree with you here.

I thought this was pretty uncontroversial, but I don't feel too strongly
about it.

>> +(defun pcmpl-git--tracked-file-predicate ()
>> +  "Return a predicate function determining if a file is tracked by git."
>
> I'd capitalize "git".

Noted.

>> +  (when-let ((files (ignore-errors
>> +                      (process-lines vc-git-program "ls-files")))
>
> Is it normal&common for `ls-files` to return an error?  If not, then
> maybe we should use `with-demoted-errors` so that the users are made
> aware of an error if it occurs.

I think that being outside of a repo is the main error situation.  But
in this particular spot I'd rather fail silently, because the
consequence of an error is rather innocuous -- we'll lack a predicate
function and therefore show some extra files completions.

>> +(defun pcmpl-git--remote-refs (remote)
>> +  "List the locally known git revisions from REMOTE.
>> +If REMOTE is nil, return the list of remotes."
>> +  (if (null remote)
>
> AFAICT you could just as well have two separate functions here since all
> callers either always provide a nil arg or never provide a nil arg.

That's true.  I was trying to save a symbol name.

>> +      (ignore-errors
>> +        (process-lines vc-git-program "remote"))
>> +    (delq nil
>> +          (mapcar
>> +           (let ((re (concat (regexp-quote remote) "/\\(.*\\)")))
>> +             (lambda (s) (when (string-match re s) (match-string 1 s))))
>> +           (vc-git-revision-table nil)))))
>
> I think the `re` needs to be anchored to avoid false positives.

Good point.

>> +;;;###autoload
>> +(defun pcomplete/git ()
>> +  "Completion for the `git' command."
>> +  (let ((subcmds (pcomplete-parse-help "git help -a"
>> +                                       :margin "^\\( +\\)[a-z]"
>> +                                       :argument "[-a-z]+")))
>> +    (while (not (member (pcomplete-arg 1) subcmds))
>> +      (pcomplete-here (append subcmds
>> +                              (pcomplete-parse-help "git help"
>> +                                                    :margin "\\(\\[\\)-"
>> +                                                    :separator " | "
>> +                                                    :description "\\`"))))
>
> I don't quite understand this `while` loop.  IIUC this is to handle the
> case where `--foo` args are passed before the `subcmd`, but if we want
> to support that use case, don't we need to change the code so it doesn't
> hardcode the `1` in (pcomplete-arg 1)?

The thing here is that each call to `pcomplete-here' shifts the args.
So `1' in this context means “the previous arg”.  In English, the above
form means: offer global switches and subcommand names as completion
candidates until the previous arg is a subcommand.

(We could additionally allow filenames, since some switches take a file
argument.)

> Maybe we should follow a scheme similar to that used in `pcomplete-cvs`,
> tho it'd probably require a new replacement for `pcomplete-opt`

I don't understand what you mean exactly here, but note that pcomplete
allows entering, for instance

    git --no-pager -C commit --amend --long ./m4/acl.m4

without completely typing any of these words.

>> +    (let ((subcmd (pcomplete-arg 1)))
>> +      (while (pcase subcmd
>> +               ((guard (pcomplete-match "\\`-" 0))
>> +                (pcomplete-here
>> +                 (pcmpl-git--expand-flags
>> +                  (pcomplete-parse-help (format "git help %s" subcmd)
>> +                                        :argument "-+\\(?:\\[no-\\]\\)?[a-z-]+=?"))))
>
> `subcmd` may contain funny chars like `&`, so we should
> `shell-quote-argument` it (tho see further down).

True, we shouldn't use a shell.  But also note that a funny char will
not appear in an element of `subcmds', since our :argument parameter is
"[-a-z]+".

>> +               ;; Complete tracked files
>> +               ((or "mv" "rm" "restore" "grep" "status" "commit")
>> +                (pcomplete-here
>> +                 (pcomplete-entries nil (pcmpl-git--tracked-file-predicate))))
>
> Regarding `git commit`:
> - I never specify files on `git commit` without using `--` first.

Well, `--' is among the completion candidates.  Is that what you want?

> - I often appreciate the completion of `--amend`.

You can complete switches after `git commit'.  This is taken care of by
the (guard (pcomplete-match "\\`-" 0)) case of the pcase.

> - It probably makes sense to only complete files that have been modified.

So, this is brings up an important point.

I'm advocating for a “worse is better” method where we try to have a
more or less comprehensive list of completion candidates but we don't
try to be too precise about the context in which each thing can appear.

If we do try to be precise, we might end up with 3592 lines of code,
which is the length of /usr/share/bash-completion/git on my machine.

What do you think?

>> +(cl-defun pcomplete-parse-help (command
>
> AFAICT, this "command" never uses any functionality of the shell, so we
> should be using `call-process` rather than `call-process-shell-command`,
> since the use of a shell here only brings trouble (it's less efficient
> and it forces us to `shell-quote-arguments` to try and avoid
> pathological errors in corner cases).

Yes, passing the output of `format' straight to a shell is of course not
acceptable for the final version of this patch.





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-08 21:53   ` Augusto Stoffel
@ 2022-09-09  2:47     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-10  9:45       ` Augusto Stoffel
  0 siblings, 1 reply; 21+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2022-09-09  2:47 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 57673

Augusto Stoffel [2022-09-08 23:53:02] wrote:
> thanks for the detailed comments.  I was also curious, on a more high
> level, about your opinion on parsing the --help messages.  It's a bit of
> a heuristic thing and it will probably lead to some false positives here
> and there.

I'm all for using `--help`.

I think any problems should be pushed upstream: the commands should be
self-describing so if `--help` can't be used reliably, they should
provide something else that can.

>>> +;;; Commentary:
>>> +
>> I don't fully agree with you here.
> I thought this was pretty uncontroversial, but I don't feel too strongly
> about it.

[ OK, you got me at my own game: I can't tell if you're pushing the
  joke further or if you're serious :-)  ]

>>> +  (when-let ((files (ignore-errors
>>> +                      (process-lines vc-git-program "ls-files")))
>>
>> Is it normal&common for `ls-files` to return an error?  If not, then
>> maybe we should use `with-demoted-errors` so that the users are made
>> aware of an error if it occurs.
>
> I think that being outside of a repo is the main error situation.  But
> in this particular spot I'd rather fail silently, because the
> consequence of an error is rather innocuous -- we'll lack a predicate
> function and therefore show some extra files completions.

Fair, enough, tho being outside of a repo should be rather unusual, no
(unless Emacs's dir tracker has gone hooftracks, admittedly).
In any case, you might want to add a brief comment about why you decided
to use `ignore-errors`.

>>> +;;;###autoload
>>> +(defun pcomplete/git ()
>>> +  "Completion for the `git' command."
>>> +  (let ((subcmds (pcomplete-parse-help "git help -a"
>>> +                                       :margin "^\\( +\\)[a-z]"
>>> +                                       :argument "[-a-z]+")))
>>> +    (while (not (member (pcomplete-arg 1) subcmds))
>>> +      (pcomplete-here (append subcmds
>>> +                              (pcomplete-parse-help "git help"
>>> +                                                    :margin "\\(\\[\\)-"
>>> +                                                    :separator " | "
>>> +                                                    :description "\\`"))))
>>
>> I don't quite understand this `while` loop.  IIUC this is to handle the
>> case where `--foo` args are passed before the `subcmd`, but if we want
>> to support that use case, don't we need to change the code so it doesn't
>> hardcode the `1` in (pcomplete-arg 1)?
>
> The thing here is that each call to `pcomplete-here' shifts the args.
> So `1' in this context means “the previous arg”.

Oh, right it's not the absolute position, now I get it, thanks.
Looks good, then.

>>> +               ;; Complete tracked files
>>> +               ((or "mv" "rm" "restore" "grep" "status" "commit")
>>> +                (pcomplete-here
>>> +                 (pcomplete-entries nil (pcmpl-git--tracked-file-predicate))))
>>
>> Regarding `git commit`:
>> - I never specify files on `git commit` without using `--` first.
>
> Well, `--' is among the completion candidates.  Is that what you want?

No, I'm just not sure we should be completing file names before a "--"
was given.  Maybe it's OK, tho.

>> - I often appreciate the completion of `--amend`.
> You can complete switches after `git commit'.  This is taken care of by
> the (guard (pcomplete-match "\\`-" 0)) case of the pcase.

Indeed, I missed that.

>> - It probably makes sense to only complete files that have been modified.
> So, this is brings up an important point.
> I'm advocating for a “worse is better” method where we try to have a
> more or less comprehensive list of completion candidates but we don't
> try to be too precise about the context in which each thing can appear.

Fair enough.  Especially since being more precise risks missing some
files which can legitimately appear.


        Stefan






^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-08 13:05   ` Augusto Stoffel
@ 2022-09-09 17:02     ` Lars Ingebrigtsen
  2022-09-10  9:20       ` Augusto Stoffel
  0 siblings, 1 reply; 21+ messages in thread
From: Lars Ingebrigtsen @ 2022-09-09 17:02 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 57673, Stefan Monnier

Augusto Stoffel <arstoffel@gmail.com> writes:

> IIUC this package is like Python's
> "native completion", i.e., it relies on sending "prefix\t\t" to the bash
> process and hopes readline will do its thing.

I see.

> Exploiting bash could work nicely if it had a
> please_complete_this_partial_command function, but this doesn't seem
> possible.  I might be wrong, though.

I wondered whether we could use those files more directly -- executing
them or something in the right context?  (Like I said, I know nothing
about how this works. 😀)






^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-09 17:02     ` Lars Ingebrigtsen
@ 2022-09-10  9:20       ` Augusto Stoffel
  0 siblings, 0 replies; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-10  9:20 UTC (permalink / raw)
  To: Lars Ingebrigtsen; +Cc: 57673, Stefan Monnier

On Fri,  9 Sep 2022 at 19:02, Lars Ingebrigtsen <larsi@gnus.org> wrote:

> Augusto Stoffel <arstoffel@gmail.com> writes:
>
>> IIUC this package is like Python's
>> "native completion", i.e., it relies on sending "prefix\t\t" to the bash
>> process and hopes readline will do its thing.
>
> I see.
>
>> Exploiting bash could work nicely if it had a
>> please_complete_this_partial_command function, but this doesn't seem
>> possible.  I might be wrong, though.
>
> I wondered whether we could use those files more directly -- executing
> them or something in the right context?  (Like I said, I know nothing
> about how this works. 😀)

Yeah, I'm not sure either, but I would suspect the MELPA package
mentioned above didn't choose the hardest path gratuitously :-).  It
would be good to hear from someone who knows for sure, though.

One additional advantage of parsing the help files which I didn't
mention before is that we get a modicum of self-documenting behavior,
since we can collect the argument descriptions.  For example, while
completing you can remind yourself which switch is for recursive, -r or
-R.





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-09  2:47     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2022-09-10  9:45       ` Augusto Stoffel
  2022-09-10 14:32         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 1 reply; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-10  9:45 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: Lars Ingebrigtsen, 57673

[-- Attachment #1: Type: text/plain, Size: 816 bytes --]

Hi Stefan,

I've attached a new iteration of the patch.  I think the git completion
should be pretty usable (but certainly can be refined in the future).
I'm also satisfied with the parser, please have a look if you are
interested.

Next I'd like to think now of a good way to add batches of simpler
commands, say all GNU coreutils.  This would entail repeating variations
of

   (defun pcomplete/gpg ()
     "Completion for the GNU Privacy Guard."
     (while (if (pcomplete-match "\\`-" 0)
                (pcomplete-here (pcomplete-from-help "gpg --help"
                                                     :narrow-end "^ -se"))
              (pcomplete-here (pcomplete-entries)))))

over and over, so I was wondering if it makes sense to add a macro to
help here.  See a suggestion at the end of pcomplete.el.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-pcomplete-Generate-completions-from-help-messages.patch --]
[-- Type: text/x-patch, Size: 22479 bytes --]

From d0b150d8ae742eb5bf329287e73b751f7ab2e2ca Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <arstoffel@gmail.com>
Date: Thu, 8 Sep 2022 11:09:42 +0200
Subject: [PATCH] pcomplete: Generate completions from --help messages

---
 lisp/pcmpl-git.el            | 110 +++++++++++++++++++++++++++
 lisp/pcmpl-gnu.el            |  30 ++++++++
 lisp/pcmpl-rpm.el            |  44 ++++++++++-
 lisp/pcmpl-x.el              |  17 +++++
 lisp/pcomplete.el            | 142 +++++++++++++++++++++++++++++++++++
 test/lisp/pcomplete-tests.el | 100 ++++++++++++++++++++++++
 6 files changed, 442 insertions(+), 1 deletion(-)
 create mode 100644 lisp/pcmpl-git.el
 create mode 100644 test/lisp/pcomplete-tests.el

diff --git a/lisp/pcmpl-git.el b/lisp/pcmpl-git.el
new file mode 100644
index 0000000000..1ff82f2b81
--- /dev/null
+++ b/lisp/pcmpl-git.el
@@ -0,0 +1,110 @@
+;;; pcmpl-git.el --- Completions for Git -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; Package: pcomplete
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides completion rules for the Git program.
+
+;;; Code:
+
+(require 'pcomplete)
+(require 'vc-git)
+
+(defun pcmpl-git--expand-flags (args)
+  "In the list of ARGS, expand arguments of the form --[no-]flag."
+  (mapcan (lambda (arg) (if (string-search "[no-]" arg)
+                            (list (string-replace "[no-]" "" arg)
+                                  (string-replace "[no-]" "no-" arg))
+                          (list arg)))
+          args))
+
+(defun pcmpl-git--tracked-file-predicate (&rest args)
+  "Return a predicate function determining if a file is tracked by Git.
+ARGS are passed to the `git ls-files' command."
+  (when-let ((files (mapcar #'expand-file-name
+                            (ignore-errors
+                              (apply #'process-lines
+                                     vc-git-program "ls-files" args)))))
+    (lambda (file)
+      (setq file (expand-file-name file))
+      (if (string-suffix-p "/" file)
+          (seq-some (lambda (f) (string-prefix-p file f))
+                    files)
+        (member file files)))))
+
+(defun pcmpl-git--remote-refs (remote)
+  "List the locally known Git revisions from REMOTE."
+  (delq nil
+        (mapcar
+         (let ((re (concat "\\`" (regexp-quote remote) "/\\(.*\\)")))
+           (lambda (s) (when (string-match re s) (match-string 1 s))))
+         (vc-git-revision-table nil))))
+
+;;;###autoload
+(defun pcomplete/git ()
+  "Completion for the `git' command."
+  (let ((subcmds (pcomplete-from-help `(,vc-git-program "help" "-a")
+                                      :margin "^\\( +\\)[a-z]"
+                                      :argument "[[:alnum:]-]+")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (completion-table-merge
+                       subcmds
+                       ;; Global switches and their file arguments
+                       (pcomplete-from-help `(,vc-git-program "help")
+                                            :margin "\\(\\[\\)-"
+                                            :separator " | "
+                                            :description "\\`")
+                       (when (pcomplete-match "\\-")
+                         (pcomplete-entries)))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (pcomplete-match "\\`-" 0))
+                (pcomplete-here
+                 (pcmpl-git--expand-flags
+                  (pcomplete-from-help `(,vc-git-program "help" ,subcmd)
+                                       :argument
+                                       "-+\\(?:\\[no-\\]\\)?[a-z-]+=?"))))
+               ;; Complete modified tracked files
+               ((or "add" "commit" "restore")
+                (pcomplete-here
+                 (pcomplete-entries nil
+                                    (pcmpl-git--tracked-file-predicate "-m"))))
+               ;; Complete all tracked files
+               ((or "mv" "rm" "grep" "status")
+                (pcomplete-here
+                 (pcomplete-entries nil (pcmpl-git--tracked-file-predicate))))
+               ;; Complete revisions
+               ((or "branch" "merge" "rebase" "switch")
+                (pcomplete-here (vc-git-revision-table nil)))
+               ;; Complete revisions and tracked files
+               ;; TODO: diff and log accept revision ranges
+               ((or "checkout" "reset" "show" "diff" "log")
+                (pcomplete-here
+                 (completion-table-in-turn
+                  (vc-git-revision-table nil)
+                  (pcomplete-entries nil (pcmpl-git--tracked-file-predicate)))))
+               ;; Complete remotes and their revisions
+               ((or "fetch" "pull" "push")
+                (pcomplete-here (process-lines vc-git-program "remote"))
+                (pcomplete-here (pcmpl-git--remote-refs (pcomplete-arg 1)))))))))
+
+(provide 'pcmpl-git)
+;;; pcmpl-git.el ends here
diff --git a/lisp/pcmpl-gnu.el b/lisp/pcmpl-gnu.el
index 3c9bf1ec9d..f957e7cbb4 100644
--- a/lisp/pcmpl-gnu.el
+++ b/lisp/pcmpl-gnu.el
@@ -394,6 +394,36 @@ pcomplete/find
     (while (pcomplete-here (pcomplete-dirs) nil #'identity))))
 
 ;;;###autoload
+(defun pcomplete/awk ()
+  "Completion for the `awk' command."
+  (while (pcomplete-here
+          (completion-table-in-turn
+           (pcomplete-from-help "awk --help"
+                                :margin "\t"
+                                :separator "  +"
+                                :description "\0"
+                                :metavar "[=a-z]+")
+           (pcomplete-entries)))))
+
+;;;###autoload
+(defun pcomplete/gpg ()
+  "Completion for the GNU Privacy Guard."
+  (while (pcomplete-here
+          (completion-table-in-turn
+           (pcomplete-from-help "gpg --help" :narrow-end "^ -se")
+           (pcomplete-entries)))))
+
+;; This is going to get tedious pretty quickly, how about introducing
+;; a macro for the simple cases?
+(define-simple-pcomplete awk "awk --help"
+                         :margin "\t"
+                         :separator "  +"
+                         :description "\0"
+                         :metavar "[=a-z]+")
+(define-simple-pcomplete gpg "gpg --help"  :narrow-end "^ -se")
+(define-simple-pcomplete ls "ls --help")
+(define-simple-pcomplete grep "grep --help")
+
 (defalias 'pcomplete/gdb 'pcomplete/xargs)
 
 ;;; pcmpl-gnu.el ends here
diff --git a/lisp/pcmpl-rpm.el b/lisp/pcmpl-rpm.el
index f7925d9d9e..7c602d0f9b 100644
--- a/lisp/pcmpl-rpm.el
+++ b/lisp/pcmpl-rpm.el
@@ -21,7 +21,8 @@
 
 ;;; Commentary:
 
-;; These functions provide completion rules for the `rpm' command.
+;; These functions provide completion rules for the `rpm' command and
+;; related tools.
 
 ;;; Code:
 
@@ -378,6 +379,47 @@ pcomplete/rpm
        (t
 	(error "You must select a mode: -q, -i, -U, --verify, etc"))))))
 
+;;; DNF
+
+(defvar pcmpl-rpm-dnf-cache-file "/var/cache/dnf/packages.db"
+  "Location of the DNF cache.")
+
+(defun pcmpl-rpm--dnf-packages (status)
+  (when (and (file-exists-p pcmpl-rpm-dnf-cache-file)
+             (executable-find "sqlite3"))
+    (with-temp-message
+        "Getting list of packages..."
+      (process-lines "sqlite3" "-batch" "-init" "/dev/null"
+                     pcmpl-rpm-dnf-cache-file
+                     (pcase-exhaustive status
+                       ('available "select pkg from available")
+                       ('installed "select pkg from installed")
+                       ('not-installed "\
+select pkg from available \
+where pkg not in (select pkg from installed)"))))))
+
+;;;###autoload
+(defun pcomplete/dnf ()
+  "Completion for the `dnf' command."
+  (let ((subcmds (pcomplete-from-help "dnf help"
+                                      :margin "^\\(\\)[a-z-]+  "
+                                      :argument "[a-z-]+")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (completion-table-merge
+                       subcmds
+                       (pcomplete-from-help "dnf help"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (pcomplete-match "\\`-" 0))
+                (pcomplete-here
+                 (pcomplete-from-help `("dnf" "help" ,subcmd))))
+               ((or "downgrade" "reinstall" "remove")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'installed)))
+               ((or "install" "mark" "reinstall" "upgrade")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'not-installed)))
+               ((or "builddep" "changelog" "info" "list" "repoquery" "updateinfo")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'available))))))))
+
 (provide 'pcmpl-rpm)
 
 ;;; pcmpl-rpm.el ends here
diff --git a/lisp/pcmpl-x.el b/lisp/pcmpl-x.el
index 261a3d4e27..78963410ee 100644
--- a/lisp/pcmpl-x.el
+++ b/lisp/pcmpl-x.el
@@ -321,5 +321,22 @@ pcomplete/bcc32
 ;;;###autoload
 (defalias 'pcomplete/bcc 'pcomplete/bcc32)
 
+;;;###autoload
+(defun pcomplete/rclone ()
+  "Completion for the `rclone' command."
+  (let ((subcmds (pcomplete-from-help "rclone help"
+                                      :margin "^  "
+                                      :argument "[a-z]+"
+                                      :narrow-start "\n\n")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (completion-table-merge
+                       subcmds
+                       (pcomplete-from-help "rclone help flags"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (if (pcomplete-match "\\`-" 0)
+                 (pcomplete-here (pcomplete-from-help
+                                  `("rclone" ,subcmd "--help")))
+               (pcomplete-here (pcomplete-entries)))))))
+
 (provide 'pcmpl-x)
 ;;; pcmpl-x.el ends here
diff --git a/lisp/pcomplete.el b/lisp/pcomplete.el
index 15b9880df8..3bf0e2be2c 100644
--- a/lisp/pcomplete.el
+++ b/lisp/pcomplete.el
@@ -119,6 +119,9 @@
 ;;; Code:
 
 (require 'comint)
+(eval-when-compile
+  (require 'cl-lib)
+  (require 'rx))
 
 (defgroup pcomplete nil
   "Programmable completion."
@@ -485,6 +488,14 @@ pcomplete-completions-at-point
           (when completion-ignore-case
             (setq table (completion-table-case-fold table)))
           (list beg (point) table
+                :annotation-function
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-annotation cand)))
+                :company-docsig
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-help cand)))
                 :predicate pred
                 :exit-function
 		;; If completion is finished, add a terminating space.
@@ -1332,6 +1343,137 @@ pcomplete-read-host-names
       (pcomplete-read-hosts pcomplete-hosts-file 'pcomplete--host-name-cache
                    'pcomplete--host-name-cache-timestamp)))
 
+;;; Parsing help messages
+
+(defvar pcomplete-from-help (make-hash-table :test #'equal)
+  "Memoization table for function `pcomplete-from-help'.")
+
+(cl-defun pcomplete-from-help (command
+                               &rest args
+                               &key
+                               (margin (rx bol (+ " ")))
+                               (argument (rx "-" (+ (any "-" alnum)) (? "=")))
+                               (metavar (rx (? " ")
+                                            (or (+ (any alnum "_-"))
+                                                (seq "[" (+? nonl) "]")
+                                                (seq "<" (+? nonl) ">")
+                                                (seq "{" (+? nonl) "}"))))
+                               (separator (rx ", " symbol-start))
+                               (description (rx (* nonl)
+                                                (* "\n" (>= 9 " ") (* nonl))))
+                               narrow-start
+                               narrow-end)
+  "Parse output of COMMAND into a list of completion candidates.
+
+COMMAND can be a list (program name and arguments) or a string to
+be executed in a shell.  It should print a help message.
+
+A list of arguments is collected after each match of MARGIN.
+Each argument should match ARGUMENT, possibly followed by a match
+of METAVAR.  If a match of SEPARATOR follows, then more
+argument-metavar pairs are collected.  Finally, a match of
+DESCRIPTION is collected.
+
+Keyword ARGS:
+
+MARGIN: regular expression after which argument descriptions are
+  to be found.  Parsing continues at the end of the first match
+  group, or at the end of the entire match.
+
+ARGUMENT: regular expression matching an argument name.  The
+  first match group (or the entire match if missing) is collected
+  as the argument name.  Parsing continues at the end of the
+  second matching group (or first group, or entire match).
+
+METAVAR: regular expression matching an argument parameter name.
+  The first match group (or the entire match if missing) is
+  collected as the parameter name and used as completion
+  annotation.  Parsing continues at the end of the second
+  matching group (or first group, or entire match).
+
+SEPARATOR: regular expression matching the separator between
+  arguments.  Parsing continues at the end of the first match
+  group, or at the end of the match.
+
+DESCRIPTION: regular expression matching the description of an
+  argument.  The first match group (or the entire match if
+  missing) is collected as the parameter name and used as
+  completion help.  Parsing continues at the end of the first
+  matching group or entire match.
+
+NARROW-START, NARROW-END: if non-nil, parsing of help message is
+  narrowed to the region between the (end of) the first match of
+  these regular expressions."
+  (with-memoization (gethash (cons command args) pcomplete-from-help)
+    (with-temp-buffer
+      (let ((default-directory (expand-file-name "~/"))
+            (command (if (stringp command)
+                         (list shell-file-name
+                               shell-command-switch
+                               command)
+                       command))
+            i result)
+        (apply #'call-process (car command) nil t nil (cdr command))
+        (goto-char (point-min))
+        (narrow-to-region (or (and narrow-start
+                                   (re-search-forward narrow-start nil t)
+                                   (or (match-beginning 1) (match-beginning 0)))
+                              (point-min))
+                          (or (and narrow-end
+                                   (re-search-forward narrow-end nil t)
+                                   (or (match-beginning 1) (match-beginning 0)))
+                              (point-max)))
+        (goto-char (point-min))
+        (while (re-search-forward margin nil t)
+          (goto-char (or (match-end 1) (match-end 0)))
+          (setq i 0)
+          (while (and (or (zerop i)
+                          (and (looking-at separator)
+                               (goto-char (or (match-end 1)
+                                              (match-end 0)))))
+                      (looking-at argument))
+            (setq i (1+ i))
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (push (or (match-string 1) (match-string 0)) result)
+            (when (looking-at metavar)
+              (goto-char (seq-some #'match-end '(2 1 0)))
+              (put-text-property 0 1
+                                 'pcomplete-annotation
+                                 (or (match-string 1) (match-string 0))
+                                 (car result))))
+          (when (looking-at description)
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (let ((help (string-clean-whitespace
+                         (or (match-string 1) (match-string 0))))
+                  (items (take i result)))
+              (while items
+                (put-text-property 0 1 'pcomplete-help help
+                                   (pop items))))))
+        (nreverse result)))))
+
+(defun pcomplete--simple-command (command args)
+  "Helper function for `define-simple-pcomplete'."
+  (while (pcomplete-here
+          (completion-table-merge
+           (apply #'pcomplete-from-help command args)
+           (pcomplete-entries)))))
+
+;; What do you think of a macro like this?
+(defmacro define-simple-pcomplete (name command &rest args)
+  "Create `pcomplete' completions for a simple command.
+COMMAND and ARGS are as in `pcomplete-from-help'.  Completion
+candidates for this command will include the parsed arguments as
+well as files."
+  (let* ((namestr (symbol-name name))
+         (docstring (if-let ((i (string-search "/" namestr)))
+                       (format "Completions for the `%s' command in `%s'."
+                               (substring namestr 0 i)
+                               (substring namestr i))
+                     (format "Completions for the `%s' command." namestr))))
+    `(defun ,(intern (concat "pcomplete/" namestr)) ()
+       ,docstring
+       (pcomplete--simple-command ,command ',args))))
+
 (provide 'pcomplete)
 
 ;;; pcomplete.el ends here
diff --git a/test/lisp/pcomplete-tests.el b/test/lisp/pcomplete-tests.el
new file mode 100644
index 0000000000..00a82502f3
--- /dev/null
+++ b/test/lisp/pcomplete-tests.el
@@ -0,0 +1,100 @@
+;;; pcomplete-tests.el --- Tests for pcomplete.el  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'ert)
+(require 'pcomplete)
+
+(ert-deftest pcomplete-test-parse-gpg-help ()
+  (cl-letf ((pcomplete-from-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process)
+             (lambda (&rest _) (insert "\
+gpg (GnuPG) 2.3.7
+
+Commands:
+
+ -s, --sign                         make a signature
+     --clear-sign                   make a clear text signature
+ -b, --detach-sign                  make a detached signature
+     --tofu-policy VALUE            set the TOFU policy for a key
+
+Options to specify keys:
+ -r, --recipient USER-ID            encrypt for USER-ID
+ -u, --local-user USER-ID           use USER-ID to sign or decrypt
+
+(See the man page for a complete listing of all commands and options)
+
+Examples:
+
+ -se -r Bob [file]          sign and encrypt for user Bob
+ --clear-sign [file]        make a clear text signature
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-from-help "gpg --help" :narrow-end "^ -se")
+      '(#("-s" 0 1 (pcomplete-help "make a signature"))
+        #("--sign" 0 1 (pcomplete-help "make a signature"))
+        #("--clear-sign" 0 1 (pcomplete-help "make a clear text signature"))
+        #("-b" 0 1 (pcomplete-help "make a detached signature"))
+        #("--detach-sign" 0 1 (pcomplete-help "make a detached signature"))
+        #("--tofu-policy" 0 1
+          (pcomplete-help "set the TOFU policy for a key" pcomplete-annotation " VALUE"))
+        #("-r" 0 1 (pcomplete-help "encrypt for USER-ID"))
+        #("--recipient" 0 1
+          (pcomplete-help "encrypt for USER-ID" pcomplete-annotation " USER-ID"))
+        #("-u" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt"))
+        #("--local-user" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt" pcomplete-annotation " USER-ID")))))))
+
+(ert-deftest pcomplete-test-parse-git-help ()
+  (cl-letf ((pcomplete-from-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process)
+             (lambda (&rest _) (insert "\
+usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
+           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
+           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
+           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
+           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
+           <command> [<args>]
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-from-help "git help"
+                           :margin "\\(\\[\\)-"
+                           :separator " | "
+                           :description "\\`")
+      '("-v" "--version" "-h" "--help"
+        #("-C" 0 1 (pcomplete-annotation " <path>"))
+        #("-c" 0 1 (pcomplete-annotation " <name>"))
+        #("--exec-path" 0 1 (pcomplete-annotation "[=<path>]"))
+        "--html-path" "--man-path" "--info-path"
+        "-p" "--paginate" "-P" "--no-pager"
+        "--no-replace-objects" "--bare"
+        #("--git-dir=" 0 1 (pcomplete-annotation "<path>"))
+        #("--work-tree=" 0 1 (pcomplete-annotation "<path>"))
+        #("--namespace=" 0 1 (pcomplete-annotation "<name>"))
+        #("--super-prefix=" 0 1 (pcomplete-annotation "<path>"))
+        #("--config-env=" 0 1 (pcomplete-annotation "<name>")))))))
+
+(provide 'pcomplete-tests)
+;;; pcomplete-tests.el ends here
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-10  9:45       ` Augusto Stoffel
@ 2022-09-10 14:32         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-10 16:12           ` Augusto Stoffel
  2022-09-14 19:15           ` Augusto Stoffel
  0 siblings, 2 replies; 21+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2022-09-10 14:32 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: Lars Ingebrigtsen, 57673

> I've attached a new iteration of the patch.  I think the git completion
> should be pretty usable (but certainly can be refined in the future).
> I'm also satisfied with the parser, please have a look if you are
> interested.

LGTM, feel free to push it to `master`.

> Next I'd like to think now of a good way to add batches of simpler
> commands, say all GNU coreutils.  This would entail repeating variations
> of
>
>    (defun pcomplete/gpg ()
>      "Completion for the GNU Privacy Guard."
>      (while (if (pcomplete-match "\\`-" 0)
>                 (pcomplete-here (pcomplete-from-help "gpg --help"
>                                                      :narrow-end "^ -se"))
>               (pcomplete-here (pcomplete-entries)))))
>
> over and over, so I was wondering if it makes sense to add a macro to
> help here.  See a suggestion at the end of pcomplete.el.

I do think it makes sense, but I think it doesn't need to be a macro:

> +;; What do you think of a macro like this?
> +(defmacro define-simple-pcomplete (name command &rest args)
> +  "Create `pcomplete' completions for a simple command.
> +COMMAND and ARGS are as in `pcomplete-from-help'.  Completion
> +candidates for this command will include the parsed arguments as
> +well as files."
> +  (let* ((namestr (symbol-name name))
> +         (docstring (if-let ((i (string-search "/" namestr)))
> +                       (format "Completions for the `%s' command in `%s'."
> +                               (substring namestr 0 i)
> +                               (substring namestr i))
> +                     (format "Completions for the `%s' command." namestr))))
> +    `(defun ,(intern (concat "pcomplete/" namestr)) ()
> +       ,docstring
> +       (pcomplete--simple-command ,command ',args))))

    (defun define-simple-pcomplete (name command &rest args)
      "Create `pcomplete' completions for a simple command.
    COMMAND and ARGS are as in `pcomplete-from-help'.  Completion
    candidates for this command will include the parsed arguments as
    well as files."
      (let* ((namestr (symbol-name name))
             (docstring (if-let ((i (string-search "/" namestr)))
                           (format "Completions for the `%s' command in `%s'."
                                   (substring namestr 0 i)
                                   (substring namestr i))
                         (format "Completions for the `%s' command." namestr))))
        (defalias (intern (concat "pcomplete/" namestr))
          (lambda ()
           (:documentation docstring)
           (pcomplete--simple-command command args)))))

Also, we may end up with various "simple" solutions, so I'd use a name
that is more explicit about how it works.  E.g. `pcomplete-via-help`?


        Stefan






^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-10 14:32         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2022-09-10 16:12           ` Augusto Stoffel
  2022-09-14 19:15           ` Augusto Stoffel
  1 sibling, 0 replies; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-10 16:12 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: Lars Ingebrigtsen, 57673

On Sat, 10 Sep 2022 at 10:32, Stefan Monnier <monnier@iro.umontreal.ca> wrote:

>> I've attached a new iteration of the patch.  I think the git completion
>> should be pretty usable (but certainly can be refined in the future).
>> I'm also satisfied with the parser, please have a look if you are
>> interested.
>
> LGTM, feel free to push it to `master`.

Sounds good, I'll send a mergeable patch in due time.

>> Next I'd like to think now of a good way to add batches of simpler
>> commands, say all GNU coreutils.  This would entail repeating variations
>> of
>>
>>    (defun pcomplete/gpg ()
>>      "Completion for the GNU Privacy Guard."
>>      (while (if (pcomplete-match "\\`-" 0)
>>                 (pcomplete-here (pcomplete-from-help "gpg --help"
>>                                                      :narrow-end "^ -se"))
>>               (pcomplete-here (pcomplete-entries)))))
>>
>> over and over, so I was wondering if it makes sense to add a macro to
>> help here.  See a suggestion at the end of pcomplete.el.
>
> I do think it makes sense, but I think it doesn't need to be a macro:

Yes, the function is wholly sufficient, except that I don't know how to
make the definitions autoloadable.

I guess there's no performance reasons to autoload these little
definitions, but we would need autoloading to keep the grouping of
commands into the different pcmpl-*.el libraries.

So how to I teach the autoload mechanism to do whatever it needs to do
every time it sees a (define-simple-pcomplete ...) form?

>> +;; What do you think of a macro like this?
>> +(defmacro define-simple-pcomplete (name command &rest args)
>> +  "Create `pcomplete' completions for a simple command.
>> +COMMAND and ARGS are as in `pcomplete-from-help'.  Completion
>> +candidates for this command will include the parsed arguments as
>> +well as files."
>> +  (let* ((namestr (symbol-name name))
>> +         (docstring (if-let ((i (string-search "/" namestr)))
>> +                       (format "Completions for the `%s' command in `%s'."
>> +                               (substring namestr 0 i)
>> +                               (substring namestr i))
>> +                     (format "Completions for the `%s' command." namestr))))
>> +    `(defun ,(intern (concat "pcomplete/" namestr)) ()
>> +       ,docstring
>> +       (pcomplete--simple-command ,command ',args))))
>
>     (defun define-simple-pcomplete (name command &rest args)
>       "Create `pcomplete' completions for a simple command.
>     COMMAND and ARGS are as in `pcomplete-from-help'.  Completion
>     candidates for this command will include the parsed arguments as
>     well as files."
>       (let* ((namestr (symbol-name name))
>              (docstring (if-let ((i (string-search "/" namestr)))
>                            (format "Completions for the `%s' command in `%s'."
>                                    (substring namestr 0 i)
>                                    (substring namestr i))
>                          (format "Completions for the `%s' command." namestr))))
>         (defalias (intern (concat "pcomplete/" namestr))
>           (lambda ()
>            (:documentation docstring)
>            (pcomplete--simple-command command args)))))
>
> Also, we may end up with various "simple" solutions, so I'd use a name
> that is more explicit about how it works.  E.g. `pcomplete-via-help`?
>
>
>         Stefan





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-10 14:32         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-10 16:12           ` Augusto Stoffel
@ 2022-09-14 19:15           ` Augusto Stoffel
  2022-09-14 19:21             ` Lars Ingebrigtsen
  1 sibling, 1 reply; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-14 19:15 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: Lars Ingebrigtsen, 57673

[-- Attachment #1: Type: text/plain, Size: 752 bytes --]

On Sat, 10 Sep 2022 at 10:32, Stefan Monnier wrote:

>> I've attached a new iteration of the patch.  I think the git completion
>> should be pretty usable (but certainly can be refined in the future).
>> I'm also satisfied with the parser, please have a look if you are
>> interested.
>
> LGTM, feel free to push it to `master`.

I've attached a new version which I consider good to merge (I don't have
direct access to the repo).  I've also included a tiny fix to Emacs's
help message.

The patch includes completion for a variety of commands (in part to test
the flexibility of the parser), including most commands from coreutils,
so it has become a bit bulky.  Given the large number of new functions,
I hope the commit message is detailed enough.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-src-emacs.c-usage_message-Remove-stray-tabs.patch --]
[-- Type: text/x-patch, Size: 831 bytes --]

From 2a315f77dae982ccf07a2686ac91c5e88539f68b Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <arstoffel@gmail.com>
Date: Wed, 14 Sep 2022 20:11:28 +0200
Subject: [PATCH 1/2] ; * src/emacs.c (usage_message): Remove stray tabs.

---
 src/emacs.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/emacs.c b/src/emacs.c
index 3c76841281..91bf0a9b59 100644
--- a/src/emacs.c
+++ b/src/emacs.c
@@ -299,7 +299,7 @@ #define MAIN_PROGRAM
 -x                          to be used in #!/usr/bin/emacs -x\n\
                               and has approximately the same meaning\n\
 			      as -Q --script\n\
---terminal, -t DEVICE       use DEVICE for terminal I/O\n		\
+--terminal, -t DEVICE       use DEVICE for terminal I/O\n\
 --user, -u USER             load ~USER/.emacs instead of your own\n\
 \n\
 ",
-- 
2.37.3


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-pcomplete-Generate-completions-from-help-messages.patch --]
[-- Type: text/x-patch, Size: 45395 bytes --]

From 0fbcad2fa00c7b0d2d0f14bccc089af4bba19955 Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <arstoffel@gmail.com>
Date: Thu, 8 Sep 2022 11:09:42 +0200
Subject: [PATCH 2/2] pcomplete: Generate completions from --help messages

* lisp/pcomplete.el (pcomplete-from-help): New function (and hash
table) to get pcomplete candidates from help messages.
(pcomplete-here-using-help): Helper function to define pcomplete for
simple commands
(pcomplete-completions-at-point): Provide annotation-function and
company-docsig properties.
* lisp/pcmpl-git.el: New file, provides pcomplete for Git.
* lisp/pcmpl-gnu.el: Add pcomplete for awk, gpg and gdb, emacs and
emacsclient.
* lisp/pcmpl-linux.el: Add pcomplete for systemctl and journalctl.
* lisp/pcmpl-rpm.el: Add pcomplete for dnf.
* lisp/pcmpl-unix.el: Add pcomplete for sudo and most commands found
in GNU Coreutils.
* lisp/pcmpl-x.el: Add pcomplete for tex, pdftex, latex, pdflatex,
rigrep and rclone.
* test/lisp/pcomplete-tests.el (pcomplete-test-parse-gpg-help,
pcomplete-test-parse-git-help): Tests for the new functions.
---
 lisp/pcmpl-git.el            | 110 ++++++++
 lisp/pcmpl-gnu.el            |  36 ++-
 lisp/pcmpl-linux.el          |  62 +++++
 lisp/pcmpl-rpm.el            |  43 ++-
 lisp/pcmpl-unix.el           | 490 +++++++++++++++++++++++++++++++++--
 lisp/pcmpl-x.el              |  43 +++
 lisp/pcomplete.el            | 138 ++++++++++
 test/lisp/pcomplete-tests.el | 100 +++++++
 8 files changed, 998 insertions(+), 24 deletions(-)
 create mode 100644 lisp/pcmpl-git.el
 create mode 100644 test/lisp/pcomplete-tests.el

diff --git a/lisp/pcmpl-git.el b/lisp/pcmpl-git.el
new file mode 100644
index 0000000000..3584fa0673
--- /dev/null
+++ b/lisp/pcmpl-git.el
@@ -0,0 +1,110 @@
+;;; pcmpl-git.el --- Completions for Git -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; Package: pcomplete
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides completion rules for the Git program.
+
+;;; Code:
+
+(require 'pcomplete)
+(require 'vc-git)
+
+(defun pcmpl-git--expand-flags (args)
+  "In the list of ARGS, expand arguments of the form --[no-]flag."
+  (mapcan (lambda (arg) (if (string-search "[no-]" arg)
+                            (list (string-replace "[no-]" "" arg)
+                                  (string-replace "[no-]" "no-" arg))
+                          (list arg)))
+          args))
+
+(defun pcmpl-git--tracked-file-predicate (&rest args)
+  "Return a predicate function determining the Git status of a file.
+Files listed by `git ls-files ARGS' satisfy the predicate."
+  (when-let ((files (mapcar #'expand-file-name
+                            (ignore-errors
+                              (apply #'process-lines
+                                     vc-git-program "ls-files" args)))))
+    (lambda (file)
+      (setq file (expand-file-name file))
+      (if (string-suffix-p "/" file)
+          (seq-some (lambda (f) (string-prefix-p file f))
+                    files)
+        (member file files)))))
+
+(defun pcmpl-git--remote-refs (remote)
+  "List the locally known Git revisions from REMOTE."
+  (delq nil
+        (mapcar
+         (let ((re (concat "\\`" (regexp-quote remote) "/\\(.*\\)")))
+           (lambda (s) (when (string-match re s) (match-string 1 s))))
+         (vc-git-revision-table nil))))
+
+;;;###autoload
+(defun pcomplete/git ()
+  "Completion for the `git' command."
+  (let ((subcommands (pcomplete-from-help `(,vc-git-program "help" "-a")
+                                          :margin "^\\( +\\)[a-z]"
+                                          :argument "[[:alnum:]-]+")))
+    (while (not (member (pcomplete-arg 1) subcommands))
+      (if (string-prefix-p "-" (pcomplete-arg))
+          (pcomplete-here (pcomplete-from-help `(,vc-git-program "help")
+                                               :margin "\\(\\[\\)-"
+                                               :separator " | "
+                                               :description "\\`"))
+        (pcomplete-here (completion-table-merge
+                         subcommands
+                         (when (string-prefix-p "-" (pcomplete-arg 1))
+                           (pcomplete-entries))))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (string-prefix-p "-" (pcomplete-arg)))
+                (pcomplete-here
+                 (pcmpl-git--expand-flags
+                  (pcomplete-from-help `(,vc-git-program "help" ,subcmd)
+                                       :argument
+                                       "-+\\(?:\\[no-\\]\\)?[a-z-]+=?"))))
+               ;; Complete modified tracked files
+               ((or "add" "commit" "restore")
+                (pcomplete-here
+                 (pcomplete-entries
+                  nil (pcmpl-git--tracked-file-predicate "-m"))))
+               ;; Complete all tracked files
+               ((or "mv" "rm" "grep" "status")
+                (pcomplete-here
+                 (pcomplete-entries nil (pcmpl-git--tracked-file-predicate))))
+               ;; Complete revisions
+               ((or "branch" "merge" "rebase" "switch")
+                (pcomplete-here (vc-git-revision-table nil)))
+               ;; Complete revisions and tracked files
+               ;; TODO: diff and log accept revision ranges
+               ((or "checkout" "reset" "show" "diff" "log")
+                (pcomplete-here
+                 (completion-table-in-turn
+                  (vc-git-revision-table nil)
+                  (pcomplete-entries nil (pcmpl-git--tracked-file-predicate)))))
+               ;; Complete remotes and their revisions
+               ((or "fetch" "pull" "push")
+                (pcomplete-here (process-lines vc-git-program "remote"))
+                (pcomplete-here (pcmpl-git--remote-refs (pcomplete-arg 1)))))))))
+
+(provide 'pcmpl-git)
+;;; pcmpl-git.el ends here
diff --git a/lisp/pcmpl-gnu.el b/lisp/pcmpl-gnu.el
index 3c9bf1ec9d..cdfde5640a 100644
--- a/lisp/pcmpl-gnu.el
+++ b/lisp/pcmpl-gnu.el
@@ -394,6 +394,40 @@ pcomplete/find
     (while (pcomplete-here (pcomplete-dirs) nil #'identity))))
 
 ;;;###autoload
-(defalias 'pcomplete/gdb 'pcomplete/xargs)
+(defun pcomplete/awk ()
+  "Completion for the `awk' command."
+  (pcomplete-here-using-help "awk --help"
+                             :margin "\t"
+                             :separator "  +"
+                             :description "\0"
+                             :metavar "[=a-z]+"))
+
+;;;###autoload
+(defun pcomplete/gpg ()
+  "Completion for the `gpg` command."
+  (pcomplete-here-using-help "gpg --help" :narrow-end "^ -se"))
+
+;;;###autoload
+(defun pcomplete/gdb ()
+  "Completion for the `gdb' command."
+  (while
+      (cond
+       ((string= "--args" (pcomplete-arg 1))
+        (funcall pcomplete-command-completion-function)
+        (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	             pcomplete-default-completion-function)))
+       ((string-prefix-p "-" (pcomplete-arg 0))
+        (pcomplete-here (pcomplete-from-help "gdb --help")))
+       (t (pcomplete-here (pcomplete-entries))))))
+
+;;;###autoload
+(defun pcomplete/emacs ()
+  "Completion for the `emacs' command."
+  (pcomplete-here-using-help "emacs --help" :margin "^\\(\\)-"))
+
+;;;###autoload
+(defun pcomplete/emacsclient ()
+  "Completion for the `emacsclient' command."
+  (pcomplete-here-using-help "emacsclient --help" :margin "^\\(\\)-"))
 
 ;;; pcmpl-gnu.el ends here
diff --git a/lisp/pcmpl-linux.el b/lisp/pcmpl-linux.el
index 7c072f3d40..a2195c1e88 100644
--- a/lisp/pcmpl-linux.el
+++ b/lisp/pcmpl-linux.el
@@ -30,6 +30,7 @@
 (provide 'pcmpl-linux)
 
 (require 'pcomplete)
+(eval-when-compile (require 'rx))
 
 ;; Functions:
 
@@ -111,4 +112,65 @@ pcmpl-linux-mountable-directories
        (pcomplete-uniquify-list points)
        (cons "swap" (pcmpl-linux-mounted-directories))))))
 
+;;; systemd
+
+(defun pcmpl-linux--systemd-units (&rest args)
+  "Run `systemd list-units ARGS' and return the output as a list."
+  (with-temp-buffer
+    (apply #'call-process
+           "systemctl" nil '(t nil) nil
+           "list-units" "--full" "--legend=no" "--plain" args)
+    (goto-char (point-min))
+    (let (result)
+      (while (re-search-forward (rx bol (group (+ (not space)))
+                                    (+ space) (+ (not space))
+                                    (+ space) (group (+ (not space)))
+                                    (+ space) (+ (not space))
+                                    (+ space) (group (* nonl)))
+                                nil t)
+        (push (match-string 1) result)
+        (put-text-property 0 1 'pcomplete-annotation
+                           (concat " " (match-string 2))
+                           (car result))
+        (put-text-property 0 1 'pcomplete-description
+                           (match-string 3)
+                           (car result)))
+      (nreverse result))))
+
+;;;###autoload
+(defun pcomplete/systemctl ()
+  "Completion for the `systemctl' command."
+  (let ((subcmds (pcomplete-from-help
+                  "systemctl --help"
+                  :margin (rx bol "  " (group) alpha)
+                  :argument (rx (+ (any alpha ?-)))
+                  :metavar (rx (group (+ " " (>= 2 (any upper "[]|."))))))))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (if (string-prefix-p "-" (pcomplete-arg 0))
+          (pcomplete-here (pcomplete-from-help "systemctl --help"
+                                               :metavar "[^ ]+"
+                                               :separator " \\(\\)-"))
+        (pcomplete-here subcmds)))
+    (let ((subcmd (pcomplete-arg 1))
+          (context (if (member "--user" pcomplete-args) "--user" "--system")))
+      (while (pcase subcmd
+               ((guard (string-prefix-p "-" (pcomplete-arg 0)))
+                (pcomplete-here
+                 (pcomplete-from-help "systemctl --help")))
+               ;; TODO: suggest only relevant units to each subcommand
+               (_ (pcomplete-here
+                   (completion-table-in-turn
+                    (pcmpl-linux--systemd-units context "--all")
+                    (pcomplete-entries)))))))))
+
+;;;###autoload
+(defun pcomplete/journalctl ()
+  "Completion for the `journalctl' command."
+  (while (if (string-prefix-p "-" (pcomplete-arg 0))
+             (pcomplete-here (pcomplete-from-help "journalctl --help"
+                                                  :metavar "[^ ]+"
+                                                  :separator " \\(\\)-"))
+           (pcomplete-here (mapcar (lambda (s) (concat s "="))
+                                   (process-lines "journalctl" "--fields"))))))
+
 ;;; pcmpl-linux.el ends here
diff --git a/lisp/pcmpl-rpm.el b/lisp/pcmpl-rpm.el
index f7925d9d9e..ebb6b72600 100644
--- a/lisp/pcmpl-rpm.el
+++ b/lisp/pcmpl-rpm.el
@@ -21,7 +21,8 @@
 
 ;;; Commentary:
 
-;; These functions provide completion rules for the `rpm' command.
+;; These functions provide completion rules for the `rpm' command and
+;; related tools.
 
 ;;; Code:
 
@@ -378,6 +379,46 @@ pcomplete/rpm
        (t
 	(error "You must select a mode: -q, -i, -U, --verify, etc"))))))
 
+;;; DNF
+
+(defvar pcmpl-rpm-dnf-cache-file "/var/cache/dnf/packages.db"
+  "Location of the DNF cache.")
+
+(defun pcmpl-rpm--dnf-packages (status)
+  (when (and (file-exists-p pcmpl-rpm-dnf-cache-file)
+             (executable-find "sqlite3"))
+    (with-temp-message
+        "Getting list of packages..."
+      (process-lines "sqlite3" "-batch" "-init" "/dev/null"
+                     pcmpl-rpm-dnf-cache-file
+                     (pcase-exhaustive status
+                       ('available "select pkg from available")
+                       ('installed "select pkg from installed")
+                       ('not-installed "\
+select pkg from available where pkg not in (select pkg from installed)"))))))
+
+;;;###autoload
+(defun pcomplete/dnf ()
+  "Completion for the `dnf' command."
+  (let ((subcmds (pcomplete-from-help "dnf help"
+                                      :margin "^\\(\\)[a-z-]+  "
+                                      :argument "[a-z-]+")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (completion-table-merge
+                       subcmds
+                       (pcomplete-from-help "dnf help"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (pcomplete-match "\\`-" 0))
+                (pcomplete-here
+                 (pcomplete-from-help `("dnf" "help" ,subcmd))))
+               ((or "downgrade" "reinstall" "remove")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'installed)))
+               ((or "install" "mark" "reinstall" "upgrade")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'not-installed)))
+               ((or "builddep" "changelog" "info" "list" "repoquery" "updateinfo")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'available))))))))
+
 (provide 'pcmpl-rpm)
 
 ;;; pcmpl-rpm.el ends here
diff --git a/lisp/pcmpl-unix.el b/lisp/pcmpl-unix.el
index 8774f091c8..0c32f814d0 100644
--- a/lisp/pcmpl-unix.el
+++ b/lisp/pcmpl-unix.el
@@ -25,7 +25,7 @@
 
 (require 'pcomplete)
 
-;; User Variables:
+;;; User Variables
 
 (defcustom pcmpl-unix-group-file "/etc/group"
   "If non-nil, a string naming the group file on your system."
@@ -56,7 +56,7 @@ pcmpl-ssh-config-file
   :group 'pcmpl-unix
   :version "24.1")
 
-;; Functions:
+;;; Shell builtins and core utilities
 
 ;;;###autoload
 (defun pcomplete/cd ()
@@ -69,34 +69,38 @@ 'pcomplete/pushd
 ;;;###autoload
 (defun pcomplete/rmdir ()
   "Completion for `rmdir'."
-  (while (pcomplete-here (pcomplete-dirs))))
+  (while (if (string-prefix-p "-" (pcomplete-arg))
+             (pcomplete-here (pcomplete-from-help "rmdir --help"))
+           (pcomplete-here (pcomplete-dirs)))))
 
 ;;;###autoload
 (defun pcomplete/rm ()
-  "Completion for `rm'."
-  (let ((pcomplete-help "(fileutils)rm invocation"))
-    (pcomplete-opt "dfirRv")
-    (while (pcomplete-here (pcomplete-all-entries) nil
-			   #'expand-file-name))))
+  "Completion for the `rm' command."
+  (pcomplete-here-using-help "rm --help"))
 
 ;;;###autoload
 (defun pcomplete/xargs ()
   "Completion for `xargs'."
   (while (string-prefix-p "-" (pcomplete-arg 0))
-    (pcomplete-here (funcall pcomplete-default-completion-function)))
+    (pcomplete-here (pcomplete-from-help "xargs --help"))
+    (when (pcomplete-match "\\`-[adEIiLnPs]\\'") (pcomplete-here)))
   (funcall pcomplete-command-completion-function)
   (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
 	       pcomplete-default-completion-function)))
 
-;; FIXME: Add completion of sudo-specific arguments.
-(defalias 'pcomplete/sudo #'pcomplete/xargs)
-
 ;;;###autoload
-(defalias 'pcomplete/time 'pcomplete/xargs)
+(defun pcomplete/time ()
+  "Completion for the `time' command."
+  (pcomplete-opt "p")
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
 
 ;;;###autoload
 (defun pcomplete/which ()
   "Completion for `which'."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "which --help")))
   (while (pcomplete-here (funcall pcomplete-command-completion-function))))
 
 (defun pcmpl-unix-read-passwd-file (file)
@@ -128,25 +132,455 @@ pcmpl-unix-user-names
   (if pcmpl-unix-passwd-file
       (pcmpl-unix-read-passwd-file pcmpl-unix-passwd-file)))
 
+;;;###autoload
+(defun pcomplete/cat ()
+  "Completion for the `cat' command."
+  (pcomplete-here-using-help "cat --help"))
+
+;;;###autoload
+(defun pcomplete/tac ()
+  "Completion for the `tac' command."
+  (pcomplete-here-using-help "tac --help"))
+
+;;;###autoload
+(defun pcomplete/nl ()
+  "Completion for the `nl' command."
+  (pcomplete-here-using-help "nl --help"))
+
+;;;###autoload
+(defun pcomplete/od ()
+  "Completion for the `od' command."
+  (pcomplete-here-using-help "od --help"))
+
+;;;###autoload
+(defun pcomplete/base32 ()
+  "Completion for the `base32' and `base64' commands."
+  (pcomplete-here-using-help "base32 --help"))
+;;;###autoload
+(defalias 'pcomplete/base64 'pcomplete/base32)
+
+;;;###autoload
+(defun pcomplete/basenc ()
+  "Completion for the `basenc' command."
+  (pcomplete-here-using-help "basenc --help"))
+
+;;;###autoload
+(defun pcomplete/fmt ()
+  "Completion for the `fmt' command."
+  (pcomplete-here-using-help "fmt --help"))
+
+;;;###autoload
+(defun pcomplete/pr ()
+  "Completion for the `pr' command."
+  (pcomplete-here-using-help "pr --help"))
+
+;;;###autoload
+(defun pcomplete/fold ()
+  "Completion for the `fold' command."
+  (pcomplete-here-using-help "fold --help"))
+
+;;;###autoload
+(defun pcomplete/head ()
+  "Completion for the `head' command."
+  (pcomplete-here-using-help "head --help"))
+
+;;;###autoload
+(defun pcomplete/tail ()
+  "Completion for the `tail' command."
+  (pcomplete-here-using-help "tail --help"))
+
+;;;###autoload
+(defun pcomplete/split ()
+  "Completion for the `split' command."
+  (pcomplete-here-using-help "split --help"))
+
+;;;###autoload
+(defun pcomplete/csplit ()
+  "Completion for the `csplit' command."
+  (pcomplete-here-using-help "csplit --help"))
+
+;;;###autoload
+(defun pcomplete/wc ()
+  "Completion for the `wc' command."
+  (pcomplete-here-using-help "wc --help"))
+
+;;;###autoload
+(defun pcomplete/sum ()
+  "Completion for the `sum' command."
+  (pcomplete-here-using-help "sum --help"))
+
+;;;###autoload
+(defun pcomplete/cksum ()
+  "Completion for the `cksum' command."
+  (pcomplete-here-using-help "cksum --help"))
+
+;;;###autoload
+(defun pcomplete/b2sum ()
+  "Completion for the `b2sum' command."
+  (pcomplete-here-using-help "b2sum --help"))
+
+;;;###autoload
+(defun pcomplete/md5sum ()
+  "Completion for checksum commands."
+  (pcomplete-here-using-help "md5sum --help"))
+;;;###autoload(defalias 'pcomplete/sha1sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha224sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha256sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha384sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha521sum 'pcomplete/md5sum)
+
+;;;###autoload
+(defun pcomplete/sort ()
+  "Completion for the `sort' command."
+  (pcomplete-here-using-help "sort --help"))
+
+;;;###autoload
+(defun pcomplete/shuf ()
+  "Completion for the `shuf' command."
+  (pcomplete-here-using-help "shuf --help"))
+
+;;;###autoload
+(defun pcomplete/uniq ()
+  "Completion for the `uniq' command."
+  (pcomplete-here-using-help "uniq --help"))
+
+;;;###autoload
+(defun pcomplete/comm ()
+  "Completion for the `comm' command."
+  (pcomplete-here-using-help "comm --help"))
+
+;;;###autoload
+(defun pcomplete/ptx ()
+  "Completion for the `ptx' command."
+  (pcomplete-here-using-help "ptx --help"))
+
+;;;###autoload
+(defun pcomplete/tsort ()
+  "Completion for the `tsort' command."
+  (pcomplete-here-using-help "tsort --help"))
+
+;;;###autoload
+(defun pcomplete/cut ()
+  "Completion for the `cut' command."
+  (pcomplete-here-using-help "cut --help"))
+
+;;;###autoload
+(defun pcomplete/paste ()
+  "Completion for the `paste' command."
+  (pcomplete-here-using-help "paste --help"))
+
+;;;###autoload
+(defun pcomplete/join ()
+  "Completion for the `join' command."
+  (pcomplete-here-using-help "join --help"))
+
+;;;###autoload
+(defun pcomplete/tr ()
+  "Completion for the `tr' command."
+  (pcomplete-here-using-help "tr --help"))
+
+;;;###autoload
+(defun pcomplete/expand ()
+  "Completion for the `expand' command."
+  (pcomplete-here-using-help "expand --help"))
+
+;;;###autoload
+(defun pcomplete/unexpand ()
+  "Completion for the `unexpand' command."
+  (pcomplete-here-using-help "unexpand --help"))
+
+;;;###autoload
+(defun pcomplete/ls ()
+  "Completion for the `ls' command."
+  (pcomplete-here-using-help "ls --help"))
+;;;###autoload(defalias 'pcomplete/dir 'pcomplete/ls)
+;;;###autoload(defalias 'pcomplete/vdir 'pcomplete/ls)
+
+;;;###autoload
+(defun pcomplete/cp ()
+  "Completion for the `cp' command."
+  (pcomplete-here-using-help "cp --help"))
+
+;;;###autoload
+(defun pcomplete/dd ()
+  "Completion for the `dd' command."
+  (let ((operands (pcomplete-from-help "dd --help"
+                                       :argument "[a-z]+="
+                                       :narrow-start "\n\n"
+                                       :narrow-end "\n\n")))
+    (while
+        (cond ((pcomplete-match "\\`[io]f=\\(.*\\)" 0)
+               (pcomplete-here (pcomplete-entries)
+                               (pcomplete-match-string 1 0)))
+              (t (pcomplete-here operands))))))
+
+;;;###autoload
+(defun pcomplete/install ()
+  "Completion for the `install' command."
+  (pcomplete-here-using-help "install --help"))
+
+;;;###autoload
+(defun pcomplete/mv ()
+  "Completion for the `mv' command."
+  (pcomplete-here-using-help "mv --help"))
+
+;;;###autoload
+(defun pcomplete/shred ()
+  "Completion for the `shred' command."
+  (pcomplete-here-using-help "shred --help"))
+
+;;;###autoload
+(defun pcomplete/ln ()
+  "Completion for the `ln' command."
+  (pcomplete-here-using-help "ln --help"))
+
+;;;###autoload
+(defun pcomplete/mkdir ()
+  "Completion for the `mkdir' command."
+  (pcomplete-here-using-help "mkdir --help"))
+
+;;;###autoload
+(defun pcomplete/mkfifo ()
+  "Completion for the `mkfifo' command."
+  (pcomplete-here-using-help "mkfifo --help"))
+
+;;;###autoload
+(defun pcomplete/mknod ()
+  "Completion for the `mknod' command."
+  (pcomplete-here-using-help "mknod --help"))
+
+;;;###autoload
+(defun pcomplete/readlink ()
+  "Completion for the `readlink' command."
+  (pcomplete-here-using-help "readlink --help"))
+
 ;;;###autoload
 (defun pcomplete/chown ()
   "Completion for the `chown' command."
-  (unless (pcomplete-match "\\`-")
-    (if (pcomplete-match "\\`[^.]*\\'" 0)
-	(pcomplete-here* (pcmpl-unix-user-names))
-      (if (pcomplete-match "\\.\\([^.]*\\)\\'" 0)
-	  (pcomplete-here* (pcmpl-unix-group-names)
-			   (pcomplete-match-string 1 0))
-	(pcomplete-here*))))
+  (while (pcomplete-match "\\`-" 0)
+    (pcomplete-here (pcomplete-from-help "chown --help")))
+  (if (pcomplete-match "\\`[^.]*\\'" 0)
+      (pcomplete-here* (pcmpl-unix-user-names))
+    (if (pcomplete-match "\\.\\([^.]*\\)\\'" 0)
+	(pcomplete-here* (pcmpl-unix-group-names)
+			 (pcomplete-match-string 1 0))
+      (pcomplete-here*)))
   (while (pcomplete-here (pcomplete-entries))))
 
 ;;;###autoload
 (defun pcomplete/chgrp ()
   "Completion for the `chgrp' command."
-  (unless (pcomplete-match "\\`-")
-    (pcomplete-here* (pcmpl-unix-group-names)))
+  (while (pcomplete-match "\\`-" 0)
+    (pcomplete-here (pcomplete-from-help "chgrp --help")))
+  (pcomplete-here* (pcmpl-unix-group-names))
   (while (pcomplete-here (pcomplete-entries))))
 
+;;;###autoload
+(defun pcomplete/chmod ()
+  "Completion for the `chmod' command."
+  (pcomplete-here-using-help "chmod --help"))
+
+;;;###autoload
+(defun pcomplete/touch ()
+  "Completion for the `touch' command."
+  (pcomplete-here-using-help "touch --help"))
+
+;;;###autoload
+(defun pcomplete/df ()
+  "Completion for the `df' command."
+  (pcomplete-here-using-help "df --help"))
+
+;;;###autoload
+(defun pcomplete/du ()
+  "Completion for the `du' command."
+  (pcomplete-here-using-help "du --help"))
+
+;;;###autoload
+(defun pcomplete/stat ()
+  "Completion for the `stat' command."
+  (pcomplete-here-using-help "stat --help"))
+
+;;;###autoload
+(defun pcomplete/sync ()
+  "Completion for the `sync' command."
+  (pcomplete-here-using-help "sync --help"))
+
+;;;###autoload
+(defun pcomplete/truncate ()
+  "Completion for the `truncate' command."
+  (pcomplete-here-using-help "truncate --help"))
+
+;;;###autoload
+(defun pcomplete/echo ()
+  "Completion for the `echo' command."
+  (pcomplete-here-using-help '("echo" "--help")))
+
+;;;###autoload
+(defun pcomplete/test ()
+  "Completion for the `test' command."
+  (pcomplete-here-using-help '("[" "--help")
+                             :margin "^ +\\([A-Z]+1 \\)?"))
+;;;###autoload(defalias (intern "pcomplete/[") 'pcomplete/test)
+
+;;;###autoload
+(defun pcomplete/tee ()
+  "Completion for the `tee' command."
+  (pcomplete-here-using-help "tee --help"))
+
+;;;###autoload
+(defun pcomplete/basename ()
+  "Completion for the `basename' command."
+  (pcomplete-here-using-help "basename --help"))
+
+;;;###autoload
+(defun pcomplete/dirname ()
+  "Completion for the `dirname' command."
+  (pcomplete-here-using-help "dirname --help"))
+
+;;;###autoload
+(defun pcomplete/pathchk ()
+  "Completion for the `pathchk' command."
+  (pcomplete-here-using-help "pathchk --help"))
+
+;;;###autoload
+(defun pcomplete/mktemp ()
+  "Completion for the `mktemp' command."
+  (pcomplete-here-using-help "mktemp --help"))
+
+;;;###autoload
+(defun pcomplete/realpath ()
+  "Completion for the `realpath' command."
+  (pcomplete-here-using-help "realpath --help"))
+
+;;;###autoload
+(defun pcomplete/id ()
+  "Completion for the `id' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "id --help")))
+  (while (pcomplete-here (pcmpl-unix-user-names))))
+
+;;;###autoload
+(defun pcomplete/groups ()
+  "Completion for the `groups' command."
+  (while (pcomplete-here (pcmpl-unix-user-names))))
+
+;;;###autoload
+(defun pcomplete/who ()
+  "Completion for the `who' command."
+  (pcomplete-here-using-help "who --help"))
+
+;;;###autoload
+(defun pcomplete/date ()
+  "Completion for the `date' command."
+  (pcomplete-here-using-help "date --help"))
+
+;;;###autoload
+(defun pcomplete/nproc ()
+  "Completion for the `nproc' command."
+  (pcomplete-here-using-help "nproc --help"))
+
+;;;###autoload
+(defun pcomplete/uname ()
+  "Completion for the `uname' command."
+  (pcomplete-here-using-help "uname --help"))
+
+;;;###autoload
+(defun pcomplete/hostname ()
+  "Completion for the `hostname' command."
+  (pcomplete-here-using-help "hostname --help"))
+
+;;;###autoload
+(defun pcomplete/uptime ()
+  "Completion for the `uptime' command."
+  (pcomplete-here-using-help "uptime --help"))
+
+;;;###autoload
+(defun pcomplete/chcon ()
+  "Completion for the `chcon' command."
+  (pcomplete-here-using-help "chcon --help"))
+
+;;;###autoload
+(defun pcomplete/runcon ()
+  "Completion for the `runcon' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "runcon --help"))
+    (when (pcomplete-match "\\`-[turl]\\'" 0) (pcomplete-here)))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/chroot ()
+  "Completion for the `chroot' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "chroot --help")))
+  (pcomplete-here (pcomplete-dirs))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/env ()
+  "Completion for the `env' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "env --help"))
+    (when (pcomplete-match "\\`-[uCS]\\'") (pcomplete-here)))
+  (while (pcomplete-match "=" 0) (pcomplete-here)) ; FIXME: Complete env vars
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/nice ()
+  "Completion for the `nice' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "nice --help"))
+    (pcomplete-here))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/nohup ()
+  "Completion for the `nohup' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "nohup --help")))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/stdbuf ()
+  "Completion for the `stdbuf' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "stdbuf --help"))
+    (when (pcomplete-match "\\`-[ioe]\\'") (pcomplete-here)))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/timeout ()
+  "Completion for the `timeout' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "timeout --help"))
+    (when (pcomplete-match "\\`-[ks]\\'") (pcomplete-here)))
+  (pcomplete-here)                      ; eat DURATION argument
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/numfmt ()
+  "Completion for the `numfmt' command."
+  (pcomplete-here-using-help "numfmt --help"))
+
+;;;###autoload
+(defun pcomplete/seq ()
+  "Completion for the `seq' command."
+  (pcomplete-here-using-help "seq --help"))
+
+;;; Network commands
 
 ;; ssh support by Phil Hagelberg.
 ;; https://www.emacswiki.org/cgi-bin/wiki/pcmpl-ssh.el
@@ -239,6 +673,18 @@ pcomplete/telnet
   (pcomplete-opt "xl(pcmpl-unix-user-names)")
   (pcmpl-unix-complete-hostname))
 
+;;; Miscellaneous
+
+;;;###autoload
+(defun pcomplete/sudo ()
+  "Completion for the `sudo' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "sudo --help"))
+    (when (pcomplete-match "\\`-[CDghpRtTUu]\\'") (pcomplete-here)))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
 (provide 'pcmpl-unix)
 
 ;;; pcmpl-unix.el ends here
diff --git a/lisp/pcmpl-x.el b/lisp/pcmpl-x.el
index 261a3d4e27..1ede867c5f 100644
--- a/lisp/pcmpl-x.el
+++ b/lisp/pcmpl-x.el
@@ -28,6 +28,22 @@
 (eval-when-compile (require 'cl-lib))
 (require 'pcomplete)
 
+;;; TeX
+
+;;;###autoload
+(defun pcomplete/tex ()
+  "Completion for the `tex' command."
+  (pcomplete-here-using-help "tex --help"
+                             :margin "^\\(?:\\[-no\\]\\)?\\(\\)-"))
+;;;###autoload(defalias 'pcomplete/pdftex 'pcomplete/tex)
+;;;###autoload(defalias 'pcomplete/latex 'pcomplete/tex)
+;;;###autoload(defalias 'pcomplete/pdflatex 'pcomplete/tex)
+
+;;;###autoload
+(defun pcomplete/luatex ()
+  "Completion for the `luatex' command."
+  (pcomplete-here-using-help "luatex --help"))
+;;;###autoload(defalias 'pcomplete/lualatex 'pcomplete/luatex)
 
 ;;;; tlmgr - https://www.tug.org/texlive/tlmgr.html
 
@@ -142,6 +158,12 @@ pcomplete/tlmgr
         (unless (pcomplete-match "^--" 0)
           (pcomplete-here* (pcomplete-dirs-or-entries)))))))
 
+;;; Grep-like tools
+
+;;;###autoload
+(defun pcomplete/rg ()
+  "Completion for the `rg' command."
+  (pcomplete-here-using-help "rg --help"))
 
 ;;;; ack - https://betterthangrep.com
 
@@ -288,6 +310,8 @@ pcomplete/ag
                                     (pcmpl-x-ag-options))))
       (pcomplete-here* (pcomplete-dirs-or-entries)))))
 
+;;; Borland
+
 ;;;###autoload
 (defun pcomplete/bcc32 ()
   "Completion function for Borland's C++ compiler."
@@ -321,5 +345,24 @@ pcomplete/bcc32
 ;;;###autoload
 (defalias 'pcomplete/bcc 'pcomplete/bcc32)
 
+;;; Network tools
+
+;;;###autoload
+(defun pcomplete/rclone ()
+  "Completion for the `rclone' command."
+  (let ((subcmds (pcomplete-from-help "rclone help"
+                                      :margin "^  "
+                                      :argument "[a-z]+"
+                                      :narrow-start "\n\n")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (completion-table-merge
+                       subcmds
+                       (pcomplete-from-help "rclone help flags"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (if (pcomplete-match "\\`-" 0)
+                 (pcomplete-here (pcomplete-from-help
+                                  `("rclone" ,subcmd "--help")))
+               (pcomplete-here (pcomplete-entries)))))))
+
 (provide 'pcmpl-x)
 ;;; pcmpl-x.el ends here
diff --git a/lisp/pcomplete.el b/lisp/pcomplete.el
index 0e3d1df781..6fe29d9dcf 100644
--- a/lisp/pcomplete.el
+++ b/lisp/pcomplete.el
@@ -119,6 +119,9 @@
 ;;; Code:
 
 (require 'comint)
+(eval-when-compile
+  (require 'cl-lib)
+  (require 'rx))
 
 (defgroup pcomplete nil
   "Programmable completion."
@@ -481,6 +484,14 @@ pcomplete-completions-at-point
           (when completion-ignore-case
             (setq table (completion-table-case-fold table)))
           (list beg (point) table
+                :annotation-function
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-annotation cand)))
+                :company-docsig
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-help cand)))
                 :predicate pred
                 :exit-function
 		;; If completion is finished, add a terminating space.
@@ -1325,6 +1336,133 @@ pcomplete-read-host-names
       (pcomplete-read-hosts pcomplete-hosts-file 'pcomplete--host-name-cache
                    'pcomplete--host-name-cache-timestamp)))
 
+;;; Parsing help messages
+
+(defvar pcomplete-from-help (make-hash-table :test #'equal)
+  "Memoization table for function `pcomplete-from-help'.")
+
+(cl-defun pcomplete-from-help (command
+                               &rest args
+                               &key
+                               (margin (rx bol (+ " ")))
+                               (argument (rx "-" (+ (any "-" alnum)) (? "=")))
+                               (metavar (rx (? " ")
+                                            (or (+ (any alnum "_-"))
+                                                (seq "[" (+? nonl) "]")
+                                                (seq "<" (+? nonl) ">")
+                                                (seq "{" (+? nonl) "}"))))
+                               (separator (rx ", " symbol-start))
+                               (description (rx (* nonl)
+                                                (* "\n" (>= 9 " ") (* nonl))))
+                               narrow-start
+                               narrow-end)
+  "Parse output of COMMAND into a list of completion candidates.
+
+COMMAND can be a string to be executed in a shell or a list of
+strings (program name and arguments).  It should print a help
+message.
+
+A list of arguments is collected after each match of MARGIN.
+Each argument should match ARGUMENT, possibly followed by a match
+of METAVAR.  If a match of SEPARATOR follows, then more
+argument-metavar pairs are collected.  Finally, a match of
+DESCRIPTION is collected.
+
+Keyword ARGS:
+
+MARGIN: regular expression after which argument descriptions are
+  to be found.  Parsing continues at the end of the first match
+  group or, failing that, the entire match.
+
+ARGUMENT: regular expression matching an argument name.  The
+  first match group (failing that, the entire match) is collected
+  as the argument name.  Parsing continues at the end of the
+  second matching group (failing that, the first group or entire
+  match).
+
+METAVAR: regular expression matching an argument parameter name.
+  The first match group (failing that, the entire match) is
+  collected as the parameter name and used as completion
+  annotation.  Parsing continues at the end of the second
+  matching group (failing that, the first group or entire match).
+
+SEPARATOR: regular expression matching the separator between
+  arguments.  Parsing continues at the end of the first match
+  group (failing that, the entire match).
+
+DESCRIPTION: regular expression matching the description of an
+  argument.  The first match group (failing that, the entire
+  match) is collected as the parameter name and used as
+  completion help.  Parsing continues at the end of the first
+  matching group (failing that, the entire match).
+
+NARROW-START, NARROW-END: if non-nil, parsing of the help message
+  is narrowed to the region between the end of the first match
+  group (failing that, the entire match) of these regular
+  expressions."
+  (with-memoization (gethash (cons command args) pcomplete-from-help)
+    (with-temp-buffer
+      (let ((case-fold-search nil)
+            (default-directory (expand-file-name "~/"))
+            (command (if (stringp command)
+                         (list shell-file-name
+                               shell-command-switch
+                               command)
+                       command))
+            i result)
+        (apply #'call-process (car command) nil t nil (cdr command))
+        (goto-char (point-min))
+        (narrow-to-region (or (and narrow-start
+                                   (re-search-forward narrow-start nil t)
+                                   (or (match-beginning 1) (match-beginning 0)))
+                              (point-min))
+                          (or (and narrow-end
+                                   (re-search-forward narrow-end nil t)
+                                   (or (match-beginning 1) (match-beginning 0)))
+                              (point-max)))
+        (goto-char (point-min))
+        (while (re-search-forward margin nil t)
+          (goto-char (or (match-end 1) (match-end 0)))
+          (setq i 0)
+          (while (and (or (zerop i)
+                          (and (looking-at separator)
+                               (goto-char (or (match-end 1)
+                                              (match-end 0)))))
+                      (looking-at argument))
+            (setq i (1+ i))
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (push (or (match-string 1) (match-string 0)) result)
+            (when (looking-at metavar)
+              (goto-char (seq-some #'match-end '(2 1 0)))
+              (put-text-property 0 1
+                                 'pcomplete-annotation
+                                 (or (match-string 1) (match-string 0))
+                                 (car result))))
+          (when (looking-at description)
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (let ((help (string-clean-whitespace
+                         (or (match-string 1) (match-string 0))))
+                  (items (take i result)))
+              (while items
+                (put-text-property 0 1 'pcomplete-help help
+                                   (pop items))))))
+        (nreverse result)))))
+
+(defun pcomplete-here-using-help (command &rest args)
+  "Perform completion for a simple command.
+Offer switches and directory entries as completion candidates.
+The switches are obtained by calling `pcomplete-from-help' with
+COMMAND and ARGS as arguments."
+  (while (cond
+          ((string= "--" (pcomplete-arg 1))
+           (while (pcomplete-here (pcomplete-entries))))
+          ((pcomplete-match "\\`--[^=]+=\\(.*\\)" 0)
+           (pcomplete-here (pcomplete-entries)
+                           (pcomplete-match-string 1 0)))
+          ((string-prefix-p "-" (pcomplete-arg 0))
+           (pcomplete-here (apply #'pcomplete-from-help command args)))
+          (t (pcomplete-here (pcomplete-entries))))))
+
 (provide 'pcomplete)
 
 ;;; pcomplete.el ends here
diff --git a/test/lisp/pcomplete-tests.el b/test/lisp/pcomplete-tests.el
new file mode 100644
index 0000000000..00a82502f3
--- /dev/null
+++ b/test/lisp/pcomplete-tests.el
@@ -0,0 +1,100 @@
+;;; pcomplete-tests.el --- Tests for pcomplete.el  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'ert)
+(require 'pcomplete)
+
+(ert-deftest pcomplete-test-parse-gpg-help ()
+  (cl-letf ((pcomplete-from-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process)
+             (lambda (&rest _) (insert "\
+gpg (GnuPG) 2.3.7
+
+Commands:
+
+ -s, --sign                         make a signature
+     --clear-sign                   make a clear text signature
+ -b, --detach-sign                  make a detached signature
+     --tofu-policy VALUE            set the TOFU policy for a key
+
+Options to specify keys:
+ -r, --recipient USER-ID            encrypt for USER-ID
+ -u, --local-user USER-ID           use USER-ID to sign or decrypt
+
+(See the man page for a complete listing of all commands and options)
+
+Examples:
+
+ -se -r Bob [file]          sign and encrypt for user Bob
+ --clear-sign [file]        make a clear text signature
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-from-help "gpg --help" :narrow-end "^ -se")
+      '(#("-s" 0 1 (pcomplete-help "make a signature"))
+        #("--sign" 0 1 (pcomplete-help "make a signature"))
+        #("--clear-sign" 0 1 (pcomplete-help "make a clear text signature"))
+        #("-b" 0 1 (pcomplete-help "make a detached signature"))
+        #("--detach-sign" 0 1 (pcomplete-help "make a detached signature"))
+        #("--tofu-policy" 0 1
+          (pcomplete-help "set the TOFU policy for a key" pcomplete-annotation " VALUE"))
+        #("-r" 0 1 (pcomplete-help "encrypt for USER-ID"))
+        #("--recipient" 0 1
+          (pcomplete-help "encrypt for USER-ID" pcomplete-annotation " USER-ID"))
+        #("-u" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt"))
+        #("--local-user" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt" pcomplete-annotation " USER-ID")))))))
+
+(ert-deftest pcomplete-test-parse-git-help ()
+  (cl-letf ((pcomplete-from-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process)
+             (lambda (&rest _) (insert "\
+usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
+           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
+           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
+           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
+           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
+           <command> [<args>]
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-from-help "git help"
+                           :margin "\\(\\[\\)-"
+                           :separator " | "
+                           :description "\\`")
+      '("-v" "--version" "-h" "--help"
+        #("-C" 0 1 (pcomplete-annotation " <path>"))
+        #("-c" 0 1 (pcomplete-annotation " <name>"))
+        #("--exec-path" 0 1 (pcomplete-annotation "[=<path>]"))
+        "--html-path" "--man-path" "--info-path"
+        "-p" "--paginate" "-P" "--no-pager"
+        "--no-replace-objects" "--bare"
+        #("--git-dir=" 0 1 (pcomplete-annotation "<path>"))
+        #("--work-tree=" 0 1 (pcomplete-annotation "<path>"))
+        #("--namespace=" 0 1 (pcomplete-annotation "<name>"))
+        #("--super-prefix=" 0 1 (pcomplete-annotation "<path>"))
+        #("--config-env=" 0 1 (pcomplete-annotation "<name>")))))))
+
+(provide 'pcomplete-tests)
+;;; pcomplete-tests.el ends here
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 19:15           ` Augusto Stoffel
@ 2022-09-14 19:21             ` Lars Ingebrigtsen
  2022-09-14 19:41               ` Augusto Stoffel
  0 siblings, 1 reply; 21+ messages in thread
From: Lars Ingebrigtsen @ 2022-09-14 19:21 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 57673, Stefan Monnier

Augusto Stoffel <arstoffel@gmail.com> writes:

> The patch includes completion for a variety of commands (in part to test
> the flexibility of the parser), including most commands from coreutils,
> so it has become a bit bulky.  Given the large number of new functions,
> I hope the commit message is detailed enough.

I've pushed the first (trivial) patch, but the second has two warnings:

In pcomplete/systemctl:
pcmpl-linux.el:154:12: Warning: Unused lexical variable `subcmd'
In pcomplete-from-help:
pcomplete.el:1344:2: Warning: docstring wider than 80 characters






^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 19:21             ` Lars Ingebrigtsen
@ 2022-09-14 19:41               ` Augusto Stoffel
  2022-09-14 19:48                 ` Lars Ingebrigtsen
  0 siblings, 1 reply; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-14 19:41 UTC (permalink / raw)
  To: Lars Ingebrigtsen; +Cc: 57673, Stefan Monnier

[-- Attachment #1: Type: text/plain, Size: 1512 bytes --]

On Wed, 14 Sep 2022 at 21:21, Lars Ingebrigtsen wrote:

> Augusto Stoffel <arstoffel@gmail.com> writes:
>
>> The patch includes completion for a variety of commands (in part to test
>> the flexibility of the parser), including most commands from coreutils,
>> so it has become a bit bulky.  Given the large number of new functions,
>> I hope the commit message is detailed enough.
>
> I've pushed the first (trivial) patch, but the second has two warnings:
>
> In pcomplete/systemctl:
> pcmpl-linux.el:154:12: Warning: Unused lexical variable `subcmd'

I've attached a patch to be applied (and squashed) on top of what I sent
previously.  Let me know if this is inconvenient and I'll send the whole
thing.

> In pcomplete-from-help:
> pcomplete.el:1344:2: Warning: docstring wider than 80 characters

Hum, I don't know how to fix this.  The long line is the function
signature, which is created mechanically by cl-defun and displays all
the default values of the keyword arguments.

The formatting is horrible:

    (pcomplete-from-help COMMAND &rest ARGS &key (MARGIN (rx bol (+ " ")))
    (ARGUMENT (rx "-" (+ (any "-" alnum)) (32 "="))) (METAVAR (rx (32 " ")
    (or (+ (any alnum "_-")) (seq "[" (+? nonl) "]") (seq "<" (+? nonl)
    ">") (seq "{" (+? nonl) "}")))) (SEPARATOR (rx ", " symbol-start))
    (DESCRIPTION (rx (* nonl) (* "\n" (>= 9 " ") (* nonl)))) NARROW-START
    NARROW-END)

But the information is good to have, because you need to know what these
regexps are in order to use the function.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-Fix-to-Warning-Unused-lexical-variable-subcmd.patch --]
[-- Type: text/x-patch, Size: 1090 bytes --]

From ef2ef880e2f29662c11a5422da11b293d1c1fe15 Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <arstoffel@gmail.com>
Date: Wed, 14 Sep 2022 21:35:00 +0200
Subject: [PATCH] Fix to Warning: Unused lexical variable `subcmd'

---
 lisp/pcmpl-linux.el | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/lisp/pcmpl-linux.el b/lisp/pcmpl-linux.el
index a2195c1e88..023c655a2a 100644
--- a/lisp/pcmpl-linux.el
+++ b/lisp/pcmpl-linux.el
@@ -158,6 +158,12 @@ pcomplete/systemctl
                 (pcomplete-here
                  (pcomplete-from-help "systemctl --help")))
                ;; TODO: suggest only relevant units to each subcommand
+               ("start"
+                (pcomplete-here
+                 (pcmpl-linux--systemd-units context "--state" "inactive,failed")))
+               ((or "restart" "stop")
+                (pcomplete-here
+                 (pcmpl-linux--systemd-units context "--state" "active")))
                (_ (pcomplete-here
                    (completion-table-in-turn
                     (pcmpl-linux--systemd-units context "--all")
-- 
2.37.3


^ permalink raw reply related	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 19:41               ` Augusto Stoffel
@ 2022-09-14 19:48                 ` Lars Ingebrigtsen
  2022-09-14 19:57                   ` Augusto Stoffel
  2022-09-14 20:40                   ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 2 replies; 21+ messages in thread
From: Lars Ingebrigtsen @ 2022-09-14 19:48 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 57673, Stefan Monnier

Augusto Stoffel <arstoffel@gmail.com> writes:

> I've attached a patch to be applied (and squashed) on top of what I sent
> previously.  Let me know if this is inconvenient and I'll send the whole
> thing.

I'd prefer the whole thing in one patch.


[...]

> +               ("start"
> +                (pcomplete-here
> +                 (pcmpl-linux--systemd-units context "--state" "inactive,failed")))
> +               ((or "restart" "stop")
> +                (pcomplete-here
> +                 (pcmpl-linux--systemd-units context "--state" "active")))

But...  subcmd isn't used here in the new lines, either, so does that
really fix the warning?

> Hum, I don't know how to fix this.  The long line is the function
> signature, which is created mechanically by cl-defun and displays all
> the default values of the keyword arguments.
>
> The formatting is horrible:
>
>     (pcomplete-from-help COMMAND &rest ARGS &key (MARGIN (rx bol (+ " ")))
>     (ARGUMENT (rx "-" (+ (any "-" alnum)) (32 "="))) (METAVAR (rx (32 " ")
>     (or (+ (any alnum "_-")) (seq "[" (+? nonl) "]") (seq "<" (+? nonl)
>     ">") (seq "{" (+? nonl) "}")))) (SEPARATOR (rx ", " symbol-start))
>     (DESCRIPTION (rx (* nonl) (* "\n" (>= 9 " ") (* nonl)))) NARROW-START
>     NARROW-END)
>
> But the information is good to have, because you need to know what these
> regexps are in order to use the function.

Oh, yeah, that's pretty bad...  we should probably fix that in the
cl-defun macro, I guess, so this doesn't have to be fixed in this patch.





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 19:48                 ` Lars Ingebrigtsen
@ 2022-09-14 19:57                   ` Augusto Stoffel
  2022-09-14 19:59                     ` Lars Ingebrigtsen
  2022-09-14 20:40                   ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  1 sibling, 1 reply; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-14 19:57 UTC (permalink / raw)
  To: Lars Ingebrigtsen; +Cc: 57673, Stefan Monnier

[-- Attachment #1: Type: text/plain, Size: 335 bytes --]

On Wed, 14 Sep 2022 at 21:48, Lars Ingebrigtsen wrote:

> Augusto Stoffel <arstoffel@gmail.com> writes:
>
>> I've attached a patch to be applied (and squashed) on top of what I sent
>> previously.  Let me know if this is inconvenient and I'll send the whole
>> thing.
>
> I'd prefer the whole thing in one patch.

Voilà.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-pcomplete-Generate-completions-from-help-messages.patch --]
[-- Type: text/x-patch, Size: 45683 bytes --]

From d8a839b5e4df4da2b6794cd406bdd63fd28f4954 Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <arstoffel@gmail.com>
Date: Thu, 8 Sep 2022 11:09:42 +0200
Subject: [PATCH] pcomplete: Generate completions from --help messages

* lisp/pcomplete.el (pcomplete-from-help): New function (and hash
table) to get pcomplete candidates from help messages.
(pcomplete-here-using-help): Helper function to define pcomplete for
simple commands
(pcomplete-completions-at-point): Provide annotation-function and
company-docsig properties.
* lisp/pcmpl-git.el: New file, provides pcomplete for Git.
* lisp/pcmpl-gnu.el: Add pcomplete for awk, gpg and gdb, emacs and
emacsclient.
* lisp/pcmpl-linux.el: Add pcomplete for systemctl and journalctl.
* lisp/pcmpl-rpm.el: Add pcomplete for dnf.
* lisp/pcmpl-unix.el: Add pcomplete for sudo and most commands found
in GNU Coreutils.
* lisp/pcmpl-x.el: Add pcomplete for tex, pdftex, latex, pdflatex,
rigrep and rclone.
* test/lisp/pcomplete-tests.el (pcomplete-test-parse-gpg-help,
pcomplete-test-parse-git-help): Tests for the new functions.
---
 lisp/pcmpl-git.el            | 110 ++++++++
 lisp/pcmpl-gnu.el            |  36 ++-
 lisp/pcmpl-linux.el          |  68 +++++
 lisp/pcmpl-rpm.el            |  43 ++-
 lisp/pcmpl-unix.el           | 490 +++++++++++++++++++++++++++++++++--
 lisp/pcmpl-x.el              |  43 +++
 lisp/pcomplete.el            | 138 ++++++++++
 test/lisp/pcomplete-tests.el | 100 +++++++
 8 files changed, 1004 insertions(+), 24 deletions(-)
 create mode 100644 lisp/pcmpl-git.el
 create mode 100644 test/lisp/pcomplete-tests.el

diff --git a/lisp/pcmpl-git.el b/lisp/pcmpl-git.el
new file mode 100644
index 0000000000..3584fa0673
--- /dev/null
+++ b/lisp/pcmpl-git.el
@@ -0,0 +1,110 @@
+;;; pcmpl-git.el --- Completions for Git -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; Package: pcomplete
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This library provides completion rules for the Git program.
+
+;;; Code:
+
+(require 'pcomplete)
+(require 'vc-git)
+
+(defun pcmpl-git--expand-flags (args)
+  "In the list of ARGS, expand arguments of the form --[no-]flag."
+  (mapcan (lambda (arg) (if (string-search "[no-]" arg)
+                            (list (string-replace "[no-]" "" arg)
+                                  (string-replace "[no-]" "no-" arg))
+                          (list arg)))
+          args))
+
+(defun pcmpl-git--tracked-file-predicate (&rest args)
+  "Return a predicate function determining the Git status of a file.
+Files listed by `git ls-files ARGS' satisfy the predicate."
+  (when-let ((files (mapcar #'expand-file-name
+                            (ignore-errors
+                              (apply #'process-lines
+                                     vc-git-program "ls-files" args)))))
+    (lambda (file)
+      (setq file (expand-file-name file))
+      (if (string-suffix-p "/" file)
+          (seq-some (lambda (f) (string-prefix-p file f))
+                    files)
+        (member file files)))))
+
+(defun pcmpl-git--remote-refs (remote)
+  "List the locally known Git revisions from REMOTE."
+  (delq nil
+        (mapcar
+         (let ((re (concat "\\`" (regexp-quote remote) "/\\(.*\\)")))
+           (lambda (s) (when (string-match re s) (match-string 1 s))))
+         (vc-git-revision-table nil))))
+
+;;;###autoload
+(defun pcomplete/git ()
+  "Completion for the `git' command."
+  (let ((subcommands (pcomplete-from-help `(,vc-git-program "help" "-a")
+                                          :margin "^\\( +\\)[a-z]"
+                                          :argument "[[:alnum:]-]+")))
+    (while (not (member (pcomplete-arg 1) subcommands))
+      (if (string-prefix-p "-" (pcomplete-arg))
+          (pcomplete-here (pcomplete-from-help `(,vc-git-program "help")
+                                               :margin "\\(\\[\\)-"
+                                               :separator " | "
+                                               :description "\\`"))
+        (pcomplete-here (completion-table-merge
+                         subcommands
+                         (when (string-prefix-p "-" (pcomplete-arg 1))
+                           (pcomplete-entries))))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (string-prefix-p "-" (pcomplete-arg)))
+                (pcomplete-here
+                 (pcmpl-git--expand-flags
+                  (pcomplete-from-help `(,vc-git-program "help" ,subcmd)
+                                       :argument
+                                       "-+\\(?:\\[no-\\]\\)?[a-z-]+=?"))))
+               ;; Complete modified tracked files
+               ((or "add" "commit" "restore")
+                (pcomplete-here
+                 (pcomplete-entries
+                  nil (pcmpl-git--tracked-file-predicate "-m"))))
+               ;; Complete all tracked files
+               ((or "mv" "rm" "grep" "status")
+                (pcomplete-here
+                 (pcomplete-entries nil (pcmpl-git--tracked-file-predicate))))
+               ;; Complete revisions
+               ((or "branch" "merge" "rebase" "switch")
+                (pcomplete-here (vc-git-revision-table nil)))
+               ;; Complete revisions and tracked files
+               ;; TODO: diff and log accept revision ranges
+               ((or "checkout" "reset" "show" "diff" "log")
+                (pcomplete-here
+                 (completion-table-in-turn
+                  (vc-git-revision-table nil)
+                  (pcomplete-entries nil (pcmpl-git--tracked-file-predicate)))))
+               ;; Complete remotes and their revisions
+               ((or "fetch" "pull" "push")
+                (pcomplete-here (process-lines vc-git-program "remote"))
+                (pcomplete-here (pcmpl-git--remote-refs (pcomplete-arg 1)))))))))
+
+(provide 'pcmpl-git)
+;;; pcmpl-git.el ends here
diff --git a/lisp/pcmpl-gnu.el b/lisp/pcmpl-gnu.el
index 3c9bf1ec9d..cdfde5640a 100644
--- a/lisp/pcmpl-gnu.el
+++ b/lisp/pcmpl-gnu.el
@@ -394,6 +394,40 @@ pcomplete/find
     (while (pcomplete-here (pcomplete-dirs) nil #'identity))))
 
 ;;;###autoload
-(defalias 'pcomplete/gdb 'pcomplete/xargs)
+(defun pcomplete/awk ()
+  "Completion for the `awk' command."
+  (pcomplete-here-using-help "awk --help"
+                             :margin "\t"
+                             :separator "  +"
+                             :description "\0"
+                             :metavar "[=a-z]+"))
+
+;;;###autoload
+(defun pcomplete/gpg ()
+  "Completion for the `gpg` command."
+  (pcomplete-here-using-help "gpg --help" :narrow-end "^ -se"))
+
+;;;###autoload
+(defun pcomplete/gdb ()
+  "Completion for the `gdb' command."
+  (while
+      (cond
+       ((string= "--args" (pcomplete-arg 1))
+        (funcall pcomplete-command-completion-function)
+        (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	             pcomplete-default-completion-function)))
+       ((string-prefix-p "-" (pcomplete-arg 0))
+        (pcomplete-here (pcomplete-from-help "gdb --help")))
+       (t (pcomplete-here (pcomplete-entries))))))
+
+;;;###autoload
+(defun pcomplete/emacs ()
+  "Completion for the `emacs' command."
+  (pcomplete-here-using-help "emacs --help" :margin "^\\(\\)-"))
+
+;;;###autoload
+(defun pcomplete/emacsclient ()
+  "Completion for the `emacsclient' command."
+  (pcomplete-here-using-help "emacsclient --help" :margin "^\\(\\)-"))
 
 ;;; pcmpl-gnu.el ends here
diff --git a/lisp/pcmpl-linux.el b/lisp/pcmpl-linux.el
index 7c072f3d40..023c655a2a 100644
--- a/lisp/pcmpl-linux.el
+++ b/lisp/pcmpl-linux.el
@@ -30,6 +30,7 @@
 (provide 'pcmpl-linux)
 
 (require 'pcomplete)
+(eval-when-compile (require 'rx))
 
 ;; Functions:
 
@@ -111,4 +112,71 @@ pcmpl-linux-mountable-directories
        (pcomplete-uniquify-list points)
        (cons "swap" (pcmpl-linux-mounted-directories))))))
 
+;;; systemd
+
+(defun pcmpl-linux--systemd-units (&rest args)
+  "Run `systemd list-units ARGS' and return the output as a list."
+  (with-temp-buffer
+    (apply #'call-process
+           "systemctl" nil '(t nil) nil
+           "list-units" "--full" "--legend=no" "--plain" args)
+    (goto-char (point-min))
+    (let (result)
+      (while (re-search-forward (rx bol (group (+ (not space)))
+                                    (+ space) (+ (not space))
+                                    (+ space) (group (+ (not space)))
+                                    (+ space) (+ (not space))
+                                    (+ space) (group (* nonl)))
+                                nil t)
+        (push (match-string 1) result)
+        (put-text-property 0 1 'pcomplete-annotation
+                           (concat " " (match-string 2))
+                           (car result))
+        (put-text-property 0 1 'pcomplete-description
+                           (match-string 3)
+                           (car result)))
+      (nreverse result))))
+
+;;;###autoload
+(defun pcomplete/systemctl ()
+  "Completion for the `systemctl' command."
+  (let ((subcmds (pcomplete-from-help
+                  "systemctl --help"
+                  :margin (rx bol "  " (group) alpha)
+                  :argument (rx (+ (any alpha ?-)))
+                  :metavar (rx (group (+ " " (>= 2 (any upper "[]|."))))))))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (if (string-prefix-p "-" (pcomplete-arg 0))
+          (pcomplete-here (pcomplete-from-help "systemctl --help"
+                                               :metavar "[^ ]+"
+                                               :separator " \\(\\)-"))
+        (pcomplete-here subcmds)))
+    (let ((subcmd (pcomplete-arg 1))
+          (context (if (member "--user" pcomplete-args) "--user" "--system")))
+      (while (pcase subcmd
+               ((guard (string-prefix-p "-" (pcomplete-arg 0)))
+                (pcomplete-here
+                 (pcomplete-from-help "systemctl --help")))
+               ;; TODO: suggest only relevant units to each subcommand
+               ("start"
+                (pcomplete-here
+                 (pcmpl-linux--systemd-units context "--state" "inactive,failed")))
+               ((or "restart" "stop")
+                (pcomplete-here
+                 (pcmpl-linux--systemd-units context "--state" "active")))
+               (_ (pcomplete-here
+                   (completion-table-in-turn
+                    (pcmpl-linux--systemd-units context "--all")
+                    (pcomplete-entries)))))))))
+
+;;;###autoload
+(defun pcomplete/journalctl ()
+  "Completion for the `journalctl' command."
+  (while (if (string-prefix-p "-" (pcomplete-arg 0))
+             (pcomplete-here (pcomplete-from-help "journalctl --help"
+                                                  :metavar "[^ ]+"
+                                                  :separator " \\(\\)-"))
+           (pcomplete-here (mapcar (lambda (s) (concat s "="))
+                                   (process-lines "journalctl" "--fields"))))))
+
 ;;; pcmpl-linux.el ends here
diff --git a/lisp/pcmpl-rpm.el b/lisp/pcmpl-rpm.el
index f7925d9d9e..ebb6b72600 100644
--- a/lisp/pcmpl-rpm.el
+++ b/lisp/pcmpl-rpm.el
@@ -21,7 +21,8 @@
 
 ;;; Commentary:
 
-;; These functions provide completion rules for the `rpm' command.
+;; These functions provide completion rules for the `rpm' command and
+;; related tools.
 
 ;;; Code:
 
@@ -378,6 +379,46 @@ pcomplete/rpm
        (t
 	(error "You must select a mode: -q, -i, -U, --verify, etc"))))))
 
+;;; DNF
+
+(defvar pcmpl-rpm-dnf-cache-file "/var/cache/dnf/packages.db"
+  "Location of the DNF cache.")
+
+(defun pcmpl-rpm--dnf-packages (status)
+  (when (and (file-exists-p pcmpl-rpm-dnf-cache-file)
+             (executable-find "sqlite3"))
+    (with-temp-message
+        "Getting list of packages..."
+      (process-lines "sqlite3" "-batch" "-init" "/dev/null"
+                     pcmpl-rpm-dnf-cache-file
+                     (pcase-exhaustive status
+                       ('available "select pkg from available")
+                       ('installed "select pkg from installed")
+                       ('not-installed "\
+select pkg from available where pkg not in (select pkg from installed)"))))))
+
+;;;###autoload
+(defun pcomplete/dnf ()
+  "Completion for the `dnf' command."
+  (let ((subcmds (pcomplete-from-help "dnf help"
+                                      :margin "^\\(\\)[a-z-]+  "
+                                      :argument "[a-z-]+")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (completion-table-merge
+                       subcmds
+                       (pcomplete-from-help "dnf help"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (pcase subcmd
+               ((guard (pcomplete-match "\\`-" 0))
+                (pcomplete-here
+                 (pcomplete-from-help `("dnf" "help" ,subcmd))))
+               ((or "downgrade" "reinstall" "remove")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'installed)))
+               ((or "install" "mark" "reinstall" "upgrade")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'not-installed)))
+               ((or "builddep" "changelog" "info" "list" "repoquery" "updateinfo")
+                (pcomplete-here (pcmpl-rpm--dnf-packages 'available))))))))
+
 (provide 'pcmpl-rpm)
 
 ;;; pcmpl-rpm.el ends here
diff --git a/lisp/pcmpl-unix.el b/lisp/pcmpl-unix.el
index 8774f091c8..0c32f814d0 100644
--- a/lisp/pcmpl-unix.el
+++ b/lisp/pcmpl-unix.el
@@ -25,7 +25,7 @@
 
 (require 'pcomplete)
 
-;; User Variables:
+;;; User Variables
 
 (defcustom pcmpl-unix-group-file "/etc/group"
   "If non-nil, a string naming the group file on your system."
@@ -56,7 +56,7 @@ pcmpl-ssh-config-file
   :group 'pcmpl-unix
   :version "24.1")
 
-;; Functions:
+;;; Shell builtins and core utilities
 
 ;;;###autoload
 (defun pcomplete/cd ()
@@ -69,34 +69,38 @@ 'pcomplete/pushd
 ;;;###autoload
 (defun pcomplete/rmdir ()
   "Completion for `rmdir'."
-  (while (pcomplete-here (pcomplete-dirs))))
+  (while (if (string-prefix-p "-" (pcomplete-arg))
+             (pcomplete-here (pcomplete-from-help "rmdir --help"))
+           (pcomplete-here (pcomplete-dirs)))))
 
 ;;;###autoload
 (defun pcomplete/rm ()
-  "Completion for `rm'."
-  (let ((pcomplete-help "(fileutils)rm invocation"))
-    (pcomplete-opt "dfirRv")
-    (while (pcomplete-here (pcomplete-all-entries) nil
-			   #'expand-file-name))))
+  "Completion for the `rm' command."
+  (pcomplete-here-using-help "rm --help"))
 
 ;;;###autoload
 (defun pcomplete/xargs ()
   "Completion for `xargs'."
   (while (string-prefix-p "-" (pcomplete-arg 0))
-    (pcomplete-here (funcall pcomplete-default-completion-function)))
+    (pcomplete-here (pcomplete-from-help "xargs --help"))
+    (when (pcomplete-match "\\`-[adEIiLnPs]\\'") (pcomplete-here)))
   (funcall pcomplete-command-completion-function)
   (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
 	       pcomplete-default-completion-function)))
 
-;; FIXME: Add completion of sudo-specific arguments.
-(defalias 'pcomplete/sudo #'pcomplete/xargs)
-
 ;;;###autoload
-(defalias 'pcomplete/time 'pcomplete/xargs)
+(defun pcomplete/time ()
+  "Completion for the `time' command."
+  (pcomplete-opt "p")
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
 
 ;;;###autoload
 (defun pcomplete/which ()
   "Completion for `which'."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "which --help")))
   (while (pcomplete-here (funcall pcomplete-command-completion-function))))
 
 (defun pcmpl-unix-read-passwd-file (file)
@@ -128,25 +132,455 @@ pcmpl-unix-user-names
   (if pcmpl-unix-passwd-file
       (pcmpl-unix-read-passwd-file pcmpl-unix-passwd-file)))
 
+;;;###autoload
+(defun pcomplete/cat ()
+  "Completion for the `cat' command."
+  (pcomplete-here-using-help "cat --help"))
+
+;;;###autoload
+(defun pcomplete/tac ()
+  "Completion for the `tac' command."
+  (pcomplete-here-using-help "tac --help"))
+
+;;;###autoload
+(defun pcomplete/nl ()
+  "Completion for the `nl' command."
+  (pcomplete-here-using-help "nl --help"))
+
+;;;###autoload
+(defun pcomplete/od ()
+  "Completion for the `od' command."
+  (pcomplete-here-using-help "od --help"))
+
+;;;###autoload
+(defun pcomplete/base32 ()
+  "Completion for the `base32' and `base64' commands."
+  (pcomplete-here-using-help "base32 --help"))
+;;;###autoload
+(defalias 'pcomplete/base64 'pcomplete/base32)
+
+;;;###autoload
+(defun pcomplete/basenc ()
+  "Completion for the `basenc' command."
+  (pcomplete-here-using-help "basenc --help"))
+
+;;;###autoload
+(defun pcomplete/fmt ()
+  "Completion for the `fmt' command."
+  (pcomplete-here-using-help "fmt --help"))
+
+;;;###autoload
+(defun pcomplete/pr ()
+  "Completion for the `pr' command."
+  (pcomplete-here-using-help "pr --help"))
+
+;;;###autoload
+(defun pcomplete/fold ()
+  "Completion for the `fold' command."
+  (pcomplete-here-using-help "fold --help"))
+
+;;;###autoload
+(defun pcomplete/head ()
+  "Completion for the `head' command."
+  (pcomplete-here-using-help "head --help"))
+
+;;;###autoload
+(defun pcomplete/tail ()
+  "Completion for the `tail' command."
+  (pcomplete-here-using-help "tail --help"))
+
+;;;###autoload
+(defun pcomplete/split ()
+  "Completion for the `split' command."
+  (pcomplete-here-using-help "split --help"))
+
+;;;###autoload
+(defun pcomplete/csplit ()
+  "Completion for the `csplit' command."
+  (pcomplete-here-using-help "csplit --help"))
+
+;;;###autoload
+(defun pcomplete/wc ()
+  "Completion for the `wc' command."
+  (pcomplete-here-using-help "wc --help"))
+
+;;;###autoload
+(defun pcomplete/sum ()
+  "Completion for the `sum' command."
+  (pcomplete-here-using-help "sum --help"))
+
+;;;###autoload
+(defun pcomplete/cksum ()
+  "Completion for the `cksum' command."
+  (pcomplete-here-using-help "cksum --help"))
+
+;;;###autoload
+(defun pcomplete/b2sum ()
+  "Completion for the `b2sum' command."
+  (pcomplete-here-using-help "b2sum --help"))
+
+;;;###autoload
+(defun pcomplete/md5sum ()
+  "Completion for checksum commands."
+  (pcomplete-here-using-help "md5sum --help"))
+;;;###autoload(defalias 'pcomplete/sha1sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha224sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha256sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha384sum 'pcomplete/md5sum)
+;;;###autoload(defalias 'pcomplete/sha521sum 'pcomplete/md5sum)
+
+;;;###autoload
+(defun pcomplete/sort ()
+  "Completion for the `sort' command."
+  (pcomplete-here-using-help "sort --help"))
+
+;;;###autoload
+(defun pcomplete/shuf ()
+  "Completion for the `shuf' command."
+  (pcomplete-here-using-help "shuf --help"))
+
+;;;###autoload
+(defun pcomplete/uniq ()
+  "Completion for the `uniq' command."
+  (pcomplete-here-using-help "uniq --help"))
+
+;;;###autoload
+(defun pcomplete/comm ()
+  "Completion for the `comm' command."
+  (pcomplete-here-using-help "comm --help"))
+
+;;;###autoload
+(defun pcomplete/ptx ()
+  "Completion for the `ptx' command."
+  (pcomplete-here-using-help "ptx --help"))
+
+;;;###autoload
+(defun pcomplete/tsort ()
+  "Completion for the `tsort' command."
+  (pcomplete-here-using-help "tsort --help"))
+
+;;;###autoload
+(defun pcomplete/cut ()
+  "Completion for the `cut' command."
+  (pcomplete-here-using-help "cut --help"))
+
+;;;###autoload
+(defun pcomplete/paste ()
+  "Completion for the `paste' command."
+  (pcomplete-here-using-help "paste --help"))
+
+;;;###autoload
+(defun pcomplete/join ()
+  "Completion for the `join' command."
+  (pcomplete-here-using-help "join --help"))
+
+;;;###autoload
+(defun pcomplete/tr ()
+  "Completion for the `tr' command."
+  (pcomplete-here-using-help "tr --help"))
+
+;;;###autoload
+(defun pcomplete/expand ()
+  "Completion for the `expand' command."
+  (pcomplete-here-using-help "expand --help"))
+
+;;;###autoload
+(defun pcomplete/unexpand ()
+  "Completion for the `unexpand' command."
+  (pcomplete-here-using-help "unexpand --help"))
+
+;;;###autoload
+(defun pcomplete/ls ()
+  "Completion for the `ls' command."
+  (pcomplete-here-using-help "ls --help"))
+;;;###autoload(defalias 'pcomplete/dir 'pcomplete/ls)
+;;;###autoload(defalias 'pcomplete/vdir 'pcomplete/ls)
+
+;;;###autoload
+(defun pcomplete/cp ()
+  "Completion for the `cp' command."
+  (pcomplete-here-using-help "cp --help"))
+
+;;;###autoload
+(defun pcomplete/dd ()
+  "Completion for the `dd' command."
+  (let ((operands (pcomplete-from-help "dd --help"
+                                       :argument "[a-z]+="
+                                       :narrow-start "\n\n"
+                                       :narrow-end "\n\n")))
+    (while
+        (cond ((pcomplete-match "\\`[io]f=\\(.*\\)" 0)
+               (pcomplete-here (pcomplete-entries)
+                               (pcomplete-match-string 1 0)))
+              (t (pcomplete-here operands))))))
+
+;;;###autoload
+(defun pcomplete/install ()
+  "Completion for the `install' command."
+  (pcomplete-here-using-help "install --help"))
+
+;;;###autoload
+(defun pcomplete/mv ()
+  "Completion for the `mv' command."
+  (pcomplete-here-using-help "mv --help"))
+
+;;;###autoload
+(defun pcomplete/shred ()
+  "Completion for the `shred' command."
+  (pcomplete-here-using-help "shred --help"))
+
+;;;###autoload
+(defun pcomplete/ln ()
+  "Completion for the `ln' command."
+  (pcomplete-here-using-help "ln --help"))
+
+;;;###autoload
+(defun pcomplete/mkdir ()
+  "Completion for the `mkdir' command."
+  (pcomplete-here-using-help "mkdir --help"))
+
+;;;###autoload
+(defun pcomplete/mkfifo ()
+  "Completion for the `mkfifo' command."
+  (pcomplete-here-using-help "mkfifo --help"))
+
+;;;###autoload
+(defun pcomplete/mknod ()
+  "Completion for the `mknod' command."
+  (pcomplete-here-using-help "mknod --help"))
+
+;;;###autoload
+(defun pcomplete/readlink ()
+  "Completion for the `readlink' command."
+  (pcomplete-here-using-help "readlink --help"))
+
 ;;;###autoload
 (defun pcomplete/chown ()
   "Completion for the `chown' command."
-  (unless (pcomplete-match "\\`-")
-    (if (pcomplete-match "\\`[^.]*\\'" 0)
-	(pcomplete-here* (pcmpl-unix-user-names))
-      (if (pcomplete-match "\\.\\([^.]*\\)\\'" 0)
-	  (pcomplete-here* (pcmpl-unix-group-names)
-			   (pcomplete-match-string 1 0))
-	(pcomplete-here*))))
+  (while (pcomplete-match "\\`-" 0)
+    (pcomplete-here (pcomplete-from-help "chown --help")))
+  (if (pcomplete-match "\\`[^.]*\\'" 0)
+      (pcomplete-here* (pcmpl-unix-user-names))
+    (if (pcomplete-match "\\.\\([^.]*\\)\\'" 0)
+	(pcomplete-here* (pcmpl-unix-group-names)
+			 (pcomplete-match-string 1 0))
+      (pcomplete-here*)))
   (while (pcomplete-here (pcomplete-entries))))
 
 ;;;###autoload
 (defun pcomplete/chgrp ()
   "Completion for the `chgrp' command."
-  (unless (pcomplete-match "\\`-")
-    (pcomplete-here* (pcmpl-unix-group-names)))
+  (while (pcomplete-match "\\`-" 0)
+    (pcomplete-here (pcomplete-from-help "chgrp --help")))
+  (pcomplete-here* (pcmpl-unix-group-names))
   (while (pcomplete-here (pcomplete-entries))))
 
+;;;###autoload
+(defun pcomplete/chmod ()
+  "Completion for the `chmod' command."
+  (pcomplete-here-using-help "chmod --help"))
+
+;;;###autoload
+(defun pcomplete/touch ()
+  "Completion for the `touch' command."
+  (pcomplete-here-using-help "touch --help"))
+
+;;;###autoload
+(defun pcomplete/df ()
+  "Completion for the `df' command."
+  (pcomplete-here-using-help "df --help"))
+
+;;;###autoload
+(defun pcomplete/du ()
+  "Completion for the `du' command."
+  (pcomplete-here-using-help "du --help"))
+
+;;;###autoload
+(defun pcomplete/stat ()
+  "Completion for the `stat' command."
+  (pcomplete-here-using-help "stat --help"))
+
+;;;###autoload
+(defun pcomplete/sync ()
+  "Completion for the `sync' command."
+  (pcomplete-here-using-help "sync --help"))
+
+;;;###autoload
+(defun pcomplete/truncate ()
+  "Completion for the `truncate' command."
+  (pcomplete-here-using-help "truncate --help"))
+
+;;;###autoload
+(defun pcomplete/echo ()
+  "Completion for the `echo' command."
+  (pcomplete-here-using-help '("echo" "--help")))
+
+;;;###autoload
+(defun pcomplete/test ()
+  "Completion for the `test' command."
+  (pcomplete-here-using-help '("[" "--help")
+                             :margin "^ +\\([A-Z]+1 \\)?"))
+;;;###autoload(defalias (intern "pcomplete/[") 'pcomplete/test)
+
+;;;###autoload
+(defun pcomplete/tee ()
+  "Completion for the `tee' command."
+  (pcomplete-here-using-help "tee --help"))
+
+;;;###autoload
+(defun pcomplete/basename ()
+  "Completion for the `basename' command."
+  (pcomplete-here-using-help "basename --help"))
+
+;;;###autoload
+(defun pcomplete/dirname ()
+  "Completion for the `dirname' command."
+  (pcomplete-here-using-help "dirname --help"))
+
+;;;###autoload
+(defun pcomplete/pathchk ()
+  "Completion for the `pathchk' command."
+  (pcomplete-here-using-help "pathchk --help"))
+
+;;;###autoload
+(defun pcomplete/mktemp ()
+  "Completion for the `mktemp' command."
+  (pcomplete-here-using-help "mktemp --help"))
+
+;;;###autoload
+(defun pcomplete/realpath ()
+  "Completion for the `realpath' command."
+  (pcomplete-here-using-help "realpath --help"))
+
+;;;###autoload
+(defun pcomplete/id ()
+  "Completion for the `id' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "id --help")))
+  (while (pcomplete-here (pcmpl-unix-user-names))))
+
+;;;###autoload
+(defun pcomplete/groups ()
+  "Completion for the `groups' command."
+  (while (pcomplete-here (pcmpl-unix-user-names))))
+
+;;;###autoload
+(defun pcomplete/who ()
+  "Completion for the `who' command."
+  (pcomplete-here-using-help "who --help"))
+
+;;;###autoload
+(defun pcomplete/date ()
+  "Completion for the `date' command."
+  (pcomplete-here-using-help "date --help"))
+
+;;;###autoload
+(defun pcomplete/nproc ()
+  "Completion for the `nproc' command."
+  (pcomplete-here-using-help "nproc --help"))
+
+;;;###autoload
+(defun pcomplete/uname ()
+  "Completion for the `uname' command."
+  (pcomplete-here-using-help "uname --help"))
+
+;;;###autoload
+(defun pcomplete/hostname ()
+  "Completion for the `hostname' command."
+  (pcomplete-here-using-help "hostname --help"))
+
+;;;###autoload
+(defun pcomplete/uptime ()
+  "Completion for the `uptime' command."
+  (pcomplete-here-using-help "uptime --help"))
+
+;;;###autoload
+(defun pcomplete/chcon ()
+  "Completion for the `chcon' command."
+  (pcomplete-here-using-help "chcon --help"))
+
+;;;###autoload
+(defun pcomplete/runcon ()
+  "Completion for the `runcon' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "runcon --help"))
+    (when (pcomplete-match "\\`-[turl]\\'" 0) (pcomplete-here)))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/chroot ()
+  "Completion for the `chroot' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "chroot --help")))
+  (pcomplete-here (pcomplete-dirs))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/env ()
+  "Completion for the `env' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "env --help"))
+    (when (pcomplete-match "\\`-[uCS]\\'") (pcomplete-here)))
+  (while (pcomplete-match "=" 0) (pcomplete-here)) ; FIXME: Complete env vars
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/nice ()
+  "Completion for the `nice' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "nice --help"))
+    (pcomplete-here))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/nohup ()
+  "Completion for the `nohup' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "nohup --help")))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/stdbuf ()
+  "Completion for the `stdbuf' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "stdbuf --help"))
+    (when (pcomplete-match "\\`-[ioe]\\'") (pcomplete-here)))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/timeout ()
+  "Completion for the `timeout' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "timeout --help"))
+    (when (pcomplete-match "\\`-[ks]\\'") (pcomplete-here)))
+  (pcomplete-here)                      ; eat DURATION argument
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
+;;;###autoload
+(defun pcomplete/numfmt ()
+  "Completion for the `numfmt' command."
+  (pcomplete-here-using-help "numfmt --help"))
+
+;;;###autoload
+(defun pcomplete/seq ()
+  "Completion for the `seq' command."
+  (pcomplete-here-using-help "seq --help"))
+
+;;; Network commands
 
 ;; ssh support by Phil Hagelberg.
 ;; https://www.emacswiki.org/cgi-bin/wiki/pcmpl-ssh.el
@@ -239,6 +673,18 @@ pcomplete/telnet
   (pcomplete-opt "xl(pcmpl-unix-user-names)")
   (pcmpl-unix-complete-hostname))
 
+;;; Miscellaneous
+
+;;;###autoload
+(defun pcomplete/sudo ()
+  "Completion for the `sudo' command."
+  (while (string-prefix-p "-" (pcomplete-arg 0))
+    (pcomplete-here (pcomplete-from-help "sudo --help"))
+    (when (pcomplete-match "\\`-[CDghpRtTUu]\\'") (pcomplete-here)))
+  (funcall pcomplete-command-completion-function)
+  (funcall (or (pcomplete-find-completion-function (pcomplete-arg 1))
+	       pcomplete-default-completion-function)))
+
 (provide 'pcmpl-unix)
 
 ;;; pcmpl-unix.el ends here
diff --git a/lisp/pcmpl-x.el b/lisp/pcmpl-x.el
index 261a3d4e27..1ede867c5f 100644
--- a/lisp/pcmpl-x.el
+++ b/lisp/pcmpl-x.el
@@ -28,6 +28,22 @@
 (eval-when-compile (require 'cl-lib))
 (require 'pcomplete)
 
+;;; TeX
+
+;;;###autoload
+(defun pcomplete/tex ()
+  "Completion for the `tex' command."
+  (pcomplete-here-using-help "tex --help"
+                             :margin "^\\(?:\\[-no\\]\\)?\\(\\)-"))
+;;;###autoload(defalias 'pcomplete/pdftex 'pcomplete/tex)
+;;;###autoload(defalias 'pcomplete/latex 'pcomplete/tex)
+;;;###autoload(defalias 'pcomplete/pdflatex 'pcomplete/tex)
+
+;;;###autoload
+(defun pcomplete/luatex ()
+  "Completion for the `luatex' command."
+  (pcomplete-here-using-help "luatex --help"))
+;;;###autoload(defalias 'pcomplete/lualatex 'pcomplete/luatex)
 
 ;;;; tlmgr - https://www.tug.org/texlive/tlmgr.html
 
@@ -142,6 +158,12 @@ pcomplete/tlmgr
         (unless (pcomplete-match "^--" 0)
           (pcomplete-here* (pcomplete-dirs-or-entries)))))))
 
+;;; Grep-like tools
+
+;;;###autoload
+(defun pcomplete/rg ()
+  "Completion for the `rg' command."
+  (pcomplete-here-using-help "rg --help"))
 
 ;;;; ack - https://betterthangrep.com
 
@@ -288,6 +310,8 @@ pcomplete/ag
                                     (pcmpl-x-ag-options))))
       (pcomplete-here* (pcomplete-dirs-or-entries)))))
 
+;;; Borland
+
 ;;;###autoload
 (defun pcomplete/bcc32 ()
   "Completion function for Borland's C++ compiler."
@@ -321,5 +345,24 @@ pcomplete/bcc32
 ;;;###autoload
 (defalias 'pcomplete/bcc 'pcomplete/bcc32)
 
+;;; Network tools
+
+;;;###autoload
+(defun pcomplete/rclone ()
+  "Completion for the `rclone' command."
+  (let ((subcmds (pcomplete-from-help "rclone help"
+                                      :margin "^  "
+                                      :argument "[a-z]+"
+                                      :narrow-start "\n\n")))
+    (while (not (member (pcomplete-arg 1) subcmds))
+      (pcomplete-here (completion-table-merge
+                       subcmds
+                       (pcomplete-from-help "rclone help flags"))))
+    (let ((subcmd (pcomplete-arg 1)))
+      (while (if (pcomplete-match "\\`-" 0)
+                 (pcomplete-here (pcomplete-from-help
+                                  `("rclone" ,subcmd "--help")))
+               (pcomplete-here (pcomplete-entries)))))))
+
 (provide 'pcmpl-x)
 ;;; pcmpl-x.el ends here
diff --git a/lisp/pcomplete.el b/lisp/pcomplete.el
index 0e3d1df781..6fe29d9dcf 100644
--- a/lisp/pcomplete.el
+++ b/lisp/pcomplete.el
@@ -119,6 +119,9 @@
 ;;; Code:
 
 (require 'comint)
+(eval-when-compile
+  (require 'cl-lib)
+  (require 'rx))
 
 (defgroup pcomplete nil
   "Programmable completion."
@@ -481,6 +484,14 @@ pcomplete-completions-at-point
           (when completion-ignore-case
             (setq table (completion-table-case-fold table)))
           (list beg (point) table
+                :annotation-function
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-annotation cand)))
+                :company-docsig
+                (lambda (cand)
+                  (when (stringp cand)
+                    (get-text-property 0 'pcomplete-help cand)))
                 :predicate pred
                 :exit-function
 		;; If completion is finished, add a terminating space.
@@ -1325,6 +1336,133 @@ pcomplete-read-host-names
       (pcomplete-read-hosts pcomplete-hosts-file 'pcomplete--host-name-cache
                    'pcomplete--host-name-cache-timestamp)))
 
+;;; Parsing help messages
+
+(defvar pcomplete-from-help (make-hash-table :test #'equal)
+  "Memoization table for function `pcomplete-from-help'.")
+
+(cl-defun pcomplete-from-help (command
+                               &rest args
+                               &key
+                               (margin (rx bol (+ " ")))
+                               (argument (rx "-" (+ (any "-" alnum)) (? "=")))
+                               (metavar (rx (? " ")
+                                            (or (+ (any alnum "_-"))
+                                                (seq "[" (+? nonl) "]")
+                                                (seq "<" (+? nonl) ">")
+                                                (seq "{" (+? nonl) "}"))))
+                               (separator (rx ", " symbol-start))
+                               (description (rx (* nonl)
+                                                (* "\n" (>= 9 " ") (* nonl))))
+                               narrow-start
+                               narrow-end)
+  "Parse output of COMMAND into a list of completion candidates.
+
+COMMAND can be a string to be executed in a shell or a list of
+strings (program name and arguments).  It should print a help
+message.
+
+A list of arguments is collected after each match of MARGIN.
+Each argument should match ARGUMENT, possibly followed by a match
+of METAVAR.  If a match of SEPARATOR follows, then more
+argument-metavar pairs are collected.  Finally, a match of
+DESCRIPTION is collected.
+
+Keyword ARGS:
+
+MARGIN: regular expression after which argument descriptions are
+  to be found.  Parsing continues at the end of the first match
+  group or, failing that, the entire match.
+
+ARGUMENT: regular expression matching an argument name.  The
+  first match group (failing that, the entire match) is collected
+  as the argument name.  Parsing continues at the end of the
+  second matching group (failing that, the first group or entire
+  match).
+
+METAVAR: regular expression matching an argument parameter name.
+  The first match group (failing that, the entire match) is
+  collected as the parameter name and used as completion
+  annotation.  Parsing continues at the end of the second
+  matching group (failing that, the first group or entire match).
+
+SEPARATOR: regular expression matching the separator between
+  arguments.  Parsing continues at the end of the first match
+  group (failing that, the entire match).
+
+DESCRIPTION: regular expression matching the description of an
+  argument.  The first match group (failing that, the entire
+  match) is collected as the parameter name and used as
+  completion help.  Parsing continues at the end of the first
+  matching group (failing that, the entire match).
+
+NARROW-START, NARROW-END: if non-nil, parsing of the help message
+  is narrowed to the region between the end of the first match
+  group (failing that, the entire match) of these regular
+  expressions."
+  (with-memoization (gethash (cons command args) pcomplete-from-help)
+    (with-temp-buffer
+      (let ((case-fold-search nil)
+            (default-directory (expand-file-name "~/"))
+            (command (if (stringp command)
+                         (list shell-file-name
+                               shell-command-switch
+                               command)
+                       command))
+            i result)
+        (apply #'call-process (car command) nil t nil (cdr command))
+        (goto-char (point-min))
+        (narrow-to-region (or (and narrow-start
+                                   (re-search-forward narrow-start nil t)
+                                   (or (match-beginning 1) (match-beginning 0)))
+                              (point-min))
+                          (or (and narrow-end
+                                   (re-search-forward narrow-end nil t)
+                                   (or (match-beginning 1) (match-beginning 0)))
+                              (point-max)))
+        (goto-char (point-min))
+        (while (re-search-forward margin nil t)
+          (goto-char (or (match-end 1) (match-end 0)))
+          (setq i 0)
+          (while (and (or (zerop i)
+                          (and (looking-at separator)
+                               (goto-char (or (match-end 1)
+                                              (match-end 0)))))
+                      (looking-at argument))
+            (setq i (1+ i))
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (push (or (match-string 1) (match-string 0)) result)
+            (when (looking-at metavar)
+              (goto-char (seq-some #'match-end '(2 1 0)))
+              (put-text-property 0 1
+                                 'pcomplete-annotation
+                                 (or (match-string 1) (match-string 0))
+                                 (car result))))
+          (when (looking-at description)
+            (goto-char (seq-some #'match-end '(2 1 0)))
+            (let ((help (string-clean-whitespace
+                         (or (match-string 1) (match-string 0))))
+                  (items (take i result)))
+              (while items
+                (put-text-property 0 1 'pcomplete-help help
+                                   (pop items))))))
+        (nreverse result)))))
+
+(defun pcomplete-here-using-help (command &rest args)
+  "Perform completion for a simple command.
+Offer switches and directory entries as completion candidates.
+The switches are obtained by calling `pcomplete-from-help' with
+COMMAND and ARGS as arguments."
+  (while (cond
+          ((string= "--" (pcomplete-arg 1))
+           (while (pcomplete-here (pcomplete-entries))))
+          ((pcomplete-match "\\`--[^=]+=\\(.*\\)" 0)
+           (pcomplete-here (pcomplete-entries)
+                           (pcomplete-match-string 1 0)))
+          ((string-prefix-p "-" (pcomplete-arg 0))
+           (pcomplete-here (apply #'pcomplete-from-help command args)))
+          (t (pcomplete-here (pcomplete-entries))))))
+
 (provide 'pcomplete)
 
 ;;; pcomplete.el ends here
diff --git a/test/lisp/pcomplete-tests.el b/test/lisp/pcomplete-tests.el
new file mode 100644
index 0000000000..00a82502f3
--- /dev/null
+++ b/test/lisp/pcomplete-tests.el
@@ -0,0 +1,100 @@
+;;; pcomplete-tests.el --- Tests for pcomplete.el  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'ert)
+(require 'pcomplete)
+
+(ert-deftest pcomplete-test-parse-gpg-help ()
+  (cl-letf ((pcomplete-from-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process)
+             (lambda (&rest _) (insert "\
+gpg (GnuPG) 2.3.7
+
+Commands:
+
+ -s, --sign                         make a signature
+     --clear-sign                   make a clear text signature
+ -b, --detach-sign                  make a detached signature
+     --tofu-policy VALUE            set the TOFU policy for a key
+
+Options to specify keys:
+ -r, --recipient USER-ID            encrypt for USER-ID
+ -u, --local-user USER-ID           use USER-ID to sign or decrypt
+
+(See the man page for a complete listing of all commands and options)
+
+Examples:
+
+ -se -r Bob [file]          sign and encrypt for user Bob
+ --clear-sign [file]        make a clear text signature
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-from-help "gpg --help" :narrow-end "^ -se")
+      '(#("-s" 0 1 (pcomplete-help "make a signature"))
+        #("--sign" 0 1 (pcomplete-help "make a signature"))
+        #("--clear-sign" 0 1 (pcomplete-help "make a clear text signature"))
+        #("-b" 0 1 (pcomplete-help "make a detached signature"))
+        #("--detach-sign" 0 1 (pcomplete-help "make a detached signature"))
+        #("--tofu-policy" 0 1
+          (pcomplete-help "set the TOFU policy for a key" pcomplete-annotation " VALUE"))
+        #("-r" 0 1 (pcomplete-help "encrypt for USER-ID"))
+        #("--recipient" 0 1
+          (pcomplete-help "encrypt for USER-ID" pcomplete-annotation " USER-ID"))
+        #("-u" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt"))
+        #("--local-user" 0 1
+          (pcomplete-help "use USER-ID to sign or decrypt" pcomplete-annotation " USER-ID")))))))
+
+(ert-deftest pcomplete-test-parse-git-help ()
+  (cl-letf ((pcomplete-from-help (make-hash-table :test #'equal))
+            ((symbol-function 'call-process)
+             (lambda (&rest _) (insert "\
+usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
+           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
+           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
+           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
+           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
+           <command> [<args>]
+"))))
+    (should
+     (equal-including-properties
+      (pcomplete-from-help "git help"
+                           :margin "\\(\\[\\)-"
+                           :separator " | "
+                           :description "\\`")
+      '("-v" "--version" "-h" "--help"
+        #("-C" 0 1 (pcomplete-annotation " <path>"))
+        #("-c" 0 1 (pcomplete-annotation " <name>"))
+        #("--exec-path" 0 1 (pcomplete-annotation "[=<path>]"))
+        "--html-path" "--man-path" "--info-path"
+        "-p" "--paginate" "-P" "--no-pager"
+        "--no-replace-objects" "--bare"
+        #("--git-dir=" 0 1 (pcomplete-annotation "<path>"))
+        #("--work-tree=" 0 1 (pcomplete-annotation "<path>"))
+        #("--namespace=" 0 1 (pcomplete-annotation "<name>"))
+        #("--super-prefix=" 0 1 (pcomplete-annotation "<path>"))
+        #("--config-env=" 0 1 (pcomplete-annotation "<name>")))))))
+
+(provide 'pcomplete-tests)
+;;; pcomplete-tests.el ends here
-- 
2.37.3


[-- Attachment #3: Type: text/plain, Size: 1716 bytes --]


>
> [...]
>
>> +               ("start"
>> +                (pcomplete-here
>> +                 (pcmpl-linux--systemd-units context "--state" "inactive,failed")))
>> +               ((or "restart" "stop")
>> +                (pcomplete-here
>> +                 (pcmpl-linux--systemd-units context "--state" "active")))
>
> But...  subcmd isn't used here in the new lines, either, so does that
> really fix the warning?

`subcmd' was always there, textually, in the `(pcase subcmd' right above
that.  But in the previous version of the code it disappeared during
macro expansion because it was not compared against anything in the
pcase patterns.

Very neat sanity check, indeed (but I did what I did on purpose because
I wanted to leave the function in an easier shape for future
refinements).

>> Hum, I don't know how to fix this.  The long line is the function
>> signature, which is created mechanically by cl-defun and displays all
>> the default values of the keyword arguments.
>>
>> The formatting is horrible:
>>
>>     (pcomplete-from-help COMMAND &rest ARGS &key (MARGIN (rx bol (+ " ")))
>>     (ARGUMENT (rx "-" (+ (any "-" alnum)) (32 "="))) (METAVAR (rx (32 " ")
>>     (or (+ (any alnum "_-")) (seq "[" (+? nonl) "]") (seq "<" (+? nonl)
>>     ">") (seq "{" (+? nonl) "}")))) (SEPARATOR (rx ", " symbol-start))
>>     (DESCRIPTION (rx (* nonl) (* "\n" (>= 9 " ") (* nonl)))) NARROW-START
>>     NARROW-END)
>>
>> But the information is good to have, because you need to know what these
>> regexps are in order to use the function.
>
> Oh, yeah, that's pretty bad...  we should probably fix that in the
> cl-defun macro, I guess, so this doesn't have to be fixed in this patch.

All right, thanks.

^ permalink raw reply related	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 19:57                   ` Augusto Stoffel
@ 2022-09-14 19:59                     ` Lars Ingebrigtsen
  0 siblings, 0 replies; 21+ messages in thread
From: Lars Ingebrigtsen @ 2022-09-14 19:59 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: 57673, Stefan Monnier

Augusto Stoffel <arstoffel@gmail.com> writes:

> `subcmd' was always there, textually, in the `(pcase subcmd' right above
> that.  But in the previous version of the code it disappeared during
> macro expansion because it was not compared against anything in the
> pcase patterns.

Ah, right.

I've now pushed your patch to Emacs 29, and will take a look at fixing
the cl-defun doc string issue tomorrowish.





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 19:48                 ` Lars Ingebrigtsen
  2022-09-14 19:57                   ` Augusto Stoffel
@ 2022-09-14 20:40                   ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-14 21:11                     ` Lars Ingebrigtsen
  2022-09-14 21:23                     ` Augusto Stoffel
  1 sibling, 2 replies; 21+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2022-09-14 20:40 UTC (permalink / raw)
  To: Lars Ingebrigtsen; +Cc: 57673, Augusto Stoffel

>>     (pcomplete-from-help COMMAND &rest ARGS &key (MARGIN (rx bol (+ " ")))
>>     (ARGUMENT (rx "-" (+ (any "-" alnum)) (32 "="))) (METAVAR (rx (32 " ")
>>     (or (+ (any alnum "_-")) (seq "[" (+? nonl) "]") (seq "<" (+? nonl)
>>     ">") (seq "{" (+? nonl) "}")))) (SEPARATOR (rx ", " symbol-start))
>>     (DESCRIPTION (rx (* nonl) (* "\n" (>= 9 " ") (* nonl)))) NARROW-START
>>     NARROW-END)
>>
>> But the information is good to have, because you need to know what these
>> regexps are in order to use the function.
>
> Oh, yeah, that's pretty bad...  we should probably fix that in the
> cl-defun macro, I guess, so this doesn't have to be fixed in this patch.

Side note: as a programmer, I don't really want to see this whole mess
in `C-h o` either.

Instead, I'd want the docstring to tell me what is the intended default
value of those keywords (rather than what is the code used to build that
default value).

Tho to be perfectly honest, I think any keyword argument whose default
value is not the same as nil is a problem (I know, sometimes there can
be good reasons for that, but we should try to avoid them as much as
possible).

So rather than

    (cl-defun pcomplete-from-help (command
                                   &rest args
                                   &key
                                   (margin (rx bol (+ " ")))
                                   (argument (rx "-" (+ (any "-" alnum)) (? "=")))
                                   (metavar (rx (? " ")
                                                (or (+ (any alnum "_-"))
                                                    (seq "[" (+? nonl) "]")
                                                    (seq "<" (+? nonl) ">")
                                                    (seq "{" (+? nonl) "}"))))
                                   (separator (rx ", " symbol-start))
                                   (description (rx (* nonl)
                                                    (* "\n" (>= 9 " ") (* nonl))))
                                   narrow-start
                                   narrow-end)

I'd rather see something like:

    (cl-defun pcomplete-from-help (command &rest args
                                   &key margin argument metavar
                                   separator description
                                   narrow-start narrow-end)
      (unless margin (setq margin (rx bol (+ " "))))
      (unless argument (setq argument (rx "-" (+ (any "-" alnum)) (? "=")))
      (unless metavar (setq metavar (rx (? " ")
                                        (or (+ (any alnum "_-"))
                                            (seq "[" (+? nonl) "]")
                                            (seq "<" (+? nonl) ">")
                                            (seq "{" (+? nonl) "}"))))
      (unless separator (setq separator (rx ", " symbol-start))
      (unless description (setq description (rx (* nonl)
                                                (* "\n" (>= 9 " ") (* nonl))))

[ which will probably not fix the overlong line problem, of course.  ]


        Stefan






^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 20:40                   ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
@ 2022-09-14 21:11                     ` Lars Ingebrigtsen
  2022-09-14 21:23                     ` Augusto Stoffel
  1 sibling, 0 replies; 21+ messages in thread
From: Lars Ingebrigtsen @ 2022-09-14 21:11 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: 57673, Augusto Stoffel

Stefan Monnier <monnier@iro.umontreal.ca> writes:

> Tho to be perfectly honest, I think any keyword argument whose default
> value is not the same as nil is a problem (I know, sometimes there can
> be good reasons for that, but we should try to avoid them as much as
> possible).

(I don't agree -- I think non-nil keyword values are fine.)





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 20:40                   ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  2022-09-14 21:11                     ` Lars Ingebrigtsen
@ 2022-09-14 21:23                     ` Augusto Stoffel
  2022-09-14 21:45                       ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  1 sibling, 1 reply; 21+ messages in thread
From: Augusto Stoffel @ 2022-09-14 21:23 UTC (permalink / raw)
  To: Stefan Monnier; +Cc: Lars Ingebrigtsen, 57673

On Wed, 14 Sep 2022 at 16:40, Stefan Monnier wrote:

> Tho to be perfectly honest, I think any keyword argument whose default
> value is not the same as nil is a problem (I know, sometimes there can
> be good reasons for that, but we should try to avoid them as much as
> possible).

Hum, that's a strong claim, and if I heard it from a clean code
influencer I would be rather suspicious.  So I'm curious why you think
that way.

My thinking is that in the body of the function you can do anything at
all, while in the arglist the only thing you can do (sanely) is to
nominate a value.  So if your default _is_ just a plain old value, it's
reassuring to see it declared in the arglist.





^ permalink raw reply	[flat|nested] 21+ messages in thread

* bug#57673: [PATCH] Parse --help messages for pcomplete
  2022-09-14 21:23                     ` Augusto Stoffel
@ 2022-09-14 21:45                       ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
  0 siblings, 0 replies; 21+ messages in thread
From: Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors @ 2022-09-14 21:45 UTC (permalink / raw)
  To: Augusto Stoffel; +Cc: Lars Ingebrigtsen, 57673

>> Tho to be perfectly honest, I think any keyword argument whose default
>> value is not the same as nil is a problem (I know, sometimes there can
>> be good reasons for that, but we should try to avoid them as much as
>> possible).
> Hum, that's a strong claim, and if I heard it from a clean code
> influencer I would be rather suspicious.  So I'm curious why you think
> that way.

It's convenient for the callers to know that they always pass nil to
mean "use the default".

It lets you write:

   (bar baz :hello foo)

instead of

   (if foo
       (bar baz :hello foo)
     (bar baz)

which gets really tiresome when you have more than 1 such keyword arg to
(maybe) pass to the function.

But my opinion is also influenced by the convenience of having only
`put` and `get` and knowing that there's no difference between a nil
property and a property that's absent.  Same holds for alists where it's
really convenient to try and stick to the principle that absent entries
are equivalent to entries associated to nil.

The convenience comes mostly from the fact that it's pervasive, rather
than from any specific use case.


        Stefan






^ permalink raw reply	[flat|nested] 21+ messages in thread

end of thread, other threads:[~2022-09-14 21:45 UTC | newest]

Thread overview: 21+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-09-08  9:34 bug#57673: [PATCH] Parse --help messages for pcomplete Augusto Stoffel
2022-09-08 12:39 ` Lars Ingebrigtsen
2022-09-08 13:05   ` Augusto Stoffel
2022-09-09 17:02     ` Lars Ingebrigtsen
2022-09-10  9:20       ` Augusto Stoffel
2022-09-08 20:49 ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-09-08 21:53   ` Augusto Stoffel
2022-09-09  2:47     ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-09-10  9:45       ` Augusto Stoffel
2022-09-10 14:32         ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-09-10 16:12           ` Augusto Stoffel
2022-09-14 19:15           ` Augusto Stoffel
2022-09-14 19:21             ` Lars Ingebrigtsen
2022-09-14 19:41               ` Augusto Stoffel
2022-09-14 19:48                 ` Lars Ingebrigtsen
2022-09-14 19:57                   ` Augusto Stoffel
2022-09-14 19:59                     ` Lars Ingebrigtsen
2022-09-14 20:40                   ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors
2022-09-14 21:11                     ` Lars Ingebrigtsen
2022-09-14 21:23                     ` Augusto Stoffel
2022-09-14 21:45                       ` Stefan Monnier via Bug reports for GNU Emacs, the Swiss army knife of text editors

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).