-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
ArgumentEscaper.cs
199 lines (171 loc) · 6.92 KB
/
ArgumentEscaper.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// 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
{
public static class ArgumentEscaper
{
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// https://docs.microsoft.com/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable<string> args)
{
var escaped = EscapeArgArray(args);
#if NET35
return string.Join(" ", escaped.ToArray());
#else
return string.Join(" ", escaped);
#endif
}
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// https://docs.microsoft.com/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
public static string EscapeAndConcatenateArgArrayForCmdProcessStart(IEnumerable<string> args)
{
var escaped = EscapeArgArrayForCmd(args);
#if NET35
return string.Join(" ", escaped.ToArray());
#else
return string.Join(" ", escaped);
#endif
}
/// <summary>
/// Undo the processing which took place to create string[] args in Main,
/// so that the next process will receive the same string[] args
///
/// See here for more info:
/// https://docs.microsoft.com/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static IEnumerable<string> EscapeArgArray(IEnumerable<string> args)
{
var escapedArgs = new List<string>();
foreach (var arg in args)
{
escapedArgs.Add(EscapeSingleArg(arg));
}
return escapedArgs;
}
/// <summary>
/// This prefixes every character with the '^' character to force cmd to
/// interpret the argument string literally. An alternative option would
/// be to do this only for cmd metacharacters.
///
/// See here for more info:
/// https://docs.microsoft.com/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static IEnumerable<string> EscapeArgArrayForCmd(IEnumerable<string> arguments)
{
var escapedArgs = new List<string>();
foreach (var arg in arguments)
{
escapedArgs.Add(EscapeArgForCmd(arg));
}
return escapedArgs;
}
public static string EscapeSingleArg(string arg)
{
var sb = new StringBuilder();
var length = arg.Length;
var needsQuotes = length == 0 || ShouldSurroundWithQuotes(arg);
var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg);
if (needsQuotes) sb.Append("\"");
for (int i = 0; i < length; ++i)
{
var backslashCount = 0;
// Consume All Backslashes
while (i < arg.Length && arg[i] == '\\')
{
backslashCount++;
i++;
}
// Escape any backslashes at the end of the arg
// when the argument is also quoted.
// This ensures the outside quote is interpreted as
// an argument delimiter
if (i == arg.Length && isQuoted)
{
sb.Append('\\', 2 * backslashCount);
}
// At then end of the arg, which isn't quoted,
// just add the backslashes, no need to escape
else if (i == arg.Length)
{
sb.Append('\\', backslashCount);
}
// Escape any preceding backslashes and the quote
else if (arg[i] == '"')
{
sb.Append('\\', (2 * backslashCount) + 1);
sb.Append('"');
}
// Output any consumed backslashes and the character
else
{
sb.Append('\\', backslashCount);
sb.Append(arg[i]);
}
}
if (needsQuotes) sb.Append("\"");
return sb.ToString();
}
/// <summary>
/// Prepare as single argument to
/// roundtrip properly through cmd.
///
/// This prefixes every character with the '^' character to force cmd to
/// interpret the argument string literally. An alternative option would
/// be to do this only for cmd metacharacters.
///
/// See here for more info:
/// https://docs.microsoft.com/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way
/// </summary>
/// <param name="args"></param>
/// <returns></returns>
private static string EscapeArgForCmd(string argument)
{
var sb = new StringBuilder();
var quoted = ShouldSurroundWithQuotes(argument);
if (quoted) sb.Append("^\"");
// Prepend every character with ^
// This is harmless when passing through cmd
// and ensures cmd metacharacters are not interpreted
// as such
foreach (var character in argument)
{
sb.Append("^");
sb.Append(character);
}
if (quoted) sb.Append("^\"");
return sb.ToString();
}
internal static bool ShouldSurroundWithQuotes(string argument)
{
// Only quote if whitespace exists in the string
return ArgumentContainsWhitespace(argument);
}
internal static bool IsSurroundedWithQuotes(string argument)
{
return argument.StartsWith("\"", StringComparison.Ordinal) &&
argument.EndsWith("\"", StringComparison.Ordinal);
}
internal static bool ArgumentContainsWhitespace(string argument)
{
return argument.Contains(" ") || argument.Contains("\t") || argument.Contains("\n");
}
}
}