Skip to content

Commit 301a914

Browse files
baronfelMiYanni
andauthored
Enable --interactive by default for sessions that are local. (#47226)
Co-authored-by: Michael Yanni <[email protected]>
1 parent 0204a8b commit 301a914

34 files changed

+510
-421
lines changed

src/BuiltInTools/dotnet-watch/CommandLineOptions.cs

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
54
using System.Collections.Immutable;
65
using System.CommandLine;
76
using System.CommandLine.Parsing;
@@ -32,15 +31,16 @@ internal sealed class CommandLineOptions
3231

3332
public string Command => ExplicitCommand ?? DefaultCommand;
3433

34+
// this option is referenced from inner logic and so needs to be reference-able
35+
public static CliOption<bool> NonInteractiveOption = new CliOption<bool>("--non-interactive") { Description = Resources.Help_NonInteractive, Arity = ArgumentArity.Zero };
36+
3537
public static CommandLineOptions? Parse(IReadOnlyList<string> args, IReporter reporter, TextWriter output, out int errorCode)
3638
{
3739
// dotnet watch specific options:
38-
39-
var quietOption = new CliOption<bool>("--quiet", "-q") { Description = Resources.Help_Quiet };
40-
var verboseOption = new CliOption<bool>("--verbose") { Description = Resources.Help_Verbose };
41-
var listOption = new CliOption<bool>("--list") { Description = Resources.Help_List };
42-
var noHotReloadOption = new CliOption<bool>("--no-hot-reload") { Description = Resources.Help_NoHotReload };
43-
var nonInteractiveOption = new CliOption<bool>("--non-interactive") { Description = Resources.Help_NonInteractive };
40+
var quietOption = new CliOption<bool>("--quiet", "-q") { Description = Resources.Help_Quiet, Arity = ArgumentArity.Zero };
41+
var verboseOption = new CliOption<bool>("--verbose") { Description = Resources.Help_Verbose, Arity = ArgumentArity.Zero };
42+
var listOption = new CliOption<bool>("--list") { Description = Resources.Help_List, Arity = ArgumentArity.Zero };
43+
var noHotReloadOption = new CliOption<bool>("--no-hot-reload") { Description = Resources.Help_NoHotReload, Arity = ArgumentArity.Zero };
4444

4545
verboseOption.Validators.Add(v =>
4646
{
@@ -54,17 +54,16 @@ internal sealed class CommandLineOptions
5454
[
5555
quietOption,
5656
verboseOption,
57-
noHotReloadOption,
58-
nonInteractiveOption,
5957
listOption,
58+
noHotReloadOption,
59+
NonInteractiveOption
6060
];
6161

6262
// Options we need to know about that are passed through to the subcommand:
63-
6463
var shortProjectOption = new CliOption<string>("-p") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
6564
var longProjectOption = new CliOption<string>("--project") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
6665
var launchProfileOption = new CliOption<string>("--launch-profile", "-lp") { Hidden = true, Arity = ArgumentArity.ZeroOrOne, AllowMultipleArgumentsPerToken = false };
67-
var noLaunchProfileOption = new CliOption<bool>("--no-launch-profile") { Hidden = true };
66+
var noLaunchProfileOption = new CliOption<bool>("--no-launch-profile") { Hidden = true, Arity = ArgumentArity.Zero };
6867

6968
var rootCommand = new CliRootCommand(Resources.Help)
7069
{
@@ -159,7 +158,7 @@ internal sealed class CommandLineOptions
159158
{
160159
Quiet = parseResult.GetValue(quietOption),
161160
NoHotReload = parseResult.GetValue(noHotReloadOption),
162-
NonInteractive = parseResult.GetValue(nonInteractiveOption),
161+
NonInteractive = parseResult.GetValue(NonInteractiveOption),
163162
Verbose = parseResult.GetValue(verboseOption),
164163
},
165164

@@ -188,11 +187,18 @@ private static IReadOnlyList<string> GetCommandArguments(
188187
// skip watch options:
189188
if (!watchOptions.Contains(optionResult.Option))
190189
{
191-
Debug.Assert(optionResult.IdentifierToken != null);
190+
if (optionResult.Option.Name.Equals("--interactive", StringComparison.Ordinal) && parseResult.GetValue(NonInteractiveOption))
191+
{
192+
// skip forwarding the interactive token (which may be computed by default) when users pass --non-interactive to watch itself
193+
continue;
194+
}
192195

196+
// Some options _may_ be computed or have defaults, so not all may have an IdentifierToken.
197+
// For those that do not, use the Option's Name instead.
198+
var optionNameToForward = optionResult.IdentifierToken?.Value ?? optionResult.Option.Name;
193199
if (optionResult.Tokens.Count == 0)
194200
{
195-
arguments.Add(optionResult.IdentifierToken.Value);
201+
arguments.Add(optionNameToForward);
196202
}
197203
else if (optionResult.Option.Name == "--property")
198204
{
@@ -201,14 +207,14 @@ private static IReadOnlyList<string> GetCommandArguments(
201207
// While dotnet-build allows "/p Name=Value", dotnet-msbuild does not.
202208
// Any command that forwards args to dotnet-msbuild will fail if we don't use colon.
203209
// See https://github.com/dotnet/sdk/issues/44655.
204-
arguments.Add($"{optionResult.IdentifierToken.Value}:{token.Value}");
210+
arguments.Add($"{optionNameToForward}:{token.Value}");
205211
}
206212
}
207213
else
208214
{
209215
foreach (var token in optionResult.Tokens)
210216
{
211-
arguments.Add(optionResult.IdentifierToken.Value);
217+
arguments.Add(optionNameToForward);
212218
arguments.Add(token.Value);
213219
}
214220
}

src/Cli/dotnet/CommonOptions.cs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,28 @@ public static CliArgument<string> DefaultToCurrentDirectory(this CliArgument<str
116116
Arity = ArgumentArity.Zero
117117
}.ForwardAs("-restore:false");
118118

119-
public static CliOption<bool> InteractiveMsBuildForwardOption =
120-
new ForwardedOption<bool>("--interactive")
121-
{
122-
Description = CommonLocalizableStrings.CommandInteractiveOptionDescription,
123-
Arity = ArgumentArity.Zero
124-
}.ForwardAs("-property:NuGetInteractive=true");
119+
private static bool IsCIEnvironmentOrRedirected() =>
120+
new Telemetry.CIEnvironmentDetectorForTelemetry().IsCIEnvironment() || Console.IsOutputRedirected;
121+
122+
/// <summary>
123+
/// A 'template' for interactive usage across the whole dotnet CLI. Use this as a base and then specialize it for your use cases.
124+
/// Despite being a 'forwarded option' there is no default forwarding configured, so if you want forwarding you can add it on a per-command basis.
125+
/// </summary>
126+
/// <param name="acceptArgument">Whether the option accepts an boolean argument. If false, the option will be a flag.</param>
127+
/// <remarks>
128+
// If not set by a user, this will default to true if the user is not in a CI environment as detected by <see cref="Telemetry.CIEnvironmentDetectorForTelemetry.IsCIEnvironment"/>.
129+
// If this is set to function as a flag, then there is no simple user-provided way to circumvent the behavior.
130+
// </remarks>
131+
public static ForwardedOption<bool> InteractiveOption(bool acceptArgument = false) =>
132+
new("--interactive")
133+
{
134+
Description = CommonLocalizableStrings.CommandInteractiveOptionDescription,
135+
Arity = acceptArgument ? ArgumentArity.ZeroOrOne : ArgumentArity.Zero,
136+
// this default is called when no tokens/options are passed on the CLI args
137+
DefaultValueFactory = (ar) => IsCIEnvironmentOrRedirected()
138+
};
139+
140+
public static CliOption<bool> InteractiveMsBuildForwardOption = InteractiveOption(acceptArgument: true).ForwardAsSingle(b => $"-property:NuGetInteractive={(b ? "true" : "false")}");
125141

126142
public static CliOption<bool> DisableBuildServersOption =
127143
new ForwardedOption<bool>("--disable-build-servers")

src/Cli/dotnet/OptionForwardingExtensions.cs

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
using System.CommandLine.Parsing;
66
using System.CommandLine.StaticCompletions;
77

8+
#nullable enable
9+
810
namespace Microsoft.DotNet.Cli
911
{
1012
public static class OptionForwardingExtensions
1113
{
12-
public static ForwardedOption<T> Forward<T>(this ForwardedOption<T> option) => option.SetForwardingFunction((T o) => new string[] { option.Name });
14+
public static ForwardedOption<T> Forward<T>(this ForwardedOption<T> option) => option.SetForwardingFunction((T? o) => [option.Name]);
15+
16+
public static ForwardedOption<T> ForwardAs<T>(this ForwardedOption<T> option, string value) => option.SetForwardingFunction((T? o) => [value]);
1317

14-
public static ForwardedOption<T> ForwardAs<T>(this ForwardedOption<T> option, string value) => option.SetForwardingFunction((T o) => new string[] { value });
18+
public static ForwardedOption<T> ForwardAsSingle<T>(this ForwardedOption<T> option, Func<T?, string> format) => option.SetForwardingFunction(format);
1519

16-
public static ForwardedOption<T> ForwardAsSingle<T>(this ForwardedOption<T> option, Func<T, string> format) => option.SetForwardingFunction(format);
20+
public static ForwardedOption<bool> ForwardIfEnabled(this ForwardedOption<bool> option, string value) => option.SetForwardingFunction((bool o) => o == true ? [value] : []);
1721

1822
/// <summary>
1923
/// Set up an option to be forwaded as an output path to MSBuild
@@ -24,47 +28,53 @@ public static class OptionForwardingExtensions
2428
/// <returns>The option</returns>
2529
public static ForwardedOption<string> ForwardAsOutputPath(this ForwardedOption<string> option, string outputPropertyName, bool surroundWithDoubleQuotes = false)
2630
{
27-
return option.SetForwardingFunction((string o) =>
31+
return option.SetForwardingFunction((string? o) =>
2832
{
33+
if (o is null)
34+
{
35+
return [];
36+
}
2937
string argVal = CommandDirectoryContext.GetFullPath(o);
3038
if (surroundWithDoubleQuotes)
3139
{
3240
// Not sure if this is necessary, but this is what "dotnet test" previously did and so we are
3341
// preserving the behavior here after refactoring
3442
argVal = TestCommandParser.SurroundWithDoubleQuotes(argVal);
3543
}
36-
return new string[]
37-
{
44+
return [
3845
$"-property:{outputPropertyName}={argVal}",
3946
"-property:_CommandLineDefinedOutputPath=true"
40-
};
47+
];
4148
});
4249
}
4350

4451
public static ForwardedOption<string[]> ForwardAsProperty(this ForwardedOption<string[]> option) => option
4552
.SetForwardingFunction((optionVals) =>
46-
optionVals
53+
(optionVals ?? [])
4754
.SelectMany(Utils.MSBuildPropertyParser.ParseProperties)
4855
.Select(keyValue => $"{option.Name}:{keyValue.key}={keyValue.value}")
4956
);
5057

51-
public static CliOption<T> ForwardAsMany<T>(this ForwardedOption<T> option, Func<T, IEnumerable<string>> format) => option.SetForwardingFunction(format);
58+
public static CliOption<T> ForwardAsMany<T>(this ForwardedOption<T> option, Func<T?, IEnumerable<string>> format) => option.SetForwardingFunction(format);
5259

5360
public static CliOption<IEnumerable<string>> ForwardAsManyArgumentsEachPrefixedByOption(this ForwardedOption<IEnumerable<string>> option, string alias) => option.ForwardAsMany(o => ForwardedArguments(alias, o));
5461

5562
public static IEnumerable<string> OptionValuesToBeForwarded(this ParseResult parseResult, CliCommand command) =>
5663
command.Options
5764
.OfType<IForwardedOption>()
58-
.SelectMany(o => o.GetForwardingFunction()(parseResult)) ?? Array.Empty<string>();
65+
.Select(o => o.GetForwardingFunction())
66+
.SelectMany(f => f is not null ? f(parseResult) : Array.Empty<string>());
5967

6068

61-
public static IEnumerable<string> ForwardedOptionValues<T>(this ParseResult parseResult, CliCommand command, string alias) =>
62-
command.Options?
69+
public static IEnumerable<string> ForwardedOptionValues<T>(this ParseResult parseResult, CliCommand command, string alias)
70+
{
71+
var func = command.Options?
6372
.Where(o => o.Name.Equals(alias) || o.Aliases.Contains(alias))?
6473
.OfType<IForwardedOption>()?
6574
.FirstOrDefault()?
66-
.GetForwardingFunction()(parseResult)
67-
?? Array.Empty<string>();
75+
.GetForwardingFunction();
76+
return func?.Invoke(parseResult) ?? [];
77+
}
6878

6979
public static CliOption<T> AllowSingleArgPerToken<T>(this CliOption<T> option)
7080
{
@@ -94,9 +104,9 @@ public static CliOption<T> WithHelpDescription<T>(this CliOption<T> option, CliC
94104
return option;
95105
}
96106

97-
private static IEnumerable<string> ForwardedArguments(string alias, IEnumerable<string> arguments)
107+
private static IEnumerable<string> ForwardedArguments(string alias, IEnumerable<string>? arguments)
98108
{
99-
foreach (string arg in arguments)
109+
foreach (string arg in arguments ?? [])
100110
{
101111
yield return alias;
102112
yield return arg;
@@ -113,34 +123,48 @@ public class ForwardedOption<T> : CliOption<T>, IForwardedOption
113123
{
114124
private Func<ParseResult, IEnumerable<string>> ForwardingFunction;
115125

116-
public ForwardedOption(string name, params string[] aliases) : base(name, aliases) { }
126+
public ForwardedOption(string name, params string[] aliases) : base(name, aliases)
127+
{
128+
ForwardingFunction = _ => [];
129+
}
117130

118-
public ForwardedOption(string name, Func<ArgumentResult, T> parseArgument, string description = null)
131+
public ForwardedOption(string name, Func<ArgumentResult, T> parseArgument, string? description = null)
119132
: base(name)
120133
{
121134
CustomParser = parseArgument;
122135
Description = description;
136+
ForwardingFunction = _ => [];
123137
}
124138

125-
public ForwardedOption<T> SetForwardingFunction(Func<T, IEnumerable<string>> func)
139+
public ForwardedOption<T> SetForwardingFunction(Func<T?, IEnumerable<string>> func)
126140
{
127141
ForwardingFunction = GetForwardingFunction(func);
128142
return this;
129143
}
130144

131-
public ForwardedOption<T> SetForwardingFunction(Func<T, string> format)
145+
public ForwardedOption<T> SetForwardingFunction(Func<T?, string> format)
132146
{
133-
ForwardingFunction = GetForwardingFunction((o) => new string[] { format(o) });
147+
ForwardingFunction = GetForwardingFunction((o) => [format(o)]);
134148
return this;
135149
}
136150

137-
public ForwardedOption<T> SetForwardingFunction(Func<T, ParseResult, IEnumerable<string>> func)
151+
public ForwardedOption<T> SetForwardingFunction(Func<T?, ParseResult, IEnumerable<string>> func)
138152
{
139-
ForwardingFunction = (ParseResult parseResult) => parseResult.GetResult(this) is not null ? func(parseResult.GetValue<T>(this), parseResult) : Array.Empty<string>();
153+
ForwardingFunction = (ParseResult parseResult) =>
154+
{
155+
if (parseResult.GetResult(this) is OptionResult argresult && argresult.GetValue<T>(this) is T validValue)
156+
{
157+
return func(validValue, parseResult) ?? [];
158+
}
159+
else
160+
{
161+
return [];
162+
}
163+
};
140164
return this;
141165
}
142166

143-
public Func<ParseResult, IEnumerable<string>> GetForwardingFunction(Func<T, IEnumerable<string>> func)
167+
public Func<ParseResult, IEnumerable<string>> GetForwardingFunction(Func<T?, IEnumerable<string>> func)
144168
{
145169
return (ParseResult parseResult) => parseResult.GetResult(this) is not null ? func(parseResult.GetValue<T>(this)) : Array.Empty<string>();
146170
}
@@ -153,7 +177,7 @@ public Func<ParseResult, IEnumerable<string>> GetForwardingFunction()
153177

154178
public class DynamicForwardedOption<T> : ForwardedOption<T>, IDynamicOption
155179
{
156-
public DynamicForwardedOption(string name, Func<ArgumentResult, T> parseArgument, string description = null)
180+
public DynamicForwardedOption(string name, Func<ArgumentResult, T> parseArgument, string? description = null)
157181
: base(name, parseArgument, description)
158182
{
159183
}

src/Cli/dotnet/commands/dotnet-msbuild/MSBuildForwardingApp.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,9 @@ private void InitializeRequiredEnvironmentVariables()
7474
/// <summary>
7575
/// Test hook returning concatenated and escaped command line arguments that would be passed to MSBuild.
7676
/// </summary>
77-
internal string GetArgumentsToMSBuild()
78-
{
79-
var argumentsUnescaped = _forwardingAppWithoutLogging.GetAllArguments();
80-
return ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(argumentsUnescaped);
81-
}
77+
internal string GetArgumentsToMSBuild() => ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(GetArgumentTokensToMSBuild());
78+
79+
internal string[] GetArgumentTokensToMSBuild() => _forwardingAppWithoutLogging.GetAllArguments();
8280

8381
public virtual int Execute()
8482
{

src/Cli/dotnet/commands/dotnet-nuget/NuGetCommandParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private static CliCommand GetDeleteCommand()
6464
{
6565
Arity = ArgumentArity.Zero
6666
});
67-
deleteCommand.Options.Add(new CliOption<bool>("--interactive"));
67+
deleteCommand.Options.Add(CommonOptions.InteractiveOption());
6868

6969
deleteCommand.SetAction(NuGetCommand.Run);
7070

@@ -125,7 +125,7 @@ private static CliCommand GetPushCommand()
125125
{
126126
Arity = ArgumentArity.Zero
127127
});
128-
pushCommand.Options.Add(new CliOption<bool>("--interactive"));
128+
pushCommand.Options.Add(CommonOptions.InteractiveOption());
129129
pushCommand.Options.Add(new CliOption<bool>("--skip-duplicate")
130130
{
131131
Arity = ArgumentArity.Zero

src/Cli/dotnet/commands/dotnet-package/add/PackageAddCommandParser.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,7 @@ internal static class PackageAddCommandParser
7070
HelpName = LocalizableStrings.CmdPackageDirectory
7171
}.ForwardAsSingle(o => $"--package-directory {o}");
7272

73-
public static readonly CliOption<bool> InteractiveOption = new ForwardedOption<bool>("--interactive")
74-
{
75-
Description = CommonLocalizableStrings.CommandInteractiveOptionDescription,
76-
Arity = ArgumentArity.Zero
77-
}.ForwardAs("--interactive");
73+
public static readonly CliOption<bool> InteractiveOption = CommonOptions.InteractiveOption().ForwardIfEnabled("--interactive");
7874

7975
public static readonly CliOption<bool> PrereleaseOption = new ForwardedOption<bool>("--prerelease")
8076
{

src/Cli/dotnet/commands/dotnet-package/list/PackageListCommandParser.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,7 @@ internal static class PackageListCommandParser
7272
}.ForwardAsManyArgumentsEachPrefixedByOption("--source")
7373
.AllowSingleArgPerToken();
7474

75-
public static readonly CliOption InteractiveOption = new ForwardedOption<bool>("--interactive")
76-
{
77-
Description = CommonLocalizableStrings.CommandInteractiveOptionDescription,
78-
Arity = ArgumentArity.Zero
79-
}.ForwardAs("--interactive");
75+
public static readonly CliOption InteractiveOption = CommonOptions.InteractiveOption().ForwardIfEnabled("--interactive");
8076

8177
public static readonly CliOption VerbosityOption = new ForwardedOption<VerbosityOptions>("--verbosity", "-v")
8278
{

0 commit comments

Comments
 (0)