Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CommandDotNet.Example.Tests/ExampleCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public void Help_Should_Include_ExtendedHelpText()
Program.GetAppRunner()
.Verify(new Scenario
{
When = {Args = null},
When = {Args = "-h"},
Then =
{
OutputContainsTexts =
Expand All @@ -41,7 +41,7 @@ public void Should_Include_Version_Option()
Program.GetAppRunner()
.Verify(new Scenario
{
When = { Args = null },
When = {Args = "-h"},
Then =
{
OutputContainsTexts =
Expand Down
37 changes: 21 additions & 16 deletions CommandDotNet.Example/Examples.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CommandDotNet.Example.Commands;
using CommandDotNet.Repl;
using Git = CommandDotNet.Example.Commands.Git;

namespace CommandDotNet.Example
Expand All @@ -17,24 +18,28 @@ namespace CommandDotNet.Example
// end-snippet
internal class Examples
{
private static bool _inSession;

[DefaultCommand]
public void StartSession(
CommandContext context,
InteractiveSession interactiveSession,
[Option('i')] bool interactive)
[Subcommand]
public class Sessions
{
if (interactive && !_inSession)
{
context.Console.WriteLine("start session");
_inSession = true;
interactiveSession.Start();
}
else
private static bool _inSession;

[DefaultCommand]
public void StartSession(
CommandContext context,
ReplSession replSession,
[Option('i')] bool interactive)
{
context.Console.WriteLine($"no session {interactive} {_inSession}");
context.ShowHelpOnExit = true;
if (interactive && !_inSession)
{
context.Console.WriteLine("start session");
_inSession = true;
replSession.Start();
}
else
{
context.Console.WriteLine($"no session {interactive} {_inSession}");
context.ShowHelpOnExit = true;
}
}
}

Expand Down
14 changes: 0 additions & 14 deletions CommandDotNet.Example/InteractiveMiddleware.cs

This file was deleted.

4 changes: 3 additions & 1 deletion CommandDotNet.Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using CommandDotNet.FluentValidation;
using CommandDotNet.NameCasing;
using CommandDotNet.Spectre;
using CommandDotNet.Repl;

namespace CommandDotNet.Example
{
Expand Down Expand Up @@ -30,7 +31,8 @@ public static AppRunner GetAppRunner(NameValueCollection? appConfigSettings = nu
.UseLocalizeDirective()
.UseLog2ConsoleDirective()
.UseFluentValidation()
.UseInteractiveMode("Example")
//.UseRepl()
.UseRepl(replOptionInfoForRootCommand: ReplOptionInfo.Default)
.UseDefaultsFromAppSetting(appConfigSettings, includeNamingConventions: true);
}
}
Expand Down
11 changes: 11 additions & 0 deletions CommandDotNet.Tests/FeatureTests/Repl/ReplTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
using CommandDotNet.TestTools.Scenarios;
using Xunit;
using Xunit.Abstractions;

namespace CommandDotNet.Tests.FeatureTests.Repl
{

}
2 changes: 2 additions & 0 deletions CommandDotNet/Execution/MiddlewareSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public static class Help
public static MiddlewareStep PrintHelpOnExit { get; } = DependencyResolver.BeginScope + 1000;
}

public static MiddlewareStep ReplSession { get; } = Help.CheckIfShouldShowHelp - 2000;

public static MiddlewareStep PipedInput { get; } =
new(MiddlewareStages.PostParseInputPreBindValues, 0);

Expand Down
6 changes: 6 additions & 0 deletions CommandDotNet/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ public static string ToStringFromPublicProperties(this object item, Indent? inde
: value?.ToString();
}

internal static T CloneWithPublicProperties<T>(this T original, bool recurse = true)
where T: class
{
return (T) ((object)original).CloneWithPublicProperties(recurse);
}

internal static object CloneWithPublicProperties(this object original, bool recurse = true)
{
if (original == null)
Expand Down
68 changes: 68 additions & 0 deletions CommandDotNet/Repl/ReplConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using CommandDotNet.Builders;

namespace CommandDotNet.Repl
{
public class ReplConfig
{
// TEST:
// Default ReplConfig
// - uses ReplOptionInfo.Default
// - Default PromptTextCallback
// - Default SessionInitMessageCallback
// - uses Config.AppName ?? UsageAppName ?? AppInfo.FileName
// - includes AppInfo.Version
// - includes DefaultSessionHelp
// - Default SessionHelpMessageCallback
// - Default ReadLine is ctx.Console.In.ReadLine
// - cannot set to null: ReadLine, PromptTextCallback, SessionInitMessageCallback, SessionHelpMessageCallback
// - can set to null: ReplOptionInfo, AppName

public string? AppName { get; set; }

private Func<CommandContext, string?>? _readLine;
public Func<CommandContext, string?> ReadLine
{
get => _readLine ?? (ctx => ctx.Console.In.ReadLine());
set => _readLine = value ?? throw new ArgumentNullException(nameof(value));
}

private Func<CommandContext, string?>? _promptTextCallback;
public Func<CommandContext, string?> PromptTextCallback
{
get => _promptTextCallback ?? (ctx => ">>> ");
set => _promptTextCallback = value ?? throw new ArgumentNullException(nameof(value));
}

private Func<CommandContext, string>? _sessionInitMessage;
public Func<CommandContext, string> SessionInitMessageCallback
{
get => _sessionInitMessage ?? DefaultSessionInit;
set => _sessionInitMessage = value ?? throw new ArgumentNullException(nameof(value));
}

private Func<CommandContext, string>? _sessionHelpMessage;
public Func<CommandContext, string> SessionHelpMessageCallback
{
get => _sessionHelpMessage ?? DefaultSessionHelp;
set => _sessionHelpMessage = value ?? throw new ArgumentNullException(nameof(value));
}

private string DefaultSessionInit(CommandContext context)
{
var appInfo = AppInfo.Instance;
var appName = AppName
?? context.AppConfig.AppSettings.Help.UsageAppName
?? appInfo.FileName;
return @$"{appName} {appInfo.Version}
Type 'help' to see interactive options
{DefaultSessionHelp(context)}";
}

private string DefaultSessionHelp(CommandContext context)
{
return @"Type '-h' or '--help' for the list of commands
Type 'exit', 'quit' or 'Ctrl+C then Enter' to exit.";
}
}
}
127 changes: 127 additions & 0 deletions CommandDotNet/Repl/ReplMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
using System;
using System.Threading.Tasks;
using CommandDotNet.Execution;
using CommandDotNet.Extensions;

namespace CommandDotNet.Repl
{
public static class ReplMiddleware
{
public static AppRunner UseRepl(this AppRunner appRunner,
ReplConfig? replConfig = null,
ReplOptionInfo? replOptionInfoForRootCommand = null)
{
/*
* TEST:
* - ReplSession with parameter resolver
* -
*
* - when using option
* - show option only
* - for root command
* - while not in a session
*/

replConfig ??= new ReplConfig();

return appRunner.Configure(c =>
{
var config = new Config(appRunner, replConfig);
c.Services.Add(config);

c.UseParameterResolver(ctx => new ReplSession(appRunner, replConfig.CloneWithPublicProperties(), ctx));

if (replOptionInfoForRootCommand is not null)
{
c.UseMiddleware(ReplSession, MiddlewareSteps.ReplSession);

var option = new Option(replOptionInfoForRootCommand.LongName, replOptionInfoForRootCommand.ShortName, TypeInfo.Flag, ArgumentArity.Zero)
{
Description = replOptionInfoForRootCommand.Description
};
config.Option = option;

c.BuildEvents.OnCommandCreated += args =>
{
var builder = args.CommandBuilder;

// do not include option if already in a session
var command = builder.Command;
if (!config.InSession && command.IsRootCommand())
{
if (command.IsExecutable)
{
throw new InvalidConfigurationException($"Root command {command.Name} has been defined as executable using [DefaultCommand] on method {command.DefinitionSource}. This is not suitable for hosting an interactive session. " +
"Either: A) Do not use this method as the default for the root command " +
$"or B) do not define {nameof(replOptionInfoForRootCommand)} and define a " +
$"command with a parameter of type {nameof(ReplSession)} to initiate the session.");
}
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<int> ReplSession(CommandContext ctx, ExecutionDelegate next)
{

/* Test:
* - session not entered when
* - ParseError
* - Help requested
* - already in session
* - option is { } but not provided
* - option is null & cmd is root and not executable
*
* - should we check if option is specified but other options also provided?
* - No! The app may have provided other options as a config for the session,
* available via ReplSession.SessionContext
*/
var parseResult = ctx.ParseResult!;
var cmd = parseResult.TargetCommand;

if (parseResult.ParseError is not null
|| parseResult.HelpWasRequested())
{
return next(ctx);
}

var config = ctx.AppConfig.Services.GetOrThrow<Config>();
if (config.InSession)
{
return next(ctx);
}

var option = config.Option;

bool ReplSessionWasRequested()
{
return cmd.HasInputValues(option.Name);
}

if (option is not null && cmd.IsRootCommand() && ReplSessionWasRequested())
{
config.InSession = true;
new ReplSession(config.AppRunner, config.ReplConfig.CloneWithPublicProperties(), ctx).Start();
return ExitCodes.Success;
}

return next(ctx);
}
}
}
24 changes: 24 additions & 0 deletions CommandDotNet/Repl/ReplOptionInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace CommandDotNet.Repl
{
public class ReplOptionInfo
{
public static readonly ReplOptionInfo Default = new (
"interactive", 'i', "enter an interactive session");

public string? LongName { get; }
public char? ShortName { get; }
public string? Description { get; }

public ReplOptionInfo(string? longName = null, char? shortName = null, string? description = null)
{
LongName = longName;
ShortName = shortName;
Description = description;

if (longName is null && shortName is null)
{
throw new InvalidConfigurationException($"must define either {longName} or {shortName} or both");
}
}
}
}
Loading