• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AnderssonPeter / PowerType / 5315603320

pending completion
5315603320

push

github

AnderssonPeter
Handle integers when parsing command line

308 of 468 branches covered (65.81%)

Branch coverage included in aggregate %.

4 of 4 new or added lines in 1 file covered. (100.0%)

661 of 908 relevant lines covered (72.8%)

61.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.03
/PowerType/Parsing/PowerShellString.cs
1
using System.Collections.ObjectModel;
2
using System.Diagnostics;
3
using System.Management.Automation.Language;
4
using System.Text;
5

6
namespace PowerType.Parsing;
7

8
[DebuggerDisplay("{EscapedValue}")]
9
public class PowerShellString
10
{
11
    private readonly static IReadOnlyDictionary<char, char> escapeLookup = new Dictionary<char, char>()
1✔
12
    {
1✔
13
        { '\0', '0' },
1✔
14
        { '\a', 'a' },
1✔
15
        { '\b', 'b' },
1✔
16
        { '\f', 'f' },
1✔
17
        { '\t', 't' },
1✔
18
        { '\v', 'v' },
1✔
19
        { '\n', 'n' },
1✔
20
        { '\r', 'r' },
1✔
21
    };
1✔
22

23
    private readonly static IReadOnlyDictionary<char, char> escapeReverseLookup = escapeLookup.ToDictionary(x => x.Value, x => x.Key);
17✔
24

25
    public readonly static PowerShellString Empty = new(StringConstantType.BareWord, String.Empty, string.Empty);
1✔
26

27
    private const char doubleQuotationMark = '"';
28
    private const char leftDoubleQuotationMark = '\u201C';
29
    private const char rightDoubleQuotationMark = '\u201D';
30
    private const char doubleLowNineQuotationMark = '\u201E';
31

32
    private static readonly char[] doubleQuoteCharacters = { doubleQuotationMark, leftDoubleQuotationMark, rightDoubleQuotationMark, doubleLowNineQuotationMark };
1✔
33

34
    private const char singleQuotationMark = '\'';
35
    private const char leftSingleQuotationMark = '\u2018';
36
    private const char rightSingleQuotationMark = '\u2019';
37
    private const char singleLowNineQuotationMark = '\u201A';
38
    private const char singleHighReversedNineQuotationMark = '\u201B';
39

40
    private static readonly char[] singleQuoteCharacters = { singleQuotationMark, leftSingleQuotationMark, rightSingleQuotationMark, singleLowNineQuotationMark, singleHighReversedNineQuotationMark };
1✔
41

42
    private readonly static IReadOnlyDictionary<StringConstantType, char[]> charsToEscape = new Dictionary<StringConstantType, char[]>()
1✔
43
    {
1✔
44
        { StringConstantType.BareWord,               singleQuoteCharacters.Concat(doubleQuoteCharacters).Concat(new char[] { '`', ' ',     '{', '}', '\0', '\a', '\b', '\f', '\t', '\v', '\n', '\r' }).ToArray() },
1✔
45
        { StringConstantType.DoubleQuoted,           doubleQuoteCharacters.Concat(                              new char[] { '`', '"',               '\0', '\a', '\b', '\f', '\t', '\v', '\n', '\r' }).ToArray() },
1✔
46
        { StringConstantType.DoubleQuotedHereString, doubleQuoteCharacters.Concat(                              new char[] { '`',                    '\0', '\a', '\b', '\f', '\t', '\v' }).ToArray() },
1✔
47
        { StringConstantType.SingleQuoted,           singleQuoteCharacters }
1✔
48
    };
1✔
49

50
    private readonly static IReadOnlyDictionary<StringConstantType, char[]> escapeChars = new Dictionary<StringConstantType, char[]>()
1✔
51
    {
1✔
52
        { StringConstantType.BareWord,                  new char[] { '`' } },
1✔
53
        { StringConstantType.DoubleQuoted,              new char[] { '`' } },
1✔
54
        { StringConstantType.DoubleQuotedHereString,    new char[] { '`' }  },
1✔
55
        { StringConstantType.SingleQuoted,              singleQuoteCharacters }
1✔
56
    };
1✔
57

58
    private readonly static IReadOnlyDictionary<StringConstantType, string[]> unsupportedStrings = new Dictionary<StringConstantType, string[]>()
1✔
59
    {
1✔
60
        { StringConstantType.SingleQuotedHereString, new string[] { "\n'@", "\r\n'@" } }
1✔
61
    };
1✔
62

63
    private readonly static IReadOnlyDictionary<StringConstantType, string[]> openingChars = new Dictionary<StringConstantType, string[]>()
1✔
64
    {
1✔
65
        { StringConstantType.DoubleQuoted,           doubleQuoteCharacters.Select(x => x.ToString()).ToArray() },
4✔
66
        { StringConstantType.DoubleQuotedHereString, doubleQuoteCharacters.SelectMany(x => new [] { "@" + x.ToString() + "\n", "@" + x.ToString() + "\r\n" }).ToArray() },
4✔
67
        { StringConstantType.SingleQuoted,           singleQuoteCharacters.Select(x => x.ToString()).ToArray() },
5✔
68
        { StringConstantType.SingleQuotedHereString, singleQuoteCharacters.SelectMany(x => new [] { "@" + x.ToString() + "\n", "@" + x.ToString() + "\r\n" }).ToArray() }
5✔
69
    };
1✔
70

71
    private readonly static IReadOnlyDictionary<StringConstantType, string[]> closingChars = new Dictionary<StringConstantType, string[]>()
1✔
72
    {
1✔
73
        { StringConstantType.DoubleQuoted,           doubleQuoteCharacters.Select(x => x.ToString()).ToArray() },
4✔
74
        { StringConstantType.DoubleQuotedHereString, doubleQuoteCharacters.SelectMany(x => new [] { "\n" + x.ToString() + "@", "\r\n" + x.ToString() + "@" }).ToArray() },
4✔
75
        { StringConstantType.SingleQuoted,           singleQuoteCharacters.Select(x => x.ToString()).ToArray() },
5✔
76
        { StringConstantType.SingleQuotedHereString, singleQuoteCharacters.SelectMany(x => new [] { "\n" + x.ToString() + "@", "\r\n" + x.ToString() + "@" }).ToArray() }
5✔
77
    };
1✔
78

79
    public PowerShellString(CommandElementAst commandElementAst)
×
80
    {
81
        if (commandElementAst is StringConstantExpressionAst stringConstant)
×
82
        {
83
            RawValue = stringConstant.Value;
×
84
            EscapedValue = stringConstant.ToString();
×
85
            Type = stringConstant.StringConstantType;
×
86
        }
87
        else if (commandElementAst is CommandParameterAst commandParameter)
×
88
        {
89
            RawValue = commandParameter.ToString();
×
90
            EscapedValue = commandParameter.ToString();
×
91
            Type = StringConstantType.BareWord;
×
92
        }
93
        else if (commandElementAst is ConstantExpressionAst constantExpression)
×
94
        {
95
            RawValue = constantExpression.ToString();
×
96
            EscapedValue = constantExpression.ToString();
×
97
            Type = StringConstantType.BareWord;
×
98
        }
99
        else
100
        {
101
            throw new InvalidOperationException("We have no idea how to handle this type: " + commandElementAst.GetType().ToString());
×
102
            /*RawValue = commandElementAst.ToString();
103
            Type = StringConstantType.DoubleQuoted;
104
            EscapedValue = Escape(Type, RawValue);*/
105
        }
106
    }
107

108
    public PowerShellString(StringConstantType type, string escapedValue, string rawValue)
301✔
109
    {
110
        EscapedValue = escapedValue;
301✔
111
        RawValue = rawValue;
301✔
112
        Type = type;
301✔
113
    }
301✔
114

115
    public static PowerShellString FromRaw(StringConstantType type, string rawValue)
116
    {
117
        var escapedValue = Escape(type, rawValue);
144✔
118
        if (openingChars.TryGetValue(type, out var openingChar))
144✔
119
        {
120
            escapedValue = openingChar.First() + escapedValue;
15✔
121
        }
122
        if (closingChars.TryGetValue(type, out var closingChar))
144✔
123
        {
124
            escapedValue += closingChar.First();
15✔
125
        }
126
        return new PowerShellString(type, escapedValue, rawValue);
144✔
127
    }
128

129
    /// <summary>
130
    /// Tries to automatically find the correct StringConstantType based on the string content.
131
    /// Highest priory is given to BareWord if that fails DoubleQuoted will be used.
132
    /// </summary>
133
    /// <param name="rawValue"></param>
134
    /// <returns></returns>
135
    public static PowerShellString FromRawSmart(string rawValue)
136
    {
137
        var escape = charsToEscape[StringConstantType.BareWord];
86✔
138
        if (escape.Any(x => rawValue.Contains(x)))
1,755✔
139
        {
140
            return FromRaw(StringConstantType.DoubleQuoted, rawValue);
8✔
141
        }
142
        return FromRaw(StringConstantType.BareWord, rawValue);
78✔
143
    }
144

145
    public static IEnumerable<PowerShellString> FromRawSmart(IEnumerable<string> rawValues) =>
146
        rawValues.Select(rawValue => FromRawSmart(rawValue));
30✔
147

148
    public static PowerShellString FromEscaped(StringConstantType type, string escapedValue)
149
    {
150
        var rawValue = Unescape(type, escapedValue);
134✔
151
        return new PowerShellString(type, escapedValue, rawValue);
134✔
152
    }
153

154
    /// <summary>
155
    /// Tries to automatically find the correct StringConstantType based on the string content.
156
    /// </summary>
157
    public static PowerShellString FromEscapedSmart(string escapedValue)
158
    {
159
        StringConstantType type = StringConstantType.BareWord;
90✔
160
        if (escapedValue.StartsWith('\''))
90✔
161
        {
162
            type = StringConstantType.SingleQuoted;
3✔
163
        }
164
        else if (escapedValue.StartsWith('"'))
87✔
165
        {
166
            type = StringConstantType.DoubleQuoted;
9✔
167
        }
168
        var index = escapedValue.IndexOf(' ');
90✔
169
        if (index == 0 || (index >= 1 && escapedValue[index - 1] != '`'))
90✔
170
        {
171
            type = StringConstantType.DoubleQuoted;
2✔
172
        }
173
        return FromEscaped(type, escapedValue);
90✔
174
    }
175

176
    public string EscapedValue { get; }
450✔
177
    public string RawValue { get; }
355✔
178
    public StringConstantType Type { get; }
166✔
179

180
    internal PowerShellString RemoveClosingFromEscaped()
181
    {
182
        if (EscapedValue == null)
5!
183
        {
184
            return this;
×
185
        }
186
        if (closingChars.TryGetValue(Type, out var closing) && IsEscapedClosed())
5✔
187
        {
188
            foreach (var c in closing)
6✔
189
            {
190
                if (EscapedValue.EndsWith(c))
2!
191
                {
192
                    return new PowerShellString(Type, EscapedValue[..^c.Length], RawValue);
2✔
193
                }
194
            }
195
        }
196
        return this;
3✔
197
    }
198

199
    internal PowerShellString RemoveOpeningFromEscaped()
200
    {
201
        if (EscapedValue == null)
5!
202
        {
203
            return this;
×
204
        }
205
        if (openingChars.TryGetValue(Type, out var opening) && IsEscapedOpened())
5!
206
        {
207
            foreach (var c in opening)
×
208
            {
209
                if (EscapedValue.StartsWith(c))
×
210
                {
211
                    return new PowerShellString(Type, EscapedValue[c.Length..], RawValue);
×
212
                }
213
            }
214
        }
215
        return this;
5✔
216
    }
217

218
    public PowerShellString Append(PowerShellString value)
219
    {
220
        var first = this.RemoveClosingFromEscaped();
5✔
221
        var last = value.RemoveOpeningFromEscaped();
5✔
222
        return new PowerShellString(Type, first.EscapedValue + last.EscapedValue, first.RawValue + last.RawValue).EnsureEscapedIsClosed();
5✔
223
    }
224

225
    public PowerShellString Convert(StringConstantType type)
226
    {
227
        if (Type == type)
×
228
        {
229
            return this;
×
230
        }
231
        return FromRaw(type, RawValue);
×
232
    }
233

234
    //todo: Escape unicode chars like ♥ they should be encoded like $([char]0x2665)
235
    internal static string Escape(StringConstantType type, string rawValue)
236
    {
237
        var charsToEscape = PowerShellString.charsToEscape.GetValueOrDefault(type);
151✔
238
        var escapeChar = PowerShellString.escapeChars.GetValueOrDefault(type)?.First();
151✔
239
        var unsupportedStrings = PowerShellString.unsupportedStrings.GetValueOrDefault(type);
151✔
240
        if (unsupportedStrings != null)
151✔
241
        {
242
            var unsupportedString = unsupportedStrings.FirstOrDefault(unsupportedString => rawValue.Contains(unsupportedString));
3✔
243
            if (unsupportedString != null)
1!
244
            {
245
                throw new InvalidOperationException($"Can't escape string of type {type} containing {unsupportedString}");
×
246
            }
247
        }
248
        if (escapeChar != null && charsToEscape != null)
151✔
249
        {
250
            var numberOfMatches = charsToEscape.Count(c => charsToEscape.Contains(c));
3,144✔
251
            if (numberOfMatches != 0)
150✔
252
            {
253
                var builder = new StringBuilder(rawValue.Length + numberOfMatches);
150✔
254
                for (int i = 0; i < rawValue.Length; i++)
2,286✔
255
                {
256
                    var c = rawValue[i];
993✔
257
                    if (charsToEscape.Contains(c))
993✔
258
                    {
259
                        builder.Append(escapeChar);
19✔
260

261
                        if (escapeLookup.TryGetValue(c, out var value))
19✔
262
                        {
263
                            builder.Append(value);
6✔
264
                        }
265
                        else
266
                        {
267
                            builder.Append(c);
13✔
268
                        }
269
                    }
270
                    else
271
                    {
272
                        builder.Append(c);
974✔
273
                    }
274
                }
275
                return builder.ToString();
150✔
276
            }
277
        }
278
        return rawValue;
1✔
279
    }
280

281
    public static string Unescape(StringConstantType type, string escapedValue)
282
    {
283
        var openingChars = PowerShellString.openingChars.GetValueOrDefault(type);
141✔
284
        var closingChars = PowerShellString.closingChars.GetValueOrDefault(type);
141✔
285
        var charsToEscape = PowerShellString.charsToEscape.GetValueOrDefault(type);
141✔
286
        var escapeChars = PowerShellString.escapeChars.GetValueOrDefault(type);
141✔
287
        var startIndex = 0;
141✔
288
        var endIndex = escapedValue.Length;
141✔
289
        if (openingChars != null)
141✔
290
        {
291
            var matchingOpeningChars = openingChars.FirstOrDefault(x => escapedValue.StartsWith(x));
297✔
292
            if (matchingOpeningChars != null)
56✔
293
            {
294
                startIndex = matchingOpeningChars.Length;
23✔
295
            }
296
        }
297

298
        if (closingChars != null)
141✔
299
        {
300
            var matchingClosingChars = closingChars.FirstOrDefault(x => escapedValue.EndsWith(x));
258✔
301
            if (matchingClosingChars != null)
56✔
302
            {
303
                endIndex = escapedValue.Length - matchingClosingChars.Length;
26✔
304
            }
305
        }
306
        if (escapeChars == null)
141✔
307
        {
308
            return escapedValue[startIndex..endIndex];
9✔
309
        }
310

311
        var builder = new StringBuilder(endIndex - startIndex); //The unescaped string will always be shorter than escaped
132✔
312
        var isEscape = false;
132✔
313
        for (int i = startIndex; i < endIndex; i++)
1,596✔
314
        {
315
            var c = escapedValue[i];
666✔
316
            if (isEscape)
666✔
317
            {
318
                if (escapeReverseLookup.TryGetValue(c, out var value))
19✔
319
                {
320
                    builder.Append(value);
5✔
321
                }
322
                else
323
                {
324
                    builder.Append(c);
14✔
325
                }
326
                isEscape = false;
19✔
327
            }
328
            else
329
            {
330
                if (escapeChars.Contains(c))
647✔
331
                {
332
                    isEscape = true;
24✔
333
                }
334
                else
335
                {
336
                    builder.Append(c);
623✔
337
                }
338
            }
339
        }
340

341
        return builder.ToString();
132✔
342
    }
343

344
    internal static int FindFirst(string value, char[] escapeChars, int index)
345
    {
346
        for (var i = index; i > 0; i--)
194✔
347
        {
348
            if (!escapeChars.Contains(value[i]))
96✔
349
            {
350
                return i + 1;
42✔
351
            }
352
        }
353
        return 0;
1✔
354
    }
355

356
    internal static bool IsEscapeChar(string value, char[] escapeChars, int index) => (FindFirst(value, escapeChars, index) - index) % 2 == 0 && escapeChars.Contains(value[index]);
37✔
357

358
    internal static bool IsEscapeChar(StringConstantType type, string value, Index index) => escapeChars.TryGetValue(type, out var escapeChar) &&  IsEscapeChar(value, escapeChar, index.GetOffset(value.Length));
35✔
359

360
    private bool IsEscapeChar(Index index) => IsEscapeChar(Type, EscapedValue, index);
23✔
361

362
    internal bool IsEscapedOpened() => !openingChars.TryGetValue(Type, out var value) || value.Any(x => EscapedValue.StartsWith(x));
78✔
363

364
    internal bool IsEscapedClosed() => !closingChars.TryGetValue(Type, out var value) || value.Any(x => EscapedValue.EndsWith(x) && (EscapedValue.Length <= x.Length || !IsEscapeChar(^(x.Length + 1))));
183✔
365

366
    public PowerShellString EnsureEscapedIsClosed()
367
    {
368
        if (closingChars.TryGetValue(Type, out var closingChar) && !IsEscapedClosed())
18✔
369
        {
370
            return new PowerShellString(Type, EscapedValue + closingChar.First(), RawValue);
10✔
371
        }
372
        return this;
8✔
373
    }
374
    public PowerShellString EnsureEscapedIsOpened()
375
    {
376
        if (closingChars.TryGetValue(Type, out var openingChar) && !IsEscapedClosed())
×
377
        {
378
            return new PowerShellString(Type, EscapedValue + openingChar.First(), RawValue);
×
379
        }
380
        return this;
×
381
    }
382

383
    public PowerShellString Normalize() => this.EnsureEscapedIsOpened().EnsureEscapedIsClosed();
×
384
}
385

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc