diff --git a/CommandDotNet.Example/CommandDotNet.Example.csproj b/CommandDotNet.Example/CommandDotNet.Example.csproj index 009a3beb8..f174f5402 100644 --- a/CommandDotNet.Example/CommandDotNet.Example.csproj +++ b/CommandDotNet.Example/CommandDotNet.Example.csproj @@ -8,6 +8,7 @@ + diff --git a/CommandDotNet.Example/Examples.cs b/CommandDotNet.Example/Examples.cs index e3c7a4a30..17bc1e3d3 100644 --- a/CommandDotNet.Example/Examples.cs +++ b/CommandDotNet.Example/Examples.cs @@ -14,27 +14,6 @@ namespace CommandDotNet.Example "Example: %AppName% [debug] [parse] [log:info] cancel-me")] internal class Examples { - private static bool _inSession; - - [DefaultMethod] - public void StartSession( - CommandContext context, - InteractiveSession interactiveSession, - [Option(ShortName = "i")] bool interactive) - { - if (interactive && !_inSession) - { - context.Console.WriteLine("start session"); - _inSession = true; - interactiveSession.Start(); - } - else - { - context.Console.WriteLine($"no session {interactive} {_inSession}"); - context.ShowHelpOnExit = true; - } - } - [SubCommand] public Git Git { get; set; } = null!; diff --git a/CommandDotNet.Example/InteractiveMiddleware.cs b/CommandDotNet.Example/InteractiveMiddleware.cs deleted file mode 100644 index 57c21f627..000000000 --- a/CommandDotNet.Example/InteractiveMiddleware.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace CommandDotNet.Example -{ - public static class InteractiveMiddleware - { - public static AppRunner UseInteractiveMode(this AppRunner appRunner, string appName) - { - return appRunner.Configure(c => - { - // use the existing appRunner to reuse the configuration. - c.UseParameterResolver(ctx => new InteractiveSession(appRunner, appName, ctx)); - }); - } - } -} \ No newline at end of file diff --git a/CommandDotNet.Example/Program.cs b/CommandDotNet.Example/Program.cs index eb615d2b7..507e6ae96 100644 --- a/CommandDotNet.Example/Program.cs +++ b/CommandDotNet.Example/Program.cs @@ -2,6 +2,7 @@ using CommandDotNet.Diagnostics; using CommandDotNet.FluentValidation; using CommandDotNet.NameCasing; +using CommandDotNet.ReadLineRepl; namespace CommandDotNet.Example { @@ -24,7 +25,7 @@ public static AppRunner GetAppRunner(NameValueCollection? appSettings = null) .UseLog2ConsoleDirective() .UseNameCasing(Case.KebabCase) .UseFluentValidation() - .UseInteractiveMode("Example") + .UseRepl(replConfig: new ReplConfig {AppName = "Example", ReplOption = {LongName = "interactive", ShortName = 'i'}}) .UseDefaultsFromAppSetting(appSettings, includeNamingConventions: true); } } diff --git a/CommandDotNet.ReadLineRepl/CommandDotNet.ReadLineRepl.csproj b/CommandDotNet.ReadLineRepl/CommandDotNet.ReadLineRepl.csproj new file mode 100644 index 000000000..a4bbd50c6 --- /dev/null +++ b/CommandDotNet.ReadLineRepl/CommandDotNet.ReadLineRepl.csproj @@ -0,0 +1,27 @@ + + + + CommandDotNet.ReadLineRepl + Provides an interactive session using the GNU style readline package + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/CommandDotNet.ReadLineRepl/MiddlewareSteps.cs b/CommandDotNet.ReadLineRepl/MiddlewareSteps.cs new file mode 100644 index 000000000..8b582a393 --- /dev/null +++ b/CommandDotNet.ReadLineRepl/MiddlewareSteps.cs @@ -0,0 +1,9 @@ +using CommandDotNet.Execution; + +namespace CommandDotNet.ReadLineRepl +{ + public static class MiddlewareSteps + { + public static MiddlewareStep ReplSession { get; } = CommandDotNet.Execution.MiddlewareSteps.Help.CheckIfShouldShowHelp - 1000; + } +} \ No newline at end of file diff --git a/CommandDotNet.ReadLineRepl/ReplConfig.cs b/CommandDotNet.ReadLineRepl/ReplConfig.cs new file mode 100644 index 000000000..bb703c573 --- /dev/null +++ b/CommandDotNet.ReadLineRepl/ReplConfig.cs @@ -0,0 +1,51 @@ +using System; +using CommandDotNet.Builders; + +namespace CommandDotNet.ReadLineRepl +{ + public class ReplConfig + { + private Func? _sessionInitMessage; + private Func? _sessionHelpMessage; + + public string? AppName { get; set; } + + public ReplOption ReplOption { get; set; } = new ReplOption(); + + public Func GetSessionInitMessage + { + get => _sessionInitMessage ?? BuildSessionInit ; + set => _sessionInitMessage = value ?? throw new ArgumentNullException(nameof(value)); + } + + + public Func GetSessionHelpMessage + { + get => _sessionHelpMessage ?? BuildSessionHelp; + set => _sessionHelpMessage = value ?? throw new ArgumentNullException(nameof(value)); + } + + private string BuildSessionInit(CommandContext context) + { + var appInfo = AppInfo.GetAppInfo(); + return @$"{AppName ?? appInfo.FileName} {appInfo.Version} +Type 'help' to see interactive options +{BuildSessionHelp(context)}"; + } + + private string BuildSessionHelp(CommandContext context) + { + return @"Type '-h' or '--help' for the list of commands +Type 'exit', 'quit' or 'Ctrl+C + Enter' to exit."; + } + } + + public class ReplOption + { + public string? LongName { get; set; } + public char? ShortName { get; set; } + public string? Description { get; set; } + + internal bool IsRequested => LongName is { } || ShortName is { }; + } +} \ No newline at end of file diff --git a/CommandDotNet.ReadLineRepl/ReplMiddleware.cs b/CommandDotNet.ReadLineRepl/ReplMiddleware.cs new file mode 100644 index 000000000..f803bc1f9 --- /dev/null +++ b/CommandDotNet.ReadLineRepl/ReplMiddleware.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading.Tasks; +using CommandDotNet.Execution; + +namespace CommandDotNet.ReadLineRepl +{ + public static class ReplMiddleware + { + public static AppRunner UseRepl(this AppRunner appRunner, ReplConfig? replConfig = null) + { + ReadLine.HistoryEnabled = true; + + replConfig ??= new ReplConfig(); + return appRunner.Configure(c => + { + c.UseMiddleware(ReplSession, MiddlewareSteps.ReplSession); + // use the existing appRunner to reuse the configuration. + c.UseParameterResolver(ctx => new ReplSession(appRunner, replConfig, ctx)); + + var config = new Config(appRunner, replConfig); + c.Services.Add(config); + + var replOption = replConfig.ReplOption; + if (replOption?.IsRequested ?? false) + { + var option = new Option(replOption!.LongName, replOption.ShortName, TypeInfo.Flag, ArgumentArity.Zero) + { + Description = replOption.Description + }; + config.Option = option; + + c.BuildEvents.OnCommandCreated += args => + { + var builder = args.CommandBuilder; + + // do not include option if already in a session + if (!config.InSession && builder.Command.IsRootCommand()) + { + builder.AddArgument(option); + } + }; + } + }); + } + + private class Config + { + public AppRunner AppRunner { get; } + public ReplConfig ReplConfig { get; } + public bool InSession { get; set; } + public Option? Option { get; set; } + + public Config(AppRunner appRunner, ReplConfig replConfig) + { + AppRunner = appRunner ?? throw new ArgumentNullException(nameof(appRunner)); + ReplConfig = replConfig ?? throw new ArgumentNullException(nameof(replConfig)); + } + } + + private static Task ReplSession(CommandContext ctx, ExecutionDelegate next) + { + var parseResult = ctx.ParseResult!; + var cmd = parseResult.TargetCommand; + if (cmd.IsRootCommand() + && !cmd.IsExecutable + && parseResult.ParseError is null + && !parseResult.HelpWasRequested()) + { + var config = ctx.AppConfig.Services.GetOrThrow(); + var option = config.Option; + if (!config.InSession && (option is null || cmd.HasInputValues(option.Name))) + { + config.InSession = true; + new ReplSession(config.AppRunner, config.ReplConfig, ctx).Start(); + return ExitCodes.Success; + } + } + + return next(ctx); + } + } +} \ No newline at end of file diff --git a/CommandDotNet.Example/InteractiveSession.cs b/CommandDotNet.ReadLineRepl/ReplSession.cs similarity index 67% rename from CommandDotNet.Example/InteractiveSession.cs rename to CommandDotNet.ReadLineRepl/ReplSession.cs index b7c54fd03..10647c11f 100644 --- a/CommandDotNet.Example/InteractiveSession.cs +++ b/CommandDotNet.ReadLineRepl/ReplSession.cs @@ -1,20 +1,19 @@ using System; using System.Linq; -using CommandDotNet.Builders; using CommandDotNet.Tokens; -namespace CommandDotNet.Example +namespace CommandDotNet.ReadLineRepl { - public class InteractiveSession + public class ReplSession { private readonly AppRunner _appRunner; - private readonly string _appName; + private readonly ReplConfig _replConfig; private readonly CommandContext _context; - public InteractiveSession(AppRunner appRunner, string appName, CommandContext context) + public ReplSession(AppRunner appRunner, ReplConfig replConfig, CommandContext context) { _appRunner = appRunner; - _appName = appName; + _replConfig = replConfig; _context = context; } @@ -29,7 +28,10 @@ public void Start() pressedCtrlC = true; }; - PrintSessionInit(); + var sessionInitMessage = _replConfig.GetSessionInitMessage(_context); + var sessionHelpMessage = _replConfig.GetSessionHelpMessage(_context); + + console.WriteLine(sessionInitMessage); bool pendingNewLine = false; void Write(string? value = null) @@ -56,7 +58,7 @@ void EnsureNewLine() { EnsureNewLine(); Write(">>>"); - var input = console.In.ReadLine(); + var input = ReadLine.Read(); if (input is null || pressedCtrlC) { pressedCtrlC = false; @@ -79,7 +81,7 @@ void EnsureNewLine() case "quit": return; case "help": - PrintSessionHelp(); + console.WriteLine(sessionHelpMessage); continue; } if (singleArg == Environment.NewLine) @@ -93,22 +95,5 @@ void EnsureNewLine() } EnsureNewLine(); } - - private void PrintSessionInit() - { - var appInfo = AppInfo.GetAppInfo(_context); - var console = _context.Console; - console.WriteLine($"{_appName} {appInfo.Version}"); - console.WriteLine("Type 'help' to see interactive options"); - console.WriteLine("Type '-h' or '--help' to options for commands"); - console.WriteLine("Type 'exit', 'quit' or 'Ctrl+C' to exit."); - } - - private void PrintSessionHelp() - { - var console = _context.Console; - console.WriteLine("Type '-h' or '--help' to options for commands"); - console.WriteLine("Type 'exit', 'quit' or 'Ctrl+C' to exit."); - } } } \ No newline at end of file diff --git a/CommandDotNet.sln b/CommandDotNet.sln index 92b23f185..6aebd6b34 100644 --- a/CommandDotNet.sln +++ b/CommandDotNet.sln @@ -40,6 +40,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandDotNet.Example.Tests EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandDotNet.DataAnnotations", "CommandDotNet.DataAnnotations\CommandDotNet.DataAnnotations.csproj", "{558AA426-06D4-4FC9-B2E5-B6742F0A5D77}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandDotNet.ReadLineRepl", "CommandDotNet.ReadLineRepl\CommandDotNet.ReadLineRepl.csproj", "{1149E03F-FF07-448A-BB42-82876459B138}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -94,6 +96,10 @@ Global {558AA426-06D4-4FC9-B2E5-B6742F0A5D77}.Debug|Any CPU.Build.0 = Debug|Any CPU {558AA426-06D4-4FC9-B2E5-B6742F0A5D77}.Release|Any CPU.ActiveCfg = Release|Any CPU {558AA426-06D4-4FC9-B2E5-B6742F0A5D77}.Release|Any CPU.Build.0 = Release|Any CPU + {1149E03F-FF07-448A-BB42-82876459B138}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1149E03F-FF07-448A-BB42-82876459B138}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1149E03F-FF07-448A-BB42-82876459B138}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1149E03F-FF07-448A-BB42-82876459B138}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE