From 4279f9ff8b726ddbba498063b2f79b08511b6bbb Mon Sep 17 00:00:00 2001
From: Kevin Lyda <kevin@lyda.ie>
Date: Sun, 18 May 2025 09:06:24 +0100
Subject: [PATCH] Make help work for compound commands

---
 NOTES.md         |  5 ++--
 repl/accounts.go | 11 -------
 repl/command.go  | 46 +++++++++++++++++++++++++----
 repl/help.go     | 77 +++++++++++++++++++++++++++++-------------------
 repl/set.go      | 10 -------
 repl/show.go     |  5 ----
 6 files changed, 90 insertions(+), 64 deletions(-)

diff --git a/NOTES.md b/NOTES.md
index 4d60cce..332d091 100644
--- a/NOTES.md
+++ b/NOTES.md
@@ -26,8 +26,9 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented
     * Messages edit: CHANGE, REPLY
     * Moving messages: COPY, MOVE
     * Mark messages: MARK, UNMARK
-    * Compound commands: SET and SHOW - make HELP work for them.
+    * ~~Compound commands: SET and SHOW - make HELP work for them.~~
     * Mail: MAIL, FORWARD, RESPOND
+  * Review each command and fully implement it.
   * Run this.Skew.Safe() before... each command?  each write?
   * Handle broadcast messages - have bulletin watch a directory and
     display files from it.  Then have them delete the file if it's older
@@ -40,7 +41,6 @@ Switch between MAIL and BULLETIN modes? MAIL commands are documented
     * Commands for a local mail system?
     * Commands to connect to Mattermost or mastodon?
     * ~~Commands to manage users.~~
-  * Handle MARK for SELECT and DIRECTORY.
 
 Done:
 
@@ -66,6 +66,7 @@ Done:
     * ~~Remove the node/cluster/newsgroup/mailing-list related flags.~~
     * ~~Remove BBOARD references.~~
     * ~~format with `par w72j1`~~
+  * ~~Handle MARK for SELECT and DIRECTORY.~~
 
 ## Module links
 
diff --git a/repl/accounts.go b/repl/accounts.go
index ad85656..1a6fb88 100644
--- a/repl/accounts.go
+++ b/repl/accounts.go
@@ -15,12 +15,6 @@ import (
 // ActionUser handles the `USER` command.
 func ActionUser(cmd *dclish.Command) error {
 	fmt.Println(cmd.Description)
-	fmt.Println(`
-The following commands are available:
-
-  ADD        ADMIN      DELETE     DISABLE    ENABLE     LIST
-  MOD        NOADMIN    NOMOD`)
-	fmt.Println()
 	return nil
 }
 
@@ -189,11 +183,6 @@ func ActionUserNomod(cmd *dclish.Command) error {
 // ActionSSH handles the `SSH` command.
 func ActionSSH(cmd *dclish.Command) error {
 	fmt.Println(cmd.Description)
-	fmt.Println(`
-The following commands are available:
-
-  ADD        DELETE     LIST`)
-	fmt.Println()
 	return nil
 }
 
diff --git a/repl/command.go b/repl/command.go
index 0dc244e..8dd6ded 100644
--- a/repl/command.go
+++ b/repl/command.go
@@ -554,7 +554,7 @@ place of an actual number, i.e. CURRENT-LAST, 1-CURRENT, etc.`,
 		Description: `To obtain help on any topic, type:
 
         HELP  topic`,
-		MaxArgs: 1,
+		MaxArgs: 2,
 		Action:  ActionHelp,
 	},
 	"INDEX": {
@@ -1102,8 +1102,17 @@ message.`,
 		},
 	},
 	"USER": {
-		Description: `Commands for managing users.`,
-		Action:      ActionUser,
+		Description: `Commands for managing users.
+
+  Format:
+    USER action
+
+The following actions are available:
+
+  ADD        ADMIN      DELETE     DISABLE    ENABLE     LIST
+  MOD        NOADMIN    NOMOD
+`,
+		Action: ActionUser,
 		Commands: dclish.Commands{
 			"ADD": {
 				Description: `  Creates a user.`,
@@ -1160,8 +1169,16 @@ message.`,
 		},
 	},
 	"SSH": {
-		Description: `The SSH command is used to manage SSH keys for users.`,
-		Action:      ActionSSH,
+		Description: `The SSH command is used to manage SSH keys for users.
+
+  Format:
+    SSH action
+
+The following commands are available:
+    
+  ADD        DELETE     LIST
+`,
+		Action: ActionSSH,
 		Commands: dclish.Commands{
 			"ADD": {
 				Description: `  Adds an  ssh key  for a user.  Only an  admin can add  an ssh  key for
@@ -1202,7 +1219,17 @@ message.`,
 characteristics of the BULLETIN Utility.
 
   Format:
-    SET option`,
+    SET option
+
+The following options are available:
+
+  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE
+  ACCESS           ALWAYS           BRIEF            DEFAULT_EXPIRE
+  EXPIRE_LIMIT     FOLDER           NOALWAYS         NOBRIEF
+  NONOTIFY         NOPROMPT_EXPIRE  NOREADNEW        NOSHOWNEW
+  NOSYSTEM         NOTIFY           PROMPT_EXPIRE    READNEW
+  SHOWNEW          SYSTEM
+`,
 		Action: ActionSet,
 		Commands: dclish.Commands{
 			"ALWAYS": {
@@ -1513,6 +1540,13 @@ that folder cannot be removed.`,
 	},
 	"SHOW": {
 		Description: `The SHOW command displays information about certain characteristics.
+
+  Format:
+    SHOW option
+
+The following options are available:
+
+  FLAGS       FOLDER      NEW         PRIVILEGES  USER        VERSION
 `,
 		Action: ActionShow,
 		Commands: dclish.Commands{
diff --git a/repl/help.go b/repl/help.go
index 0b39ea9..e2d9c81 100644
--- a/repl/help.go
+++ b/repl/help.go
@@ -92,30 +92,39 @@ in a state such that they would be inaccessible by other users.)
              +-----------------+--------+--------+`,
 }
 
-func init() {
-	// Add all command help.
-	buf := &strings.Builder{}
-	for c := range commands {
-		fmt.Fprint(buf, commands[c].Description)
-		if len(commands[c].Flags) > 0 {
-			flgs := make([]string, len(commands[c].Flags))
+var subhelpmap = map[string]map[string]string{}
+
+func generateHelp(hmap map[string]string, cmds dclish.Commands) {
+	for c := range cmds {
+		buf := &strings.Builder{}
+		fmt.Fprint(buf, cmds[c].Description)
+		if len(cmds[c].Flags) > 0 {
+			flgs := make([]string, len(cmds[c].Flags))
 			i := 0
-			for flg := range commands[c].Flags {
+			for flg := range cmds[c].Flags {
 				flgs[i] = flg
 				i++
 			}
 			sort.Strings(flgs)
 			for i := range flgs {
-				if strings.HasPrefix(commands[c].Flags[flgs[i]].Description, "/") {
-					fmt.Fprintf(buf, "\n\n%s", commands[c].Flags[flgs[i]].Description)
+				if strings.HasPrefix(cmds[c].Flags[flgs[i]].Description, "/") {
+					fmt.Fprintf(buf, "\n\n%s", cmds[c].Flags[flgs[i]].Description)
 				} else {
-					fmt.Fprintf(buf, "\n\n%s\n\n%s", flgs[i], commands[c].Flags[flgs[i]].Description)
+					fmt.Fprintf(buf, "\n\n%s\n\n%s", flgs[i], cmds[c].Flags[flgs[i]].Description)
 				}
 			}
 		}
-		helpmap[c] = buf.String()
-		buf.Reset()
+		hmap[c] = buf.String()
+		if cmds[c].Commands != nil {
+			subhelpmap[c] = map[string]string{}
+			generateHelp(subhelpmap[c], cmds[c].Commands)
+		}
 	}
+}
+
+func init() {
+	// Add all command help.
+	generateHelp(helpmap, commands)
 
 	// Add a list of topics.
 	topics := make([]string, len(helpmap))
@@ -129,7 +138,7 @@ func init() {
 	maxlen = maxlen + 2
 	sort.Strings(topics)
 
-	buf.Reset()
+	buf := &strings.Builder{}
 	linelen := 2
 	fmt.Fprint(buf,
 		"\n\nThe following commands and topics are available for more help\n\n  ")
@@ -154,34 +163,42 @@ func init() {
 	helpmap["HELP"] += buf.String()
 }
 
-// ActionHelp handles the `HELP` command.
-func ActionHelp(cmd *dclish.Command) error {
-	if len(cmd.Args) == 0 {
-		fmt.Printf("%s\n", helpmap["HELP"])
-		return nil
-	}
-	wordup := strings.ToUpper(cmd.Args[0])
-	helptext, ok := helpmap[wordup]
+func findHelp(hmap map[string]string, args []string, fullcmd string) string {
+	wordup := strings.ToUpper(args[0])
+	helptext, ok := hmap[wordup]
 	if !ok {
 		possibles := []string{}
-		for word := range helpmap {
+		for word := range hmap {
 			if strings.HasPrefix(word, wordup) {
 				possibles = append(possibles, word)
 			}
 		}
 		switch len(possibles) {
 		case 0:
-			fmt.Printf("ERROR: Topic not found: '%s'.\n", cmd.Args[0])
-			return nil
+			return fmt.Sprintf("ERROR: Topic not found: '%s'.\n", fullcmd)
 		case 1:
-			helptext = helpmap[possibles[0]]
+			if len(args) == 2 {
+				return findHelp(subhelpmap[possibles[0]], args[1:], strings.Join(args, " "))
+			}
+			return hmap[possibles[0]]
 		default:
-			fmt.Printf("ERROR: Ambiguous topic '%s' (matches %s)\n",
-				cmd.Args[0], strings.Join(possibles, ", "))
-			return nil
+			return fmt.Sprintf("ERROR: Ambiguous topic '%s' (matches %s)\n",
+				args[0], strings.Join(possibles, ", "))
 		}
+	}
+	if len(args) == 2 {
+		return findHelp(subhelpmap[wordup], args[1:], strings.Join(args, " "))
+	}
+	return helptext
+}
 
+// ActionHelp handles the `HELP` command.
+func ActionHelp(cmd *dclish.Command) error {
+	if len(cmd.Args) == 0 {
+		fmt.Printf("%s\n", helpmap["HELP"])
+		return nil
 	}
-	pager.Pager(helptext)
+
+	pager.Pager(findHelp(helpmap, cmd.Args, cmd.Args[0]))
 	return nil
 }
diff --git a/repl/set.go b/repl/set.go
index 605d99e..51e674c 100644
--- a/repl/set.go
+++ b/repl/set.go
@@ -15,16 +15,6 @@ import (
 // ActionSet handles the `SET` command.
 func ActionSet(cmd *dclish.Command) error {
 	fmt.Println(cmd.Description)
-	fmt.Println(`
-The following commands are available:
-
-  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE  NOPROMPT_EXPIRE
-  ACCESS           ALWAYS           BRIEF            DEFAULT_EXPIRE
-  EXPIRE_LIMIT     FOLDER           NOALWAYS         NOBRIEF
-  NONOTIFY         NOPROMPT_EXPIRE  NOREADNEW        NOSHOWNEW     
-  NOSYSTEM         NOTIFY           PROMPT_EXPIRE    READNEW     
-  SHOWNEW          SYSTEM`)
-	fmt.Println()
 	return nil
 }
 
diff --git a/repl/show.go b/repl/show.go
index a0ce307..a2d628d 100644
--- a/repl/show.go
+++ b/repl/show.go
@@ -16,11 +16,6 @@ import (
 // ActionShow handles the `SHOW` command.
 func ActionShow(cmd *dclish.Command) error {
 	fmt.Println(cmd.Description)
-	fmt.Println(`
-The following commands are available:
-
-  FLAGS       FOLDER      NEW         PRIVILEGES  USER        VERSION`)
-	fmt.Println()
 	return nil
 }
 
-- 
GitLab