8000 Add noman-help-format to specify argument ordering by nverno · Pull Request #10 · andykuszyk/noman.el · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add noman-help-format to specify argument ordering #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 90 additions & 49 deletions noman.el
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@
(defgroup noman nil "Command line help reader." :group 'applications :prefix "noman-")

(defcustom noman-parsing-functions
'(("aws" . noman--make-aws-button))
'(("aws" . noman--make-aws-button)
("npm" . noman--make-npm-button)
("go" . noman--make-go-button))
"Alist of form: ((COMMAND . PARSER)).
COMMAND is a string matched against the requested command.
PARSER is a function which accepts a line of text and returns a button or nil."
:type 'alist)
:type '(repeat (cons (string :tag "Command")
(symbol :tag "Function"))))

(defcustom noman-reuse-buffers
t
Expand All @@ -56,6 +59,18 @@ Any other value results in a new buffer being created for each command, with the
If set, this value will used when displaying help for shell built-in commands."
:type 'string)

(defcustom noman-help-format
'(("npm" (command "help" args))
("go" (command "help" args)))
"Control how help is called for subcommands.
COMMAND is replaced with the main command.
ARGS is replaced with arguments for subcommand help.
Strings are interpreted as is."
:type '(repeat (list (string :tag "Command")
(repeat (choice (const command)
(const args)
string)))))

(defvar-local noman--last-command nil "The last command that noman executed.")
(defvar-local noman--buttons nil "A list of buttons in the current noman buffer.")
(defvar noman--history nil "History of recent noman commands.")
Expand Down Expand Up @@ -94,36 +109,39 @@ If set, this value will used when displaying help for shell built-in commands."
(let ((subcommand (button-label button)))
(noman (format "%s %s" noman--last-command subcommand))))

(defun noman--make-aws-button (line)
(defun noman--make-aws-button (&rest _)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these functions need to take &rest arguments? It doesn't look like we need to pass any arguments to them any more, and they're not using the _ argument. Could they be parameterless?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I tried running it for npm and Go locally, and found that the subcommands weren't correctly parsed as buttons 🤔

Did you reevaluate noman-parsing-functions to make sure it was using the functions for npm/go? Those commands are buttonized when I run them locally, Ill try to add some tests.

Copy link
Contributor Author
@nverno nverno Jun 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do these functions need to take &rest arguments? It doesn't look like we need to pass any arguments to them any more, and they're not using the _ argument. Could they be parameterless?

You're right, that was leftover from when I was experimenting... I was wondering if they should take a context argument - something that could hold parsing state.

For example, if the parsing function might want to only buttonize matches in a certain part of the help output, eg. after seeing a line like "Additional commands:" or something. So the parsing function could have a state like context.in_commands = true.

WDYT?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a good idea for the future, and I like your subcommand-p idea 👍

However, if we're not using that argument just now, I think it would be better to make those functions parameterless. We can always introduce a parameter in the future when we need to (doing so would certainly make the aws parsing more accurate).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, I've just re-read your implementation, and I misunderstood it the first time round.

Yes, this is a good idea, let's keep it in 🙂

"Return button for aws-style command LINE."
(when-let
((first-match
(string-match "^ +o +\\([A-Za-z0-9\\-]+\\)$" line))
(beg (match-beginning 1))
(end (match-end 1))
(bol (line-beginning-position)))
(make-button
(+ bol beg)
(+ bol end)
'action
#'noman--follow-link)))

(defun noman--button (line)
(when (looking-at "^ +o +\\([A-Za-z0-9\\-]+\\)$")
(list (cons (match-beginning 1) (match-end 1)))))

(defun noman--make-npm-button (&optional subcommand-p)
"Return button positions for npm subcommands.
SUBCOMMAND-P is non-nil when parsing a subcommand."
(if subcommand-p
(when (looking-at "^ • npm help \\([a-z-]+\\)")
(list (cons (match-beginning 1) (match-end 1))))
(if (looking-at-p "^ \\{4\\}[a-z-]+,")
(let (res)
(while (re-search-forward "\\([a-z-]+\\),?" (line-end-position) t)
(push (cons (match-beginning 1) (match-end 1)) res))
res))))

(defun noman--make-go-button (&optional subcommand-p)
"Return button positions for go subcommands.
SUBCOMMAND-P is non-nil when parsing a subcommand."
(if subcommand-p
(when (looking-at-p "^See also: ")
(let (res)
(while (re-search-forward "go \\([a-z.]+\\)" (line-end-position) t)
(push (cons (match-beginning 1) (match-end 1)) res))
res))
(if (looking-at "^\t\\([a-z.-]+\\)")
(list (cons (match-beginning 1) (match-end 1))))))

(defun noman--button (&rest _)
"Return default command LINE button."
(when-let
(((string-prefix-p " " line))
(first-match
(string-match
"^ +\\([A-Za-z]+[A-Za-z0-9\\-]+\\):* \\{2\\}.*$"
line))
(beg (match-beginning 1))
(end (match-end 1))
(bol (line-beginning-position)))
(make-button
(+ bol beg)
(+ bol end)
'action
#'noman--follow-link)))
(when (looking-at "^ \\([A-Za-z]+[A-Za-z0-9\\-]+\\):* \\{2\\}.*$")
(list (cons (match-beginning 1) (match-end 1)))))

(defun noman--button-func (cmd)
"Gets the function to use for parsing subcommands for the given CMD."
Expand All @@ -135,13 +153,18 @@ If set, this value will used when displaying help for shell built-in commands."
"Evaluate CMD's button function on each line in `current-buffer'.
Return list of created buttons."
(let ((button-func (noman--button-func cmd))
(subcommand-p (string-search " " cmd))
(buttons nil))
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(push (funcall button-func
(buffer-substring (line-beginning-position) (line-end-position)))
buttons)
(when-let ((btns (funcall button-func subcommand-p)))
(or (listp btns) (setq btns (list btns)))
(dolist (pos btns)
(push (if (overlayp pos) pos
(make-button (car pos) (cdr pos)
'action #'noman--follow-link))
buttons)))
(forward-line)))
(delq nil buttons)))

Expand All @@ -162,11 +185,41 @@ Return list of created buttons."

(defun noman--generate-buffer-name (cmd)
"Generate a buffer name from CMD.
If noman-reuse-buffers is t, *noman* will always be returned."
If `noman-reuse-buffers' is t, *noman* will always be returned."
(if noman-reuse-buffers
"*noman*"
(format "*noman %s*" cmd)))

(defun noman--build-help-command (cmd &optional args)
"Build help command for CMD with ARGS."
(if-let ((order (car (assoc-default cmd noman-help-format))))
(delq nil (mapcan (lambda (arg)
(pcase arg
((pred stringp) (list arg))
('command (list cmd))
('args args)
(_ (error "Unmatched help arg: '%S'" arg))))
order))
(cons cmd (append args '("--help")))))

(defun noman--build-alt-help-command (args)
"Build alternative help command from ARGS.
Swaps \"help\" for \"--help\" and vice versa."
(mapcar (lambda (e)
(pcase e
("help" "--help")
("--help" "help")
(_ e)))
args))

(defun noman--call-help-commands (args)
"Call help command ARGS and its alternative if it fails."
(let ((cmd (car args))
(args (cdr args)))
(unless (zerop (apply #'call-process cmd nil t nil args))
(erase-buffer)
(apply #'call-process cmd nil t nil (noman--build-alt-help-command args)))))

(defun noman--buffer (cmd)
"Prepare and display noman CMD's noman buffer."
(let* ((tokens (split-string cmd))
Expand All @@ -190,21 +243,9 @@ If noman-reuse-buffers is t, *noman* will always be returned."
nil))
((string-suffix-p " not found" type)
(user-error "Command '%s' not found" prefix))
(t (unless (= (apply #'call-process
prefix
nil
t
nil
`(,@(cdr tokens) "--help"))
0)
(erase-buffer)
(apply #'call-process
prefix
nil
t
nil
`(,@(cdr tokens) "help"))
(replace-regexp-in-region "." "" (point-min) (point-max)))
(t (let ((args (noman--build-help-command prefix (cdr tokens))))
(noman--call-help-commands args)
(replace-regexp-in-region "." "" (point-min) (point-max)))
(when-let ((versioninfo
(save-excursion
(with-temp-buffer
Expand Down
68 changes: 68 additions & 0 deletions tests/noman-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,45 @@ fi
(message name)
name))

(defun make-npm ()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests look good, thanks for adding them 🙏

(let ((name (make-temp-file "npm")))
(f-write-text "#!/bin/bash
if [[ \"$1\" == \"--help\" ]]; then
echo '
npm <command>

Usage:

npm install install all the dependencies in your project
npm install <foo> add the <foo> dependency to your project

All commands:

access, adduser, audit, bugs, cache, ci, completion,
config, dedupe, deprecate, diff, dist-tag, docs, doctor,
'
fi

if [[ \"$1\" == \"access\" ]]; then
echo '
=NPM-ACCESS(1) NPM-ACCESS(1)

NAME
npm-access - Set access level on published packages

See Also
• libnpmaccess ⟨https://npm.im/libnpmaccess⟩

• npm help team

• npm help publish
'
fi
" 'utf-8-emacs name)
(chmod name #o777)
(message name)
name))

(defun noman--test-setup ()
(setq noman-reuse-buffers nil)
(kill-matching-buffers "\\*noman.*" nil t))
Expand Down Expand Up @@ -231,6 +270,35 @@ fi
"Create and run a particular image in a pod."
(buffer-substring-no-properties (point-min) (point-max)))))))

(ert-deftest noman-should-parse-npm ()
(noman--test-setup)
(let* ((npm (make-npm))
(buffer (format "*noman %s*" npm)))
(add-to-list 'noman-parsing-functions `(,npm . noman--make-npm-button))
(noman npm)
(with-current-buffer (get-buffer buffer)
(should (string-equal buffer (buffer-name)))
(should (> (point-max) 0))
(should (search-forward "npm <command>" nil t 1))
(should (= (count-buttons) 14)))))

(ert-deftest noman-should-parse-npm-subcommands ()
(noman--test-setup)
(let* ((npm (make-npm))
(buffer (format "*noman %s*" npm))
(access-buffer (format "*noman %s access*" npm)))
(add-to-list 'noman-parsing-functions `(,npm . noman--make-npm-button))
(noman npm)
(with-current-buffer (get-buffer buffer)
(noman-menu "access")
(should (get-buffer access-buffer))
(with-current-buffer (get-buffer access-buffer)
(should (string-equal access-buffer (buffer-name)))
(should
(search-forward
"npm-access - Set access level on published packages" nil t 1))
(should (= (count-buttons) 2))))))

(ert-deftest noman-with-prefix-arg-allows-shell-built-ins ()
(let* ((type-buffer-name "*noman type*")
(noman-shell-file-name "/bin/bash"))
Expand Down
Loading
0