Skip to content

Commit 98842bc

Browse files
authored
Add -e option to dotnet run (#45795)
1 parent bee76bd commit 98842bc

39 files changed

+802
-474
lines changed

src/Cli/dotnet/CommonLocalizableStrings.resx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,25 @@ setx PATH "%PATH%;{0}"
698698
<data name="CannotSpecifyBothRuntimeAndOsOptions" xml:space="preserve">
699699
<value>Specifying both the `-r|--runtime` and `-os` options is not supported.</value>
700700
</data>
701+
<data name="CmdEnvironmentVariableDescription" xml:space="preserve">
702+
<value>Sets the value of an environment variable.
703+
Creates the variable if it does not exist, overrides if it does.
704+
This will force the tests to be run in an isolated process.
705+
This argument can be specified multiple times to provide multiple variables.
706+
707+
Examples:
708+
-e VARIABLE=abc
709+
-e VARIABLE="value with spaces"
710+
-e VARIABLE="value;seperated with;semicolons"
711+
-e VAR1=abc -e VAR2=def -e VAR3=ghi
712+
</value>
713+
</data>
714+
<data name="CmdEnvironmentVariableExpression" xml:space="preserve">
715+
<value>NAME="VALUE"</value>
716+
</data>
717+
<data name="IncorrectlyFormattedEnvironmentVariables" xml:space="preserve">
718+
<value>Incorrectly formatted environment variables: {0}</value>
719+
</data>
701720
<data name="SelfContainedOptionDescription" xml:space="preserve">
702721
<value>Publish the .NET runtime with your application so the runtime doesn't need to be installed on the target machine.
703722
The default is 'false.' However, when targeting .NET 7 or lower, the default is 'true' if a runtime identifier is specified.</value>

src/Cli/dotnet/CommonOptions.cs

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.CommandLine;
55
using System.CommandLine.Completions;
6+
using System.CommandLine.Parsing;
67
using Microsoft.DotNet.Cli.Utils;
78
using Microsoft.DotNet.Tools;
89
using Microsoft.DotNet.Tools.Common;
@@ -174,6 +175,52 @@ internal static string ArchOptionValue(ParseResult parseResult) =>
174175
// Flip the argument so that if this option is specified we get selfcontained=false
175176
.SetForwardingFunction((arg, p) => ForwardSelfContainedOptions(!arg, p));
176177

178+
public static readonly CliOption<IReadOnlyDictionary<string, string>> EnvOption = new("--environment", "-e")
179+
{
180+
Description = CommonLocalizableStrings.CmdEnvironmentVariableDescription,
181+
HelpName = CommonLocalizableStrings.CmdEnvironmentVariableExpression,
182+
CustomParser = ParseEnvironmentVariables,
183+
// Can't allow multiple arguments because the separator needs to be parsed as part of the environment variable value.
184+
AllowMultipleArgumentsPerToken = false
185+
};
186+
187+
private static IReadOnlyDictionary<string, string> ParseEnvironmentVariables(ArgumentResult argumentResult)
188+
{
189+
var result = new Dictionary<string, string>(
190+
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal);
191+
192+
List<CliToken>? invalid = null;
193+
194+
foreach (var token in argumentResult.Tokens)
195+
{
196+
var separator = token.Value.IndexOf('=');
197+
var (name, value) = (separator >= 0)
198+
? (token.Value[0..separator], token.Value[(separator + 1)..])
199+
: (token.Value, "");
200+
201+
name = name.Trim();
202+
203+
if (name != "")
204+
{
205+
result[name] = value;
206+
}
207+
else
208+
{
209+
invalid ??= [];
210+
invalid.Add(token);
211+
}
212+
}
213+
214+
if (invalid != null)
215+
{
216+
argumentResult.AddError(string.Format(
217+
CommonLocalizableStrings.IncorrectlyFormattedEnvironmentVariables,
218+
string.Join(", ", invalid.Select(x => $"'{x.Value}'"))));
219+
}
220+
221+
return result;
222+
}
223+
177224
public static readonly CliOption<string> TestPlatformOption = new("--Platform");
178225

179226
public static readonly CliOption<string> TestFrameworkOption = new("--Framework");
@@ -259,13 +306,6 @@ private static IEnumerable<string> ForwardSelfContainedOptions(bool isSelfContai
259306
return selfContainedProperties;
260307
}
261308

262-
private static bool UserSpecifiedRidOption(ParseResult parseResult) =>
263-
(parseResult.GetResult(RuntimeOption) ??
264-
parseResult.GetResult(LongFormRuntimeOption) ??
265-
parseResult.GetResult(ArchitectureOption) ??
266-
parseResult.GetResult(LongFormArchitectureOption) ??
267-
parseResult.GetResult(OperatingSystemOption)) is not null;
268-
269309
internal static CliOption<T> AddCompletions<T>(this CliOption<T> option, Func<CompletionContext, IEnumerable<CompletionItem>> completionSource)
270310
{
271311
option.CompletionSources.Add(completionSource);
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
22
"profiles": {
33
"dotnet": {
4-
"commandName": "Project"
4+
"commandName": "Project",
5+
"commandLineArgs": "run -e MyCoolEnvironmentVariableKey=OverriddenEnvironmentVariableValue",
6+
"workingDirectory": "C:\\sdk5\\artifacts\\tmp\\Debug\\EnvOptionLaun---86C8BD0D"
57
}
68
}
7-
}
9+
}

src/Cli/dotnet/commands/dotnet-run/Program.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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+
using System.Collections.Immutable;
45
using System.CommandLine;
56
using Microsoft.DotNet.Cli;
67
using Microsoft.DotNet.Cli.Utils;
@@ -61,8 +62,9 @@ public static RunCommand FromParseResult(ParseResult parseResult)
6162
noRestore: parseResult.HasOption(RunCommandParser.NoRestoreOption) || parseResult.HasOption(RunCommandParser.NoBuildOption),
6263
interactive: parseResult.HasOption(RunCommandParser.InteractiveOption),
6364
verbosity: parseResult.HasOption(CommonOptions.VerbosityOption) ? parseResult.GetValue(CommonOptions.VerbosityOption) : null,
64-
restoreArgs: restoreArgs.ToArray(),
65-
args: nonBinLogArgs.ToArray()
65+
restoreArgs: [.. restoreArgs],
66+
args: [.. nonBinLogArgs],
67+
environmentVariables: parseResult.GetValue(CommonOptions.EnvOption) ?? ImmutableDictionary<string, string>.Empty
6668
);
6769

6870
return command;

src/Cli/dotnet/commands/dotnet-run/RunCommand.cs

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ private record RunProperties(string? RunCommand, string? RunArguments, string? R
2828
public bool Interactive { get; private set; }
2929
public string[] RestoreArgs { get; private set; }
3030

31+
/// <summary>
32+
/// Environment variables specified on command line via -e option.
33+
/// </summary>
34+
public IReadOnlyDictionary<string, string> EnvironmentVariables { get; private set; }
35+
3136
private bool ShouldBuild => !NoBuild;
3237

3338
public string LaunchProfile { get; private set; }
@@ -43,7 +48,8 @@ public RunCommand(
4348
bool interactive,
4449
VerbosityOptions? verbosity,
4550
string[] restoreArgs,
46-
string[] args)
51+
string[] args,
52+
IReadOnlyDictionary<string, string> environmentVariables)
4753
{
4854
NoBuild = noBuild;
4955
ProjectFileFullPath = DiscoverProjectFilePath(projectFileOrDirectory);
@@ -54,6 +60,7 @@ public RunCommand(
5460
NoRestore = noRestore;
5561
Verbosity = verbosity;
5662
RestoreArgs = GetRestoreArguments(restoreArgs);
63+
EnvironmentVariables = environmentVariables;
5764
}
5865

5966
public int Execute()
@@ -76,10 +83,18 @@ public int Execute()
7683
try
7784
{
7885
ICommand targetCommand = GetTargetCommand();
79-
var launchSettingsCommand = ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
86+
ApplyLaunchSettingsProfileToCommand(targetCommand, launchSettings);
87+
88+
// Env variables specified on command line override those specified in launch profile:
89+
foreach (var (name, value) in EnvironmentVariables)
90+
{
91+
targetCommand.EnvironmentVariable(name, value);
92+
}
93+
8094
// Ignore Ctrl-C for the remainder of the command's execution
8195
Console.CancelKeyPress += (sender, e) => { e.Cancel = true; };
82-
return launchSettingsCommand.Execute().ExitCode;
96+
97+
return targetCommand.Execute().ExitCode;
8398
}
8499
catch (InvalidProjectFileException e)
85100
{
@@ -89,29 +104,31 @@ public int Execute()
89104
}
90105
}
91106

92-
private ICommand ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings)
107+
private void ApplyLaunchSettingsProfileToCommand(ICommand targetCommand, ProjectLaunchSettingsModel? launchSettings)
93108
{
94-
if (launchSettings != null)
109+
if (launchSettings == null)
95110
{
96-
if (!string.IsNullOrEmpty(launchSettings.ApplicationUrl))
97-
{
98-
targetCommand.EnvironmentVariable("ASPNETCORE_URLS", launchSettings.ApplicationUrl);
99-
}
111+
return;
112+
}
100113

101-
targetCommand.EnvironmentVariable("DOTNET_LAUNCH_PROFILE", launchSettings.LaunchProfileName);
114+
if (!string.IsNullOrEmpty(launchSettings.ApplicationUrl))
115+
{
116+
targetCommand.EnvironmentVariable("ASPNETCORE_URLS", launchSettings.ApplicationUrl);
117+
}
102118

103-
foreach (var entry in launchSettings.EnvironmentVariables)
104-
{
105-
string value = Environment.ExpandEnvironmentVariables(entry.Value);
106-
//NOTE: MSBuild variables are not expanded like they are in VS
107-
targetCommand.EnvironmentVariable(entry.Key, value);
108-
}
109-
if (string.IsNullOrEmpty(targetCommand.CommandArgs) && launchSettings.CommandLineArgs != null)
110-
{
111-
targetCommand.SetCommandArgs(launchSettings.CommandLineArgs);
112-
}
119+
targetCommand.EnvironmentVariable("DOTNET_LAUNCH_PROFILE", launchSettings.LaunchProfileName);
120+
121+
foreach (var entry in launchSettings.EnvironmentVariables)
122+
{
123+
string value = Environment.ExpandEnvironmentVariables(entry.Value);
124+
//NOTE: MSBuild variables are not expanded like they are in VS
125+
targetCommand.EnvironmentVariable(entry.Key, value);
126+
}
127+
128+
if (string.IsNullOrEmpty(targetCommand.CommandArgs) && launchSettings.CommandLineArgs != null)
129+
{
130+
targetCommand.SetCommandArgs(launchSettings.CommandLineArgs);
113131
}
114-
return targetCommand;
115132
}
116133

117134
private bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel? launchSettingsModel)

src/Cli/dotnet/commands/dotnet-run/RunCommandParser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ private static CliCommand ConstructCommand()
8181
command.Options.Add(CommonOptions.OperatingSystemOption);
8282
command.Options.Add(CommonOptions.DisableBuildServersOption);
8383
command.Options.Add(CommonOptions.ArtifactsPathOption);
84+
command.Options.Add(CommonOptions.EnvOption);
8485

8586
command.Arguments.Add(ApplicationArguments);
8687

src/Cli/dotnet/commands/dotnet-test/LocalizableStrings.resx

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -269,22 +269,6 @@ For MSTest before 2.2.4, the timeout is used for all testcases.</value>
269269
<data name="HangTimeoutArgumentName" xml:space="preserve">
270270
<value>TIMESPAN</value>
271271
</data>
272-
<data name="CmdEnvironmentVariableDescription" xml:space="preserve">
273-
<value>Sets the value of an environment variable.
274-
Creates the variable if it does not exist, overrides if it does.
275-
This will force the tests to be run in an isolated process.
276-
This argument can be specified multiple times to provide multiple variables.
277-
278-
Examples:
279-
-e VARIABLE=abc
280-
-e VARIABLE="value with spaces"
281-
-e VARIABLE="value;seperated with;semicolons"
282-
-e VAR1=abc -e VAR2=def -e VAR3=ghi
283-
</value>
284-
</data>
285-
<data name="CmdEnvironmentVariableExpression" xml:space="preserve">
286-
<value>NAME="VALUE"</value>
287-
</data>
288272
<data name="NoSerializerRegisteredWithIdErrorMessage" xml:space="preserve">
289273
<value>No serializer registered with ID '{0}'</value>
290274
</data>

src/Cli/dotnet/commands/dotnet-test/Program.cs

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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+
using System.Collections.Immutable;
45
using System.CommandLine;
56
using System.Runtime.Versioning;
67
using System.Text.RegularExpressions;
@@ -228,8 +229,14 @@ private static TestCommand FromParseResult(ParseResult result, string[] settings
228229
noRestore,
229230
msbuildPath);
230231

231-
// Apply environment variables provided by the user via --environment (-e) parameter, if present
232-
SetEnvironmentVariablesFromParameters(testCommand, result);
232+
// Apply environment variables provided by the user via --environment (-e) option, if present
233+
if (result.GetValue(CommonOptions.EnvOption) is { } environmentVariables)
234+
{
235+
foreach (var (name, value) in environmentVariables)
236+
{
237+
testCommand.EnvironmentVariable(name, value);
238+
}
239+
}
233240

234241
// Set DOTNET_PATH if it isn't already set in the environment as it is required
235242
// by the testhost which uses the apphost feature (Windows only).
@@ -303,31 +310,6 @@ private static bool ContainsBuiltTestSources(string[] args)
303310
return false;
304311
}
305312

306-
private static void SetEnvironmentVariablesFromParameters(TestCommand testCommand, ParseResult parseResult)
307-
{
308-
CliOption<IEnumerable<string>> option = TestCommandParser.EnvOption;
309-
310-
if (parseResult.GetResult(option) is null)
311-
{
312-
return;
313-
}
314-
315-
foreach (string env in parseResult.GetValue(option))
316-
{
317-
string name = env;
318-
string value = string.Empty;
319-
320-
int equalsIndex = env.IndexOf('=');
321-
if (equalsIndex > 0)
322-
{
323-
name = env.Substring(0, equalsIndex);
324-
value = env.Substring(equalsIndex + 1);
325-
}
326-
327-
testCommand.EnvironmentVariable(name, value);
328-
}
329-
}
330-
331313
/// <returns>A case-insensitive dictionary of any properties passed from the user and their values.</returns>
332314
private static Dictionary<string, string> GetUserSpecifiedExplicitMSBuildProperties(ParseResult parseResult)
333315
{

src/Cli/dotnet/commands/dotnet-test/TestCommandParser.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,6 @@ internal static class TestCommandParser
3232
Description = LocalizableStrings.CmdListTestsDescription
3333
}.ForwardAs("-property:VSTestListTests=true");
3434

35-
public static readonly CliOption<IEnumerable<string>> EnvOption = new CliOption<IEnumerable<string>>("--environment", "-e")
36-
{
37-
Description = LocalizableStrings.CmdEnvironmentVariableDescription,
38-
HelpName = LocalizableStrings.CmdEnvironmentVariableExpression
39-
}.AllowSingleArgPerToken();
40-
4135
public static readonly CliOption<string> FilterOption = new ForwardedOption<string>("--filter")
4236
{
4337
Description = LocalizableStrings.CmdTestCaseFilterDescription,
@@ -219,7 +213,7 @@ private static CliCommand GetVSTestCliCommand()
219213

220214
command.Options.Add(SettingsOption);
221215
command.Options.Add(ListTestsOption);
222-
command.Options.Add(EnvOption);
216+
command.Options.Add(CommonOptions.EnvOption);
223217
command.Options.Add(FilterOption);
224218
command.Options.Add(AdapterOption);
225219
command.Options.Add(LoggerOption);

src/Cli/dotnet/commands/dotnet-test/xlf/LocalizableStrings.cs.xlf

Lines changed: 0 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)