|
| 1 | +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. |
| 2 | + |
| 3 | +using System; |
| 4 | +using System.Collections.Generic; |
| 5 | +using System.Linq; |
| 6 | +using CommandLine.Infrastructure; |
| 7 | +using CSharpx; |
| 8 | +using RailwaySharp.ErrorHandling; |
| 9 | +using System.Text.RegularExpressions; |
| 10 | + |
| 11 | +namespace CommandLine.Core |
| 12 | +{ |
| 13 | + static class GetoptTokenizer |
| 14 | + { |
| 15 | + public static Result<IEnumerable<Token>, Error> Tokenize( |
| 16 | + IEnumerable<string> arguments, |
| 17 | + Func<string, NameLookupResult> nameLookup) |
| 18 | + { |
| 19 | + return GetoptTokenizer.Tokenize(arguments, nameLookup, ignoreUnknownArguments:false, allowDashDash:true, posixlyCorrect:false); |
| 20 | + } |
| 21 | + |
| 22 | + public static Result<IEnumerable<Token>, Error> Tokenize( |
| 23 | + IEnumerable<string> arguments, |
| 24 | + Func<string, NameLookupResult> nameLookup, |
| 25 | + bool ignoreUnknownArguments, |
| 26 | + bool allowDashDash, |
| 27 | + bool posixlyCorrect) |
| 28 | + { |
| 29 | + var errors = new List<Error>(); |
| 30 | + Action<string> onBadFormatToken = arg => errors.Add(new BadFormatTokenError(arg)); |
| 31 | + Action<string> unknownOptionError = name => errors.Add(new UnknownOptionError(name)); |
| 32 | + Action<string> doNothing = name => {}; |
| 33 | + Action<string> onUnknownOption = ignoreUnknownArguments ? doNothing : unknownOptionError; |
| 34 | + |
| 35 | + int consumeNext = 0; |
| 36 | + Action<int> onConsumeNext = (n => consumeNext = consumeNext + n); |
| 37 | + bool forceValues = false; |
| 38 | + |
| 39 | + var tokens = new List<Token>(); |
| 40 | + |
| 41 | + var enumerator = arguments.GetEnumerator(); |
| 42 | + while (enumerator.MoveNext()) |
| 43 | + { |
| 44 | + switch (enumerator.Current) { |
| 45 | + case null: |
| 46 | + break; |
| 47 | + |
| 48 | + case string arg when forceValues: |
| 49 | + tokens.Add(Token.ValueForced(arg)); |
| 50 | + break; |
| 51 | + |
| 52 | + case string arg when consumeNext > 0: |
| 53 | + tokens.Add(Token.Value(arg)); |
| 54 | + consumeNext = consumeNext - 1; |
| 55 | + break; |
| 56 | + |
| 57 | + case "--" when allowDashDash: |
| 58 | + forceValues = true; |
| 59 | + break; |
| 60 | + |
| 61 | + case "--": |
| 62 | + tokens.Add(Token.Value("--")); |
| 63 | + if (posixlyCorrect) forceValues = true; |
| 64 | + break; |
| 65 | + |
| 66 | + case "-": |
| 67 | + // A single hyphen is always a value (it usually means "read from stdin" or "write to stdout") |
| 68 | + tokens.Add(Token.Value("-")); |
| 69 | + if (posixlyCorrect) forceValues = true; |
| 70 | + break; |
| 71 | + |
| 72 | + case string arg when arg.StartsWith("--"): |
| 73 | + tokens.AddRange(TokenizeLongName(arg, nameLookup, onBadFormatToken, onUnknownOption, onConsumeNext)); |
| 74 | + break; |
| 75 | + |
| 76 | + case string arg when arg.StartsWith("-"): |
| 77 | + tokens.AddRange(TokenizeShortName(arg, nameLookup, onUnknownOption, onConsumeNext)); |
| 78 | + break; |
| 79 | + |
| 80 | + case string arg: |
| 81 | + // If we get this far, it's a plain value |
| 82 | + tokens.Add(Token.Value(arg)); |
| 83 | + if (posixlyCorrect) forceValues = true; |
| 84 | + break; |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + return Result.Succeed<IEnumerable<Token>, Error>(tokens.AsEnumerable(), errors.AsEnumerable()); |
| 89 | + } |
| 90 | + |
| 91 | + public static Result<IEnumerable<Token>, Error> ExplodeOptionList( |
| 92 | + Result<IEnumerable<Token>, Error> tokenizerResult, |
| 93 | + Func<string, Maybe<char>> optionSequenceWithSeparatorLookup) |
| 94 | + { |
| 95 | + var tokens = tokenizerResult.SucceededWith().Memoize(); |
| 96 | + |
| 97 | + var exploded = new List<Token>(tokens is ICollection<Token> coll ? coll.Count : tokens.Count()); |
| 98 | + var nothing = Maybe.Nothing<char>(); // Re-use same Nothing instance for efficiency |
| 99 | + var separator = nothing; |
| 100 | + foreach (var token in tokens) { |
| 101 | + if (token.IsName()) { |
| 102 | + separator = optionSequenceWithSeparatorLookup(token.Text); |
| 103 | + exploded.Add(token); |
| 104 | + } else { |
| 105 | + // Forced values are never considered option values, so they should not be split |
| 106 | + if (separator.MatchJust(out char sep) && sep != '\0' && !token.IsValueForced()) { |
| 107 | + if (token.Text.Contains(sep)) { |
| 108 | + exploded.AddRange(token.Text.Split(sep).Select(Token.ValueFromSeparator)); |
| 109 | + } else { |
| 110 | + exploded.Add(token); |
| 111 | + } |
| 112 | + } else { |
| 113 | + exploded.Add(token); |
| 114 | + } |
| 115 | + separator = nothing; // Only first value after a separator can possibly be split |
| 116 | + } |
| 117 | + } |
| 118 | + return Result.Succeed(exploded as IEnumerable<Token>, tokenizerResult.SuccessMessages()); |
| 119 | + } |
| 120 | + |
| 121 | + public static Func< |
| 122 | + IEnumerable<string>, |
| 123 | + IEnumerable<OptionSpecification>, |
| 124 | + Result<IEnumerable<Token>, Error>> |
| 125 | + ConfigureTokenizer( |
| 126 | + StringComparer nameComparer, |
| 127 | + bool ignoreUnknownArguments, |
| 128 | + bool enableDashDash, |
| 129 | + bool posixlyCorrect) |
| 130 | + { |
| 131 | + return (arguments, optionSpecs) => |
| 132 | + { |
| 133 | + var tokens = GetoptTokenizer.Tokenize(arguments, name => NameLookup.Contains(name, optionSpecs, nameComparer), ignoreUnknownArguments, enableDashDash, posixlyCorrect); |
| 134 | + var explodedTokens = GetoptTokenizer.ExplodeOptionList(tokens, name => NameLookup.HavingSeparator(name, optionSpecs, nameComparer)); |
| 135 | + return explodedTokens; |
| 136 | + }; |
| 137 | + } |
| 138 | + |
| 139 | + private static IEnumerable<Token> TokenizeShortName( |
| 140 | + string arg, |
| 141 | + Func<string, NameLookupResult> nameLookup, |
| 142 | + Action<string> onUnknownOption, |
| 143 | + Action<int> onConsumeNext) |
| 144 | + { |
| 145 | + |
| 146 | + // First option char that requires a value means we swallow the rest of the string as the value |
| 147 | + // But if there is no rest of the string, then instead we swallow the next argument |
| 148 | + string chars = arg.Substring(1); |
| 149 | + int len = chars.Length; |
| 150 | + if (len > 0 && Char.IsDigit(chars[0])) |
| 151 | + { |
| 152 | + // Assume it's a negative number |
| 153 | + yield return Token.Value(arg); |
| 154 | + yield break; |
| 155 | + } |
| 156 | + for (int i = 0; i < len; i++) |
| 157 | + { |
| 158 | + var s = new String(chars[i], 1); |
| 159 | + switch(nameLookup(s)) |
| 160 | + { |
| 161 | + case NameLookupResult.OtherOptionFound: |
| 162 | + yield return Token.Name(s); |
| 163 | + |
| 164 | + if (i+1 < len) |
| 165 | + { |
| 166 | + // Rest of this is the value (e.g. "-sfoo" where "-s" is a string-consuming arg) |
| 167 | + yield return Token.Value(chars.Substring(i+1)); |
| 168 | + yield break; |
| 169 | + } |
| 170 | + else |
| 171 | + { |
| 172 | + // Value is in next param (e.g., "-s foo") |
| 173 | + onConsumeNext(1); |
| 174 | + } |
| 175 | + break; |
| 176 | + |
| 177 | + case NameLookupResult.NoOptionFound: |
| 178 | + onUnknownOption(s); |
| 179 | + break; |
| 180 | + |
| 181 | + default: |
| 182 | + yield return Token.Name(s); |
| 183 | + break; |
| 184 | + } |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + private static IEnumerable<Token> TokenizeLongName( |
| 189 | + string arg, |
| 190 | + Func<string, NameLookupResult> nameLookup, |
| 191 | + Action<string> onBadFormatToken, |
| 192 | + Action<string> onUnknownOption, |
| 193 | + Action<int> onConsumeNext) |
| 194 | + { |
| 195 | + string[] parts = arg.Substring(2).Split(new char[] { '=' }, 2); |
| 196 | + string name = parts[0]; |
| 197 | + string value = (parts.Length > 1) ? parts[1] : null; |
| 198 | + // A parameter like "--stringvalue=" is acceptable, and makes stringvalue be the empty string |
| 199 | + if (String.IsNullOrWhiteSpace(name) || name.Contains(" ")) |
| 200 | + { |
| 201 | + onBadFormatToken(arg); |
| 202 | + yield break; |
| 203 | + } |
| 204 | + switch(nameLookup(name)) |
| 205 | + { |
| 206 | + case NameLookupResult.NoOptionFound: |
| 207 | + onUnknownOption(name); |
| 208 | + yield break; |
| 209 | + |
| 210 | + case NameLookupResult.OtherOptionFound: |
| 211 | + yield return Token.Name(name); |
| 212 | + if (value == null) // NOT String.IsNullOrEmpty |
| 213 | + { |
| 214 | + onConsumeNext(1); |
| 215 | + } |
| 216 | + else |
| 217 | + { |
| 218 | + yield return Token.Value(value); |
| 219 | + } |
| 220 | + break; |
| 221 | + |
| 222 | + default: |
| 223 | + yield return Token.Name(name); |
| 224 | + break; |
| 225 | + } |
| 226 | + } |
| 227 | + } |
| 228 | +} |
0 commit comments