~trs-80/eldoc-sh-doxygen

0e3047b0b017a25e098b0bb303f7564f738bd929 — User 2 years ago
Initial commit
4 files changed, 473 insertions(+), 0 deletions(-)

A README.md
A README.org
A assets/gplv3-with-text-136x68.png
A eldoc-sh-doxygen.el
A  => README.md +119 -0
@@ 1,119 @@
1.  [eldoc-sh-doxygen](#eldocshdoxygen)
    1.  [Introduction](#introduction)
    2.  [Doxygen comments](#doxygen-comments)
    3.  [Installation](#installation)
    4.  [Setup / Starting](#setup-starting)
    5.  [Roadmap](#roadmap)
    6.  [Release History](#release-history)
    7.  [Related Tools](#related-tools)

[![img](assets/gplv3-with-text-136x68.png)](https://www.fsf.org/about/what-is-free-software)

# eldoc-sh-doxygen

## Introduction

Emacs ElDoc support for sh-mode (editing shell scripts).

ElDoc is the thing where, with point on a function, you get some text in the message area with the function name and it's argument list.  Which is different from completion.

One particularly nice feature of this package, is that it recursively searches through any additional `source`'d files in order to find all functions which should be callable from the current file.

Since shell languages are not really self-documenting in the same way that Emacs (Lisp) is, we need to provide some sort of structured data to parse.  I chose [Doxygen](https://www.doxygen.nl/index.html) for this as it's already widely supported in other languages and has a lot of already existing [tools](#related-tools) and options.

-   N.B.: Without adding the Doxygen style comment tags mentioned in the [next section](#doxygen-comments) (`@param` at minimum), this package does nothing.

## Doxygen comments

In order for any of this to work, you MUST comment your functions using Doxygen notation/tags, for example:

    
    ## @fn debian_package_insure ()
    ## @brief Insure Debian package is installed (if not, then install it).
    ## @param Package The package name you want to check for / install.  Required.
    ## @param Release The Debian target release (e.g., stable, buster-backports,
    ## etc.).  Optional.
    debian_package_insure () {
    
        local Package="$1"
        local Release="$2"
    
        # Issue a message
        echo "Checking status of Debian package $Package..."
    
        # [rest of function...]
    }

-   A few important notes about the above:
    -   Only the `@param` keywords are currently recognized/required by this package.
        -   Although while you are at it, you may want to add the rest if you want to automatically generate nice looking HTML (and man pages, and other sorts of docs).  See Doxygen itself for more about that.
    
    -   The blank line above the comment block is required for this package.
    
    -   The double comment symbol `##` is also required by this package (and also by `bash-doxygen` if you use that (see [Related Tools](#related-tools))).

## Installation

1.  Easiest way (considering updates) is just to clone this repo for the time being:
    
        ~/ $ cd git/ext
        ~/git/ext $ git clone https://git.sr.ht/~trs-80/eldoc-sh-doxygen

2.  Add something like the following to your init file:
    
        (add-to-list 'load-path "~/git/ext/eldoc-sh-doxygen")
        (require 'eldoc-sh-doxygen)

## Setup / Starting

1.  Do *one* of the following, *either*:
    1.  To run once manually while in an `sh-mode` buffer, press `M-:` and evaluate the following snippet:
        
            (add-function :before-until (local 'eldoc-documentation-function)
            	      #'sh-eldoc-documentation-function)
    
    2.  Or, if you want it to run every time `sh-mode` is started, put the same snippet to a custom function which we will in turn insert to the variable `sh-mode-hook`, like so:
        
            (defun my-sh-mode-setup ()
              "Personalized customization for `sh-mode'."
              (add-function :before-until (local 'eldoc-documentation-function)
            		#'sh-eldoc-documentation-function))
            
            (add-hook sh-mode-hook #'my-sh-mode-setup)

## Roadmap

There are a few things I do have some ideas – but not time, currently – about working on:

-   Maybe sooner:
    -   [ ] Make the current argument you are typing show up in bold (like it does in Emacs Lisp mode for instance).
    
    -   [ ] Improve regexp to include any characters which are legal in a shell function name (currently only `[A-Za-z_]` are recognized which is quite naive).
    
    -   [ ] Improve regexp(s) to account for the keyword `function` coming before a function name (currently we simply search for the function name followed by a space and then `()` which is quite naive).

-   Maybe later (if ever):
    -   [ ] The function `elisp-get-fnsym-args-string` (which is the Emacs Lisp equivalent, and model I based my `eldoc-sh-get-function-args-string` function upon) will return a brief description in lieu of an argument list if it can't find the latter.  Perhaps implement some similar functionality.
    
    -   [ ] Refactor what I consider to be some hacky code in places (although so far everything seems to work).
        -   Search for keyword `HACK` in comments to find some of these if you are interested (other keywords used are `NOTE`, `REVIEW`, `TODO`, etc.).
    
    -   [ ] Maybe implement importing tags from Doxygen (which as I understand can generate them) as an alternative to searching through buffers with regexps to gather the function arguments.

-   Other
    -   If you are really having some problem, please do create an issue in the tracker here.  I don't have a lot of time these days, but I'll do what I can to help.
    
    -   Last but certainly not least, [patches welcome](https://drewdevault.com/2019/01/01/Patches-welcome.html) of course!  ;)

## Release History

-   **2021-10-12 - v0.1.0**
    -   I just barely got this initally working and quickly wrote a README and packaged it up for quick release.  It "works for me" but I currently consider it alpha software.  See also [Roadmap](#roadmap).

## Related Tools

-   [bash-doxygen](https://github.com/Anvil/bash-doxygen) - A basic doxygen filter (originally written in GNU sed) allowing you to add inline-documentation to your bash shell scripts.
    -   You will actually need this in addition to Doxygen itself in order for Doxygen to be able to generate all the nice docs automatically.
    
    -   But it is not required at all for *this* package to work in Emacs.


A  => README.org +152 -0
@@ 1,152 @@
[[https://www.fsf.org/about/what-is-free-software][file:assets/gplv3-with-text-136x68.png]]

* eldoc-sh-doxygen
  :PROPERTIES:
  :CUSTOM_ID:            eldocshdoxygen
  :END:

** Introduction
   :PROPERTIES:
   :CUSTOM_ID:            introduction
   :END:

Emacs ElDoc support for sh-mode (editing shell scripts).

ElDoc is the thing where, with point on a function, you get some text in the message area with the function name and it's argument list.  Which is different from completion.

One particularly nice feature of this package, is that it recursively searches through any additional ~source~'d files in order to find all functions which should be callable from the current file.

Since shell languages are not really self-documenting in the same way that Emacs (Lisp) is, we need to provide some sort of structured data to parse.  I chose [[https://www.doxygen.nl/index.html][Doxygen]] for this as it's already widely supported in other languages and has a lot of already existing [[#related-tools][tools]] and options.

- N.B.: Without adding the Doxygen style comment tags mentioned in the [[#doxygen-comments][next section]] (~@param~ at minimum), this package does nothing.

** Doxygen comments
   :PROPERTIES:
   :CUSTOM_ID:            doxygen-comments
   :END:

In order for any of this to work, you MUST comment your functions using Doxygen notation/tags, for example:

#+begin_src shell

  ## @fn debian_package_insure ()
  ## @brief Insure Debian package is installed (if not, then install it).
  ## @param Package The package name you want to check for / install.  Required.
  ## @param Release The Debian target release (e.g., stable, buster-backports,
  ## etc.).  Optional.
  debian_package_insure () {

      local Package="$1"
      local Release="$2"

      # Issue a message
      echo "Checking status of Debian package $Package..."

      # [rest of function...]
  }
#+end_src

- A few important notes about the above:

  - Only the ~@param~ keywords are currently recognized/required by this package.

    - Although while you are at it, you may want to add the rest if you want to automatically generate nice looking HTML (and man pages, and other sorts of docs).  See Doxygen itself for more about that.

  - The blank line above the comment block is required for this package.

  - The double comment symbol ~##~ is also required by this package (and also by ~bash-doxygen~ if you use that (see [[#related-tools][Related Tools]])).

** Installation
   :PROPERTIES:
   :CUSTOM_ID:            installation
   :END:

1. Easiest way (considering updates) is just to clone this repo for the time being:

   #+begin_src shell
     ~/ $ cd git/ext
     ~/git/ext $ git clone https://git.sr.ht/~trs-80/eldoc-sh-doxygen
   #+end_src

2. Add something like the following to your init file:

   #+begin_src emacs-lisp
     (add-to-list 'load-path "~/git/ext/eldoc-sh-doxygen")
     (require 'eldoc-sh-doxygen)
   #+end_src

** Setup / Starting
   :PROPERTIES:
   :CUSTOM_ID:            setup-starting
   :END:

1. Do /one/ of the following, /either/:

   1. To run once manually while in an ~sh-mode~ buffer, press =M-:= and evaluate the following snippet:

      #+begin_src emacs-lisp
	(add-function :before-until (local 'eldoc-documentation-function)
		      #'sh-eldoc-documentation-function)
      #+end_src

   2. Or, if you want it to run every time ~sh-mode~ is started, put the same snippet to a custom function which we will in turn insert to the variable ~sh-mode-hook~, like so:

      #+begin_src emacs-lisp
	(defun my-sh-mode-setup ()
	  "Personalized customization for `sh-mode'."
	  (add-function :before-until (local 'eldoc-documentation-function)
			#'sh-eldoc-documentation-function))

	(add-hook sh-mode-hook #'my-sh-mode-setup)
      #+end_src

** Roadmap
   :PROPERTIES:
   :CUSTOM_ID:            roadmap
   :END:

There are a few things I do have some ideas -- but not time, currently -- about working on:

- Maybe sooner:

  - [ ] Make the current argument you are typing show up in bold (like it does in Emacs Lisp mode for instance).

  - [ ] Improve regexp to include any characters which are legal in a shell function name (currently only ~[A-Za-z_]~ are recognized which is quite naive).

  - [ ] Improve regexp(s) to account for the keyword ~function~ coming before a function name (currently we simply search for the function name followed by a space and then ~()~ which is quite naive).

- Maybe later (if ever):

  - [ ] The function ~elisp-get-fnsym-args-string~ (which is the Emacs Lisp equivalent, and model I based my ~eldoc-sh-get-function-args-string~ function upon) will return a brief description in lieu of an argument list if it can't find the latter.  Perhaps implement some similar functionality.

  - [ ] Refactor what I consider to be some hacky code in places (although so far everything seems to work).

    - Search for keyword ~HACK~ in comments to find some of these if you are interested (other keywords used are ~NOTE~, ~REVIEW~, ~TODO~, etc.).

  - [ ] Maybe implement importing tags from Doxygen (which as I understand can generate them) as an alternative to searching through buffers with regexps to gather the function arguments.

- Other

  - If you are really having some problem, please do create an issue in the tracker here.  I don't have a lot of time these days, but I'll do what I can to help.

  - Last but certainly not least, [[https://drewdevault.com/2019/01/01/Patches-welcome.html][patches welcome]] of course!  ;)

** Release History
   :PROPERTIES:
   :CUSTOM_ID:            release-history
   :END:

- *2021-10-12 - v0.1.0*

  - I just barely got this initally working and quickly wrote a README and packaged it up for quick release.  It "works for me" but I currently consider it alpha software.  See also [[#roadmap][Roadmap]].

** Related Tools
   :PROPERTIES:
   :CUSTOM_ID:            related-tools
   :END:

- [[https://github.com/Anvil/bash-doxygen][bash-doxygen]] - A basic doxygen filter (originally written in GNU sed) allowing you to add inline-documentation to your bash shell scripts.

  - You will actually need this in addition to Doxygen itself in order for Doxygen to be able to generate all the nice docs automatically.

  - But it is not required at all for /this/ package to work in Emacs.

A  => assets/gplv3-with-text-136x68.png +0 -0
A  => eldoc-sh-doxygen.el +202 -0
@@ 1,202 @@
;;; eldoc-sh-doxygen.el --- ElDoc support for sh-mode (editing shell scripts).  -*- lexical-binding: t -*-

;; Copyright 2021 TRS-80

;; Author: TRS-80 <sourcehut.trs-80@isnotmyreal.name>
;; Version: 0.1
;; Package-Requires: ()
;; Keywords: convenience
;; URL: https://sr.ht/~trs-80/eldoc-sh-doxygen

;; This file is NOT part of GNU Emacs.

;;; License:

;; This program 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.
;; 
;; This program 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 this program.  If not, see
;; <https://www.gnu.org/licenses/>.

;;; Commentary

;; ElDoc support for sh-mode (editing shell scripts).
;;
;; ElDoc is the thing where, with point on a function, you get
;; some text in the message area with the function name and it's
;; argument list.  Which is different from completion.
;; 
;; Since shell languages are not really self-documenting in the
;; same way that Emacs (Lisp) is, we need to provide some sort of
;; structured data to parse.  I chose Doxygen for this as it's
;; already widely supported in other languages and has a lot of
;; already existing tools and options.
;; 
;; N.B.: Without adding these (above mentioned) Doxygen style
;; comment tags (@param at minimum), this package does nothing.
;;
;; See README.org for (much) more information.
;; 




;;; Code

;;* Declarations

;;** Required Packages 


;;** Constants


;;** Variables




;;* Functions

(defun eldoc-sh--current-symbol ()
  (let ((c (char-after (point))))
    (car
     (and c
	  (memq (char-syntax c) '(?w))
	  (member (current-word)
		  (eldoc-sh-function-list))))))


(defun eldoc-sh-documentation-function ()
  "`eldoc-documentation-function' (which see) for `sh-mode'."
  (let ((current-symbol (eldoc-sh--current-symbol)))
    (when current-symbol
      (concat current-symbol " "
	      (eldoc-sh-get-function-args-string current-symbol)))))


(defun eldoc-sh-function-alist ()
  "Return alist of all functions available from current file.

Each element of alist consists of (func-name file-name).

This function leverages the function
`eldoc-sh-function-source-recurse' to provide the list of files
to search through."
  (mapcar
   ;; reverses each pair so that function is first
   (lambda (pair)
     ;; This filters empty results out of the list (like the one
     ;; at the end of each sub-list).
     (unless (string= "" pair)
       (reverse
	(split-string
	 (substring-no-properties pair nil -5)
	 "\0"))))
   (split-string
    ;; NOTE: For each file in mapcar below, it creates another
    ;; list element.  So I used mapconcat to essentially turn
    ;; the list back into one long string, which we feed into
    ;; split-string above.  (TRS-80 2021-04-30)
    (mapconcat 'identity
	       (mapcar (lambda (file)
			 (shell-command-to-string
			  ;; REVIEW: Regexp almost certainly
			  ;; needs to be expanded, to include any
			  ;; legal character in a function name.
			  ;; (TRS-80 2021-10-13)
			  (concat "grep -HEZ '^[A-Za-z_]+ ()' \""
				  file "\"")))
		       (eldoc-sh-function-source-recurse))
	       "\n")
    "\n")))


(defun eldoc-sh-function-jump-definition (func-name)
  "Jump to shell function definition for FUNC-NAME (a string).

Point will be put at beginning of line where FUNC-NAME is
defined."
  (let* ((alist (assoc func-name (eldoc-sh-function-alist)))
	 (func-file (nth 1 alist)))
    (find-file func-file)
    (goto-char (point-min))
    (re-search-forward (concat "^" func-name " ()"))
    (beginning-of-line)))


(defun eldoc-sh-function-list ()
  "Return list of all functions available from current file.

Like `eldoc-sh-function-alist' (which it in turn calls in fact)
however this returns a plain list, instead of an alist."
  (let ((func-alist (eldoc-sh-function-alist)))
    (remove nil
	    (mapcar (lambda (elem)
		      (car elem))
		    func-alist))))


(defun eldoc-sh-get-function-args-string (fun)
  "Return a string containing the parameter list of the function FUN."
  (save-excursion
    (eldoc-sh-function-jump-definition fun)
    (let ((end (point))
	  (arg-list '()))
      (re-search-backward "^$")
      (while (re-search-forward "^## @param "
				end 1)
	(push (buffer-substring-no-properties
	       (point)
	       (progn (forward-word)
		      (point)))
	      arg-list))
      (if (null arg-list)
	  "()"
	(format "%s" (reverse arg-list))))))


(defun eldoc-sh-function-source-recurse ()
  "Return list of files sourced from current one.

Each new sourced file will be in turn searched recursively for
additional source commands until all sourced files are found."
  (let ((files-to-check (list (buffer-file-name)))
	(files-to-return '()))
    (while (consp files-to-check)
      (let ((curr-file (expand-file-name (pop files-to-check))))
	;; TODO: Need to think more about how to handle case
	;; where sourced file is not available on this system.
	;; (TRS-80 2021-05-02)
	(when (file-exists-p curr-file)
	  (push curr-file files-to-return)
	  (mapcar (lambda (file)
		    (unless (string= "" file)
		      (push file files-to-check)))
		  (split-string
		   (concat
		    (shell-command-to-string
		     (concat "grep -Eh '^source ' "
			     curr-file " | cut -c 8-"))
		    (shell-command-to-string
		     (concat "grep -Eh '^f_source_or_die [^(]' "
			     curr-file " | cut -c 17-")))
		   "\n")))))
    files-to-return))




;;* Provide / End

(provide 'eldoc-sh-doxygen)

;;; eldoc-sh-doxygen.el ends here