Skip to content

Commit 35d0bba

Browse files
Add more robust quoting for dotnet commands (#347)
* Fixed some minor issues with `Program.cs` * Added quoting system for `dotnet` commands
1 parent 7b65872 commit 35d0bba

File tree

4 files changed

+203
-8
lines changed

4 files changed

+203
-8
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace Incrementalist.Cmd.Commands
6+
{
7+
/// <summary>
8+
/// Handles parsing of complex command line arguments with proper quote handling
9+
/// </summary>
10+
public static class CommandLineArgumentParser
11+
{
12+
/// <summary>
13+
/// Combines command line arguments into a properly quoted string
14+
/// </summary>
15+
public static string CombineArguments(IEnumerable<string> arguments)
16+
{
17+
if (arguments == null) throw new ArgumentNullException(nameof(arguments));
18+
19+
var sb = new StringBuilder();
20+
foreach (var arg in arguments)
21+
{
22+
if (sb.Length > 0)
23+
sb.Append(' ');
24+
25+
// Check if we need to quote this argument
26+
var needsQuoting = arg.Contains(' ') || arg.Contains('\t') || arg.Contains('"');
27+
28+
if (!needsQuoting)
29+
{
30+
sb.Append(arg);
31+
}
32+
else
33+
{
34+
sb.Append('"');
35+
36+
// Replace any embedded quotes with escaped quotes
37+
sb.Append(arg.Replace("\"", "\\\""));
38+
39+
sb.Append('"');
40+
}
41+
}
42+
43+
return sb.ToString();
44+
}
45+
46+
/// <summary>
47+
/// Splits a command line string into individual arguments, preserving quoted sections
48+
/// </summary>
49+
public static string[] SplitArguments(string commandLine)
50+
{
51+
if (string.IsNullOrEmpty(commandLine)) return Array.Empty<string>();
52+
53+
var args = new List<string>();
54+
var currentArg = new StringBuilder();
55+
var inQuotes = false;
56+
var escaped = false;
57+
58+
for (var i = 0; i < commandLine.Length; i++)
59+
{
60+
var c = commandLine[i];
61+
62+
if (escaped)
63+
{
64+
currentArg.Append(c);
65+
escaped = false;
66+
continue;
67+
}
68+
69+
if (c == '\\')
70+
{
71+
escaped = true;
72+
continue;
73+
}
74+
75+
if (c == '"')
76+
{
77+
inQuotes = !inQuotes;
78+
continue;
79+
}
80+
81+
if (!inQuotes && char.IsWhiteSpace(c))
82+
{
83+
if (currentArg.Length > 0)
84+
{
85+
args.Add(currentArg.ToString());
86+
currentArg.Clear();
87+
}
88+
continue;
89+
}
90+
91+
currentArg.Append(c);
92+
}
93+
94+
if (currentArg.Length > 0)
95+
args.Add(currentArg.ToString());
96+
97+
return args.ToArray();
98+
}
99+
}
100+
}

src/Incrementalist.Cmd/Commands/RunDotNetCommandTask.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private async Task<int> RunIncrementalBuild(IEnumerable<string> affectedProjects
106106
private async Task<int> RunCommand(string target)
107107
{
108108
// For dotnet CLI commands like 'build', 'test', etc., the project/solution path comes last
109-
var args = string.Join(" ", _dotnetArgs);
109+
var args = CommandLineArgumentParser.CombineArguments(_dotnetArgs);
110110
if (!args.Contains("--project") && !args.Contains("-p"))
111111
args = $"{args} \"{target}\"";
112112

src/Incrementalist.Cmd/Program.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,11 @@ private static async Task<int> Main(string[] args)
4949
SetTitle();
5050

5151
// Split args at -- to separate incrementalist args from dotnet args
52-
var splitIndex = Array.IndexOf(args, "--");
53-
var incrementalistArgs = splitIndex >= 0 ? args.Take(splitIndex).ToArray() : args;
54-
var dotnetArgs = splitIndex >= 0 ? args.Skip(splitIndex + 1).ToArray() : Array.Empty<string>();
52+
var allArgs = string.Join(" ", args);
53+
var parts = allArgs.Split(new[] { " -- " }, StringSplitOptions.None);
54+
55+
var incrementalistArgs = parts.Length > 0 ? CommandLineArgumentParser.SplitArguments(parts[0]) : Array.Empty<string>();
56+
var dotnetArgs = parts.Length > 1 ? CommandLineArgumentParser.SplitArguments(parts[1]) : Array.Empty<string>();
5557

5658
SlnOptions options = null;
5759
var result = Parser.Default.ParseArguments<SlnOptions>(incrementalistArgs).MapResult(r =>
@@ -116,10 +118,10 @@ private static async Task<int> RunIncrementalist(SlnOptions options)
116118
if (!DiffHelper.HasBranch(repoResult.repo, options.GitBranch))
117119
{
118120
logger.LogError("Current git repository doesn't have any branch named [{0}]. Shutting down.", options.GitBranch);
119-
logger.LogDebug("Here are all of the currently known branches in this repository:");
121+
logger.LogInformation("Here are all of the currently known branches in this repository:");
120122
foreach (var b in repoResult.repo.Branches)
121123
{
122-
logger.LogDebug(b.FriendlyName);
124+
logger.LogInformation(b.FriendlyName);
123125
}
124126

125127
return -4;
@@ -131,7 +133,7 @@ private static async Task<int> RunIncrementalist(SlnOptions options)
131133
if (options.ListFolders)
132134
await AnalyzeFolderDiff(options, workingFolder, logger);
133135
else
134-
await AnaylzeSolutionDIff(options, workingFolder, logger);
136+
await AnalyzeSolutionDIff(options, workingFolder, logger);
135137
}
136138

137139
return 0;
@@ -158,7 +160,7 @@ private static async Task AnalyzeFolderDiff(SlnOptions options, DirectoryInfo wo
158160
HandleAffectedFiles(options, affectedFilesStr, affectedFiles.Count, logger);
159161
}
160162

161-
private static async Task AnaylzeSolutionDIff(SlnOptions options, DirectoryInfo workingFolder, ILogger logger)
163+
private static async Task AnalyzeSolutionDIff(SlnOptions options, DirectoryInfo workingFolder, ILogger logger)
162164
{
163165
// Locate and register the default instance of MSBuild installed on this machine.
164166
MSBuildLocator.RegisterDefaults();
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Linq;
3+
using Incrementalist.Cmd.Commands;
4+
using Xunit;
5+
6+
namespace Incrementalist.Tests
7+
{
8+
public class CommandLineArgumentParserTests
9+
{
10+
[Fact]
11+
public void CombineArguments_WithSimpleArgs_ShouldNotQuote()
12+
{
13+
var args = new[] { "test", "-c", "Release", "--no-build" };
14+
var result = CommandLineArgumentParser.CombineArguments(args);
15+
Assert.Equal("test -c Release --no-build", result);
16+
}
17+
18+
[Fact]
19+
public void CombineArguments_WithSpaces_ShouldQuote()
20+
{
21+
var args = new[] { "test", "--collect:\"XPlat Code Coverage\"", "--logger:\"console;verbosity=normal\"" };
22+
var result = CommandLineArgumentParser.CombineArguments(args);
23+
Assert.Equal("test \"--collect:\\\"XPlat Code Coverage\\\"\" \"--logger:\\\"console;verbosity=normal\\\"\"", result);
24+
}
25+
26+
[Fact]
27+
public void SplitArguments_WithQuotedSpaces_ShouldPreserveQuotes()
28+
{
29+
var input = "test -c Release --no-build \"--collect:XPlat Code Coverage\" \"--logger:console;verbosity=normal\"";
30+
var result = CommandLineArgumentParser.SplitArguments(input);
31+
32+
Assert.Equal(new[]
33+
{
34+
"test",
35+
"-c",
36+
"Release",
37+
"--no-build",
38+
"--collect:XPlat Code Coverage",
39+
"--logger:console;verbosity=normal"
40+
}, result);
41+
}
42+
43+
[Fact]
44+
public void SplitArguments_WithEscapedQuotes_ShouldHandleCorrectly()
45+
{
46+
var input = "test \"--logger:\\\"trx\\\"\" \"--collect:\\\"XPlat Code Coverage\\\"\"";
47+
var result = CommandLineArgumentParser.SplitArguments(input);
48+
49+
Assert.Equal(new[]
50+
{
51+
"test",
52+
"--logger:\"trx\"",
53+
"--collect:\"XPlat Code Coverage\""
54+
}, result);
55+
}
56+
57+
[Fact]
58+
public void CombineAndSplit_ShouldBeSymmetric()
59+
{
60+
var originalArgs = new[]
61+
{
62+
"test",
63+
"-c",
64+
"Release",
65+
"--no-build",
66+
"--logger:\"trx\"",
67+
"--collect:\"XPlat Code Coverage\"",
68+
"--logger:\"console;verbosity=normal\"",
69+
"--results-directory:TestResults"
70+
};
71+
72+
var combined = CommandLineArgumentParser.CombineArguments(originalArgs);
73+
var split = CommandLineArgumentParser.SplitArguments(combined);
74+
75+
Assert.Equal(originalArgs, split);
76+
}
77+
78+
[Theory]
79+
[InlineData(null)]
80+
[InlineData("")]
81+
public void SplitArguments_WithEmptyInput_ReturnsEmptyArray(string input)
82+
{
83+
var result = CommandLineArgumentParser.SplitArguments(input);
84+
Assert.Empty(result);
85+
}
86+
87+
[Fact]
88+
public void CombineArguments_WithNull_ThrowsArgumentNullException()
89+
{
90+
Assert.Throws<ArgumentNullException>(() => CommandLineArgumentParser.CombineArguments(null));
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)