├── .github
└── workflows
│ ├── dotnet-core.yml
│ └── publishing-nuget.yml
├── .gitignore
├── CommandCore.Library.UnitTests
├── CommandCore.Library.UnitTests.csproj
├── CommandParserTests.cs
├── CommandVerbRunnerTests.cs
├── HelpGeneratorTests.cs
├── OptionsParserTest.cs
├── TestTypes
│ ├── TestOptions.cs
│ ├── TestVerb.cs
│ ├── TestVerbWithAttribute.cs
│ └── TestView.cs
└── VerbTypeFinderTests.cs
├── CommandCore.Library
├── Attributes
│ ├── OptionNameAttribute.cs
│ └── VerbNameAttribute.cs
├── BasicEntryAssemblyProvider.cs
├── CommandCore.Library.csproj
├── CommandCoreApp.cs
├── CommandCoreVerbRunner.cs
├── CommandParser.cs
├── HelpGenerator.cs
├── ICommandCoreVerbRunner.cs
├── IHelpGenerator.cs
├── Interfaces
│ ├── ICommandParser.cs
│ ├── IEntryAssemblyProvider.cs
│ ├── IOptionsParser.cs
│ ├── IVerbRunner.cs
│ └── IVerbTypeFinder.cs
├── Models
│ └── ParsedVerb.cs
├── OptionsParser.cs
├── PublicBase
│ ├── VerbBase.cs
│ ├── VerbOptionsBase.cs
│ └── VerbViewBase.cs
└── VerbTypeFinder.cs
├── CommandCore.LightIoC.UnitTests
├── CommandCore.LightIoC.UnitTests.csproj
├── TestServiceProvider.cs
└── TestTypes
│ ├── AnAbstractType.cs
│ ├── ChildOne.cs
│ ├── ChildTwo.cs
│ ├── GrandChildOne.cs
│ ├── IAnAbstractType.cs
│ ├── IChildOne.cs
│ ├── IChildThreeWithTwoConstructors.cs
│ ├── IChildTwo.cs
│ ├── IGrandChildOne.cs
│ ├── IRootType.cs
│ └── RootType.cs
├── CommandCore.LightIoC
├── BasicServiceProvider.cs
├── CommandCore.LightIoC.csproj
└── IServiceProvider.cs
├── CommandCore.TestConsole
├── CommandCore.TestConsole.csproj
├── Options
│ └── AddOptions.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Verbs
│ └── Add.cs
└── Views
│ └── AddView.cs
├── CommandCore.sln
├── LICENSE
├── README.md
├── command-core-banner.png
├── command-core-logo.png
└── logo.png
/.github/workflows/dotnet-core.yml:
--------------------------------------------------------------------------------
1 | name: Build Project
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 |
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - uses: actions/checkout@v2
16 | - name: Setup .NET Core
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: 3.1.101
20 | - name: Install dependencies
21 | run: dotnet restore
22 | - name: Build
23 | run: dotnet build --configuration Release --no-restore
24 | - name: Test
25 | run: dotnet test --no-restore --verbosity normal
26 | - name: Generate coverage reports for Unit Test projects
27 | run: dotnet test /p:CollectCoverage=true /p:CoverletOutput=TestResults/ /p:CoverletOutputFormat=lcov
28 | - name: Publish CommandCore.Library.UnitTests to Coveralls
29 | uses: coverallsapp/github-action@master
30 | with:
31 | github-token: ${{ secrets.GITHUB_TOKEN }}
32 | path-to-lcov: ./CommandCore.Library.UnitTests/TestResults/coverage.info
33 | parallel: true
34 | flag-name: CommandCore.Library.UnitTests
35 | - name: Publish CommandCore.LightIoC.UnitTests to Coveralls
36 | uses: coverallsapp/github-action@master
37 | with:
38 | github-token: ${{ secrets.GITHUB_TOKEN }}
39 | path-to-lcov: ./CommandCore.LightIoC.UnitTests/TestResults/coverage.info
40 | parallel: true
41 | flag-name: CommandCore.LightIoC.UnitTests
42 | - name: Publishing to Coveralls is complete
43 | uses: coverallsapp/github-action@master
44 | with:
45 | github-token: ${{ secrets.GITHUB_TOKEN }}
46 | parallel-finished: true
--------------------------------------------------------------------------------
/.github/workflows/publishing-nuget.yml:
--------------------------------------------------------------------------------
1 | name: Publish Package to Nuget
2 | on:
3 | create:
4 | tags:
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - name: Setup .NET Core
11 | uses: actions/setup-dotnet@v1
12 | with:
13 | dotnet-version: 3.1.101
14 | - name: Install dependencies
15 | run: dotnet restore
16 | - name: Build
17 | run: dotnet build --configuration Release --no-restore
18 | - name: Test
19 | run: dotnet test --no-restore --verbosity normal
20 |
21 | - name: Pack CommandCore.LightIoC
22 | run: dotnet pack CommandCore.LightIoC.csproj -p:Version=${GITHUB_REF#refs/*/} --no-restore -c Release
23 | working-directory: ./CommandCore.LightIoC/
24 |
25 | - name: Publish CommandCore.LightIoC to Nuget
26 | run: dotnet nuget push CommandCore.LightIoC.${GITHUB_REF#refs/*/}.nupkg -k ${{secrets.NUGETAPIKEY}} -s https://api.nuget.org/v3/index.json
27 | working-directory: ./CommandCore.LightIoC/bin/Release/
28 |
29 | - name: Pack CommandCore.Library
30 | run: dotnet pack CommandCore.Library.csproj -p:Version=${GITHUB_REF#refs/*/} --no-restore -c Release
31 | working-directory: ./CommandCore.Library/
32 |
33 | - name: Publish CommandCore.Library to Nuget
34 | run: dotnet nuget push CommandCore.Library.${GITHUB_REF#refs/*/}.nupkg -k ${{secrets.NUGETAPIKEY}} -s https://api.nuget.org/v3/index.json
35 | working-directory: ./CommandCore.Library/bin/Release/
36 |
37 |
--------------------------------------------------------------------------------
/.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 | .idea/**
14 |
15 | # User-specific files (MonoDevelop/Xamarin Studio)
16 | *.userprefs
17 |
18 | # Mono auto generated files
19 | mono_crash.*
20 |
21 | # Build results
22 | [Dd]ebug/
23 | [Dd]ebugPublic/
24 | [Rr]elease/
25 | [Rr]eleases/
26 | x64/
27 | x86/
28 | [Ww][Ii][Nn]32/
29 | [Aa][Rr][Mm]/
30 | [Aa][Rr][Mm]64/
31 | bld/
32 | [Bb]in/
33 | [Oo]bj/
34 | [Ll]og/
35 | [Ll]ogs/
36 |
37 | # Visual Studio 2015/2017 cache/options directory
38 | .vs/
39 | # Uncomment if you have tasks that create the project's static files in wwwroot
40 | #wwwroot/
41 |
42 | # Visual Studio 2017 auto generated files
43 | Generated\ Files/
44 |
45 | # MSTest test Results
46 | [Tt]est[Rr]esult*/
47 | [Bb]uild[Ll]og.*
48 |
49 | # NUnit
50 | *.VisualState.xml
51 | TestResult.xml
52 | nunit-*.xml
53 |
54 | # Build Results of an ATL Project
55 | [Dd]ebugPS/
56 | [Rr]eleasePS/
57 | dlldata.c
58 |
59 | # Benchmark Results
60 | BenchmarkDotNet.Artifacts/
61 |
62 | # .NET Core
63 | project.lock.json
64 | project.fragment.lock.json
65 | artifacts/
66 |
67 | # ASP.NET Scaffolding
68 | ScaffoldingReadMe.txt
69 |
70 | # StyleCop
71 | StyleCopReport.xml
72 |
73 | # Files built by Visual Studio
74 | *_i.c
75 | *_p.c
76 | *_h.h
77 | *.ilk
78 | *.meta
79 | *.obj
80 | *.iobj
81 | *.pch
82 | *.pdb
83 | *.ipdb
84 | *.pgc
85 | *.pgd
86 | *.rsp
87 | *.sbr
88 | *.tlb
89 | *.tli
90 | *.tlh
91 | *.tmp
92 | *.tmp_proj
93 | *_wpftmp.csproj
94 | *.log
95 | *.vspscc
96 | *.vssscc
97 | .builds
98 | *.pidb
99 | *.svclog
100 | *.scc
101 |
102 | # Chutzpah Test files
103 | _Chutzpah*
104 |
105 | # Visual C++ cache files
106 | ipch/
107 | *.aps
108 | *.ncb
109 | *.opendb
110 | *.opensdf
111 | *.sdf
112 | *.cachefile
113 | *.VC.db
114 | *.VC.VC.opendb
115 |
116 | # Visual Studio profiler
117 | *.psess
118 | *.vsp
119 | *.vspx
120 | *.sap
121 |
122 | # Visual Studio Trace Files
123 | *.e2e
124 |
125 | # TFS 2012 Local Workspace
126 | $tf/
127 |
128 | # Guidance Automation Toolkit
129 | *.gpState
130 |
131 | # ReSharper is a .NET coding add-in
132 | _ReSharper*/
133 | *.[Rr]e[Ss]harper
134 | *.DotSettings.user
135 |
136 | # TeamCity is a build add-in
137 | _TeamCity*
138 |
139 | # DotCover is a Code Coverage Tool
140 | *.dotCover
141 |
142 | # AxoCover is a Code Coverage Tool
143 | .axoCover/*
144 | !.axoCover/settings.json
145 |
146 | # Coverlet is a free, cross platform Code Coverage Tool
147 | coverage*[.json, .xml, .info]
148 |
149 | # Visual Studio code coverage results
150 | *.coverage
151 | *.coveragexml
152 |
153 | # NCrunch
154 | _NCrunch_*
155 | .*crunch*.local.xml
156 | nCrunchTemp_*
157 |
158 | # MightyMoose
159 | *.mm.*
160 | AutoTest.Net/
161 |
162 | # Web workbench (sass)
163 | .sass-cache/
164 |
165 | # Installshield output folder
166 | [Ee]xpress/
167 |
168 | # DocProject is a documentation generator add-in
169 | DocProject/buildhelp/
170 | DocProject/Help/*.HxT
171 | DocProject/Help/*.HxC
172 | DocProject/Help/*.hhc
173 | DocProject/Help/*.hhk
174 | DocProject/Help/*.hhp
175 | DocProject/Help/Html2
176 | DocProject/Help/html
177 |
178 | # Click-Once directory
179 | publish/
180 |
181 | # Publish Web Output
182 | *.[Pp]ublish.xml
183 | *.azurePubxml
184 | # Note: Comment the next line if you want to checkin your web deploy settings,
185 | # but database connection strings (with potential passwords) will be unencrypted
186 | *.pubxml
187 | *.publishproj
188 |
189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
190 | # checkin your Azure Web App publish settings, but sensitive information contained
191 | # in these scripts will be unencrypted
192 | PublishScripts/
193 |
194 | # NuGet Packages
195 | *.nupkg
196 | # NuGet Symbol Packages
197 | *.snupkg
198 | # The packages folder can be ignored because of Package Restore
199 | **/[Pp]ackages/*
200 | # except build/, which is used as an MSBuild target.
201 | !**/[Pp]ackages/build/
202 | # Uncomment if necessary however generally it will be regenerated when needed
203 | #!**/[Pp]ackages/repositories.config
204 | # NuGet v3's project.json files produces more ignorable files
205 | *.nuget.props
206 | *.nuget.targets
207 |
208 | # Microsoft Azure Build Output
209 | csx/
210 | *.build.csdef
211 |
212 | # Microsoft Azure Emulator
213 | ecf/
214 | rcf/
215 |
216 | # Windows Store app package directories and files
217 | AppPackages/
218 | BundleArtifacts/
219 | Package.StoreAssociation.xml
220 | _pkginfo.txt
221 | *.appx
222 | *.appxbundle
223 | *.appxupload
224 |
225 | # Visual Studio cache files
226 | # files ending in .cache can be ignored
227 | *.[Cc]ache
228 | # but keep track of directories ending in .cache
229 | !?*.[Cc]ache/
230 |
231 | # Others
232 | ClientBin/
233 | ~$*
234 | *~
235 | *.dbmdl
236 | *.dbproj.schemaview
237 | *.jfm
238 | *.pfx
239 | *.publishsettings
240 | orleans.codegen.cs
241 |
242 | # Including strong name files can present a security risk
243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
244 | #*.snk
245 |
246 | # Since there are multiple workflows, uncomment next line to ignore bower_components
247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
248 | #bower_components/
249 |
250 | # RIA/Silverlight projects
251 | Generated_Code/
252 |
253 | # Backup & report files from converting an old project file
254 | # to a newer Visual Studio version. Backup files are not needed,
255 | # because we have git ;-)
256 | _UpgradeReport_Files/
257 | Backup*/
258 | UpgradeLog*.XML
259 | UpgradeLog*.htm
260 | ServiceFabricBackup/
261 | *.rptproj.bak
262 |
263 | # SQL Server files
264 | *.mdf
265 | *.ldf
266 | *.ndf
267 |
268 | # Business Intelligence projects
269 | *.rdl.data
270 | *.bim.layout
271 | *.bim_*.settings
272 | *.rptproj.rsuser
273 | *- [Bb]ackup.rdl
274 | *- [Bb]ackup ([0-9]).rdl
275 | *- [Bb]ackup ([0-9][0-9]).rdl
276 |
277 | # Microsoft Fakes
278 | FakesAssemblies/
279 |
280 | # GhostDoc plugin setting file
281 | *.GhostDoc.xml
282 |
283 | # Node.js Tools for Visual Studio
284 | .ntvs_analysis.dat
285 | node_modules/
286 |
287 | # Visual Studio 6 build log
288 | *.plg
289 |
290 | # Visual Studio 6 workspace options file
291 | *.opt
292 |
293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
294 | *.vbw
295 |
296 | # Visual Studio LightSwitch build output
297 | **/*.HTMLClient/GeneratedArtifacts
298 | **/*.DesktopClient/GeneratedArtifacts
299 | **/*.DesktopClient/ModelManifest.xml
300 | **/*.Server/GeneratedArtifacts
301 | **/*.Server/ModelManifest.xml
302 | _Pvt_Extensions
303 |
304 | # Paket dependency manager
305 | .paket/paket.exe
306 | paket-files/
307 |
308 | # FAKE - F# Make
309 | .fake/
310 |
311 | # CodeRush personal settings
312 | .cr/personal
313 |
314 | # Python Tools for Visual Studio (PTVS)
315 | __pycache__/
316 | *.pyc
317 |
318 | # Cake - Uncomment if you are using it
319 | # tools/**
320 | # !tools/packages.config
321 |
322 | # Tabs Studio
323 | *.tss
324 |
325 | # Telerik's JustMock configuration file
326 | *.jmconfig
327 |
328 | # BizTalk build output
329 | *.btp.cs
330 | *.btm.cs
331 | *.odx.cs
332 | *.xsd.cs
333 |
334 | # OpenCover UI analysis results
335 | OpenCover/
336 |
337 | # Azure Stream Analytics local run output
338 | ASALocalRun/
339 |
340 | # MSBuild Binary and Structured Log
341 | *.binlog
342 |
343 | # NVidia Nsight GPU debugger configuration file
344 | *.nvuser
345 |
346 | # MFractors (Xamarin productivity tool) working folder
347 | .mfractor/
348 |
349 | # Local History for Visual Studio
350 | .localhistory/
351 |
352 | # BeatPulse healthcheck temp database
353 | healthchecksdb
354 |
355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
356 | MigrationBackup/
357 |
358 | # Ionide (cross platform F# VS Code tools) working folder
359 | .ionide/
360 |
361 | # Fody - auto-generated XML schema
362 | FodyWeavers.xsd
363 |
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/CommandCore.Library.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 | false
7 |
8 | CommandCore.Library.UnitTests
9 |
10 | CommandCore.Library.UnitTests
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/CommandParserTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Xunit;
3 |
4 | namespace CommandCore.Library.UnitTests
5 | {
6 | public class CommandParserTests
7 | {
8 | [Fact]
9 | public void When_Passed_A_Simple_Command_They_Are_Parsed_Properly()
10 | {
11 | var commandParser = new CommandParser();
12 | var arguments = new[]
13 | {
14 | "add",
15 | "--firstname", "tarik",
16 | "--lastname", "guney",
17 | "--zipcode", "55555"
18 | };
19 |
20 | var parsedVerb = commandParser.ParseCommand(arguments);
21 |
22 | Assert.NotNull(parsedVerb);
23 | Assert.Equal("add", parsedVerb.VerbName);
24 | Assert.Equal(new Dictionary>
25 | {
26 | {"firstname", new List {"tarik"}},
27 | {"lastname", new List {"guney"}},
28 | {"zipcode", new List {"55555"}}
29 | }, parsedVerb.Options);
30 | }
31 |
32 | [Fact]
33 | public void When_Passed_No_Verb_Default_Verb_Is_Used()
34 | {
35 | var commandParser = new CommandParser();
36 | var arguments = new[]
37 | {
38 | "--firstname", "tarik",
39 | "--lastname", "guney"
40 | };
41 | var parsedVerb = commandParser.ParseCommand(arguments);
42 | Assert.NotEmpty(parsedVerb.VerbName);
43 | Assert.Equal("default", parsedVerb.VerbName);
44 | Assert.Equal(new Dictionary>
45 | {
46 | {"firstname", new List {"tarik"}},
47 | {"lastname", new List {"guney"}},
48 | }, parsedVerb.Options);
49 | }
50 |
51 | [Fact]
52 | public void When_No_Arguments_Passed_Only_Default_Verb_Returned()
53 | {
54 | var commandParser = new CommandParser();
55 | var parsedVerb = commandParser.ParseCommand(new string[0]);
56 | Assert.Equal("default", parsedVerb.VerbName);
57 | Assert.Null(parsedVerb.Options);
58 | }
59 |
60 | [Fact]
61 | public void When_Passed_Null_Arguments_Default_Verb_Selected()
62 | {
63 | var commandParser = new CommandParser();
64 | var parsedVerb = commandParser.ParseCommand(null);
65 | Assert.Equal("default", parsedVerb.VerbName);
66 | Assert.Null(parsedVerb.Options);
67 | }
68 |
69 | [Fact]
70 | public void When_Passed_Alias_Parsed_Properly()
71 | {
72 | var commandParser = new CommandParser();
73 | var arguments = new[]
74 | {
75 | "-t", "test",
76 | "--name", "tarik",
77 | "-g", "guney"
78 | };
79 | var parsedVerb = commandParser.ParseCommand(arguments);
80 | Assert.Equal("default", parsedVerb.VerbName);
81 | Assert.Equal(new Dictionary>()
82 | {
83 | {"t", new List {"test"}},
84 | {"name", new List {"tarik"}},
85 | {"g", new List {"guney"}}
86 | }, parsedVerb.Options);
87 | }
88 |
89 | [Fact]
90 | public void When_Alias_And_Full_Verbs_Randomized_In_Order_They_Are_Parsed_Properly()
91 | {
92 | var commandParser = new CommandParser();
93 | var arguments = new[]
94 | {
95 | "-t", "test",
96 | "--name", "tarik",
97 | "-g", "guney",
98 | "-hello", "world"
99 | };
100 |
101 | var parsedVerb = commandParser.ParseCommand(arguments);
102 | Assert.Equal("default", parsedVerb.VerbName);
103 | Assert.Equal(new Dictionary>()
104 | {
105 | {"t", new List {"test"}},
106 | {"name", new List {"tarik"}},
107 | {"g", new List {"guney"}},
108 | {"hello", new List {"world"}}
109 | }, parsedVerb.Options);
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/CommandVerbRunnerTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CommandCore.Library.Interfaces;
4 | using CommandCore.Library.UnitTests.TestTypes;
5 | using CommandCore.LightIoC;
6 | using Moq;
7 | using Xunit;
8 |
9 | namespace CommandCore.Library.UnitTests
10 | {
11 | public class CommandVerbRunnerTests
12 | {
13 | private readonly Mock _commandParseMock;
14 | private readonly Mock _verbTypeFinder;
15 | private readonly Mock _optionsParser;
16 | private readonly Mock _helpGeneratorMock;
17 | private readonly LightIoC.IServiceProvider _serviceProviderMock;
18 |
19 | public CommandVerbRunnerTests()
20 | {
21 | _commandParseMock = new Mock();
22 | _verbTypeFinder = new Mock();
23 | _optionsParser = new Mock();
24 | _helpGeneratorMock = new Mock();
25 | _serviceProviderMock = new BasicServiceProvider();
26 | }
27 |
28 | [Fact]
29 | public void If_There_Is_No_Verb_Type_Then_Invalid_Operation_Exception_Is_Thrown()
30 | {
31 | _commandParseMock.Setup(a => a.ParseCommand(It.IsAny()))
32 | .Returns(new ParsedVerb() {VerbName = "default"});
33 | _verbTypeFinder.Setup(a => a.FindByName(It.IsAny())).Returns((Type?) null);
34 |
35 | var runner = new CommandCoreVerbRunner(_commandParseMock.Object, _verbTypeFinder.Object,
36 | _optionsParser.Object,
37 | _helpGeneratorMock.Object, _serviceProviderMock);
38 |
39 | Assert.Throws(() => runner.Run(new string[0]));
40 | }
41 |
42 | [Fact]
43 | public void If_Help_Flag_Passed_Help_Generator_Invoked()
44 | {
45 | var runner = new CommandCoreVerbRunner(_commandParseMock.Object, _verbTypeFinder.Object,
46 | _optionsParser.Object,
47 | _helpGeneratorMock.Object, _serviceProviderMock);
48 |
49 | runner.Run(new string[] {"--help"});
50 | // Normally I don't like verifying the internal calls like this, but for this is an exception since I need
51 | // to know if the generator is called. Perhaps, in the future, I can check if something is returned.
52 | _helpGeneratorMock.Verify(a => a.Build());
53 | }
54 |
55 | [Fact]
56 | public void If_Every_Thing_Passed_Properly_Zero_Return_Code_Returns()
57 | {
58 | var args = new[] {"--name", "tarik"};
59 | var testVerbInfo = new ParsedVerb()
60 | {
61 | VerbName = "default", Options = new Dictionary>()
62 | {
63 | {"name", new List {"tarik"}}
64 | }
65 | };
66 | _commandParseMock.Setup(a => a.ParseCommand(It.IsAny()))
67 | .Returns(testVerbInfo);
68 |
69 | _verbTypeFinder.Setup(a => a.FindByName(It.IsAny()))
70 | .Returns(typeof(TestVerb));
71 |
72 | _optionsParser.Setup(a => a.CreatePopulatedOptionsObject(typeof(TestVerb), testVerbInfo))
73 | .Returns(new TestOptions()
74 | {
75 | Name = "tarik"
76 | });
77 |
78 | var runner = new CommandCoreVerbRunner(_commandParseMock.Object, _verbTypeFinder.Object,
79 | _optionsParser.Object,
80 | _helpGeneratorMock.Object, _serviceProviderMock);
81 |
82 | Assert.Equal(0, runner.Run(args));
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/HelpGeneratorTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CommandCore.Library.Interfaces;
4 | using CommandCore.Library.UnitTests.TestTypes;
5 | using Moq;
6 | using Xunit;
7 |
8 | namespace CommandCore.Library.UnitTests
9 | {
10 | public class HelpGeneratorTests
11 | {
12 | [Fact]
13 | public void When_Types_Found_Help_Generated()
14 | {
15 | var typeFinderMock = new Mock();
16 | typeFinderMock.Setup(a => a.FindAll()).Returns(new List() {typeof(TestVerb)});
17 | var helpGenerator = new HelpGenerator(typeFinderMock.Object);
18 | var helpText = helpGenerator.Build();
19 | Assert.NotEmpty(helpText.ToString());
20 | Assert.True(helpText.ToString().Contains("TestVerb"));
21 | }
22 |
23 | [Fact]
24 | public void When_There_Is_No_Type_It_Prints_Warning()
25 | {
26 | var typeFinderMock = new Mock();
27 | typeFinderMock.Setup(a => a.FindAll()).Returns(new List());
28 | var helpGenerator = new HelpGenerator(typeFinderMock.Object);
29 | var helpText = helpGenerator.Build();
30 | Assert.NotEmpty(helpText.ToString());
31 | Assert.Equal("No help is found!", helpText.ToString());
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/OptionsParserTest.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using CommandCore.Library.Interfaces;
3 | using CommandCore.Library.UnitTests.TestTypes;
4 | using Xunit;
5 |
6 | namespace CommandCore.Library.UnitTests
7 | {
8 | public class OptionsParserTest
9 | {
10 | [Fact]
11 | public void When_Every_Thing_Is_Simple_Things_Work_As_Expected()
12 | {
13 | IOptionsParser parser = new OptionsParser();
14 | var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
15 | {
16 | VerbName = "TestVerb",
17 | Options = new Dictionary>()
18 | {
19 | {"Name", new List {"tarik"}},
20 | {"Age", new List {"33"}},
21 | {"ismale", new List {"true"}},
22 | {"countries", new List {"usa", "germany", "turkey"}},
23 | {"scores", new List {"2", "3", "4"}},
24 | {"skills", new List {"programming"}},
25 | {"ids", new List()}
26 | }
27 | });
28 | Assert.NotNull(optionsObject);
29 | Assert.Equal("tarik", optionsObject.Name);
30 | Assert.Equal(33, optionsObject.Age);
31 | Assert.True(optionsObject.Male);
32 | Assert.Equal(new List {"usa", "germany", "turkey"}, optionsObject.Countries);
33 | Assert.Equal(new List {2, 3, 4}, optionsObject.Scores);
34 | IReadOnlyList expectedSkills = new List() {"programming"};
35 | Assert.Equal(expectedSkills, optionsObject.Skills);
36 | Assert.Null(optionsObject.Ids);
37 | }
38 |
39 | [Fact]
40 | public void When_Options_Name_Differ_They_Are_Ignored_During_Parsing()
41 | {
42 | IOptionsParser parser = new OptionsParser();
43 | var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
44 | {
45 | VerbName = "TestVerb",
46 | Options = new Dictionary>()
47 | {
48 | {"Name", new List {"tarik"}},
49 | {"age", new List {"33"}},
50 | {"ismale", new List {"true"}}
51 | }
52 | });
53 | Assert.NotNull(optionsObject);
54 | Assert.Equal("tarik", optionsObject.Name);
55 | Assert.Equal(0, optionsObject.Age);
56 | Assert.True(optionsObject.Male);
57 | }
58 |
59 | [Fact]
60 | public void When_Nothing_Is_Passed_Options_Get_Their_Default_Values()
61 | {
62 | IOptionsParser parser = new OptionsParser();
63 | var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
64 | {
65 | VerbName = "TestVerb",
66 | Options = new Dictionary>()
67 | });
68 | Assert.NotNull(optionsObject);
69 | Assert.Null(optionsObject.Name);
70 | Assert.Equal(0, optionsObject.Age);
71 | Assert.False(optionsObject.Male);
72 | }
73 |
74 | [Fact]
75 | public void When_All_Options_Are_Alias_They_Are_Parsed()
76 | {
77 | IOptionsParser parser = new OptionsParser();
78 | var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
79 | {
80 | VerbName = "TestVerb",
81 | Options = new Dictionary>()
82 | {
83 | {"n", new List {"tarik"}},
84 | {"a", new List {"33"}},
85 | {"m", new List {"true"}}
86 | }
87 | });
88 | Assert.NotNull(optionsObject);
89 | Assert.Equal("tarik", optionsObject.Name);
90 | Assert.Equal(33, optionsObject.Age);
91 | Assert.True(optionsObject.Male);
92 | }
93 |
94 | [Fact]
95 | public void When_There_Is_No_Option_Name_Mapping_Then_Property_Name_Is_Used()
96 | {
97 | IOptionsParser optionsParser = new OptionsParser();
98 | var optionsObject = (TestOptions) optionsParser.CreatePopulatedOptionsObject(typeof(TestVerb),
99 | new ParsedVerb()
100 | {
101 | VerbName = "TestVerb",
102 | Options = new Dictionary>
103 | {
104 | {"Money", new List {"12.55"}}
105 | }
106 | });
107 |
108 | Assert.NotNull(optionsObject);
109 | Assert.Equal(12.55m, optionsObject.Money);
110 | }
111 |
112 | [Fact]
113 | public void When_There_Are_Multiple_Option_Bindings_One_Of_Them_Got_Selected()
114 | {
115 | IOptionsParser parser = new OptionsParser();
116 | var optionsObject = (TestOptions) parser.CreatePopulatedOptionsObject(typeof(TestVerb), new ParsedVerb()
117 | {
118 | VerbName = "TestVerb",
119 | Options = new Dictionary>()
120 | {
121 | {"fn", new List {"tarik"}},
122 | {"a", new List {"33"}},
123 | {"im", new List {"true"}}
124 | }
125 | });
126 | Assert.NotNull(optionsObject);
127 | Assert.Equal("tarik", optionsObject.Name);
128 | Assert.Equal(33, optionsObject.Age);
129 | Assert.True(optionsObject.Male);
130 | }
131 | }
132 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/TestTypes/TestOptions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.Generic;
3 | using CommandCore.Library.Attributes;
4 | using CommandCore.Library.PublicBase;
5 |
6 | namespace CommandCore.Library.UnitTests.TestTypes
7 | {
8 | internal class TestOptions : VerbOptionsBase
9 | {
10 | [OptionName("Name", Alias = "n")]
11 | [OptionName("fn")]
12 | public string Name { get; set; }
13 |
14 | [OptionName("Age", Alias = "a")]
15 | public int Age { get; set; }
16 |
17 | [OptionName("ismale", Alias = "m")]
18 | [OptionName("im")]
19 | public bool Male { get; set; }
20 |
21 | public decimal Money { get; set; }
22 |
23 | [OptionName("countries")]
24 | public string[] Countries { get; set; }
25 |
26 | [OptionName("scores")]
27 | public List Scores { get; set; }
28 |
29 | [OptionName("skills")]
30 | public IReadOnlyList Skills { get; set; }
31 |
32 | [OptionName("ids")]
33 | public IList Ids { get; set; }
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/TestTypes/TestVerb.cs:
--------------------------------------------------------------------------------
1 | using CommandCore.Library.PublicBase;
2 |
3 | namespace CommandCore.Library.UnitTests.TestTypes
4 | {
5 | internal class TestVerb : VerbBase
6 | {
7 | public override VerbViewBase Run()
8 | {
9 | return new TestView();
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/TestTypes/TestVerbWithAttribute.cs:
--------------------------------------------------------------------------------
1 | using CommandCore.Library.Attributes;
2 | using CommandCore.Library.PublicBase;
3 |
4 | namespace CommandCore.Library.UnitTests.TestTypes
5 | {
6 | [VerbName("test")]
7 | internal class TestVerbWithAttribute : VerbBase
8 | {
9 | public override VerbViewBase Run()
10 | {
11 | throw new System.NotImplementedException();
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/TestTypes/TestView.cs:
--------------------------------------------------------------------------------
1 | using CommandCore.Library.PublicBase;
2 |
3 | namespace CommandCore.Library.UnitTests.TestTypes
4 | {
5 | public class TestView: VerbViewBase
6 | {
7 | public override void RenderResponse()
8 | {
9 |
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/CommandCore.Library.UnitTests/VerbTypeFinderTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Reflection;
4 | using CommandCore.Library.Interfaces;
5 | using CommandCore.Library.UnitTests.TestTypes;
6 | using Moq;
7 | using Xunit;
8 |
9 | namespace CommandCore.Library.UnitTests
10 | {
11 | public class VerbTypeFinderTests
12 | {
13 | private readonly Mock _assemblyProviderMock;
14 |
15 | public VerbTypeFinderTests()
16 | {
17 | _assemblyProviderMock = new Mock();
18 | }
19 |
20 | [Fact]
21 | public void When_Assembly_Passed_Returns_All_Types()
22 | {
23 | var assemblyMock = new Mock();
24 | assemblyMock.Setup(a => a.GetTypes()).Returns(new[] {typeof(TestVerb)});
25 | _assemblyProviderMock.Setup(a => a.GetEntryAssembly())
26 | .Returns(assemblyMock.Object);
27 | var typeFinder = new VerbTypeFinder(_assemblyProviderMock.Object);
28 | var actualTypes = typeFinder.FindAll();
29 | Assert.NotNull(actualTypes);
30 | Assert.Equal(new List {typeof(TestVerb)}, actualTypes);
31 | }
32 |
33 | [Fact]
34 | public void When_Assembly_Passed_Finding_By_Name_Returns_Type_Using_Class_Name()
35 | {
36 | var assemblyMock = new Mock();
37 | assemblyMock.Setup(a => a.GetTypes()).Returns(new[] {typeof(TestVerb)});
38 | _assemblyProviderMock.Setup(a => a.GetEntryAssembly())
39 | .Returns(assemblyMock.Object);
40 | var typeFinder = new VerbTypeFinder(_assemblyProviderMock.Object);
41 | var actualType = typeFinder.FindByName("TestVerb");
42 | Assert.NotNull(actualType);
43 | Assert.Equal(typeof(TestVerb), actualType);
44 | }
45 |
46 | [Fact]
47 | public void When_Assembly_Passed_Finding_By_Name_Returns_Type_Using_Attribute_Name()
48 | {
49 | var assemblyMock = new Mock();
50 | assemblyMock.Setup(a => a.GetTypes()).Returns(new[] {typeof(TestVerbWithAttribute)});
51 | _assemblyProviderMock.Setup(a => a.GetEntryAssembly())
52 | .Returns(assemblyMock.Object);
53 | var typeFinder = new VerbTypeFinder(_assemblyProviderMock.Object);
54 | var actualType = typeFinder.FindByName("test");
55 | Assert.NotNull(actualType);
56 | Assert.Equal(typeof(TestVerbWithAttribute), actualType);
57 | }
58 |
59 | [Theory]
60 | [InlineData((string) null)]
61 | [InlineData("")]
62 | [InlineData(" ")]
63 | public void When_Null_Empty_VerbName_Passed_Throw_Invalid_Operations_Exception(string verbName)
64 | {
65 | var assemblyMock = new Mock();
66 | assemblyMock.Setup(a => a.GetTypes()).Returns(new[] {typeof(TestVerbWithAttribute)});
67 | _assemblyProviderMock.Setup(a => a.GetEntryAssembly())
68 | .Returns(assemblyMock.Object);
69 | var typeFinder = new VerbTypeFinder(_assemblyProviderMock.Object);
70 | Assert.Throws(()=>typeFinder.FindByName(verbName));
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Attributes/OptionNameAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CommandCore.Library.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
6 | public class OptionNameAttribute : Attribute
7 | {
8 | public string Name { get; }
9 | public string Alias { get; set; }
10 | public string Description { get; set; }
11 |
12 | public OptionNameAttribute(string name)
13 | {
14 | Name = name;
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Attributes/VerbNameAttribute.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CommandCore.Library.Attributes
4 | {
5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
6 | public class VerbNameAttribute: Attribute
7 | {
8 | public string Name { get; }
9 | public string Description { get; set; }
10 |
11 | public VerbNameAttribute(string name)
12 | {
13 | Name = name;
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/CommandCore.Library/BasicEntryAssemblyProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using CommandCore.Library.Interfaces;
3 |
4 | namespace CommandCore.Library
5 | {
6 | internal class BasicEntryAssemblyProvider : IEntryAssemblyProvider
7 | {
8 | public Assembly GetEntryAssembly()
9 | {
10 | return Assembly.GetEntryAssembly()!;
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/CommandCore.Library/CommandCore.Library.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 | enable
6 |
7 |
8 |
9 | Tarik Guney
10 | CommandCore.Library
11 | A simple command line parsing library that helps creating CLI apps using MVC pattern. There are many command line parsing libraries out there, but most of them are unnecessarily complicated. Command Core library is built using a well-understood desing pattern: MVC. It cleanly separates the building blocks and makes the CLI development a scalable, extensible, and more importantly simpler endeavor.You can find more information and the source code of this project at its Github page https://github.com/tarikguney/command-core
12 | https://github.com/tarikguney/command-core.git
13 | git
14 | https://www.nuget.org/packages/CommandCore.Library/
15 | https://github.com/tarikguney/command-core/blob/master/LICENSE
16 | https://raw.githubusercontent.com/tarikguney/command-core/master/logo.png
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | <_Parameter1>CommandCore.Library.UnitTests
26 |
27 |
28 | <_Parameter1>DynamicProxyGenAssembly2
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/CommandCore.Library/CommandCoreApp.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CommandCore.Library.Interfaces;
3 | using CommandCore.LightIoC;
4 | using IServiceProvider = CommandCore.LightIoC.IServiceProvider;
5 |
6 | namespace CommandCore.Library
7 | {
8 | ///
9 | /// CommandCoreApp is the entry point of the CommandCore library. It represents the CommandCore application
10 | /// and provides the necessary parsing and type finding functionality for internal use. You should start with this
11 | /// class to start using MVC capabilities.
12 | ///
13 | public class CommandCoreApp
14 | {
15 | private Action _configureServiceAction;
16 |
17 | public int Parse(string[] args)
18 | {
19 | var serviceProvider = new BasicServiceProvider();
20 | RegisterServices(serviceProvider);
21 | _configureServiceAction?.Invoke(serviceProvider);
22 | serviceProvider.Register(typeof(IServiceProvider), serviceProvider);
23 | return serviceProvider.Resolve().Run(args);
24 | }
25 |
26 | ///
27 | /// Register and configure custom dependencies to the LightIoC container.
28 | ///
29 | public void ConfigureServices(Action customServiceProvider)
30 | {
31 | _configureServiceAction = customServiceProvider;
32 | }
33 |
34 | private void RegisterServices(IServiceProvider serviceProvider)
35 | {
36 | serviceProvider.Register();
37 | serviceProvider.Register();
38 | serviceProvider.Register();
39 | serviceProvider.Register();
40 | serviceProvider.Register();
41 | serviceProvider.Register();
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/CommandCore.Library/CommandCoreVerbRunner.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CommandCore.Library.Interfaces;
3 | using CommandCore.Library.PublicBase;
4 | using IServiceProvider = CommandCore.LightIoC.IServiceProvider;
5 |
6 | namespace CommandCore.Library
7 | {
8 | ///
9 | /// This is the internal class that mainly an abstraction layer for injectable dependencies that start
10 | /// the application.
11 | ///
12 | internal class CommandCoreVerbRunner : ICommandCoreVerbRunner
13 | {
14 | private readonly ICommandParser _commandParser;
15 | private readonly IVerbTypeFinder _verbTypeFinder;
16 | private readonly IOptionsParser _optionsParser;
17 | private readonly IHelpGenerator _helpGenerator;
18 | private readonly IServiceProvider _serviceProvider;
19 |
20 | public CommandCoreVerbRunner(ICommandParser commandParser, IVerbTypeFinder verbTypeFinder,
21 | IOptionsParser optionsParser, IHelpGenerator helpGenerator, IServiceProvider serviceProvider)
22 | {
23 | _commandParser = commandParser;
24 | _verbTypeFinder = verbTypeFinder;
25 | _optionsParser = optionsParser;
26 | _helpGenerator = helpGenerator;
27 | _serviceProvider = serviceProvider;
28 | }
29 |
30 | public int Run(string[] args)
31 | {
32 | if (args.Length > 0 && args[0] == "--help")
33 | {
34 | var help = _helpGenerator.Build();
35 | Console.WriteLine(help);
36 | return 0;
37 | }
38 |
39 | var parsedVerb = _commandParser.ParseCommand(args);
40 | var verbType = _verbTypeFinder.FindByName(parsedVerb.VerbName!);
41 |
42 | if (verbType == null)
43 | {
44 | throw new InvalidOperationException(
45 | "Cannot find any verb class that can handle verbs. If your application does not have verbs, add a verb with the name default!");
46 | }
47 |
48 | var options = _optionsParser.CreatePopulatedOptionsObject(verbType!, parsedVerb);
49 | // The reason why we are registering and resolving it is because to inject the dependencies
50 | // the verb type might have.
51 | _serviceProvider.Register(verbType, verbType);
52 | var verbObject = (IVerbRunner) _serviceProvider.Resolve(verbType);
53 |
54 | if (options != null)
55 | {
56 | var optionsPropertyInfo = verbType!.GetProperty("Options");
57 | optionsPropertyInfo!.SetValue(verbObject, options);
58 | }
59 |
60 | VerbViewBase view = verbObject.Run();
61 | // Running the view to render the result to the console (stdout).
62 | // This could be a good extension point for various redirections.
63 | view.RenderResponse();
64 | return Environment.ExitCode;
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/CommandCore.Library/CommandParser.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using CommandCore.Library.Interfaces;
4 |
5 | namespace CommandCore.Library
6 | {
7 | public class CommandParser : ICommandParser
8 | {
9 | public ParsedVerb ParseCommand(string[] arguments)
10 | {
11 | if (arguments == null)
12 | {
13 | return new ParsedVerb() {VerbName = "default"};
14 | }
15 |
16 | var argumentsClone = (string[]) arguments.Clone();
17 | var parsedVerb = new ParsedVerb();
18 |
19 | var firstArgIsVerb = argumentsClone.Length > 0 && !argumentsClone[0].StartsWith("-");
20 | var startingPoint = 0;
21 | if (firstArgIsVerb)
22 | {
23 | startingPoint = 1;
24 | parsedVerb.VerbName = arguments[0];
25 | }
26 | else
27 | {
28 | // If there is no verb defined, we need a default verb that handles all of the coming requests.
29 | parsedVerb.VerbName = "default";
30 | }
31 |
32 | if (argumentsClone.Length <= 1)
33 | {
34 | return parsedVerb;
35 | }
36 |
37 | var options = new Dictionary>();
38 |
39 | for (int i = startingPoint; i < arguments.Length; i++)
40 | {
41 | var arg = arguments[i];
42 | if (arg.StartsWith("-") || arg.StartsWith("--"))
43 | {
44 | options.Add(arg.Trim('-'), new List());
45 | }
46 | else
47 | {
48 | options[options.Keys.Last()].Add(arg);
49 | }
50 | }
51 |
52 | parsedVerb.Options = options;
53 | return parsedVerb;
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/CommandCore.Library/HelpGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using System.Reflection;
3 | using System.Text;
4 | using CommandCore.Library.Attributes;
5 | using CommandCore.Library.Interfaces;
6 |
7 | namespace CommandCore.Library
8 | {
9 | internal class HelpGenerator : IHelpGenerator
10 | {
11 | private readonly IVerbTypeFinder _verbTypeFinder;
12 |
13 | public HelpGenerator(IVerbTypeFinder verbTypeFinder)
14 | {
15 | _verbTypeFinder = verbTypeFinder;
16 | }
17 |
18 | public StringBuilder Build()
19 | {
20 | var helpBuilder = new StringBuilder();
21 | var allTypes = _verbTypeFinder.FindAll();
22 |
23 | if (allTypes == null || allTypes.Count == 0)
24 | {
25 | helpBuilder.Append("No help is found!");
26 | return helpBuilder;
27 | }
28 |
29 | helpBuilder.AppendLine("VERBS:");
30 | helpBuilder.AppendLine("----------------------");
31 | foreach (var verbType in allTypes)
32 | {
33 | var attributes = verbType.GetCustomAttributes().ToList();
34 | var verbName = attributes.FirstOrDefault()?.Name ?? verbType.Name;
35 | helpBuilder.Append(
36 | $"{verbName}");
37 | if (attributes.Count > 1)
38 | {
39 | var alternativeNames = attributes.Skip(1).Select(a => a.Name).Aggregate((a, b) => $"{a},{b}");
40 | helpBuilder.Append($" ({alternativeNames}) ");
41 | }
42 |
43 | // If there is description to show for teh verb, show it after a colon.
44 | if (!string.IsNullOrWhiteSpace(attributes?.FirstOrDefault()?.Description))
45 | {
46 | helpBuilder.AppendLine($": {attributes?.FirstOrDefault()?.Description}");
47 | }
48 |
49 | var optionProperties = verbType.BaseType!.GetGenericArguments()[0].GetProperties();
50 | if (optionProperties.Length == 0)
51 | {
52 | continue;
53 | }
54 |
55 | helpBuilder.AppendLine($" Options:");
56 | foreach (var optionPropertyInfo in optionProperties)
57 | {
58 | var optionsAttributes = optionPropertyInfo.GetCustomAttributes().ToList();
59 | var firstAttribute = optionsAttributes.FirstOrDefault();
60 |
61 | var optionName = firstAttribute?.Name ?? optionPropertyInfo.Name;
62 | helpBuilder.Append(
63 | $" --{optionName}");
64 | if (!string.IsNullOrWhiteSpace(firstAttribute?.Alias))
65 | {
66 | helpBuilder.Append($" -{firstAttribute!.Alias}");
67 | }
68 |
69 | if (optionsAttributes.Count > 1)
70 | {
71 | var altOptionNames = optionsAttributes.Skip(1)
72 | .Select(a => "--" + a.Name +
73 | (string.IsNullOrWhiteSpace(a.Alias) ? "" : " -" + a.Alias))
74 | .Aggregate((a, b) => $"{a},{b}");
75 | helpBuilder.Append($" ({altOptionNames}) ");
76 | }
77 |
78 | if (!string.IsNullOrWhiteSpace(firstAttribute?.Description))
79 | {
80 | helpBuilder.Append($": {firstAttribute!.Description}");
81 | }
82 |
83 | helpBuilder.AppendLine();
84 | }
85 | }
86 |
87 | return helpBuilder;
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/CommandCore.Library/ICommandCoreVerbRunner.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.Library
2 | {
3 | internal interface ICommandCoreVerbRunner
4 | {
5 | int Run(string[] args);
6 | }
7 | }
--------------------------------------------------------------------------------
/CommandCore.Library/IHelpGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace CommandCore.Library
4 | {
5 | internal interface IHelpGenerator
6 | {
7 | StringBuilder Build();
8 | }
9 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Interfaces/ICommandParser.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.Library.Interfaces
2 | {
3 | internal interface ICommandParser
4 | {
5 | ParsedVerb ParseCommand(string[] arguments);
6 | }
7 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Interfaces/IEntryAssemblyProvider.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 |
3 | namespace CommandCore.Library.Interfaces
4 | {
5 | internal interface IEntryAssemblyProvider
6 | {
7 | Assembly GetEntryAssembly();
8 | }
9 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Interfaces/IOptionsParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CommandCore.Library.PublicBase;
3 |
4 | namespace CommandCore.Library.Interfaces
5 | {
6 | internal interface IOptionsParser
7 | {
8 | VerbOptionsBase? CreatePopulatedOptionsObject(Type verbType, ParsedVerb parsedVerb);
9 | }
10 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Interfaces/IVerbRunner.cs:
--------------------------------------------------------------------------------
1 | using CommandCore.Library.PublicBase;
2 |
3 | namespace CommandCore.Library.Interfaces
4 | {
5 | public interface IVerbRunner
6 | {
7 | VerbViewBase Run();
8 | }
9 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Interfaces/IVerbTypeFinder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace CommandCore.Library.Interfaces
5 | {
6 | internal interface IVerbTypeFinder
7 | {
8 | Type? FindByName(string verbName);
9 | IReadOnlyList FindAll();
10 | }
11 | }
--------------------------------------------------------------------------------
/CommandCore.Library/Models/ParsedVerb.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | public class ParsedVerb
4 | {
5 | public string? VerbName { get; set; }
6 |
7 | // ToDo: Ideally the value should be anything. I don't know how I should design this right now.
8 | // The reason is simple: Some arguments are flag attributes.
9 | public IReadOnlyDictionary>? Options { get; set; }
10 | }
--------------------------------------------------------------------------------
/CommandCore.Library/OptionsParser.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.Linq;
6 | using System.Reflection;
7 | using CommandCore.Library.Attributes;
8 | using CommandCore.Library.Interfaces;
9 | using CommandCore.Library.PublicBase;
10 |
11 | namespace CommandCore.Library
12 | {
13 | internal class OptionsParser : IOptionsParser
14 | {
15 | public VerbOptionsBase CreatePopulatedOptionsObject(Type verbType, ParsedVerb parsedVerb)
16 | {
17 | var verbOptionsType = verbType.BaseType!.GetGenericArguments()[0];
18 | var options = (VerbOptionsBase) Activator.CreateInstance(verbOptionsType)!;
19 | var optionProperties =
20 | verbOptionsType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
21 |
22 | if (parsedVerb.Options == null)
23 | {
24 | return options;
25 | }
26 |
27 | foreach (var propertyInfo in optionProperties.Where(a => a.CanRead && a.CanWrite))
28 | {
29 | var parameterNameAttributes = propertyInfo.GetCustomAttributes().ToList();
30 |
31 | string? ParameterNameFunc() =>
32 | parameterNameAttributes?.FirstOrDefault(a => parsedVerb.Options.ContainsKey(a.Name))
33 | ?.Name;
34 |
35 | string? ParameterAliasFunc() =>
36 | parameterNameAttributes.FirstOrDefault(a =>
37 | !string.IsNullOrWhiteSpace(a.Alias) && parsedVerb.Options.ContainsKey(a.Alias))
38 | ?.Alias;
39 |
40 | var parameterName = ParameterNameFunc()
41 | ?? ParameterAliasFunc()
42 | ?? propertyInfo.Name;
43 |
44 |
45 | if (parsedVerb.Options!.ContainsKey(parameterName))
46 | {
47 | var argumentValues = parsedVerb.Options[parameterName];
48 |
49 | // I am aware of the relative complexity of the following big ass if conditions. I will simplify
50 | // it one, hopefully. But the idea is simple: A property type may be an array, a collection, or a scalar type.
51 | // And, we are paring them accordingly.
52 | var propType = propertyInfo.PropertyType;
53 |
54 | if (propType == typeof(bool))
55 | {
56 | propertyInfo.SetValue(options, argumentValues.Count <= 0 || bool.Parse(argumentValues[0]));
57 | }
58 |
59 | // Zero count means something for the boolean properties since what matters is whether the flag is present or not
60 | // But for the other properties, it means nothing, so skipping property set since the user might have
61 | // assigned the properties a default value. We would not want to override it with null or default primitive type value.
62 | if (argumentValues.Count == 0)
63 | {
64 | continue;
65 | }
66 |
67 | if (propType.IsArray)
68 | {
69 | // Creating an instance of a new array using the property's array element type, and setting
70 | // the property value with it after filling it with the converted values.
71 | var elementType = propType.GetElementType()!;
72 | var array = Array.CreateInstance(elementType, argumentValues.Count);
73 | for (var i = 0; i < argumentValues.Count; i++)
74 | {
75 | array.SetValue(Convert.ChangeType(argumentValues[i], elementType), i);
76 | }
77 |
78 | propertyInfo.SetValue(options, array);
79 | }
80 | else if (propType.IsGenericType &&
81 | (propType.GetGenericTypeDefinition() == typeof(IList<>) ||
82 | propType.GetGenericTypeDefinition() == typeof(IReadOnlyList<>) ||
83 | propType.GetGenericTypeDefinition().GetInterfaces().Any(a =>
84 | a.IsGenericType && (a.GetGenericTypeDefinition() == typeof(IList<>) ||
85 | a.GetGenericTypeDefinition() == typeof(IReadOnlyList<>)))))
86 |
87 | {
88 | // Creating an instance of a generic list using the property's generic argument, and setting
89 | // the property value with it after filling it with the converted values.
90 | var elementType = propType.GetGenericArguments()[0];
91 | var listType = typeof(List<>);
92 | var constructedListType = listType.MakeGenericType(elementType);
93 | IList instance = (IList) Activator.CreateInstance(constructedListType)!;
94 | foreach (var arg in argumentValues)
95 | {
96 | instance.Add(Convert.ChangeType(arg, elementType));
97 | }
98 |
99 | propertyInfo.SetValue(options, instance);
100 | }
101 | else
102 | {
103 | var converter = TypeDescriptor.GetConverter(propType);
104 | propertyInfo.SetValue(options, converter.ConvertFrom(argumentValues[0]));
105 | }
106 | }
107 | }
108 |
109 | return options!;
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/CommandCore.Library/PublicBase/VerbBase.cs:
--------------------------------------------------------------------------------
1 | using CommandCore.Library.Interfaces;
2 |
3 | namespace CommandCore.Library.PublicBase
4 | {
5 | public abstract class VerbBase: IVerbRunner where T: VerbOptionsBase
6 | {
7 | public T? Options { get; set; }
8 |
9 | public abstract VerbViewBase Run();
10 | }
11 | }
--------------------------------------------------------------------------------
/CommandCore.Library/PublicBase/VerbOptionsBase.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.Library.PublicBase
2 | {
3 | public abstract class VerbOptionsBase
4 | {
5 |
6 | }
7 | }
--------------------------------------------------------------------------------
/CommandCore.Library/PublicBase/VerbViewBase.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.Library.PublicBase
2 | {
3 | public abstract class VerbViewBase
4 | {
5 | public abstract void RenderResponse();
6 | }
7 | }
--------------------------------------------------------------------------------
/CommandCore.Library/VerbTypeFinder.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reflection;
5 | using CommandCore.Library.Attributes;
6 | using CommandCore.Library.Interfaces;
7 | using CommandCore.Library.PublicBase;
8 |
9 | namespace CommandCore.Library
10 | {
11 | internal class VerbTypeFinder : IVerbTypeFinder
12 | {
13 | private readonly IEntryAssemblyProvider _entryAssemblyProvider;
14 |
15 | public VerbTypeFinder(IEntryAssemblyProvider entryAssemblyProvider)
16 | {
17 | _entryAssemblyProvider = entryAssemblyProvider;
18 | }
19 |
20 | public IReadOnlyList FindAll()
21 | {
22 | // TODO using GetEntryAssembly() might not be the perfect solution. Needs more testing here.
23 | var allTypes = _entryAssemblyProvider.GetEntryAssembly().GetTypes()
24 | .Where(a => a.BaseType != null && a.BaseType!.IsGenericType &&
25 | a.BaseType.GetGenericTypeDefinition() == typeof(VerbBase<>)).ToList();
26 | return allTypes;
27 | }
28 |
29 | public Type? FindByName(string verbName)
30 | {
31 | if (string.IsNullOrWhiteSpace(verbName))
32 | {
33 | throw new InvalidOperationException($"{nameof(verbName)} may not be empty or null.");
34 | }
35 |
36 | // Stopping when the verb is found would be more performing, but this implementation is better for
37 | // future optimization like caching.
38 | var allTypes = FindAll();
39 |
40 | return allTypes.FirstOrDefault(verbType =>
41 | {
42 | var verbNameAttributes = verbType.GetCustomAttributes();
43 | // Applying multiple attributes are allowed in case multiple verb names are associated with a type.
44 | var verbTypeName = verbNameAttributes?.FirstOrDefault(a=>a.Name == verbName)?.Name ?? verbType.Name;
45 | // To keep the naming predictable and consistent, making a case-sensitive comparison here.
46 | return verbTypeName.Equals(verbName);
47 | });
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/CommandCore.LightIoC.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestServiceProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using CommandCore.LightIoC.UnitTests.TestTypes;
4 | using Xunit;
5 |
6 | namespace CommandCore.LightIoC.UnitTests
7 | {
8 | public class TestServiceProvider
9 | {
10 | [Fact]
11 | public void When_Registered_Resolve_Instantiates_Dependency_Tree()
12 | {
13 | var serviceProvider = new BasicServiceProvider();
14 | serviceProvider.Register();
15 | serviceProvider.Register();
16 | serviceProvider.Register();
17 | serviceProvider.Register();
18 |
19 | var instance = serviceProvider.Resolve();
20 | Assert.NotNull(instance);
21 |
22 | Assert.NotNull(instance.ChildOne);
23 | Assert.NotNull(instance.ChildTwo);
24 |
25 | Assert.NotNull(instance.ChildTwo.GrandChildOne);
26 | }
27 |
28 | [Fact]
29 | public void When_More_Constructors_Throws_Exception()
30 | {
31 | var serviceProvider = new BasicServiceProvider();
32 | serviceProvider.Register();
33 |
34 | Assert.Throws(() => serviceProvider.Resolve());
35 | }
36 |
37 | [Fact]
38 | public void When_Not_Registered_Resolution_Throws_Key_Not_Found_Exception()
39 | {
40 | var serviceProvider = new BasicServiceProvider();
41 | serviceProvider.Register();
42 | serviceProvider.Register();
43 |
44 | Assert.Throws(() => serviceProvider.Resolve());
45 | }
46 |
47 | [Fact]
48 | public void When_Registering_Abstract_Service_Class_Throws_Invalid_Operations_Exception()
49 | {
50 | var serviceProvider = new BasicServiceProvider();
51 | Assert.Throws(() => serviceProvider.Register());
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/AnAbstractType.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public abstract class AnAbstractType: IAnAbstractType
4 | {
5 |
6 | }
7 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/ChildOne.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public class ChildOne : IChildOne
4 | {
5 | }
6 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/ChildTwo.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public class ChildTwo : IChildTwo
4 | {
5 | public IGrandChildOne GrandChildOne { get; }
6 |
7 | public ChildTwo(IGrandChildOne grandChildOne)
8 | {
9 | GrandChildOne = grandChildOne;
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/GrandChildOne.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public class GrandChildOne : IGrandChildOne
4 | {
5 | }
6 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/IAnAbstractType.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public interface IAnAbstractType
4 | {
5 |
6 | }
7 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/IChildOne.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public interface IChildOne
4 | {
5 | }
6 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/IChildThreeWithTwoConstructors.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public interface IChildThreeWithTwoConstructors
4 | {
5 | }
6 |
7 | public class ChildThreeWithTwoConstructors : IChildThreeWithTwoConstructors
8 | {
9 | public ChildThreeWithTwoConstructors(IChildOne childOne)
10 | {
11 | }
12 |
13 | public ChildThreeWithTwoConstructors(IChildTwo childTwo)
14 | {
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/IChildTwo.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public interface IChildTwo
4 | {
5 | IGrandChildOne GrandChildOne { get; }
6 | }
7 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/IGrandChildOne.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public interface IGrandChildOne
4 | {
5 | }
6 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/IRootType.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public interface IRootType
4 | {
5 | IChildOne ChildOne { get; }
6 | IChildTwo ChildTwo { get; }
7 | }
8 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC.UnitTests/TestTypes/RootType.cs:
--------------------------------------------------------------------------------
1 | namespace CommandCore.LightIoC.UnitTests.TestTypes
2 | {
3 | public class RootType : IRootType
4 | {
5 | public IChildOne ChildOne { get; }
6 | public IChildTwo ChildTwo { get; }
7 |
8 | public RootType(IChildOne childOne, IChildTwo childTwo)
9 | {
10 | ChildOne = childOne;
11 | ChildTwo = childTwo;
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/CommandCore.LightIoC/BasicServiceProvider.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Reflection;
6 |
7 | namespace CommandCore.LightIoC
8 | {
9 | public class BasicServiceProvider : IServiceProvider
10 | {
11 | private readonly ConcurrentDictionary _typeRegistry = new ConcurrentDictionary();
12 |
13 | private readonly ConcurrentDictionary
14 | _instanceRegistry = new ConcurrentDictionary();
15 |
16 | ///
17 | /// Registers a service with its concrete type to build the dependency tree for a requested type.
18 | ///
19 | /// If the concrete type is not instantiable. For instance, using an abstract class for the concrete type throws this exception.
20 | public void Register()
21 | {
22 | if (typeof(T).IsAbstract)
23 | {
24 | throw new InvalidOperationException(
25 | $"Type {typeof(T).FullName} may not be abstract. Abstract classes cannot be instantiated, hence not allowed to be registered.");
26 | }
27 |
28 | if (!typeof(S).IsAssignableFrom(typeof(T)))
29 | {
30 | throw new InvalidOperationException(
31 | $"Type {typeof(T).FullName} is not assignable to {typeof(S).FullName}");
32 | }
33 |
34 | _typeRegistry[typeof(S)] = typeof(T);
35 | }
36 |
37 | public void Register(Type service, Type implementation)
38 | {
39 | if (implementation.IsAbstract)
40 | {
41 | throw new InvalidOperationException(
42 | $"Type {implementation.FullName} may not be abstract. Abstract classes cannot be instantiated, hence not allowed to be registered.");
43 | }
44 |
45 | if (!service.IsAssignableFrom(implementation))
46 | {
47 | throw new InvalidOperationException(
48 | $"Type {implementation.FullName} is not assignable to {service.FullName}");
49 | }
50 |
51 | _typeRegistry[service] = implementation;
52 | }
53 |
54 | public void Register(Type service, object implementation)
55 | {
56 | if (implementation == null)
57 | {
58 | throw new InvalidOperationException(
59 | $"Service type {service.FullName} may not be registered with a null implementation instance.");
60 | }
61 |
62 | var implType = implementation.GetType();
63 | if (!service.IsAssignableFrom(implType))
64 | {
65 | throw new InvalidOperationException(
66 | $"Type {implType.FullName} is not assignable to {service.FullName}");
67 | }
68 |
69 | _typeRegistry[service] = implementation.GetType();
70 | _instanceRegistry[service] = implementation;
71 | }
72 |
73 | public T Resolve()
74 | {
75 | return (T) CreateInstance(typeof(T));
76 | }
77 |
78 | public object Resolve(Type serviceType)
79 | {
80 | return CreateInstance(serviceType);
81 | }
82 |
83 | ///
84 | /// Runs recursively to instantiate all of the dependent types along the way to resolve a given type.
85 | ///
86 | private object CreateInstance(Type type)
87 | {
88 | if (!_typeRegistry.ContainsKey(type))
89 | {
90 | throw new KeyNotFoundException($"Type {type.FullName} is not registered to the LightIoC container.");
91 | }
92 |
93 | var registeredType = _typeRegistry[type];
94 | var constructors = registeredType.GetConstructors(BindingFlags.Instance | BindingFlags.Public);
95 | if (constructors.Length > 1)
96 | {
97 | throw new Exception(
98 | $"There must be only one constructor method defined for type {registeredType.FullName}");
99 | }
100 |
101 | // This is not really possible since if nothing is defined, the parameterless constructor becomes the default one.
102 | if (constructors.Length == 0)
103 | {
104 | throw new Exception($"There could not be found any constructor method for {registeredType.FullName}");
105 | }
106 |
107 | var injectedTypes = constructors[0].GetParameters().Select(a => a.ParameterType);
108 |
109 | if (!injectedTypes.Any())
110 | {
111 | return Activator.CreateInstance(registeredType);
112 | }
113 |
114 | var instances = new List