diff --git a/noman.el b/noman.el index e0ef63f..e516e41 100644 --- a/noman.el +++ b/noman.el @@ -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 @@ -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.") @@ -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 _) "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." @@ -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))) @@ -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)) @@ -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 diff --git a/tests/noman-tests.el b/tests/noman-tests.el index 60349e7..20b87fb 100644 --- a/tests/noman-tests.el +++ b/tests/noman-tests.el @@ -141,6 +141,45 @@ fi (message name) name)) +(defun make-npm () + (let ((name (make-temp-file "npm"))) + (f-write-text "#!/bin/bash +if [[ \"$1\" == \"--help\" ]]; then + echo ' +npm + +Usage: + +npm install install all the dependencies in your project +npm install add the 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)) @@ -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 " 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"))