Skip to content

Commit adf7e9a

Browse files
committed
Add support for dotnet sln add file and dotnet sln add folder
1 parent e6c8e57 commit adf7e9a

File tree

62 files changed

+6325
-2089
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+6325
-2089
lines changed

src/Cli/dotnet/SlnFileExtensions.cs

Lines changed: 482 additions & 385 deletions
Large diffs are not rendered by default.

src/Cli/dotnet/SlnProjectExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,18 @@ public static string GetFullSolutionFolderPath(this SlnProject slnProject)
3232

3333
return path;
3434
}
35+
36+
public static SlnSection GetSolutionItemsSectionOrDefault(this SlnProject project) =>
37+
project.Sections.GetSection("SolutionItems", SlnSectionType.PreProcess);
38+
39+
public static bool ContainsSolutionItem(this SlnProject project, string solutionItemName)
40+
{
41+
var section = project.GetSolutionItemsSectionOrDefault();
42+
if (section == null) { return false; }
43+
44+
// solution item names are case-insensitive
45+
return new Dictionary<string, string>(section.GetContent(), StringComparer.OrdinalIgnoreCase)
46+
.ContainsKey(solutionItemName);
47+
}
3548
}
3649
}

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

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,15 +132,42 @@
132132
<data name="AppHelpText" xml:space="preserve">
133133
<value>Projects to add or to remove from the solution.</value>
134134
</data>
135+
<data name="CouldNotFindFile" xml:space="preserve">
136+
<value>Could not find file `{0}`.</value>
137+
</data>
135138
<data name="AddAppFullName" xml:space="preserve">
136139
<value>Add one or more projects to a solution file.</value>
137140
</data>
141+
<data name="AddFileFullName" xml:space="preserve">
142+
<value>Add one or more solution items to a solution file.</value>
143+
</data>
144+
<data name="AddFolderFullName" xml:space="preserve">
145+
<value>Add one or more solution folders to a solution file.</value>
146+
</data>
138147
<data name="AddProjectPathArgumentName" xml:space="preserve">
139148
<value>PROJECT_PATH</value>
140149
</data>
150+
<data name="ProjectPathArgumentShouldNotBeProvidedForDotnetSlnAddFile" xml:space="preserve">
151+
<value>PROJECT_PATH should not be provided for `dotnet sln add file`</value>
152+
</data>
153+
<data name="ProjectPathArgumentShouldNotBeProvidedForDotnetSlnAddFolder" xml:space="preserve">
154+
<value>PROJECT_PATH should not be provided for `dotnet sln add folder`</value>
155+
</data>
141156
<data name="AddProjectPathArgumentDescription" xml:space="preserve">
142157
<value>The paths to the projects to add to the solution.</value>
143158
</data>
159+
<data name="AddFilePathArgumentName" xml:space="preserve">
160+
<value>FILE_PATH</value>
161+
</data>
162+
<data name="AddFilePathArgumentDescription" xml:space="preserve">
163+
<value>The paths to the solution items to add to the solution.</value>
164+
</data>
165+
<data name="AddFolderPathArgumentName" xml:space="preserve">
166+
<value>FOLDER_PATH</value>
167+
</data>
168+
<data name="AddFolderPathArgumentDescription" xml:space="preserve">
169+
<value>The paths to the solution folders to add to the solution.</value>
170+
</data>
144171
<data name="RemoveProjectPathArgumentName" xml:space="preserve">
145172
<value>PROJECT_PATH</value>
146173
</data>
@@ -150,6 +177,32 @@
150177
<data name="RemoveAppFullName" xml:space="preserve">
151178
<value>Remove one or more projects from a solution file.</value>
152179
</data>
180+
<data name="SpecifyAtLeastOneFileToAdd" xml:space="preserve">
181+
<value>You must specify at least one file to add.</value>
182+
</data>
183+
<data name="SpecifyAtLeastOneFolderToAdd" xml:space="preserve">
184+
<value>You must specify at least one folder to add.</value>
185+
</data>
186+
<data name="SolutionAlreadyContainsFile" xml:space="preserve">
187+
<value>The solution {0} already contains the solution item `{1}`</value>
188+
</data>
189+
<data name="SolutionAlreadyContainsFolder" xml:space="preserve">
190+
<value>The solution {0} already contains the solution folder `{1}`</value>
191+
</data>
192+
<data name="SolutionItemAddedToTheSolution" xml:space="preserve">
193+
<value>The solution item `{0}` was added to the solution folder `{1}`</value>
194+
</data>
195+
<data name="SolutionFolderAddedToTheSolution" xml:space="preserve">
196+
<value>The solution folder `{0}` was added to the solution</value>
197+
</data>
198+
<data name="SolutionFolderNameCannot" xml:space="preserve">
199+
<value>Solution Folder names cannot:
200+
- contain any of the following characters: / : ? \ * &quot; &lt; &gt; |
201+
- contain Unicode control characters
202+
- contain surrogate characters
203+
- be system reserved names, including CON, AUX, PRN, COM1 or LPT2
204+
- be . or ..</value>
205+
</data>
153206
<data name="RemoveSubcommandHelpText" xml:space="preserve">
154207
<value>Remove the specified project(s) from the solution. The project is not impacted.</value>
155208
</data>
@@ -162,15 +215,42 @@
162215
<data name="ProjectsHeader" xml:space="preserve">
163216
<value>Project(s)</value>
164217
</data>
165-
<data name="InRoot" xml:space="preserve">
218+
<data name="SolutionElementType" xml:space="preserve">
219+
<value>The type of the solution element to add or remove. Allowed values are project, item, and folder.</value>
220+
</data>
221+
<data name="AddProjectInRootArgumentDescription" xml:space="preserve">
166222
<value>Place project in root of the solution, rather than creating a solution folder.</value>
167223
</data>
224+
<data name="AddFileInRootArgumentDescription" xml:space="preserve">
225+
<value>Place file in root of the solution, rather than creating a solution folder.</value>
226+
</data>
227+
<data name="AddFolderInRootArgumentDescription" xml:space="preserve">
228+
<value>Place folder in root of the solution, rather than creating a solution folder.</value>
229+
</data>
168230
<data name="AddProjectSolutionFolderArgumentDescription" xml:space="preserve">
169231
<value>The destination solution folder path to add the projects to.</value>
170232
</data>
233+
<data name="AddFileSolutionFolderArgumentDescription" xml:space="preserve">
234+
<value>The destination solution folder path to add the files to.</value>
235+
</data>
236+
<data name="AddFolderSolutionFolderArgumentDescription" xml:space="preserve">
237+
<value>The destination solution folder path to add the folders to.</value>
238+
</data>
239+
<data name="SolutionItemWithTheSameNameExists" xml:space="preserve">
240+
<value>There is already an existing solution item '{0}' with the same name in the solution folder '{1}'.</value>
241+
</data>
171242
<data name="SolutionFolderAndInRootMutuallyExclusive" xml:space="preserve">
172243
<value>The --solution-folder and --in-root options cannot be used together; use only one of the options.</value>
173244
</data>
245+
<data name="CannotAddTheSameSolutionToItself" xml:space="preserve">
246+
<value>Cannot add the same solution to itself.</value>
247+
</data>
248+
<data name="CannotAddExistingProjectAsSolutionItem" xml:space="preserve">
249+
<value>An existing project cannot be added as a solution item.</value>
250+
</data>
251+
<data name="CannotAddExistingSolutionItem" xml:space="preserve">
252+
<value>An existing solution item cannot be added.</value>
253+
</data>
174254
<data name="ListSolutionFoldersArgumentDescription" xml:space="preserve">
175255
<value>Display solution folder paths.</value>
176256
</data>
Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,96 @@
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.Buffers;
5+
using System.IO;
46
using Microsoft.DotNet.Cli;
57
using Microsoft.DotNet.Cli.Utils;
68

7-
namespace Microsoft.DotNet.Tools.Sln
9+
namespace Microsoft.DotNet.Tools.Sln;
10+
11+
internal static class SlnArgumentValidator
812
{
9-
internal static class SlnArgumentValidator
13+
private static readonly SearchValues<char> s_invalidCharactersInSolutionFolderName = SearchValues.Create("/:?\\*\"<>|");
14+
private static readonly string[] s_invalidSolutionFolderNames =
15+
[
16+
// system reserved names
17+
"CON", "AUX", "PRN", "COM1", "COM2", "COM3", "COM4", "LPT1", "LPT2", "LPT3",
18+
// relative path components
19+
".", "..",
20+
];
21+
22+
public enum CommandType
23+
{
24+
Add,
25+
Remove
26+
}
27+
public static void ParseAndValidateArguments(IReadOnlyList<string> _arguments, CommandType commandType, bool _inRoot = false, string relativeRoot = null, string subcommand = null)
1028
{
11-
public enum CommandType
29+
if (_arguments.Count == 0)
1230
{
13-
Add,
14-
Remove
31+
string message = commandType == CommandType.Add
32+
? CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd
33+
: CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove;
34+
throw new GracefulException(message);
1535
}
16-
public static void ParseAndValidateArguments(string _fileOrDirectory, IReadOnlyCollection<string> _arguments, CommandType commandType, bool _inRoot = false, string relativeRoot = null)
36+
37+
bool hasRelativeRoot = !string.IsNullOrEmpty(relativeRoot);
38+
39+
if (_inRoot && hasRelativeRoot)
1740
{
18-
if (_arguments.Count == 0)
19-
{
20-
string message = commandType == CommandType.Add ? CommonLocalizableStrings.SpecifyAtLeastOneProjectToAdd : CommonLocalizableStrings.SpecifyAtLeastOneProjectToRemove;
21-
throw new GracefulException(message);
22-
}
23-
24-
bool hasRelativeRoot = !string.IsNullOrEmpty(relativeRoot);
25-
26-
if (_inRoot && hasRelativeRoot)
27-
{
28-
// These two options are mutually exclusive
29-
throw new GracefulException(LocalizableStrings.SolutionFolderAndInRootMutuallyExclusive);
30-
}
31-
32-
var slnFile = _arguments.FirstOrDefault(path => path.EndsWith(".sln"));
33-
if (slnFile != null)
34-
{
35-
string args;
36-
if (_inRoot)
37-
{
38-
args = $"--{SlnAddParser.InRootOption.Name} ";
39-
}
40-
else if (hasRelativeRoot)
41-
{
42-
args = $"--{SlnAddParser.SolutionFolderOption.Name} {string.Join(" ", relativeRoot)} ";
43-
}
44-
else
45-
{
46-
args = "";
47-
}
48-
49-
var projectArgs = string.Join(" ", _arguments.Where(path => !path.EndsWith(".sln")));
50-
string command = commandType == CommandType.Add ? "add" : "remove";
51-
throw new GracefulException(new string[]
52-
{
53-
string.Format(CommonLocalizableStrings.SolutionArgumentMisplaced, slnFile),
54-
CommonLocalizableStrings.DidYouMean,
55-
$" dotnet solution {slnFile} {command} {args}{projectArgs}"
56-
});
57-
}
41+
// These two options are mutually exclusive
42+
throw new GracefulException(LocalizableStrings.SolutionFolderAndInRootMutuallyExclusive);
5843
}
44+
45+
// Something is wrong if there is a .sln file as an argument, so suggest that the arguments may have been misplaced.
46+
// However, it is possible to add .sln file as a solution item, so don't suggest in the case of dotnet sln add file.
47+
var slnFile = _arguments.FirstOrDefault(path => path.EndsWith(".sln", StringComparison.OrdinalIgnoreCase));
48+
if (slnFile == null || subcommand == "file")
49+
{
50+
return;
51+
}
52+
53+
string args = _inRoot
54+
? $"{SlnAddParser.InRootOption.Name} "
55+
: hasRelativeRoot ? $"{SlnAddParser.SolutionFolderOption.Name} {string.Join(" ", relativeRoot)} " : "";
56+
57+
var nonSolutionArguments = string.Join(
58+
" ",
59+
_arguments.Where(a => !a.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)));
60+
61+
string command = commandType switch
62+
{
63+
CommandType.Add => "add",
64+
CommandType.Remove => "remove",
65+
_ => throw new InvalidOperationException($"Unable to handle command type {commandType}"),
66+
};
67+
throw new GracefulException(
68+
[
69+
string.Format(CommonLocalizableStrings.SolutionArgumentMisplaced, slnFile),
70+
CommonLocalizableStrings.DidYouMean,
71+
subcommand == null
72+
? $" dotnet solution {slnFile} {command} {args}{nonSolutionArguments}"
73+
: $" dotnet solution {slnFile} {command} {subcommand} {args}{nonSolutionArguments}"
74+
]);
75+
}
76+
77+
public static bool IsValidSolutionFolderName(string folderName)
78+
{
79+
if (string.IsNullOrWhiteSpace(folderName))
80+
return false;
81+
82+
if (folderName.AsSpan().IndexOfAny(s_invalidCharactersInSolutionFolderName) >= 0)
83+
return false;
84+
85+
if (folderName.Any(char.IsControl))
86+
return false;
87+
88+
if (folderName.Any(char.IsSurrogate))
89+
return false;
90+
91+
if (s_invalidSolutionFolderNames.Contains(folderName, StringComparer.OrdinalIgnoreCase))
92+
return false;
93+
94+
return true;
5995
}
6096
}

0 commit comments

Comments
 (0)