diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/StringExtensions.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/StringExtensions.cs
index c6bd2d3213b9..63f2b953ecfe 100644
--- a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/StringExtensions.cs
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/StringExtensions.cs
@@ -1,10 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Text.Json;
+
namespace Microsoft.DotNet.Cli.Utils.Extensions;
public static class StringExtensions
{
+ ///
+ /// Strips CLI option prefixes like - , -- , or / from a string to reveal the user-facing name.
+ ///
+ ///
+ ///
public static string RemovePrefix(this string name)
{
int prefixLength = GetPrefixLength(name);
@@ -26,4 +33,11 @@ static int GetPrefixLength(string name)
return 0;
}
}
+
+ ///
+ /// Converts a string to camel case using the JSON naming policy. Camel-case means that the first letter of the string is lowercase, and the first letter of each subsequent word is uppercase.
+ ///
+ /// A string to ensure is camel-cased
+ /// The camel-cased string
+ public static string ToCamelCase(this string value) => JsonNamingPolicy.CamelCase.ConvertName(value);
}
diff --git a/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/TypeExtensions.cs b/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/TypeExtensions.cs
new file mode 100644
index 000000000000..4c78dac7e82f
--- /dev/null
+++ b/src/Cli/Microsoft.DotNet.Cli.Utils/Extensions/TypeExtensions.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.DotNet.Cli.Utils.Extensions;
+
+public static class TypeExtensions
+{
+ ///
+ /// Converts a Type (potentially containing generic parameters) from CLI representation (e.g. System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib, Version=10.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]] )
+ /// to a more readable string representation (e.g. System.Collections.Generic.List<System.Int32> ).
+ ///
+ ///
+ /// This is used when outputting the Type information for the CLI schema JSON.
+ ///
+ public static string ToCliTypeString(this Type type)
+ {
+ var typeName = type.FullName ?? string.Empty;
+ if (!type.IsGenericType)
+ {
+ return typeName;
+ }
+
+ var genericTypeName = typeName.Substring(0, typeName.IndexOf('`'));
+ var genericTypes = string.Join(", ", type.GenericTypeArguments.Select(generic => generic.ToCliTypeString()));
+ return $"{genericTypeName}<{genericTypes}>";
+ }
+}
diff --git a/src/Cli/dotnet/CliSchema.cs b/src/Cli/dotnet/CliSchema.cs
new file mode 100644
index 000000000000..0496977bbd73
--- /dev/null
+++ b/src/Cli/dotnet/CliSchema.cs
@@ -0,0 +1,213 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.CommandLine;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Schema;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using Microsoft.DotNet.Cli.Telemetry;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.DotNet.Cli.Utils.Extensions;
+using Command = System.CommandLine.Command;
+using CommandResult = System.CommandLine.Parsing.CommandResult;
+
+namespace Microsoft.DotNet.Cli;
+
+internal static class CliSchema
+{
+ // Using UnsafeRelaxedJsonEscaping because this JSON is not transmitted over the web. Therefore, HTML-sensitive characters are not encoded.
+ // See: https://learn.microsoft.com/dotnet/api/system.text.encodings.web.javascriptencoder.unsaferelaxedjsonescaping
+ // Force the newline to be "\n" instead of the default "\r\n" for consistency across platforms (and for testing)
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions = new()
+ {
+ WriteIndented = true,
+ NewLine = "\n",
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ RespectNullableAnnotations = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ // needed to workaround https://github.com/dotnet/aspnetcore/issues/55692, but will need to be removed when
+ // we tackle AOT in favor of the source-generated JsonTypeInfo stuff
+ TypeInfoResolver = new DefaultJsonTypeInfoResolver()
+ };
+
+ public record ArgumentDetails(string? description, int order, bool hidden, string? helpName, string valueType, bool hasDefaultValue, object? defaultValue, ArityDetails arity);
+ public record ArityDetails(int minimum, int? maximum);
+ public record OptionDetails(
+ string? description,
+ bool hidden,
+ string[]? aliases,
+ string? helpName,
+ string valueType,
+ bool hasDefaultValue,
+ object? defaultValue,
+ ArityDetails arity,
+ bool required,
+ bool recursive
+ );
+ public record CommandDetails(
+ string? description,
+ bool hidden,
+ string[]? aliases,
+ Dictionary? arguments,
+ Dictionary? options,
+ Dictionary? subcommands);
+ public record RootCommandDetails(
+ string name,
+ string version,
+ string? description,
+ bool hidden,
+ string[]? aliases,
+ Dictionary? arguments,
+ Dictionary? options,
+ Dictionary? subcommands
+ ) : CommandDetails(description, hidden, aliases, arguments, options, subcommands);
+
+
+ public static void PrintCliSchema(CommandResult commandResult, TextWriter outputWriter, ITelemetry? telemetryClient)
+ {
+ var command = commandResult.Command;
+ RootCommandDetails transportStructure = CreateRootCommandDetails(command);
+ var result = JsonSerializer.Serialize(transportStructure, s_jsonSerializerOptions);
+ outputWriter.Write(result.AsSpan());
+ outputWriter.Flush();
+ var commandString = CommandHierarchyAsString(commandResult);
+ var telemetryProperties = new Dictionary { { "command", commandString } };
+ telemetryClient?.TrackEvent("schema", telemetryProperties, null);
+ }
+
+ public static object GetJsonSchema()
+ {
+ var node = s_jsonSerializerOptions.GetJsonSchemaAsNode(typeof(RootCommandDetails), new JsonSchemaExporterOptions());
+ return node.ToJsonString(s_jsonSerializerOptions);
+ }
+
+ private static ArityDetails CreateArityDetails(ArgumentArity arity)
+ {
+ return new ArityDetails(
+ minimum: arity.MinimumNumberOfValues,
+ maximum: arity.MaximumNumberOfValues == ArgumentArity.ZeroOrMore.MaximumNumberOfValues ? null : arity.MaximumNumberOfValues
+ );
+ }
+
+ private static RootCommandDetails CreateRootCommandDetails(Command command)
+ {
+ var arguments = CreateArgumentsDictionary(command.Arguments);
+ var options = CreateOptionsDictionary(command.Options);
+ var subcommands = CreateSubcommandsDictionary(command.Subcommands);
+
+ return new RootCommandDetails(
+ name: command.Name,
+ version: Product.Version,
+ description: command.Description?.ReplaceLineEndings("\n"),
+ hidden: command.Hidden,
+ aliases: DetermineAliases(command.Aliases),
+ arguments: arguments,
+ options: options,
+ subcommands: subcommands
+ );
+ }
+
+ private static Dictionary? CreateArgumentsDictionary(IList arguments)
+ {
+ if (arguments.Count == 0)
+ {
+ return null;
+ }
+ var dict = new Dictionary();
+ foreach ((var index, var argument) in arguments.Index())
+ {
+ dict[argument.Name] = CreateArgumentDetails(index, argument);
+ }
+ return dict;
+ }
+
+ private static Dictionary? CreateOptionsDictionary(IList options)
+ {
+ if (options.Count == 0)
+ {
+ return null;
+ }
+ var dict = new Dictionary();
+ foreach (var option in options.OrderBy(o => o.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ dict[option.Name] = CreateOptionDetails(option);
+ }
+ return dict;
+ }
+
+ private static Dictionary? CreateSubcommandsDictionary(IList subcommands)
+ {
+ if (subcommands.Count == 0)
+ {
+ return null;
+ }
+ var dict = new Dictionary();
+ foreach (var subcommand in subcommands.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase))
+ {
+ dict[subcommand.Name] = CreateCommandDetails(subcommand);
+ }
+ return dict;
+ }
+
+ private static string[]? DetermineAliases(ICollection aliases)
+ {
+ if (aliases.Count == 0)
+ {
+ return null;
+ }
+
+ // Order the aliases to ensure consistent output.
+ return aliases.Order().ToArray();
+ }
+
+ private static CommandDetails CreateCommandDetails(Command subCommand) => new CommandDetails(
+ subCommand.Description?.ReplaceLineEndings("\n"),
+ subCommand.Hidden,
+ DetermineAliases(subCommand.Aliases),
+ CreateArgumentsDictionary(subCommand.Arguments),
+ CreateOptionsDictionary(subCommand.Options),
+ CreateSubcommandsDictionary(subCommand.Subcommands)
+ );
+
+ private static OptionDetails CreateOptionDetails(Option option) => new OptionDetails(
+ option.Description?.ReplaceLineEndings("\n"),
+ option.Hidden,
+ DetermineAliases(option.Aliases),
+ option.HelpName,
+ option.ValueType.ToCliTypeString(),
+ option.HasDefaultValue,
+ option.HasDefaultValue ? option.GetDefaultValue() : null,
+ CreateArityDetails(option.Arity),
+ option.Required,
+ option.Recursive
+ );
+
+ private static ArgumentDetails CreateArgumentDetails(int index, Argument argument) => new ArgumentDetails(
+ argument.Description?.ReplaceLineEndings("\n"),
+ index,
+ argument.Hidden,
+ argument.HelpName,
+ argument.ValueType.ToCliTypeString(),
+ argument.HasDefaultValue,
+ argument.HasDefaultValue ? argument.GetDefaultValue() : null,
+ CreateArityDetails(argument.Arity)
+ );
+
+ // Produces a string that represents the command call.
+ // For example, calling the workload install command produces `dotnet workload install`.
+ private static string CommandHierarchyAsString(CommandResult commandResult)
+ {
+ var commands = new List();
+ var currentResult = commandResult;
+ while (currentResult is not null)
+ {
+ commands.Add(currentResult.Command.Name);
+ currentResult = currentResult.Parent as CommandResult;
+ }
+
+ return string.Join(" ", commands.AsEnumerable().Reverse());
+ }
+}
diff --git a/src/Cli/dotnet/CliStrings.resx b/src/Cli/dotnet/CliStrings.resx
index f6726ff3aef6..39e1a3e261df 100644
--- a/src/Cli/dotnet/CliStrings.resx
+++ b/src/Cli/dotnet/CliStrings.resx
@@ -811,4 +811,7 @@ The default is 'false.' However, when targeting .NET 7 or lower, the default is
Accept all confirmation prompts using "yes."
-
+
+ Display the command schema as JSON.
+
+
\ No newline at end of file
diff --git a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs
index 7f9328e21891..cab013fb6267 100644
--- a/src/Cli/dotnet/Extensions/ParseResultExtensions.cs
+++ b/src/Cli/dotnet/Extensions/ParseResultExtensions.cs
@@ -99,7 +99,7 @@ public static bool IsDotnetBuiltInCommand(this ParseResult parseResult)
public static bool IsTopLevelDotnetCommand(this ParseResult parseResult)
{
- return parseResult.CommandResult.Command.Equals(Microsoft.DotNet.Cli.Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult());
+ return parseResult.CommandResult.Command.Equals(Parser.RootCommand) && string.IsNullOrEmpty(parseResult.RootSubCommandResult());
}
public static bool CanBeInvoked(this ParseResult parseResult)
diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs
index 9572176d73fd..b9f3777c887e 100644
--- a/src/Cli/dotnet/Parser.cs
+++ b/src/Cli/dotnet/Parser.cs
@@ -5,6 +5,7 @@
using System.CommandLine;
using System.CommandLine.Completions;
+using System.CommandLine.Invocation;
using System.Reflection;
using Microsoft.DotNet.Cli.Commands.Build;
using Microsoft.DotNet.Cli.Commands.BuildServer;
@@ -98,22 +99,31 @@ public static class Parser
public static readonly Option VersionOption = new("--version")
{
- Arity = ArgumentArity.Zero,
+ Arity = ArgumentArity.Zero
};
public static readonly Option InfoOption = new("--info")
{
- Arity = ArgumentArity.Zero,
+ Arity = ArgumentArity.Zero
};
public static readonly Option ListSdksOption = new("--list-sdks")
{
- Arity = ArgumentArity.Zero,
+ Arity = ArgumentArity.Zero
};
public static readonly Option ListRuntimesOption = new("--list-runtimes")
{
+ Arity = ArgumentArity.Zero
+ };
+
+ public static readonly Option CliSchemaOption = new("--cli-schema")
+ {
+ Description = CliStrings.SDKSchemaCommandDefinition,
Arity = ArgumentArity.Zero,
+ Recursive = true,
+ Hidden = true,
+ Action = new PrintCliSchemaAction()
};
// Argument
@@ -152,6 +162,7 @@ private static Command ConfigureCommandLine(RootCommand rootCommand)
rootCommand.Options.Add(InfoOption);
rootCommand.Options.Add(ListSdksOption);
rootCommand.Options.Add(ListRuntimesOption);
+ rootCommand.Options.Add(CliSchemaOption);
// Add argument
rootCommand.Arguments.Add(DotnetSubCommand);
@@ -178,11 +189,8 @@ private static Command ConfigureCommandLine(RootCommand rootCommand)
return rootCommand;
}
- public static Command GetBuiltInCommand(string commandName)
- {
- return Subcommands
- .FirstOrDefault(c => c.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
- }
+ public static Command GetBuiltInCommand(string commandName) =>
+ Subcommands.FirstOrDefault(c => c.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase));
///
/// Implements token-per-line response file handling for the CLI. We use this instead of the built-in S.CL handling
@@ -385,4 +393,17 @@ public override void Write(HelpContext context)
}
}
}
+
+ private class PrintCliSchemaAction : SynchronousCommandLineAction
+ {
+ internal PrintCliSchemaAction()
+ {
+ Terminating = true;
+ }
+ public override int Invoke(ParseResult parseResult)
+ {
+ CliSchema.PrintCliSchema(parseResult.CommandResult, parseResult.Configuration.Output, Program.TelemetryClient);
+ return 0;
+ }
+ }
}
diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs
index a630d6874637..e589eade68f4 100644
--- a/src/Cli/dotnet/Program.cs
+++ b/src/Cli/dotnet/Program.cs
@@ -24,6 +24,7 @@ public class Program
{
private static readonly string ToolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel";
+ public static ITelemetry TelemetryClient;
public static int Main(string[] args)
{
using AutomaticEncodingRestorer _ = new();
@@ -113,12 +114,12 @@ public static int Main(string[] args)
}
}
- internal static int ProcessArgs(string[] args, ITelemetry telemetryClient = null)
+ internal static int ProcessArgs(string[] args)
{
- return ProcessArgs(args, new TimeSpan(0), telemetryClient);
+ return ProcessArgs(args, new TimeSpan(0));
}
- internal static int ProcessArgs(string[] args, TimeSpan startupTime, ITelemetry telemetryClient = null)
+ internal static int ProcessArgs(string[] args, TimeSpan startupTime)
{
Dictionary performanceData = [];
@@ -138,16 +139,20 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime, ITelemetry
}
PerformanceLogEventSource.Log.BuiltInCommandParserStop();
- using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel =
- new FirstTimeUseNoticeSentinel())
+ using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel())
{
IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = disposableFirstTimeUseNoticeSentinel;
IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel();
- IFileSentinel toolPathSentinel = new FileSentinel(
- new FilePath(
- Path.Combine(
- CliFolderPathCalculator.DotnetUserProfileFolderPath,
- ToolPathSentinelFileName)));
+ IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, ToolPathSentinelFileName)));
+
+ PerformanceLogEventSource.Log.TelemetryRegistrationStart();
+
+ TelemetryClient ??= new Telemetry.Telemetry(firstTimeUseNoticeSentinel);
+ TelemetryEventEntry.Subscribe(TelemetryClient.TrackEvent);
+ TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
+
+ PerformanceLogEventSource.Log.TelemetryRegistrationStop();
+
if (parseResult.GetValue(Parser.DiagOption) && parseResult.IsDotnetBuiltInCommand())
{
// We found --diagnostic or -d, but we still need to determine whether the option should
@@ -218,19 +223,11 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime, ITelemetry
skipFirstTimeUseCheck: getStarOptionPassed);
PerformanceLogEventSource.Log.FirstTimeConfigurationStop();
}
-
- PerformanceLogEventSource.Log.TelemetryRegistrationStart();
-
- telemetryClient ??= new Telemetry.Telemetry(firstTimeUseNoticeSentinel);
- TelemetryEventEntry.Subscribe(telemetryClient.TrackEvent);
- TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);
-
- PerformanceLogEventSource.Log.TelemetryRegistrationStop();
}
if (CommandLoggingContext.IsVerbose)
{
- Console.WriteLine($"Telemetry is: {(telemetryClient.Enabled ? "Enabled" : "Disabled")}");
+ Console.WriteLine($"Telemetry is: {(TelemetryClient.Enabled ? "Enabled" : "Disabled")}");
}
PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart();
performanceData.Add("Startup Time", startupTime.TotalMilliseconds);
@@ -280,10 +277,10 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime, ITelemetry
}
PerformanceLogEventSource.Log.TelemetryClientFlushStart();
- telemetryClient.Flush();
+ TelemetryClient.Flush();
PerformanceLogEventSource.Log.TelemetryClientFlushStop();
- telemetryClient.Dispose();
+ TelemetryClient.Dispose();
return exitCode;
}
diff --git a/src/Cli/dotnet/xlf/CliStrings.cs.xlf b/src/Cli/dotnet/xlf/CliStrings.cs.xlf
index d75034ea120d..49914585c2c5 100644
--- a/src/Cli/dotnet/xlf/CliStrings.cs.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.cs.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.de.xlf b/src/Cli/dotnet/xlf/CliStrings.de.xlf
index ae733e728a94..34cad75a2414 100644
--- a/src/Cli/dotnet/xlf/CliStrings.de.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.de.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.es.xlf b/src/Cli/dotnet/xlf/CliStrings.es.xlf
index c892bdb580d7..988ed76df4b1 100644
--- a/src/Cli/dotnet/xlf/CliStrings.es.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.es.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.fr.xlf b/src/Cli/dotnet/xlf/CliStrings.fr.xlf
index 734d2e61febc..119fcb5d05a7 100644
--- a/src/Cli/dotnet/xlf/CliStrings.fr.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.fr.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.it.xlf b/src/Cli/dotnet/xlf/CliStrings.it.xlf
index 45a95ee78e50..f7f6f87c1e4d 100644
--- a/src/Cli/dotnet/xlf/CliStrings.it.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.it.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.ja.xlf b/src/Cli/dotnet/xlf/CliStrings.ja.xlf
index aa3b9e681408..70179580d90d 100644
--- a/src/Cli/dotnet/xlf/CliStrings.ja.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.ja.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.ko.xlf b/src/Cli/dotnet/xlf/CliStrings.ko.xlf
index bb61d36696a1..06121a6e4a63 100644
--- a/src/Cli/dotnet/xlf/CliStrings.ko.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.ko.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.pl.xlf b/src/Cli/dotnet/xlf/CliStrings.pl.xlf
index a94bf6de633b..fce6565ab924 100644
--- a/src/Cli/dotnet/xlf/CliStrings.pl.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.pl.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf
index 2691d9f9387a..9087e0fc80f4 100644
--- a/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.pt-BR.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.ru.xlf b/src/Cli/dotnet/xlf/CliStrings.ru.xlf
index 82a0e443c4ae..d601e834f9e2 100644
--- a/src/Cli/dotnet/xlf/CliStrings.ru.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.ru.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.tr.xlf b/src/Cli/dotnet/xlf/CliStrings.tr.xlf
index 50e8ae5140e2..822c277232ce 100644
--- a/src/Cli/dotnet/xlf/CliStrings.tr.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.tr.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf
index 024729998c17..bc5ca328c86f 100644
--- a/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hans.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf
index f34d51d738bf..30a23cebfc00 100644
--- a/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf
+++ b/src/Cli/dotnet/xlf/CliStrings.zh-Hant.xlf
@@ -928,6 +928,11 @@ setx PATH "%PATH%;{0}"
Enable diagnostic output.
+
+ Display the command schema as JSON.
+ Display the command schema as JSON.
+
+
The '--self-contained' and '--no-self-contained' options cannot be used together.
The '--self-contained' and '--no-self-contained' options cannot be used together.
diff --git a/test/dotnet.Tests/CliSchemaTests.cs b/test/dotnet.Tests/CliSchemaTests.cs
new file mode 100644
index 000000000000..d0a50c640b4e
--- /dev/null
+++ b/test/dotnet.Tests/CliSchemaTests.cs
@@ -0,0 +1,979 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.Cli;
+using Microsoft.DotNet.Cli.Utils;
+
+namespace Microsoft.DotNet.Tests;
+
+public class CliSchemaTests : SdkTest
+{
+ public CliSchemaTests(ITestOutputHelper log) : base(log)
+ {
+ }
+
+ private static readonly string SolutionListJson = $$"""
+{
+ "name": "list",
+ "version": "{{Product.Version}}",
+ "description": "List all projects in a solution file.",
+ "hidden": false,
+ "options": {
+ "--solution-folders": {
+ "description": "Display solution folder paths.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ }
+ }
+}
+""";
+
+ private static readonly string CleanJson = $$"""
+{
+ "name": "clean",
+ "version": "{{Product.Version}}",
+ "description": ".NET Clean Command",
+ "hidden": false,
+ "arguments": {
+ "PROJECT | SOLUTION": {
+ "description": "The project or solution file to operate on. If a file is not specified, the command will search the current directory for one.",
+ "order": 0,
+ "hidden": false,
+ "valueType": "System.Collections.Generic.IEnumerable",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0
+ }
+ }
+ },
+ "options": {
+ "--artifacts-path": {
+ "description": "The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path.",
+ "hidden": false,
+ "helpName": "ARTIFACTS_DIR",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--configuration": {
+ "description": "The configuration to clean for. The default for most projects is 'Debug'.",
+ "hidden": false,
+ "aliases": [
+ "-c"
+ ],
+ "helpName": "CONFIGURATION",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--disable-build-servers": {
+ "description": "Force the command to ignore any persistent build servers.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--framework": {
+ "description": "The target framework to clean for. The target framework must also be specified in the project file.",
+ "hidden": false,
+ "aliases": [
+ "-f"
+ ],
+ "helpName": "FRAMEWORK",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--interactive": {
+ "description": "Allows the command to stop and wait for user input or action (for example to complete authentication).",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": true,
+ "defaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--nologo": {
+ "description": "Do not display the startup banner or the copyright message.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--output": {
+ "description": "The directory containing the build artifacts to clean.",
+ "hidden": false,
+ "aliases": [
+ "-o"
+ ],
+ "helpName": "OUTPUT_DIR",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--runtime": {
+ "description": "The target runtime to clean for.",
+ "hidden": false,
+ "aliases": [
+ "-r"
+ ],
+ "helpName": "RUNTIME_IDENTIFIER",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--verbosity": {
+ "description": "Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].",
+ "hidden": false,
+ "aliases": [
+ "-v"
+ ],
+ "helpName": "LEVEL",
+ "valueType": "Microsoft.DotNet.Cli.VerbosityOptions",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ }
+ }
+}
+""";
+
+ private static readonly string ReferenceJson = $$"""
+{
+ "name": "reference",
+ "version": "{{Product.Version}}",
+ "description": ".NET Remove Command",
+ "hidden": false,
+ "options": {
+ "--project": {
+ "description": "The project file to operate on. If a file is not specified, the command will search the current directory for one.",
+ "hidden": false,
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": true
+ }
+ },
+ "subcommands": {
+ "add": {
+ "description": "Add a project-to-project reference to the project.",
+ "hidden": false,
+ "arguments": {
+ "PROJECT_PATH": {
+ "description": "The paths to the projects to add as references.",
+ "order": 0,
+ "hidden": false,
+ "valueType": "System.Collections.Generic.IEnumerable",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1
+ }
+ }
+ },
+ "options": {
+ "--framework": {
+ "description": "Add the reference only when targeting a specific framework.",
+ "hidden": false,
+ "aliases": [
+ "-f"
+ ],
+ "helpName": "FRAMEWORK",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--interactive": {
+ "description": "Allows the command to stop and wait for user input or action (for example to complete authentication).",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": true,
+ "defaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ }
+ }
+ },
+ "list": {
+ "description": "List all project-to-project references of the project.",
+ "hidden": false
+ },
+ "remove": {
+ "description": "Remove a project-to-project reference from the project.",
+ "hidden": false,
+ "arguments": {
+ "PROJECT_PATH": {
+ "description": "The paths to the referenced projects to remove.",
+ "order": 0,
+ "hidden": false,
+ "valueType": "System.Collections.Generic.IEnumerable",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1
+ }
+ }
+ },
+ "options": {
+ "--framework": {
+ "description": "Remove the reference only when targeting a specific framework.",
+ "hidden": false,
+ "aliases": [
+ "-f"
+ ],
+ "helpName": "FRAMEWORK",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ }
+ }
+ }
+ }
+}
+""";
+
+ private static readonly string WorkloadInstallJson = $$"""
+{
+ "name": "install",
+ "version": "{{Product.Version}}",
+ "description": "Install one or more workloads.",
+ "hidden": false,
+ "arguments": {
+ "workloadId": {
+ "description": "The NuGet package ID of the workload to install.",
+ "order": 0,
+ "hidden": false,
+ "helpName": "WORKLOAD_ID",
+ "valueType": "System.Collections.Generic.IEnumerable",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1
+ }
+ }
+ },
+ "options": {
+ "--configfile": {
+ "description": "The NuGet configuration file to use.",
+ "hidden": false,
+ "helpName": "FILE",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--disable-parallel": {
+ "description": "Prevent restoring multiple projects in parallel.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--download-to-cache": {
+ "description": "Download packages needed to install a workload to a folder that can be used for offline installation.",
+ "hidden": true,
+ "helpName": "DIRECTORY",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--from-cache": {
+ "description": "Complete the operation from cache (offline).",
+ "hidden": true,
+ "helpName": "DIRECTORY",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--from-rollback-file": {
+ "description": "Update workloads based on specified rollback definition file.",
+ "hidden": true,
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--ignore-failed-sources": {
+ "description": "Treat package source failures as warnings.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--include-previews": {
+ "description": "Allow prerelease workload manifests.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--interactive": {
+ "description": "Allows the command to stop and wait for user input or action (for example to complete authentication).",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": true,
+ "defaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-cache": {
+ "description": "Do not cache packages and http requests.",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-http-cache": {
+ "description": "Do not cache packages and http requests.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--print-download-link-only": {
+ "description": "Only print the list of links to download without downloading.",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--sdk-version": {
+ "description": "The version of the SDK.",
+ "hidden": true,
+ "helpName": "VERSION",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--skip-manifest-update": {
+ "description": "Skip updating the workload manifests.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--skip-sign-check": {
+ "description": "Skip signature verification of workload packages and installers.",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--source": {
+ "description": "The NuGet package source to use during the restore. To specify multiple sources, repeat the option.",
+ "hidden": false,
+ "aliases": [
+ "-s"
+ ],
+ "helpName": "SOURCE",
+ "valueType": "System.String[]",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--temp-dir": {
+ "description": "Specify a temporary directory for this command to download and extract NuGet packages (must be secure).",
+ "hidden": false,
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--verbosity": {
+ "description": "Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].",
+ "hidden": false,
+ "aliases": [
+ "-v"
+ ],
+ "helpName": "LEVEL",
+ "valueType": "Microsoft.DotNet.Cli.VerbosityOptions",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--version": {
+ "description": "A workload version to display or one or more workloads and their versions joined by the '@' character.",
+ "hidden": false,
+ "valueType": "System.Collections.Generic.IEnumerable",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1
+ },
+ "required": false,
+ "recursive": false
+ }
+ }
+}
+""";
+
+ private static readonly string BuildJson = $$"""
+{
+ "name": "build",
+ "version": "{{Product.Version}}",
+ "description": ".NET Builder",
+ "hidden": false,
+ "arguments": {
+ "PROJECT | SOLUTION | FILE": {
+ "description": "The project or solution or C# (file-based program) file to operate on. If a file is not specified, the command will search the current directory for a project or solution.",
+ "order": 0,
+ "hidden": false,
+ "valueType": "System.String[]",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0
+ }
+ }
+ },
+ "options": {
+ "--arch": {
+ "description": "The target architecture.",
+ "hidden": false,
+ "aliases": [
+ "-a"
+ ],
+ "helpName": "ARCH",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--artifacts-path": {
+ "description": "The artifacts path. All output from the project, including build, publish, and pack output, will go in subfolders under the specified path.",
+ "hidden": false,
+ "helpName": "ARTIFACTS_DIR",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--configfile": {
+ "description": "",
+ "hidden": true,
+ "helpName": "FILE",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--configuration": {
+ "description": "The configuration to use for building the project. The default for most projects is 'Debug'.",
+ "hidden": false,
+ "aliases": [
+ "-c"
+ ],
+ "helpName": "CONFIGURATION",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--debug": {
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--disable-build-servers": {
+ "description": "Force the command to ignore any persistent build servers.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--disable-parallel": {
+ "description": "",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--force": {
+ "description": "Force all dependencies to be resolved even if the last restore was successful.\nThis is equivalent to deleting project.assets.json.",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--framework": {
+ "description": "The target framework to build for. The target framework must also be specified in the project file.",
+ "hidden": false,
+ "aliases": [
+ "-f"
+ ],
+ "helpName": "FRAMEWORK",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--ignore-failed-sources": {
+ "description": "",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--interactive": {
+ "description": "Allows the command to stop and wait for user input or action (for example to complete authentication).",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": true,
+ "defaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-cache": {
+ "description": "",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-dependencies": {
+ "description": "Do not build project-to-project references and only build the specified project.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-http-cache": {
+ "description": "",
+ "hidden": true,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-incremental": {
+ "description": "Do not use incremental building.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-restore": {
+ "description": "Do not restore the project before building.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--no-self-contained": {
+ "description": "Publish your application as a framework dependent application. A compatible .NET runtime must be installed on the target machine to run your application.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--nologo": {
+ "description": "Do not display the startup banner or the copyright message.",
+ "hidden": false,
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--os": {
+ "description": "The target operating system.",
+ "hidden": false,
+ "helpName": "OS",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--output": {
+ "description": "The output directory to place built artifacts in.",
+ "hidden": false,
+ "aliases": [
+ "-o"
+ ],
+ "helpName": "OUTPUT_DIR",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--packages": {
+ "description": "",
+ "hidden": true,
+ "helpName": "PACKAGES_DIR",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--property": {
+ "hidden": true,
+ "aliases": [
+ "--p",
+ "-p",
+ "-property",
+ "/p",
+ "/property"
+ ],
+ "valueType": "System.String[]",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--runtime": {
+ "description": "The target runtime to build for.",
+ "hidden": false,
+ "aliases": [
+ "-r"
+ ],
+ "helpName": "RUNTIME_IDENTIFIER",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--self-contained": {
+ "description": "Publish the .NET runtime with your application so the runtime doesn't need to be installed on the target machine.\nThe default is 'false.' However, when targeting .NET 7 or lower, the default is 'true' if a runtime identifier is specified.",
+ "hidden": false,
+ "aliases": [
+ "--sc"
+ ],
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--source": {
+ "description": "",
+ "hidden": true,
+ "helpName": "SOURCE",
+ "valueType": "System.Collections.Generic.IEnumerable",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--use-current-runtime": {
+ "description": "Use current runtime as the target runtime.",
+ "hidden": false,
+ "aliases": [
+ "--ucr"
+ ],
+ "valueType": "System.Boolean",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 0,
+ "maximum": 0
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--verbosity": {
+ "description": "Set the MSBuild verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], d[etailed], and diag[nostic].",
+ "hidden": false,
+ "aliases": [
+ "-v"
+ ],
+ "helpName": "LEVEL",
+ "valueType": "Microsoft.DotNet.Cli.VerbosityOptions",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ },
+ "--version-suffix": {
+ "description": "Set the value of the $(VersionSuffix) property to use when building the project.",
+ "hidden": false,
+ "helpName": "VERSION_SUFFIX",
+ "valueType": "System.String",
+ "hasDefaultValue": false,
+ "arity": {
+ "minimum": 1,
+ "maximum": 1
+ },
+ "required": false,
+ "recursive": false
+ }
+ }
+}
+""";
+
+ public static TheoryData CommandsJson => new()
+ {
+ { new[] { "solution", "list", "--cli-schema" }, SolutionListJson },
+ { new[] { "clean", "--cli-schema" }, CleanJson },
+ { new[] { "reference", "--cli-schema" }, ReferenceJson },
+ { new[] { "workload", "install", "--cli-schema" }, WorkloadInstallJson },
+ { new[] { "build", "--cli-schema" }, BuildJson }
+ };
+
+ [Theory]
+ [MemberData(nameof(CommandsJson))]
+ public void PrintCliSchema_WritesExpectedJson(string[] commandArgs, string json)
+ {
+ var stream = new MemoryStream();
+ var writer = new StreamWriter(stream);
+ CliSchema.PrintCliSchema(Parser.Instance.Parse(commandArgs).CommandResult, writer, null);
+ stream.Position = 0;
+ var reader = new StreamReader(stream);
+ var output = reader.ReadToEnd();
+ output.Should().BeEquivalentTo(json.ReplaceLineEndings("\n"));
+ }
+
+ [Fact]
+ public void CanGenerateJsonSchemaForCLIOutput()
+ {
+ var schema = CliSchema.GetJsonSchema();
+ schema.Should().NotBeNull();
+ }
+}