|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Collections.Generic; |
| 6 | + |
| 7 | +namespace Internal.CommandLine |
| 8 | +{ |
| 9 | + internal static class ArgumentLexer |
| 10 | + { |
| 11 | + public static IReadOnlyList<ArgumentToken> Lex(IEnumerable<string> args, Func<string, IEnumerable<string>> responseFileReader = null) |
| 12 | + { |
| 13 | + var result = new List<ArgumentToken>(); |
| 14 | + |
| 15 | + // We'll split the arguments into tokens. |
| 16 | + // |
| 17 | + // A token combines the modifier (/, -, --), the option name, and the option |
| 18 | + // value. |
| 19 | + // |
| 20 | + // Please note that this code doesn't combine arguments. It only provides |
| 21 | + // some pre-processing over the arguments to split out the modifier, |
| 22 | + // option, and value: |
| 23 | + // |
| 24 | + // { "--out", "out.exe" } ==> { new ArgumentToken("--", "out", null), |
| 25 | + // new ArgumentToken(null, null, "out.exe") } |
| 26 | + // |
| 27 | + // {"--out:out.exe" } ==> { new ArgumentToken("--", "out", "out.exe") } |
| 28 | + // |
| 29 | + // The reason it doesn't combine arguments is because it depends on the actual |
| 30 | + // definition. For example, if --out is a flag (meaning it's of type bool) then |
| 31 | + // out.exe in the first example wouldn't be considered its value. |
| 32 | + // |
| 33 | + // The code also handles the special -- token which indicates that the following |
| 34 | + // arguments shouldn't be considered options. |
| 35 | + // |
| 36 | + // Finally, this code will also expand any reponse file entries, assuming the caller |
| 37 | + // gave us a non-null reader. |
| 38 | + |
| 39 | + var hasSeenDashDash = false; |
| 40 | + |
| 41 | + foreach (var arg in ExpandResponseFiles(args, responseFileReader)) |
| 42 | + { |
| 43 | + // If we've seen a -- already, then we'll treat one as a plain name, that is |
| 44 | + // without a modifier or value. |
| 45 | + |
| 46 | + if (!hasSeenDashDash && arg == @"--") |
| 47 | + { |
| 48 | + hasSeenDashDash = true; |
| 49 | + continue; |
| 50 | + } |
| 51 | + |
| 52 | + string modifier; |
| 53 | + string name; |
| 54 | + string value; |
| 55 | + |
| 56 | + if (hasSeenDashDash) |
| 57 | + { |
| 58 | + modifier = null; |
| 59 | + name = arg; |
| 60 | + value = null; |
| 61 | + } |
| 62 | + else |
| 63 | + { |
| 64 | + // If we haven't seen the -- separator, we're looking for options. |
| 65 | + // Options have leading modifiers, i.e. /, -, or --. |
| 66 | + // |
| 67 | + // Options can also have values, such as: |
| 68 | + // |
| 69 | + // -f:false |
| 70 | + // --name=hello |
| 71 | + |
| 72 | + |
| 73 | + if (!TryExtractOption(arg, out modifier, out string nameAndValue)) |
| 74 | + { |
| 75 | + name = arg; |
| 76 | + value = null; |
| 77 | + } |
| 78 | + else if (!TrySplitNameValue(nameAndValue, out name, out value)) |
| 79 | + { |
| 80 | + name = nameAndValue; |
| 81 | + value = null; |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + var token = new ArgumentToken(modifier, name, value); |
| 86 | + result.Add(token); |
| 87 | + } |
| 88 | + |
| 89 | + // Single letter options can be combined, for example the following two |
| 90 | + // forms are considered equivalent: |
| 91 | + // |
| 92 | + // (1) -xdf |
| 93 | + // (2) -x -d -f |
| 94 | + // |
| 95 | + // In order to free later phases from handling this case, we simply expand |
| 96 | + // single letter options to the second form. |
| 97 | + |
| 98 | + for (var i = result.Count - 1; i >= 0; i--) |
| 99 | + { |
| 100 | + if (IsOptionBundle(result[i])) |
| 101 | + ExpandOptionBundle(result, i); |
| 102 | + } |
| 103 | + |
| 104 | + return result.ToArray(); |
| 105 | + } |
| 106 | + |
| 107 | + private static IEnumerable<string> ExpandResponseFiles(IEnumerable<string> args, Func<string, IEnumerable<string>> responseFileReader) |
| 108 | + { |
| 109 | + foreach (var arg in args) |
| 110 | + { |
| 111 | + if (responseFileReader == null || !arg.StartsWith(@"@")) |
| 112 | + { |
| 113 | + yield return arg; |
| 114 | + } |
| 115 | + else |
| 116 | + { |
| 117 | + var fileName = arg.Substring(1); |
| 118 | + |
| 119 | + var responseFileArguments = responseFileReader(fileName); |
| 120 | + |
| 121 | + // The reader can suppress expanding this response file by |
| 122 | + // returning null. In that case, we'll treat the response |
| 123 | + // file token as a regular argument. |
| 124 | + |
| 125 | + if (responseFileArguments == null) |
| 126 | + { |
| 127 | + yield return arg; |
| 128 | + } |
| 129 | + else |
| 130 | + { |
| 131 | + foreach (var responseFileArgument in responseFileArguments) |
| 132 | + yield return responseFileArgument.Trim(); |
| 133 | + } |
| 134 | + } |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + private static bool IsOptionBundle(ArgumentToken token) |
| 139 | + { |
| 140 | + return token.IsOption && |
| 141 | + token.Modifier == @"-" && |
| 142 | + token.Name.Length > 1; |
| 143 | + } |
| 144 | + |
| 145 | + private static void ExpandOptionBundle(IList<ArgumentToken> receiver, int index) |
| 146 | + { |
| 147 | + var options = receiver[index].Name; |
| 148 | + receiver.RemoveAt(index); |
| 149 | + |
| 150 | + foreach (var c in options) |
| 151 | + { |
| 152 | + var name = char.ToString(c); |
| 153 | + var expandedToken = new ArgumentToken(@"-", name, null); |
| 154 | + receiver.Insert(index, expandedToken); |
| 155 | + index++; |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + private static bool TryExtractOption(string text, out string modifier, out string remainder) |
| 160 | + { |
| 161 | + return TryExtractOption(text, @"--", out modifier, out remainder) || |
| 162 | + TryExtractOption(text, @"-", out modifier, out remainder); |
| 163 | + } |
| 164 | + |
| 165 | + private static bool TryExtractOption(string text, string prefix, out string modifier, out string remainder) |
| 166 | + { |
| 167 | + if (text.StartsWith(prefix)) |
| 168 | + { |
| 169 | + remainder = text.Substring(prefix.Length); |
| 170 | + modifier = prefix; |
| 171 | + return true; |
| 172 | + } |
| 173 | + |
| 174 | + remainder = null; |
| 175 | + modifier = null; |
| 176 | + return false; |
| 177 | + } |
| 178 | + |
| 179 | + private static bool TrySplitNameValue(string text, out string name, out string value) |
| 180 | + { |
| 181 | + for (int idx = 0; idx < text.Length; idx++) |
| 182 | + { |
| 183 | + char ch = text[idx]; |
| 184 | + if (ch == ':' || ch == '=') |
| 185 | + { |
| 186 | + name = text.Substring(0, idx); |
| 187 | + value = text.Substring(idx + 1); |
| 188 | + return true; |
| 189 | + } |
| 190 | + } |
| 191 | + name = null; |
| 192 | + value = null; |
| 193 | + return false; |
| 194 | + } |
| 195 | + } |
| 196 | +} |
0 commit comments