elvish icon indicating copy to clipboard operation
elvish copied to clipboard

A help system for the built-in commands

Open iandol opened this issue 2 years ago • 26 comments

As rasied on the user group, I asked about the possibility of having the documentation for each command available via the command line, so we could type e.g. > help randint and get the documentation that is already present in the go source code, e.g. https://github.com/elves/elvish/blob/master/pkg/eval/builtin_fn_num.go#L576

Currently this documentation is in markdown. The simplest solution would be to convert from markdown to plain text, resulting in something like (for this I used pandoc -t plain):

elvdoc:fn randint

    randint $low $high

Output a pseudo-random integer in the half-open interval [$low, $high).
Example:

    ~> # Emulate dice
    randint 1 7

This would already be a big advance. Of course for cherries on top it would be nice if links could be preserved and code could get syntax highlighting. I do not know anything about go and how feasible this would be, I assume there are many markdown packages available...

iandol avatar Nov 20 '21 10:11 iandol

We already have code written to extract the Elvish var and command documentation and convert it to HTML using pandoc (see website/tools/md-to-html). Building on that to generate plain text embedded in the elvish binary and available to a hypothetical help command should be straightforward.

Another approach is that taken by Fish. It converts its command documentation to "man" pages that are installed in a fish specific man dir. Fish then alters the MANPATH env var so that directory is searched first. That leverages the mature man ecosystem but has a couple of drawbacks. First, it requires installing not just the elvish binary but also additional files. Second, it probably isn't (easily) portable to other platforms such as Windows or Plan 9. The first point could be considered a positive by anyone who wants the smallest possible Elvish binary, but that can also be addressed by a separate make target that doesn't generate the embedded help text.

More interesting questions involve the behavior of the hypothetical help command. Ideally it would reflow text to match the terminal width but that could be omitted from the first implementation. Should it automatically present the text using a "pager" program like less or simply assume most command help text is short enough that a pager won't usually add value? What should the output of help be when invoked with no arguments? When invoked with a module name (e.g., help str:) it should probably output a list of vars and commands in the module and nothing else. But if the output is automatically piped through a pager then something like the corresponding web reference page might be preferable.

krader1961 avatar Nov 21 '21 02:11 krader1961

Yes, please!

But also, being able to get just the command arguments would be really useful. Such as, randint $low? $high or range &step=1 $low? $high, as a simple reminder of command usage. Much of the time, that is all that is really needed.

As an added bonus, show that info automatically, in the place otherwise used for showing syntax errors.

hanche avatar Nov 21 '21 06:11 hanche

Having an auto-popup of just the command line inputs would also be awesome. Perhaps that could be fleshed out as a separate feature once the help sources are available.

I suspect most help entries are nowhere near long enough to require a pager. help alone should probably just give its own use and settings, a general introduction and link to the online docs.

iandol avatar Nov 21 '21 07:11 iandol

Having an auto-popup of just the command line inputs would also be awesome. Perhaps that could be fleshed out as a separate feature once the help sources are available.

I'm reasonably certain any user could write this in elvish w/ clever use of the edit module, but it would only work for normal elvish functions.

Actually, would it be a better approach to attach the metadata we normally get with elvish fns (eg arg-names, body, def, etc) to the builtins? Then add an optional docstring key-value pair which normal users could also provide for their own functions?

crinklywrappr avatar Nov 25 '21 09:11 crinklywrappr

Actually, would it be a better approach to attach the metadata we normally get with elvish fns (eg arg-names, body, def, etc) to the builtins? Then add an optional docstring key-value pair which normal users could also provide for their own functions?

Yes! I have wanted that for ages, but didn't ask because I was afraid of asking too much, maybe. But he who doesn't ask, doesn't get …

hanche avatar Nov 25 '21 10:11 hanche

@crinklywrappr and @hanche: Supporting introspection of builtins has already been requested, see #680.

krader1961 avatar Nov 25 '21 19:11 krader1961

@hanche and @crinklywrappr, I decided to start working on this. Introspection of builtins as requested by issue #680 might be useful for providing a simple one line command "usage" help text. But it is inadequate as a mechanism for resolving this issue since we also want to be able to provide a detailed explanation of the variable or command. And that is only possible by parsing the //elvdoc: comments in the source code. Also, obviously, issue #680 is not resolved. Whereas there is an established mechanism for extracting //elvdoc: comments from the source code. Which can be leveraged to provide both a one-liner help text and a more detailed help text.

krader1961 avatar Jun 29 '22 03:06 krader1961

See issue #738 for some important background regarding the current situation.

I was hoping to leave the text in Markdown format and have Elvish directly render the Markdown. However, importing https://github.com/MichaelMure/mdr more than doubled the size of the elvish binary: 9.3MB to 22MB on my primary development server. I also tried https://pkg.go.dev/github.com/quackduck/go-term-markdown which has been updated recently. It was almost as bad in terms of the increase in the size of the Elvish binary at just 1MB smaller than the previous package. Clearly, it isn't going to be possible to have Elvish directly translate Markdown to the terminal since we can't accept such huge increases in the size of the Elvish binary. 😞

If we want to retain some formatting the obvious alternative is the legacy UNIX "man" format. That, however, won't work on Windows unless MSYS2 is installed (and it's not clear we can get it to work even then). I'm also not happy with having to transcode from Markdown to the ancient Man syntax. Not to mention dealing with the differences in command line switches and behavior of the various man commands on BSD, AT&T, and GNU/Linux distros. Which is why I'd prefer a solution that doesn't involve shelling out to an external command.

A plain text solution as described in the opening comment isn't very difficult. But I would hope we could do better. We could use HTML but that would require the user install a CLI command like lynx to render the text. Which is no better, and arguably worse, than the man file format since we can reasonably expect a man command to be installed (albeit not on Windows).

I considered using ReStructuredText, either on the fly using Pandoc or permanently. Note that the Fish shell (another popular non-POSIX shell) uses it as its preferred format. However, AFAICT there is no actively maintained Go ReStructuredText (RST) package that would be suitable for our needs. The Fish shell project relies on the Sphinx project for its support of ReStructuredText but doesn't seem like a good fit for this project.

So I'm wondering what other people think about how best to expose the existing Markdown based variable and command documentation interactively. Should we start with the modest goal of exposing the documentation as plain text without regard to the actual size of the user's terminal?

krader1961 avatar Jun 30 '22 03:06 krader1961

I have a couple of ideas, both of which may be dumb.

  1. You can sidestep the problem of binary size by releasing a plug-in e.g. https://github.com/elves/sample-plugin

  2. You can echo raw markdown to the terminal and instruct users to pipe the text to their markdown renderer of choice.

The second approach is appealing to me, personally. It's begging for other people to promote elvish in their personal projects.

crinklywrappr avatar Jun 30 '22 04:06 crinklywrappr

@crinklywrappr, Thanks for the feedback. Plugins require Cgo and don't work on Windows. So that is not a viable solution. Requiring the user to feed the help output through another program to render it is, shall we say, not friendly. 😄

I was hoping to avoid hardcoding the line length and highlighting. That way the output could be dynamically adapted to the terminal width and whether a light or dark color scheme is desired. But I think for the first implementation I'll simply generate text with a fixed line length (80 chars) and hardcoded highlighting escape sequences based on the current Elvish editor (basically, the highlighting you see in the elv.sh documentation).

krader1961 avatar Jun 30 '22 22:06 krader1961

Something not already discussed is what should be the result of help unknown-command. Should it explicitly fail? Should it run man unknown-command? Something else?

krader1961 avatar Jul 04 '22 03:07 krader1961

Documentation for command not found?

crinklywrappr avatar Jul 04 '22 04:07 crinklywrappr

I think help unknown-command should just print a (possibly styled red or something) text saying the command is unknown, rather than try to be super helpful and run man. Once you go down that route, why not info?

And it should not raise an error either. The resulting noise is just annoying, and it is primarily for interactive use anyhow, not for use in a program.

But it might be useful to have an option to make it output the styled help text to the value stream, in case some user wants to do something creative with it. And if that option is given, it should raise an error if the command is not known.

hanche avatar Jul 04 '22 07:07 hanche

I've been chipping away at implementing a help command by focusing on the transformation of Github flavored Markdown to raw text suitable for display in a terminal using ANSI X3.64 escape sequences. I was sort of happy with the results of adding a "raw" option to the website/cmd/elvdoc and website/cmd/highlight programs. Having found https://github.com/Orange-OpenSource/pandoc-terminal-writer to handle other Markdown constructs I'm very excited that the help command will produce really good, rather than good enough, output.

@hanche, I'm ambivalent about special-casing help unknown-command to not raise an exception and instead simply write an error message to stderr. Doing so is arguably the more user friendly option but is contrary to how every other command behaves that reports an invalid set of arguments.

P.S., I'm also inclined to implement a search option similar to man -k in the initial help command implementation. I'm still mulling over whether that behavior should be the default if the string doesn't match a known Elvish variable or function or require an option to enable it. Obviously, there should be an option to enable searching the documentation for matching a string. The question is whether the lack of an exact match of a variable or function should automatically enable searching the documentation. I'm inclined to not enable searching the documentation if an exact match is not found but am ambivalent.

krader1961 avatar Jul 06 '22 03:07 krader1961

What do you think about using the edit module to make the help searchable & scrollable? Maybe bind to Ctrl+h?

crinklywrappr avatar Jul 06 '22 03:07 crinklywrappr

What do you think about using the edit module to make the help searchable & scrollable? Maybe bind to Ctrl+h?

That is a good question and proposal but outside the scope of simply implementing a help command that can be used like this: help randint or help &text=random. Once the core data structures are implemented and a CLI help command is available it is reasonable to extend the use of that information into a new "help mode" analogous to the "location" and "navigation" modes.

krader1961 avatar Jul 06 '22 04:07 krader1961

@krader1961 wrote:

I'm ambivalent about special-casing help unknown-command to not raise an exception and instead simply write an error message to stderr. Doing so is arguably the more user friendly option but is contrary to how every other command behaves that reports an invalid set of arguments.

Yes but … Elvish is a programming language as well as an interactive shell. All those other commands can and will be used in elvish programs, and so it is essential that they raise an exception when something goes wrong. The help command, on the other hand, is purely interactive. It really does not make much sense to include it in a larger program. And so, I don't see the need for it to raise an exception if you ask it for a help text that does not exist. (Or, if I am wrong and there is such a need, it could come with an &err option that will cause it to raise exceptions in this case. This should very rarely be needed, if ever.) If you use the command wrongly, for example, if you give it an option that it does not recognise, or too many arguments, or arguments of the wrong type, that is different. By all means, let it raise an exception in such cases.

By the way, it might be a good idea, once a help command exists, to allow giving it no arguments at all, in which case it could provide some basic information for new users to help them get going.

hanche avatar Jul 06 '22 20:07 hanche

I am under the impression the help command is non-interactive.

crinklywrappr avatar Jul 06 '22 22:07 crinklywrappr

I am under the impression the help command is non-interactive.

It's interactive in the sense the output is meant to be displayed on a terminal and read by a human. It is not expected that the output will be piped through any other Elvish command. Do you have some ideas for how the help command would be used in a pipeline (other than possibly piping its output into a pager like the less command) or in combination with other Elvish commands?

krader1961 avatar Jul 06 '22 22:07 krader1961

Do you have some ideas for how the help command would be used in a pipeline

I had the idea to pipe the builtin ns vars to fzf, and display the help result on the right.

crinklywrappr avatar Jul 06 '22 22:07 crinklywrappr

I had the idea to pipe the builtin ns vars to fzf, and display the help result on the right.

That sounds as if it is equivalent piping the output into a pager; e.g., help compare | less. Which is still interactive in the sense that the output is displayed in a terminal to be read by the user. As opposed to being processed by any of the other Elvish commands.

krader1961 avatar Jul 06 '22 23:07 krader1961

Yeah, I'm sure whatever you come up with will be great. I'm excited! 😄

crinklywrappr avatar Jul 06 '22 23:07 crinklywrappr

Okay, I've got a help implementation I'm finally happy with. There's still some work to do (mostly unit tests) but I think it's ready for testing by other people. You can build from https://github.com/krader1961/elvish/tree/issue-1432-help-command if you want to try it. Or contact me on the IM channels and I'll provide a binary for your platform if I can. The screenshot shows three ways to use the new help command.

Screen Shot 2022-07-10 at 18 52 25

krader1961 avatar Jul 11 '22 02:07 krader1961

Love it!

With the help epm: example, is that tab completion or did you press <enter>?

crinklywrappr avatar Jul 11 '22 02:07 crinklywrappr

With the help epm: example, is that tab completion or did you press ?

I pressed Enter to actually execute the help command. It should be straightforward to implement tab completion but I feel that should be a separate change since this is already huge (over 1100 lines if you exclude the generated documentation.go and over 2000 lines if you don't).

krader1961 avatar Jul 11 '22 03:07 krader1961

FWIW, I previously asked this question: Something not already discussed is what should be the result of help unknown-command. I decided to treat that as if the &search=$true option was used; i.e., perform an implicit search for functions/variables whose documentation includes the string. For example, help NoSuchText is equivalent to help &search NoSuchText (assuming, of course, that NoSuchText is not a function or variable in the documentation).

krader1961 avatar Jul 19 '22 04:07 krader1961

Note that @xiaq wants a different solution than @krader1961's nice pull request, so it seems we will have to wait longer for a working help system. At least as an end user, @krader1961's system worked wonderfully, I used it daily, and I hope waiting for a "better" architecture will not sink this into the indefinite future…

Here is @xiaq's planning comment:

https://github.com/elves/elvish/pull/1583#issuecomment-1257311073

iandol avatar Sep 30 '22 05:09 iandol

FWIW, I am cogitating on @xiaq's criticism of my pull-request. I disagree with the complaint about the added dependency on the glow command since it only requires the go command which is already a dependency to compile Elvish. However, it seems like the core complaint vis-a-vis the glow dependency, is that you can't just do go install ..... That should be easy to solve.

Regarding @xiaq's second point that my change doesn't address Elvish documentation from third-party modules that was explicitly a non-goal to keep the initial change a reasonable size. I also fail to see how implementing that capability solves the problem of rendering the //elvdoc: comments embedded in the source for the builtins since there is no requirement a user has the Elvish source installed. My intent was to follow up my first change with another one that introduced a command for the user to run that would generate something like a ~/.local/share/elvish/elvdoc.json file the builtin help command would incorporate in its output.

krader1961 avatar Oct 11 '22 03:10 krader1961

@xiaq also said "To be able to do 1, the hard part is teaching Elvish to render Markdown in the terminal." I wanted to do that because it would make it possible to respect the user's terminal width. However, as I noted in this comment the increase in size of the Elvish binary was prohibitive. I don't think trying to create a Markdown renderer limited to the current requirements of the Elvish builtins is

a) likely to result in significant decreases in the size of the Elvish binary due to supporting run-time rendering of Markdown, and

b) doing so means we're always going to have to deal with modifying the Elvish specific Markdown renderer to fix bugs and add support for Markdown features used by builtin documentation and third-party documentation.

The latter point seems like a good argument for not adding additional code to Elvish that is not central to its purpose. Rendering Markdown is something that many other projects, like glow, have already solved and are actively maintained. It's not clear why Elvish should take on the burden of maintaining its own Markdown rendering code since that capability is not central to its behavior.

krader1961 avatar Oct 11 '22 04:10 krader1961

The argument my original change doesn't allow you to just run go build ./cmd/elvish (or the equivalent go install) can be solved in two ways:

a) Stub the variables fnHelpDocs and varHelpDocs so that a normal make get only needs to generate code that populates those vars rather than define them.

b) Commit the artifacts created by make pkg/help/documentation.go into the repository.

I was going to implement solution (a) but am leaning towards (b). Yes, option (b) means there could be drift between the pre-generated documentation and the actual documentation if people commit changes that change the documentation without running make pkg/help/documentation.go. But it should be possible to guard against that via a CI step that verifies make pkg/help/documentation.go doesn't change the relevant artifacts. The question is how to make it easy for contributors to generate the documentation artifacts and include them in their commits without having to wait for the CI environment to tell them their change has problems and they need to regenerate the documentation. In other words, adding support for the help command shouldn't make it harder, or at least more error prone from a CI workflow perspective, for people to change the documentation of embedded commands.

krader1961 avatar Oct 20 '22 02:10 krader1961