Cli nested commands - #7
Merged
Merged
Conversation
Run() always captures, which is the inverse of what a shell does. In bash, `ssh host` or `vim` takes over the terminal and you opt into capture with $(...). CShell had no way to express the default case, so anything that owns the terminal - ssh, vim, git rebase -i, gh auth login, an agent CLI - either hung on a stdin pipe nothing would write to, or drew a full screen UI into a pipe where nobody could see it. Run()'s own remarks already described the trap; this gives it a way out. Exec() returns Task<int> rather than a Command because there is genuinely nothing to expose: with no redirection, StandardOutput, PipeTo and RedirectTo would all have to throw. A separate method whose return type says so beats a Command that is half unusable. ExecOptions is likewise smaller than Shell.Options - an attached process has nothing to redirect, pipe or encode - and keeps working directory, environment, cancellation, and a StartInfo escape hatch. ConsoleState restores the terminal afterwards. A full screen program turns off echo and line input on the way in and restores them on the way out, unless it is killed - leaving the console silently swallowing keystrokes and the user typing a command they cannot see to fix it. Windows uses GetConsoleMode/SetConsoleMode; Unix shells out to `stty -g`, which avoids a pile of termios interop for the one thing needed. Since Exec exists to run exactly that sort of program, and to be interrupted, the library is the right place for it rather than every caller. Verified against a real caller: Airlock's interactive ssh hand-off now goes through ExecAsync, and a full session including a remote `exit 42` comes back intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQo1wkkf7nu2n1BZN3Uozm
Cli's stated ceiling was "no subcommands", so a script with verbs had to take the verb as a positional and hand-roll a second parse per branch -- losing generated help, losing the unknown-switch error, and putting every verb's switches in one undifferentiated list. Command(name, help, c => ...) gives a verb a command line of its own, and because the thing handed to the lambda is a Cli, commands nest: `svc nuget push` reads the way `dotnet nuget push` does. Every level has its own help, its own unknown-switch error, and its own name in the message. The lambda runs only for the command that was actually typed. That is what makes typed out-variables safe inside it -- nothing binds for a command nobody asked for -- and it lets --help list the commands without building any of them. Run()/RunAsync() declare what a command does, beside the declarations it uses, because an out variable cannot be read after its lambda returns. RunAsync is a separate name rather than a Run overload so that `Run(async () => ...)` cannot bind to the void one as an async void that nobody awaits; it pairs with ParseAsync(), and Parse() throws and says so. What a handler returns lands on CliResult.ExitCode without exiting the process; ShouldExit still tells that apart from a line that could not be read. The parse boundary is the first token that is not a switch, which is the same rule Rest() already uses, and it is only unambiguous because an option's value attaches. Switches at a command level are globals, readable from the command's own result; a typed one must be declared after the first Command(), since before then the parser does not know the line splits. One typed after the command word is refused with the fix rather than a dead end: svc start: '--verbose' is a global switch -- write it before the command: 'svc --verbose start'. CliResult gains Command, CommandPath, Parent and HandlerRan, and its Switch, Option and WhatIf walk the parent chain. Nothing changes for a Cli that declares no commands: all 110 existing Cli tests pass untouched, alongside 47 new ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vga8G5aDrA2qYn8BSCgTsd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces several significant enhancements and new features to the CShell library, most notably the addition of an
ExecAPI for running processes attached to the current console, and major improvements to theClicommand line parser including support for nested commands. The documentation and tests have also been updated to reflect these new capabilities.New Process Execution API:
Exec()andExecAsync()methods to theCShellclass to run external processes attached to the current console, returning their exit code and supporting options such as working directory, environment variables, and cancellation. This allows for interactive programs and better shell-like behavior. (src/CShell.cssrc/CShell.csR160-R292)Run(),Exec(), andStart(), including when to use each method and their effects on input/output redirection. (README.mdREADME.mdL200-R299)Exec()andExecAsync(), covering exit codes, working directory, environment, argument handling, cancellation, and error cases. (Tests/CShell.Tests/Exec.Tests.csTests/CShell.Tests/Exec.Tests.csR1-R168)Command Line Parser Improvements:
Cliparser to support nested commands (verbs), allowing for complex command hierarchies similar to tools likedotnet. Each command can have its own options, help, and handlers, and global switches are supported. (README.md[1]src/CliResult.cs[2] [3]CliResultclass to track command hierarchy, parent commands, and handler execution status, enabling better error reporting and help generation. (src/CliResult.cs[1] [2]README.md. (README.mdREADME.mdL179-R251)Versioning and Release Notes:
3.1.0and added a changelog entry describing the new features. (src/CShell.csproj[1]README.md[2]These changes make CShell more powerful and flexible for scripting and command line tool development, with improved process control and modern CLI patterns.