elves / elvish

Powerful scripting language & versatile interactive shell
https://elv.sh/
BSD 2-Clause "Simplified" License
5.65k stars 299 forks source link

Add external command callbacks #1681

Open krader1961 opened 1 year ago

krader1961 commented 1 year ago

While working on resolving issue #1661 I noticed that pragma unknown-command = disallow mostly does what I needed and made it easy to identify most (all?) of the external commands used by the epm builtin. I say most because it does not warn about explicit external commands; e.g., e:cat. The epm builtin currently does not use such explicit external commands (AFAICT), but if it did the unknown-command pragma would not be useful for identifying all of the external commands it uses. A callback that is run before any external command is executed would solve this need and potentially have other uses. The callback should, obviously, be invoked with the external command path and any arguments. It's not clear whether the value stream output of the callback (interpreted as a bool value) should affect whether the command is actually run. If that feature were implemented it would presumably mean treating the external command as if it exited with a non-zero status if the callback indicated the external command should not be run. I don't see a good use-case for that behavior and am therefore not inclined to implement it but I think it's an open question.

krader1961 commented 1 year ago

Having slept on the idea I've decided there should be a $before-external-cmd and $after-external-cmd pair of callback lists.

In the before variant each callback in the list is called with a list composed of the full path to the command, the original command name, and all arguments. If it outputs $true the next callback, if any, is invoked or the command is run. If it outputs $false no remaining callbacks are run (including the "after" callbacks) and the external command is not run. The idea is that this allows a $before-external-cmd callback to substitute different behavior for the external command. Possibly via emulation using only Elvish builtins or via some other external command (which itself is subject to the callbacks). Care needs to be taken to avoid infinite recursion. At a minimum a sane limit on callback recursion needs to be imposed by Elvish itself.

In the after variant each callback in the list is called with the time in fractional seconds the external command ran to completion, its numeric exit status, the full path to the command, the original command name, and all arguments. The after variant is primarily meant for logging which external commands are run but there may be other uses.

krader1961 commented 1 year ago

The callbacks should include additional info to make it easier to know the context in which the external command is being run; e.g., which module, script, or interactive context invoking the external command. This came to light in response to a change proposed by @xiaq to my PR #1680 to replace some external commands in the epm module with builtins.