mirror of
				https://github.com/Tyrrrz/CliFx.git
				synced 2025-10-25 15:19:17 +00:00 
			
		
		
		
	Compare commits
	
		
			536 Commits
		
	
	
		
			0.0.1
			...
			432c8a66af
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 432c8a66af | ||
|  | 078ddeaf07 | ||
|  | 0fa2ebc636 | ||
|  | c79a8c6502 | ||
|  | cfbd8f9e76 | ||
|  | e329f0fc78 | ||
|  | 357426c536 | ||
|  | bc2164499b | ||
|  | 20481d4e24 | ||
|  | 2cb9335e25 | ||
|  | f5ff6193e8 | ||
|  | 36b2b07a1d | ||
|  | 73bf19d766 | ||
|  | 093b6767c4 | ||
|  | e4671e50bb | ||
|  | 40beb283d5 | ||
|  | 71fe231f28 | ||
|  | 8546c54c23 | ||
|  | 0fc88a42ba | ||
|  | cb8f4b122e | ||
|  | 540f307f42 | ||
|  | a62ce71424 | ||
|  | ab48098e06 | ||
|  | 0532d724a1 | ||
|  | 545c7c3fbd | ||
|  | a813436577 | ||
|  | fcc93603a7 | ||
|  | 2d3c221b48 | ||
|  | 651146c97b | ||
|  | 82b0c6fd98 | ||
|  | a4376c955b | ||
|  | f7645afbdb | ||
|  | e20672328b | ||
|  | e99a95ef7c | ||
|  | 3e7eb08eca | ||
|  | cfd28c133e | ||
|  | 034d3cec66 | ||
|  | 3fc7054f80 | ||
|  | 2323a57c39 | ||
|  | bcb34055ac | ||
|  | 24fd87b1e1 | ||
|  | fe935b5775 | ||
|  | 7dcd523bfe | ||
|  | cad1c14474 | ||
|  | 57db910489 | ||
|  | ae9c4e6d1e | ||
|  | 30bc1d3330 | ||
|  | a5a4ad05a0 | ||
|  | 0b77895ca5 | ||
|  | 54994755b1 | ||
|  | aee63cb9f2 | ||
|  | 4bdd3ccc6c | ||
|  | 6aa72e45e8 | ||
|  | 76e8d47e03 | ||
|  | 6304b8ab9c | ||
|  | 98b50d0e8e | ||
|  | 5aea869c2a | ||
|  | 425c8f4022 | ||
|  | 490398f773 | ||
|  | 5854f36756 | ||
|  | ec6c72e6a3 | ||
|  | 41bc64be4a | ||
|  | 7df0e77e4d | ||
|  | 914e8e17cd | ||
|  | 40f106d0b0 | ||
|  | 566dd4a9a7 | ||
|  | 9beb439323 | ||
|  | 029257a915 | ||
|  | d330fbbb63 | ||
|  | 236867f547 | ||
|  | b41e9b4929 | ||
|  | ff06b8896f | ||
|  | 0fe9c89fa0 | ||
|  | 8646c9de5e | ||
|  | a33c42a163 | ||
|  | 55cea48cbd | ||
|  | e67eda3515 | ||
|  | 4412c20e97 | ||
|  | 9eb84c6649 | ||
|  | 2ef37ab6d9 | ||
|  | 38a73772fc | ||
|  | aed53eb090 | ||
|  | 21b601da66 | ||
|  | a4726fcefd | ||
|  | ab24ca8604 | ||
|  | 3533bff344 | ||
|  | 1b096b679e | ||
|  | cb61b31e9d | ||
|  | d8f183c404 | ||
|  | c95b6c32d5 | ||
|  | d2e390c691 | ||
|  | 66ef221586 | ||
|  | 2d3bb30125 | ||
|  | 5d72692aa5 | ||
|  | 3be17db784 | ||
|  | 4aef8ce8fb | ||
|  | 8c1cff3bb7 | ||
|  | 669d8bfe20 | ||
|  | 4dce7bddb4 | ||
|  | a621e89e89 | ||
|  | 5ea11e3a23 | ||
|  | 7cb61182d2 | ||
|  | 99c59431c4 | ||
|  | f376081489 | ||
|  | 00a1e12b5c | ||
|  | 81f8b17451 | ||
|  | aa8315b68d | ||
|  | e52781c25a | ||
|  | 01f29a5375 | ||
|  | 013cb8f66b | ||
|  | 9c715f458e | ||
|  | 90d93a57ee | ||
|  | 8da4a61eb7 | ||
|  | f718370642 | ||
|  | 83c7af72bf | ||
|  | eff84fd7ae | ||
|  | f66fa97b87 | ||
|  | 9f309b5d4a | ||
|  | 456099591a | ||
|  | bf7f607f9b | ||
|  | a4041ab019 | ||
|  | a0fde872ec | ||
|  | f0c040c7b9 | ||
|  | a09818d452 | ||
|  | 1c331df4b1 | ||
|  | dc20fe9730 | ||
|  | 31ae0271b9 | ||
|  | 6ed6d2ced9 | ||
|  | 01a4846159 | ||
|  | 02dc7de127 | ||
|  | a1ff1a1539 | ||
|  | a02951f755 | ||
|  | 7cb25254e8 | ||
|  | 3d9ad16117 | ||
|  | d0ad3bc45d | ||
|  | 6541ce568d | ||
|  | 32d3b66185 | ||
|  | 48f157a51e | ||
|  | b1995fa4f7 | ||
|  | 74bc973f77 | ||
|  | 3420c3d039 | ||
|  | b10577fec5 | ||
|  | af96d0d31d | ||
|  | bd29ad31cc | ||
|  | 15150cb3ed | ||
|  | aac9c968eb | ||
|  | 85a9f157ad | ||
|  | d24a79361d | ||
|  | 5785720f31 | ||
|  | 3f6f0b9f1b | ||
|  | 128bb5be8b | ||
|  | 36b3814f4e | ||
|  | c4a975d5f1 | ||
|  | 79d86d39c1 | ||
|  | c476700168 | ||
|  | 5e97ebe7f0 | ||
|  | 64cbdaaeab | ||
|  | ae1f03914c | ||
|  | ff25dccf8a | ||
|  | 6e0d881682 | ||
|  | 89fd42888a | ||
|  | eeac82a6e7 | ||
|  | c641c6fbe2 | ||
|  | 5ec732fe9a | ||
|  | 6d87411dbf | ||
|  | ed3054c855 | ||
|  | 5d00de4dfe | ||
|  | 016ec8b186 | ||
|  | 9141092919 | ||
|  | 1fe97b0140 | ||
|  | 6ad5989c25 | ||
|  | 7e1db916fc | ||
|  | 1c69d5c80d | ||
|  | ab87225f1f | ||
|  | 6d33c5cdad | ||
|  | e4c899c6c2 | ||
|  | 35b3ad0d63 | ||
|  | 4e70557b47 | ||
|  | 0a8d58255a | ||
|  | d3fbc9c643 | ||
|  | 1cbf8776be | ||
|  | 16e33f7b8f | ||
|  | 5c848056c5 | ||
|  | 864efd3179 | ||
|  | 7f206a0c77 | ||
|  | 22c15f8ec6 | ||
|  | 59373eadc2 | ||
|  | ed3e4f471e | ||
|  | 41cb8647b5 | ||
|  | c7015181e1 | ||
|  | 86742755e8 | ||
|  | 33f95d941d | ||
|  | 1328592cb5 | ||
|  | 0711b863ea | ||
|  | a2f5cd54be | ||
|  | 7836ec610f | ||
|  | 2e489927eb | ||
|  | 02e8d19e48 | ||
|  | eb7107fb0a | ||
|  | a396009b62 | ||
|  | 1d9c7e942c | ||
|  | 0f3abb9db4 | ||
|  | 896482821c | ||
|  | aa3094ee54 | ||
|  | 712580e3d7 | ||
|  | c08102f85f | ||
|  | 5e684c8b36 | ||
|  | 300ae70564 | ||
|  | 76f0c77f1e | ||
|  | 0f7cea4ed1 | ||
|  | 32ee0b2bd6 | ||
|  | 4ff1e1d3e1 | ||
|  | 8e96d2701d | ||
|  | 8e307df231 | ||
|  | ff38f4916a | ||
|  | 7cbbb220b4 | ||
|  | ae2d4299f0 | ||
|  | 21bc69d116 | ||
|  | 05a70175cc | ||
|  | 33ec2eb3a0 | ||
|  | f6ef6cd4c0 | ||
|  | a9ef693dc1 | ||
|  | 98bbd666dc | ||
|  | 4e7ed830f8 | ||
|  | ef87ff76fc | ||
|  | 2feeb21270 | ||
|  | 9990387cfa | ||
|  | bc1bdca7c6 | ||
|  | 2a992d37df | ||
|  | 15c87aecbb | ||
|  | 10a46451ac | ||
|  | e4c6a4174b | ||
|  | 4c65f7bbee | ||
|  | 5f21de0df5 | ||
|  | 9b01b67d98 | ||
|  | 4508f5e211 | ||
|  | f0cbc46df4 | ||
|  | 6c96e9e173 | ||
|  | 51cca36d2a | ||
|  | 84672c92f6 | ||
|  | b1d01898b6 | ||
|  | 441a47a1a8 | ||
|  | 8abd7219a1 | ||
|  | df73a0bfe8 | ||
|  | 55d12dc721 | ||
|  | a6ee44c1bb | ||
|  | 76816e22f1 | ||
|  | daf25e59d6 | ||
|  | f2b4e53615 | ||
|  | 2d519ab190 | ||
|  | 2d479c9cb6 | ||
|  | 2bb7e13e51 | ||
|  | 6e1dfdcdd4 | ||
|  | 5ba647e5c1 | ||
|  | 853492695f | ||
|  | d5d72c7c50 | ||
|  | d676b5832e | ||
|  | 28097afc1e | ||
|  | fda96586f3 | ||
|  | fc5af8dbbc | ||
|  | 4835e64388 | ||
|  | 0999c33f93 | ||
|  | 595805255a | ||
|  | 65eaa912cf | ||
|  | 038f48b78e | ||
|  | d7460244b7 | ||
|  | 02766868fc | ||
|  | 8d7d25a144 | ||
|  | 17ded54e24 | ||
|  | 54a4c32ddf | ||
|  | 6d46e82145 | ||
|  | fd4a2a18fe | ||
|  | bfe99d620e | ||
|  | c5a111207f | ||
|  | 544945c0e6 | ||
|  | c616cdd750 | ||
|  | d3c396956d | ||
|  | d0cbbc6d9a | ||
|  | 49c7905150 | ||
|  | f5a992a16e | ||
|  | bade0a0048 | ||
|  | 7d3d79b861 | ||
|  | 58df63a7ad | ||
|  | b938eef013 | ||
|  | 94f63631db | ||
|  | 90d1b11430 | ||
|  | 550e54b86d | ||
|  | 90a01e729b | ||
|  | ac01c2aecb | ||
|  | 4acffe925c | ||
|  | 18f53eeeef | ||
|  | 03d6942540 | ||
|  | 9be811a89a | ||
|  | f9f5a4696b | ||
|  | d6da687170 | ||
|  | eba66d0878 | ||
|  | 8c682766bd | ||
|  | 39d626c8d8 | ||
|  | a338ac8ce2 | ||
|  | 11637127cb | ||
|  | 4e12aefafb | ||
|  | 144d3592fb | ||
|  | 6f82c2f0f9 | ||
|  | b8c60717d5 | ||
|  | fec6850c39 | ||
|  | 6a378ad946 | ||
|  | 11579f11b1 | ||
|  | 60a3b26fd1 | ||
|  | 3abdfb1acf | ||
|  | 9557d386e2 | ||
|  | d0d024c427 | ||
|  | f765af6061 | ||
|  | 7f2202e869 | ||
|  | 14ad9d5738 | ||
|  | b120138de3 | ||
|  | 8df1d607c1 | ||
|  | c06f2810b9 | ||
|  | d52a205f13 | ||
|  | 0ec12e57c1 | ||
|  | c322b7029c | ||
|  | 6a38c04c11 | ||
|  | 5e53107def | ||
|  | 36cea937de | ||
|  | 438d6b98ac | ||
|  | 8e1488c395 | ||
|  | 65d321b476 | ||
|  | c6d2359d6b | ||
|  | 0d32876bad | ||
|  | c063251d89 | ||
|  | 3831cfc7c0 | ||
|  | b17341b56c | ||
|  | 5bda964fb5 | ||
|  | 432430489a | ||
|  | 9a20101f30 | ||
|  | b491818779 | ||
|  | 69c24c8dfc | ||
|  | 004f906148 | ||
|  | ac83233dc2 | ||
|  | 082910c968 | ||
|  | 11e3e0f85d | ||
|  | 42f4d7d5a7 | ||
|  | bed22b6500 | ||
|  | 17449e0794 | ||
|  | 4732166f5f | ||
|  | f5e37b96fc | ||
|  | 4cef596fe8 | ||
|  | 19b87717c1 | ||
|  | 7e4c6b20ff | ||
|  | fb2071ed2b | ||
|  | 7d2f934310 | ||
|  | 95a00b0952 | ||
|  | cb3fee65f3 | ||
|  | 65628b145a | ||
|  | 802bbfccc6 | ||
|  | 6e7742a4f3 | ||
|  | f6a1a40471 | ||
|  | 33ca4da260 | ||
|  | cbb72b16ae | ||
|  | c58629e999 | ||
|  | 387fb72718 | ||
|  | e04f0da318 | ||
|  | d25873ee10 | ||
|  | a28223fc8b | ||
|  | 1dab27de55 | ||
|  | 698629b153 | ||
|  | 65b66b0d27 | ||
|  | 7d3ba612c4 | ||
|  | 8c3b8d1f49 | ||
|  | fdd39855ad | ||
|  | 671532efce | ||
|  | 5b124345b0 | ||
|  | b812bd1423 | ||
|  | c854f5fb8d | ||
|  | f38bd32510 | ||
|  | 765fa5503e | ||
|  | 57f168723b | ||
|  | 79e1a2e3d7 | ||
|  | f4f6d04857 | ||
|  | 015ede0d15 | ||
|  | 4fd7f7c3ca | ||
|  | 896dd49eb4 | ||
|  | 4365ad457a | ||
|  | fb3617980e | ||
|  | 7690aae456 | ||
|  | 076678a08c | ||
|  | 104279d6e9 | ||
|  | 515d51a91d | ||
|  | 4fdf543190 | ||
|  | 4e1ab096c9 | ||
|  | 8aa6911cca | ||
|  | f0362019ed | ||
|  | 82895f2e42 | ||
|  | 4cf622abe5 | ||
|  | d4e22a78d6 | ||
|  | 3883c831e9 | ||
|  | 63441688fe | ||
|  | e48839b938 | ||
|  | ed87373dc3 | ||
|  | 6ce52c70f7 | ||
|  | d2b0b16121 | ||
|  | d67a9fe762 | ||
|  | ce2a3153e6 | ||
|  | d4b54231fb | ||
|  | 70bfe0bf91 | ||
|  | 9690c380d3 | ||
|  | 85caa275ae | ||
|  | 32026e59c0 | ||
|  | 486ccb9685 | ||
|  | 7b766f70f3 | ||
|  | f73e96488f | ||
|  | af63fa5a1f | ||
|  | e8f53c9463 | ||
|  | 9564cd5d30 | ||
|  | ed458c3980 | ||
|  | 25538f99db | ||
|  | 36436e7a4b | ||
|  | a6070332c9 | ||
|  | 25cbfdb4b8 | ||
|  | d1b5107c2c | ||
|  | 03873d63cd | ||
|  | 89aba39964 | ||
|  | ab57a103d1 | ||
|  | d0b2ebc061 | ||
|  | 857257ca73 | ||
|  | 3587155c7e | ||
|  | ae05e0db96 | ||
|  | 41c0493e66 | ||
|  | 43a304bb26 | ||
|  | cd3892bf83 | ||
|  | 3f7c02342d | ||
|  | c65cdf465e | ||
|  | b5d67ecf24 | ||
|  | a94b2296e1 | ||
|  | fa05e4df3f | ||
|  | b70b25076e | ||
|  | 0662f341e6 | ||
|  | 80bf477f3b | ||
|  | e4a502d9d6 | ||
|  | 13b15b98ed | ||
|  | 80465e0e51 | ||
|  | 9a1ce7e7e5 | ||
|  | b45da64664 | ||
|  | df01dc055e | ||
|  | 31dd24d189 | ||
|  | 2a76dfe1c8 | ||
|  | 59ee2e34d8 | ||
|  | 9e04f79469 | ||
|  | cd55898011 | ||
|  | 272c079767 | ||
|  | 256b693466 | ||
|  | 89cc3c8785 | ||
|  | 43e3042bac | ||
|  | c906833ac7 | ||
|  | dd882a6372 | ||
|  | 3017c3d6c3 | ||
|  | 4b98dbf51f | ||
|  | e652f9bda4 | ||
|  | 21c550d99c | ||
|  | 23d29a8309 | ||
|  | 70796c1254 | ||
|  | 1b62b2ded2 | ||
|  | a9f4958c92 | ||
|  | 66f9b1a256 | ||
|  | de8513c6fa | ||
|  | 105dc88ccd | ||
|  | b736eeaf7d | ||
|  | 04415cbfc1 | ||
|  | 45c2b9c4e0 | ||
|  | 78ffaeb4b2 | ||
|  | 08e2874eb4 | ||
|  | 6648ae22eb | ||
|  | bd6b1a1134 | ||
|  | d5b95bf1f1 | ||
|  | f5c34ca454 | ||
|  | 63f583b02a | ||
|  | fa82f892e4 | ||
|  | 5a696c181b | ||
|  | 7d7edaf30f | ||
|  | 172ec1f15e | ||
|  | e5bbda5892 | ||
|  | fc1568ce20 | ||
|  | efd8bbe89f | ||
|  | 2d8b0b4c88 | ||
|  | 87688ec29e | ||
|  | ddc1ae8537 | ||
|  | 5104a2ebf9 | ||
|  | b6ea1c3df0 | ||
|  | cf521a9fb3 | ||
|  | b5fa60a26b | ||
|  | 500378070d | ||
|  | 24c892b1ab | ||
|  | f1554fd08a | ||
|  | 5a08b8c19b | ||
|  | 7dfbb40860 | ||
|  | 743241cb3b | ||
|  | 384482a47c | ||
|  | 86fdf72d9c | ||
|  | dc067ba224 | ||
|  | a322632e46 | ||
|  | f09caa876f | ||
|  | 018320582b | ||
|  | 18429827df | ||
|  | b050ca4d67 | ||
|  | f8cd2a56b2 | ||
|  | 6a06cdc422 | ||
|  | b0d9626e74 | ||
|  | f47cd3774e | ||
|  | ed72571ddc | ||
|  | e7e47b1c9d | ||
|  | 50df046754 | ||
|  | 041a995c62 | ||
|  | 5174d5354b | ||
|  | 9856e784f5 | ||
|  | 16676cff8c | ||
|  | d9c27dc82a | ||
|  | 5bb175fd4b | ||
|  | d72391df1f | ||
|  | c1ee1a968a | ||
|  | 4e9effe481 | ||
|  | 5ac9b33056 | ||
|  | a64a8fc651 | ||
|  | 24eef8957d | ||
|  | dd2789790e | ||
|  | d2599af90b | ||
|  | 2bdb2bddc8 | ||
|  | 77c7faa759 | ||
|  | 4ba9413012 | ||
|  | 3611aa51e6 | ||
|  | 74ee927498 | ||
|  | 79cf994386 | ||
|  | 7a5a32d27b | ||
|  | 1543076bf4 | ||
|  | 63d798977d | ||
|  | e0211fc141 | ||
|  | fd6ed3ca72 | ||
|  | 3a9ac3d36c | 
							
								
								
									
										
											BIN
										
									
								
								.assets/help-screen.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.assets/help-screen.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 14 KiB | 
							
								
								
									
										63
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										63
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -1,63 +0,0 @@ | ||||
| ############################################################################### | ||||
| # Set default behavior to automatically normalize line endings. | ||||
| ############################################################################### | ||||
| * text=auto | ||||
|  | ||||
| ############################################################################### | ||||
| # Set default behavior for command prompt diff. | ||||
| # | ||||
| # This is need for earlier builds of msysgit that does not have it on by | ||||
| # default for csharp files. | ||||
| # Note: This is only used by command line | ||||
| ############################################################################### | ||||
| #*.cs     diff=csharp | ||||
|  | ||||
| ############################################################################### | ||||
| # Set the merge driver for project and solution files | ||||
| # | ||||
| # Merging from the command prompt will add diff markers to the files if there | ||||
| # are conflicts (Merging from VS is not affected by the settings below, in VS | ||||
| # the diff markers are never inserted). Diff markers may cause the following  | ||||
| # file extensions to fail to load in VS. An alternative would be to treat | ||||
| # these files as binary and thus will always conflict and require user | ||||
| # intervention with every merge. To do so, just uncomment the entries below | ||||
| ############################################################################### | ||||
| #*.sln       merge=binary | ||||
| #*.csproj    merge=binary | ||||
| #*.vbproj    merge=binary | ||||
| #*.vcxproj   merge=binary | ||||
| #*.vcproj    merge=binary | ||||
| #*.dbproj    merge=binary | ||||
| #*.fsproj    merge=binary | ||||
| #*.lsproj    merge=binary | ||||
| #*.wixproj   merge=binary | ||||
| #*.modelproj merge=binary | ||||
| #*.sqlproj   merge=binary | ||||
| #*.wwaproj   merge=binary | ||||
|  | ||||
| ############################################################################### | ||||
| # behavior for image files | ||||
| # | ||||
| # image files are treated as binary by default. | ||||
| ############################################################################### | ||||
| #*.jpg   binary | ||||
| #*.png   binary | ||||
| #*.gif   binary | ||||
|  | ||||
| ############################################################################### | ||||
| # diff behavior for common document formats | ||||
| #  | ||||
| # Convert binary document formats to text before diffing them. This feature | ||||
| # is only available from the command line. Turn it on by uncommenting the  | ||||
| # entries below. | ||||
| ############################################################################### | ||||
| #*.doc   diff=astextplain | ||||
| #*.DOC   diff=astextplain | ||||
| #*.docx  diff=astextplain | ||||
| #*.DOCX  diff=astextplain | ||||
| #*.dot   diff=astextplain | ||||
| #*.DOT   diff=astextplain | ||||
| #*.pdf   diff=astextplain | ||||
| #*.PDF   diff=astextplain | ||||
| #*.rtf   diff=astextplain | ||||
| #*.RTF   diff=astextplain | ||||
							
								
								
									
										77
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								.github/ISSUE_TEMPLATE/bug-report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| name: 🐛 Bug report | ||||
| description: Report broken functionality. | ||||
| labels: [bug] | ||||
|  | ||||
| body: | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         - Avoid generic or vague titles such as "Something's not working" or "A couple of problems" — be as descriptive as possible. | ||||
|         - Keep your issue focused on one single problem. If you have multiple bug reports, please create a separate issue for each of them. | ||||
|         - Issues should represent **complete and actionable** work items. If you are unsure about something or have a question, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead. | ||||
|         - Remember that **CliFx** is an open-source project funded by the community. If you find it useful, **please consider [donating](https://tyrrrz.me/donate) to support its development**. | ||||
|  | ||||
|         ___ | ||||
|  | ||||
|   - type: input | ||||
|     attributes: | ||||
|       label: Version | ||||
|       description: Which version of the package does this bug affect? Make sure you're not using an outdated version. | ||||
|       placeholder: v1.0.0 | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: input | ||||
|     attributes: | ||||
|       label: Platform | ||||
|       description: Which platform do you experience this bug on? | ||||
|       placeholder: .NET 7.0 / Windows 11 | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: Steps to reproduce | ||||
|       description: > | ||||
|         Minimum steps required to reproduce the bug, including prerequisites, code snippets, or other relevant items. | ||||
|         The information provided in this field must be readily actionable, meaning that anyone should be able to reproduce the bug by following these steps. | ||||
|         If the reproduction steps are too complex to fit in this field, please provide a link to a repository instead. | ||||
|       placeholder: | | ||||
|         - Step 1 | ||||
|         - Step 2 | ||||
|         - Step 3 | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: textarea | ||||
|     attributes: | ||||
|       label: Details | ||||
|       description: Clear and thorough explanation of the bug, including any additional information you may find relevant. | ||||
|       placeholder: | | ||||
|         - Expected behavior: ... | ||||
|         - Actual behavior: ... | ||||
|     validations: | ||||
|       required: true | ||||
|  | ||||
|   - type: checkboxes | ||||
|     attributes: | ||||
|       label: Checklist | ||||
|       description: Quick list of checks to ensure that everything is in order. | ||||
|       options: | ||||
|         - label: I have looked through existing issues to make sure that this bug has not been reported before | ||||
|           required: true | ||||
|         - label: I have provided a descriptive title for this issue | ||||
|           required: true | ||||
|         - label: I have made sure that this bug is reproducible on the latest version of the package | ||||
|           required: true | ||||
|         - label: I have provided all the information needed to reproduce this bug as efficiently as possible | ||||
|           required: true | ||||
|         - label: I have sponsored this project | ||||
|           required: false | ||||
|         - label: I have not read any of the above and just checked all the boxes to submit the issue | ||||
|           required: false | ||||
|  | ||||
|   - type: markdown | ||||
|     attributes: | ||||
|       value: | | ||||
|         If you are struggling to provide actionable reproduction steps, or if something else is preventing you from creating a complete bug report, please start a [discussion](https://github.com/Tyrrrz/CliFx/discussions/new) instead. | ||||
							
								
								
									
										11
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: ⚠ Feature request | ||||
|     url: https://github.com/Tyrrrz/.github/blob/master/docs/project-status.md | ||||
|     about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests. | ||||
|   - name: 🗨 Discussions | ||||
|     url: https://github.com/Tyrrrz/CliFx/discussions/new | ||||
|     about: Ask and answer questions. | ||||
|   - name: 💬 Discord server | ||||
|     url: https://discord.gg/2SUWKFnHSm | ||||
|     about: Chat with the project community. | ||||
							
								
								
									
										22
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: github-actions | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: monthly | ||||
|     labels: | ||||
|       - enhancement | ||||
|     groups: | ||||
|       actions: | ||||
|         patterns: | ||||
|           - "*" | ||||
|   - package-ecosystem: nuget | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: monthly | ||||
|     labels: | ||||
|       - enhancement | ||||
|     groups: | ||||
|       nuget: | ||||
|         patterns: | ||||
|           - "*" | ||||
							
								
								
									
										34
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| name: main | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|     inputs: | ||||
|       package-version: | ||||
|         type: string | ||||
|         description: Package version | ||||
|         required: false | ||||
|       deploy: | ||||
|         type: boolean | ||||
|         description: Deploy package | ||||
|         required: false | ||||
|         default: false | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
|     tags: | ||||
|       - "*" | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - master | ||||
|  | ||||
| jobs: | ||||
|   main: | ||||
|     uses: Tyrrrz/.github/.github/workflows/nuget.yml@master | ||||
|     with: | ||||
|       deploy: ${{ inputs.deploy || github.ref_type == 'tag' }} | ||||
|       package-version: ${{ inputs.package-version || (github.ref_type == 'tag' && github.ref_name) || format('0.0.0-ci-{0}', github.sha) }} | ||||
|       dotnet-version: 9.0.x | ||||
|     secrets: | ||||
|       CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | ||||
|       NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} | ||||
|       DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} | ||||
							
								
								
									
										340
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										340
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,340 +1,12 @@ | ||||
| ## Ignore Visual Studio temporary files, build results, and | ||||
| ## files generated by popular Visual Studio add-ons. | ||||
| ## | ||||
| ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore | ||||
|  | ||||
| # User-specific files | ||||
| *.rsuser | ||||
| .vs/ | ||||
| .idea/ | ||||
| *.suo | ||||
| *.user | ||||
| *.userosscache | ||||
| *.sln.docstates | ||||
|  | ||||
| # User-specific files (MonoDevelop/Xamarin Studio) | ||||
| *.userprefs | ||||
|  | ||||
| # Build results | ||||
| [Dd]ebug/ | ||||
| [Dd]ebugPublic/ | ||||
| [Rr]elease/ | ||||
| [Rr]eleases/ | ||||
| x64/ | ||||
| x86/ | ||||
| [Aa][Rr][Mm]/ | ||||
| [Aa][Rr][Mm]64/ | ||||
| bld/ | ||||
| [Bb]in/ | ||||
| [Oo]bj/ | ||||
| [Ll]og/ | ||||
| bin/ | ||||
| obj/ | ||||
|  | ||||
| # Visual Studio 2015/2017 cache/options directory | ||||
| .vs/ | ||||
| # Uncomment if you have tasks that create the project's static files in wwwroot | ||||
| #wwwroot/ | ||||
|  | ||||
| # Visual Studio 2017 auto generated files | ||||
| Generated\ Files/ | ||||
|  | ||||
| # MSTest test Results | ||||
| [Tt]est[Rr]esult*/ | ||||
| [Bb]uild[Ll]og.* | ||||
|  | ||||
| # NUNIT | ||||
| *.VisualState.xml | ||||
| TestResult.xml | ||||
|  | ||||
| # Build Results of an ATL Project | ||||
| [Dd]ebugPS/ | ||||
| [Rr]eleasePS/ | ||||
| dlldata.c | ||||
|  | ||||
| # Benchmark Results | ||||
| BenchmarkDotNet.Artifacts/ | ||||
|  | ||||
| # .NET Core | ||||
| project.lock.json | ||||
| project.fragment.lock.json | ||||
| artifacts/ | ||||
|  | ||||
| # StyleCop | ||||
| StyleCopReport.xml | ||||
|  | ||||
| # Files built by Visual Studio | ||||
| *_i.c | ||||
| *_p.c | ||||
| *_h.h | ||||
| *.ilk | ||||
| *.meta | ||||
| *.obj | ||||
| *.iobj | ||||
| *.pch | ||||
| *.pdb | ||||
| *.ipdb | ||||
| *.pgc | ||||
| *.pgd | ||||
| *.rsp | ||||
| *.sbr | ||||
| *.tlb | ||||
| *.tli | ||||
| *.tlh | ||||
| *.tmp | ||||
| *.tmp_proj | ||||
| *_wpftmp.csproj | ||||
| *.log | ||||
| *.vspscc | ||||
| *.vssscc | ||||
| .builds | ||||
| *.pidb | ||||
| *.svclog | ||||
| *.scc | ||||
|  | ||||
| # Chutzpah Test files | ||||
| _Chutzpah* | ||||
|  | ||||
| # Visual C++ cache files | ||||
| ipch/ | ||||
| *.aps | ||||
| *.ncb | ||||
| *.opendb | ||||
| *.opensdf | ||||
| *.sdf | ||||
| *.cachefile | ||||
| *.VC.db | ||||
| *.VC.VC.opendb | ||||
|  | ||||
| # Visual Studio profiler | ||||
| *.psess | ||||
| *.vsp | ||||
| *.vspx | ||||
| *.sap | ||||
|  | ||||
| # Visual Studio Trace Files | ||||
| *.e2e | ||||
|  | ||||
| # TFS 2012 Local Workspace | ||||
| $tf/ | ||||
|  | ||||
| # Guidance Automation Toolkit | ||||
| *.gpState | ||||
|  | ||||
| # ReSharper is a .NET coding add-in | ||||
| _ReSharper*/ | ||||
| *.[Rr]e[Ss]harper | ||||
| *.DotSettings.user | ||||
|  | ||||
| # JustCode is a .NET coding add-in | ||||
| .JustCode | ||||
|  | ||||
| # TeamCity is a build add-in | ||||
| _TeamCity* | ||||
|  | ||||
| # DotCover is a Code Coverage Tool | ||||
| *.dotCover | ||||
|  | ||||
| # AxoCover is a Code Coverage Tool | ||||
| .axoCover/* | ||||
| !.axoCover/settings.json | ||||
|  | ||||
| # Visual Studio code coverage results | ||||
| *.coverage | ||||
| *.coveragexml | ||||
|  | ||||
| # NCrunch | ||||
| _NCrunch_* | ||||
| .*crunch*.local.xml | ||||
| nCrunchTemp_* | ||||
|  | ||||
| # MightyMoose | ||||
| *.mm.* | ||||
| AutoTest.Net/ | ||||
|  | ||||
| # Web workbench (sass) | ||||
| .sass-cache/ | ||||
|  | ||||
| # Installshield output folder | ||||
| [Ee]xpress/ | ||||
|  | ||||
| # DocProject is a documentation generator add-in | ||||
| DocProject/buildhelp/ | ||||
| DocProject/Help/*.HxT | ||||
| DocProject/Help/*.HxC | ||||
| DocProject/Help/*.hhc | ||||
| DocProject/Help/*.hhk | ||||
| DocProject/Help/*.hhp | ||||
| DocProject/Help/Html2 | ||||
| DocProject/Help/html | ||||
|  | ||||
| # Click-Once directory | ||||
| publish/ | ||||
|  | ||||
| # Publish Web Output | ||||
| *.[Pp]ublish.xml | ||||
| *.azurePubxml | ||||
| # Note: Comment the next line if you want to checkin your web deploy settings, | ||||
| # but database connection strings (with potential passwords) will be unencrypted | ||||
| *.pubxml | ||||
| *.publishproj | ||||
|  | ||||
| # Microsoft Azure Web App publish settings. Comment the next line if you want to | ||||
| # checkin your Azure Web App publish settings, but sensitive information contained | ||||
| # in these scripts will be unencrypted | ||||
| PublishScripts/ | ||||
|  | ||||
| # NuGet Packages | ||||
| *.nupkg | ||||
| # The packages folder can be ignored because of Package Restore | ||||
| **/[Pp]ackages/* | ||||
| # except build/, which is used as an MSBuild target. | ||||
| !**/[Pp]ackages/build/ | ||||
| # Uncomment if necessary however generally it will be regenerated when needed | ||||
| #!**/[Pp]ackages/repositories.config | ||||
| # NuGet v3's project.json files produces more ignorable files | ||||
| *.nuget.props | ||||
| *.nuget.targets | ||||
|  | ||||
| # Microsoft Azure Build Output | ||||
| csx/ | ||||
| *.build.csdef | ||||
|  | ||||
| # Microsoft Azure Emulator | ||||
| ecf/ | ||||
| rcf/ | ||||
|  | ||||
| # Windows Store app package directories and files | ||||
| AppPackages/ | ||||
| BundleArtifacts/ | ||||
| Package.StoreAssociation.xml | ||||
| _pkginfo.txt | ||||
| *.appx | ||||
|  | ||||
| # Visual Studio cache files | ||||
| # files ending in .cache can be ignored | ||||
| *.[Cc]ache | ||||
| # but keep track of directories ending in .cache | ||||
| !?*.[Cc]ache/ | ||||
|  | ||||
| # Others | ||||
| ClientBin/ | ||||
| ~$* | ||||
| *~ | ||||
| *.dbmdl | ||||
| *.dbproj.schemaview | ||||
| *.jfm | ||||
| *.pfx | ||||
| *.publishsettings | ||||
| orleans.codegen.cs | ||||
|  | ||||
| # Including strong name files can present a security risk | ||||
| # (https://github.com/github/gitignore/pull/2483#issue-259490424) | ||||
| #*.snk | ||||
|  | ||||
| # Since there are multiple workflows, uncomment next line to ignore bower_components | ||||
| # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) | ||||
| #bower_components/ | ||||
|  | ||||
| # RIA/Silverlight projects | ||||
| Generated_Code/ | ||||
|  | ||||
| # Backup & report files from converting an old project file | ||||
| # to a newer Visual Studio version. Backup files are not needed, | ||||
| # because we have git ;-) | ||||
| _UpgradeReport_Files/ | ||||
| Backup*/ | ||||
| UpgradeLog*.XML | ||||
| UpgradeLog*.htm | ||||
| ServiceFabricBackup/ | ||||
| *.rptproj.bak | ||||
|  | ||||
| # SQL Server files | ||||
| *.mdf | ||||
| *.ldf | ||||
| *.ndf | ||||
|  | ||||
| # Business Intelligence projects | ||||
| *.rdl.data | ||||
| *.bim.layout | ||||
| *.bim_*.settings | ||||
| *.rptproj.rsuser | ||||
| *- Backup*.rdl | ||||
|  | ||||
| # Microsoft Fakes | ||||
| FakesAssemblies/ | ||||
|  | ||||
| # GhostDoc plugin setting file | ||||
| *.GhostDoc.xml | ||||
|  | ||||
| # Node.js Tools for Visual Studio | ||||
| .ntvs_analysis.dat | ||||
| node_modules/ | ||||
|  | ||||
| # Visual Studio 6 build log | ||||
| *.plg | ||||
|  | ||||
| # Visual Studio 6 workspace options file | ||||
| *.opt | ||||
|  | ||||
| # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) | ||||
| *.vbw | ||||
|  | ||||
| # Visual Studio LightSwitch build output | ||||
| **/*.HTMLClient/GeneratedArtifacts | ||||
| **/*.DesktopClient/GeneratedArtifacts | ||||
| **/*.DesktopClient/ModelManifest.xml | ||||
| **/*.Server/GeneratedArtifacts | ||||
| **/*.Server/ModelManifest.xml | ||||
| _Pvt_Extensions | ||||
|  | ||||
| # Paket dependency manager | ||||
| .paket/paket.exe | ||||
| paket-files/ | ||||
|  | ||||
| # FAKE - F# Make | ||||
| .fake/ | ||||
|  | ||||
| # JetBrains Rider | ||||
| .idea/ | ||||
| *.sln.iml | ||||
|  | ||||
| # CodeRush personal settings | ||||
| .cr/personal | ||||
|  | ||||
| # Python Tools for Visual Studio (PTVS) | ||||
| __pycache__/ | ||||
| *.pyc | ||||
|  | ||||
| # Cake - Uncomment if you are using it | ||||
| # tools/** | ||||
| # !tools/packages.config | ||||
|  | ||||
| # Tabs Studio | ||||
| *.tss | ||||
|  | ||||
| # Telerik's JustMock configuration file | ||||
| *.jmconfig | ||||
|  | ||||
| # BizTalk build output | ||||
| *.btp.cs | ||||
| *.btm.cs | ||||
| *.odx.cs | ||||
| *.xsd.cs | ||||
|  | ||||
| # OpenCover UI analysis results | ||||
| OpenCover/ | ||||
|  | ||||
| # Azure Stream Analytics local run output | ||||
| ASALocalRun/ | ||||
|  | ||||
| # MSBuild Binary and Structured Log | ||||
| *.binlog | ||||
|  | ||||
| # NVidia Nsight GPU debugger configuration file | ||||
| *.nvuser | ||||
|  | ||||
| # MFractors (Xamarin productivity tool) working folder | ||||
| .mfractor/ | ||||
|  | ||||
| # Local History for Visual Studio | ||||
| .localhistory/ | ||||
|  | ||||
| # BeatPulse healthcheck temp database | ||||
| healthchecksdb | ||||
| # Test results | ||||
| TestResults/ | ||||
							
								
								
									
										32
									
								
								CliFx.Benchmarks/Benchmarks.CliFx.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								CliFx.Benchmarks/Benchmarks.CliFx.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     [Command] | ||||
|     public class CliFxCommand : ICommand | ||||
|     { | ||||
|         [CommandOption("str", 's')] | ||||
|         public string? StrOption { get; set; } | ||||
|  | ||||
|         [CommandOption("int", 'i')] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [CommandOption("bool", 'b')] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|     } | ||||
|  | ||||
|     [Benchmark(Description = "CliFx", Baseline = true)] | ||||
|     public async ValueTask<int> ExecuteWithCliFx() => | ||||
|         await new CliApplicationBuilder() | ||||
|             .AddCommand<CliFxCommand>() | ||||
|             .Build() | ||||
|             .RunAsync(Arguments, new Dictionary<string, string>()); | ||||
| } | ||||
							
								
								
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.Clipr.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.Clipr.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using clipr; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class CliprCommand | ||||
|     { | ||||
|         [NamedArgument('s', "str")] | ||||
|         public string? StrOption { get; set; } | ||||
|  | ||||
|         [NamedArgument('i', "int")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [NamedArgument('b', "bool", Constraint = NumArgsConstraint.Optional, Const = true)] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         public void Execute() { } | ||||
|     } | ||||
|  | ||||
|     [Benchmark(Description = "Clipr")] | ||||
|     public void ExecuteWithClipr() => CliParser.Parse<CliprCommand>(Arguments).Execute(); | ||||
| } | ||||
							
								
								
									
										19
									
								
								CliFx.Benchmarks/Benchmarks.Cocona.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								CliFx.Benchmarks/Benchmarks.Cocona.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using Cocona; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class CoconaCommand | ||||
|     { | ||||
|         public void Execute( | ||||
|             [Option("str", ['s'])] string? strOption, | ||||
|             [Option("int", ['i'])] int intOption, | ||||
|             [Option("bool", ['b'])] bool boolOption | ||||
|         ) { } | ||||
|     } | ||||
|  | ||||
|     [Benchmark(Description = "Cocona")] | ||||
|     public void ExecuteWithCocona() => CoconaApp.Run<CoconaCommand>(Arguments); | ||||
| } | ||||
							
								
								
									
										27
									
								
								CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								CliFx.Benchmarks/Benchmarks.CommandLineParser.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using CommandLine; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class CommandLineParserCommand | ||||
|     { | ||||
|         [Option('s', "str")] | ||||
|         public string? StrOption { get; set; } | ||||
|  | ||||
|         [Option('i', "int")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [Option('b', "bool")] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         public void Execute() { } | ||||
|     } | ||||
|  | ||||
|     [Benchmark(Description = "CommandLineParser")] | ||||
|     public void ExecuteWithCommandLineParser() => | ||||
|         new Parser() | ||||
|             .ParseArguments(Arguments, typeof(CommandLineParserCommand)) | ||||
|             .WithParsed<CommandLineParserCommand>(c => c.Execute()); | ||||
| } | ||||
							
								
								
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.McMaster.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.McMaster.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using McMaster.Extensions.CommandLineUtils; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class McMasterCommand | ||||
|     { | ||||
|         [Option("--str|-s")] | ||||
|         public string? StrOption { get; set; } | ||||
|  | ||||
|         [Option("--int|-i")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [Option("--bool|-b")] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         public int OnExecute() => 0; | ||||
|     } | ||||
|  | ||||
|     [Benchmark(Description = "McMaster.Extensions.CommandLineUtils")] | ||||
|     public int ExecuteWithMcMaster() => CommandLineApplication.Execute<McMasterCommand>(Arguments); | ||||
| } | ||||
							
								
								
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.PowerArgs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								CliFx.Benchmarks/Benchmarks.PowerArgs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using PowerArgs; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class PowerArgsCommand | ||||
|     { | ||||
|         [ArgShortcut("--str"), ArgShortcut("-s")] | ||||
|         public string? StrOption { get; set; } | ||||
|  | ||||
|         [ArgShortcut("--int"), ArgShortcut("-i")] | ||||
|         public int IntOption { get; set; } | ||||
|  | ||||
|         [ArgShortcut("--bool"), ArgShortcut("-b")] | ||||
|         public bool BoolOption { get; set; } | ||||
|  | ||||
|         public void Main() { } | ||||
|     } | ||||
|  | ||||
|     [Benchmark(Description = "PowerArgs")] | ||||
|     public void ExecuteWithPowerArgs() => Args.InvokeMain<PowerArgsCommand>(Arguments); | ||||
| } | ||||
							
								
								
									
										33
									
								
								CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								CliFx.Benchmarks/Benchmarks.SystemCommandLine.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| using System.CommandLine; | ||||
| using System.Threading.Tasks; | ||||
| using BenchmarkDotNet.Attributes; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     public class SystemCommandLineCommand | ||||
|     { | ||||
|         public static void ExecuteHandler(string s, int i, bool b) { } | ||||
|  | ||||
|         public Task<int> ExecuteAsync(string[] args) | ||||
|         { | ||||
|             var stringOption = new Option<string>(["--str", "-s"]); | ||||
|             var intOption = new Option<int>(["--int", "-i"]); | ||||
|             var boolOption = new Option<bool>(["--bool", "-b"]); | ||||
|  | ||||
|             var command = new RootCommand(); | ||||
|             command.AddOption(stringOption); | ||||
|             command.AddOption(intOption); | ||||
|             command.AddOption(boolOption); | ||||
|  | ||||
|             command.SetHandler(ExecuteHandler, stringOption, intOption, boolOption); | ||||
|  | ||||
|             return command.InvokeAsync(args); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [Benchmark(Description = "System.CommandLine")] | ||||
|     public async Task<int> ExecuteWithSystemCommandLine() => | ||||
|         await new SystemCommandLineCommand().ExecuteAsync(Arguments); | ||||
| } | ||||
							
								
								
									
										18
									
								
								CliFx.Benchmarks/Benchmarks.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								CliFx.Benchmarks/Benchmarks.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| using BenchmarkDotNet.Attributes; | ||||
| using BenchmarkDotNet.Configs; | ||||
| using BenchmarkDotNet.Order; | ||||
| using BenchmarkDotNet.Running; | ||||
|  | ||||
| namespace CliFx.Benchmarks; | ||||
|  | ||||
| [RankColumn] | ||||
| [Orderer(SummaryOrderPolicy.FastestToSlowest)] | ||||
| public partial class Benchmarks | ||||
| { | ||||
|     private static readonly string[] Arguments = ["--str", "hello world", "-i", "13", "-b"]; | ||||
|  | ||||
|     public static void Main() => | ||||
|         BenchmarkRunner.Run<Benchmarks>( | ||||
|             DefaultConfig.Instance.WithOptions(ConfigOptions.DisableOptimizationsValidator) | ||||
|         ); | ||||
| } | ||||
							
								
								
									
										24
									
								
								CliFx.Benchmarks/CliFx.Benchmarks.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								CliFx.Benchmarks/CliFx.Benchmarks.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <NuGetAudit>false</NuGetAudit> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> | ||||
|     <PackageReference Include="clipr" Version="1.6.1" /> | ||||
|     <PackageReference Include="Cocona" Version="2.2.0" /> | ||||
|     <PackageReference Include="CommandLineParser" Version="2.9.1" /> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" /> | ||||
|     <PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" /> | ||||
|     <PackageReference Include="PowerArgs" Version="4.0.3" /> | ||||
|     <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\CliFx\CliFx.csproj" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										22
									
								
								CliFx.Benchmarks/Readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								CliFx.Benchmarks/Readme.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| ## CliFx.Benchmarks | ||||
|  | ||||
| All benchmarks below were ran with the following configuration: | ||||
|  | ||||
| ```ini | ||||
| BenchmarkDotNet=v0.12.0, OS=Windows 10.0.14393.3443 (1607/AnniversaryUpdate/Redstone1) | ||||
| Intel Core i5-4460 CPU 3.20GHz (Haswell), 1 CPU, 4 logical and 4 physical cores | ||||
| Frequency=3124994 Hz, Resolution=320.0006 ns, Timer=TSC | ||||
| .NET Core SDK=3.1.100 | ||||
|   [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT | ||||
|   DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT | ||||
| ``` | ||||
|  | ||||
| | Method                               |        Mean |     Error |     StdDev | Ratio | RatioSD | Rank | | ||||
| | ------------------------------------ | ----------: | --------: | ---------: | ----: | ------: | ---: | | ||||
| | CommandLineParser                    |    24.79 us |  0.166 us |   0.155 us |  0.49 |    0.00 |    1 | | ||||
| | CliFx                                |    50.27 us |  0.248 us |   0.232 us |  1.00 |    0.00 |    2 | | ||||
| | Clipr                                |   160.22 us |  0.817 us |   0.764 us |  3.19 |    0.02 |    3 | | ||||
| | McMaster.Extensions.CommandLineUtils |   166.45 us |  1.111 us |   1.039 us |  3.31 |    0.03 |    4 | | ||||
| | System.CommandLine                   |   170.27 us |  0.599 us |   0.560 us |  3.39 |    0.02 |    5 | | ||||
| | PowerArgs                            |   306.12 us |  1.495 us |   1.398 us |  6.09 |    0.03 |    6 | | ||||
| | Cocona                               | 1,856.07 us | 48.727 us | 141.367 us | 37.88 |    2.60 |    7 | | ||||
							
								
								
									
										20
									
								
								CliFx.Demo/CliFx.Demo.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								CliFx.Demo/CliFx.Demo.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <ApplicationIcon>../favicon.ico</ApplicationIcon> | ||||
|     <PublishAot>true</PublishAot> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\CliFx\CliFx.csproj" /> | ||||
|     <ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										55
									
								
								CliFx.Demo/Commands/BookAddCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								CliFx.Demo/Commands/BookAddCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Demo.Utils; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| [Command("book add", Description = "Adds a book to the library.")] | ||||
| public class BookAddCommand(LibraryProvider libraryProvider) : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = "title", Description = "Book title.")] | ||||
|     public required string Title { get; init; } | ||||
|  | ||||
|     [CommandOption("author", 'a', Description = "Book author.")] | ||||
|     public required string Author { get; init; } | ||||
|  | ||||
|     [CommandOption("published", 'p', Description = "Book publish date.")] | ||||
|     public DateTimeOffset Published { get; init; } = | ||||
|         new( | ||||
|             Random.Shared.Next(1800, 2020), | ||||
|             Random.Shared.Next(1, 12), | ||||
|             Random.Shared.Next(1, 28), | ||||
|             Random.Shared.Next(1, 23), | ||||
|             Random.Shared.Next(1, 59), | ||||
|             Random.Shared.Next(1, 59), | ||||
|             TimeSpan.Zero | ||||
|         ); | ||||
|  | ||||
|     [CommandOption("isbn", 'n', Description = "Book ISBN.")] | ||||
|     public Isbn Isbn { get; init; } = | ||||
|         new( | ||||
|             Random.Shared.Next(0, 999), | ||||
|             Random.Shared.Next(0, 99), | ||||
|             Random.Shared.Next(0, 99999), | ||||
|             Random.Shared.Next(0, 99), | ||||
|             Random.Shared.Next(0, 9) | ||||
|         ); | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         if (libraryProvider.TryGetBook(Title) is not null) | ||||
|             throw new CommandException($"Book '{Title}' already exists.", 10); | ||||
|  | ||||
|         var book = new Book(Title, Author, Published, Isbn); | ||||
|         libraryProvider.AddBook(book); | ||||
|  | ||||
|         console.WriteLine($"Book '{Title}' added."); | ||||
|         console.WriteBook(book); | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										27
									
								
								CliFx.Demo/Commands/BookCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								CliFx.Demo/Commands/BookCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Demo.Utils; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| [Command("book", Description = "Retrieves a book from the library.")] | ||||
| public class BookCommand(LibraryProvider libraryProvider) : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")] | ||||
|     public required string Title { get; init; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         var book = libraryProvider.TryGetBook(Title); | ||||
|  | ||||
|         if (book is null) | ||||
|             throw new CommandException($"Book '{Title}' not found.", 10); | ||||
|  | ||||
|         console.WriteBook(book); | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										29
									
								
								CliFx.Demo/Commands/BookListCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								CliFx.Demo/Commands/BookListCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Demo.Utils; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| [Command("book list", Description = "Lists all books in the library.")] | ||||
| public class BookListCommand(LibraryProvider libraryProvider) : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         var library = libraryProvider.GetLibrary(); | ||||
|  | ||||
|         for (var i = 0; i < library.Books.Count; i++) | ||||
|         { | ||||
|             // Add margin | ||||
|             if (i != 0) | ||||
|                 console.WriteLine(); | ||||
|  | ||||
|             // Render book | ||||
|             var book = library.Books[i]; | ||||
|             console.WriteBook(book); | ||||
|         } | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										28
									
								
								CliFx.Demo/Commands/BookRemoveCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								CliFx.Demo/Commands/BookRemoveCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Commands; | ||||
|  | ||||
| [Command("book remove", Description = "Removes a book from the library.")] | ||||
| public class BookRemoveCommand(LibraryProvider libraryProvider) : ICommand | ||||
| { | ||||
|     [CommandParameter(0, Name = "title", Description = "Title of the book to remove.")] | ||||
|     public required string Title { get; init; } | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         var book = libraryProvider.TryGetBook(Title); | ||||
|  | ||||
|         if (book is null) | ||||
|             throw new CommandException($"Book '{Title}' not found.", 10); | ||||
|  | ||||
|         libraryProvider.RemoveBook(book); | ||||
|  | ||||
|         console.WriteLine($"Book '{Title}' removed."); | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										5
									
								
								CliFx.Demo/Domain/Book.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								CliFx.Demo/Domain/Book.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| public record Book(string Title, string Author, DateTimeOffset Published, Isbn Isbn); | ||||
							
								
								
									
										31
									
								
								CliFx.Demo/Domain/Isbn.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								CliFx.Demo/Domain/Isbn.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| public partial record Isbn( | ||||
|     int EanPrefix, | ||||
|     int RegistrationGroup, | ||||
|     int Registrant, | ||||
|     int Publication, | ||||
|     int CheckDigit | ||||
| ) | ||||
| { | ||||
|     public override string ToString() => | ||||
|         $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}"; | ||||
| } | ||||
|  | ||||
| public partial record Isbn | ||||
| { | ||||
|     public static Isbn Parse(string value, IFormatProvider formatProvider) | ||||
|     { | ||||
|         var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries); | ||||
|  | ||||
|         return new Isbn( | ||||
|             int.Parse(components[0], formatProvider), | ||||
|             int.Parse(components[1], formatProvider), | ||||
|             int.Parse(components[2], formatProvider), | ||||
|             int.Parse(components[3], formatProvider), | ||||
|             int.Parse(components[4], formatProvider) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										27
									
								
								CliFx.Demo/Domain/Library.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								CliFx.Demo/Domain/Library.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| public partial record Library(IReadOnlyList<Book> Books) | ||||
| { | ||||
|     public Library WithBook(Book book) | ||||
|     { | ||||
|         var books = Books.ToList(); | ||||
|         books.Add(book); | ||||
|  | ||||
|         return new Library(books); | ||||
|     } | ||||
|  | ||||
|     public Library WithoutBook(Book book) | ||||
|     { | ||||
|         var books = Books.Where(b => b != book).ToArray(); | ||||
|  | ||||
|         return new Library(books); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public partial record Library | ||||
| { | ||||
|     public static Library Empty { get; } = new([]); | ||||
| } | ||||
							
								
								
									
										6
									
								
								CliFx.Demo/Domain/LibraryJsonContext.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								CliFx.Demo/Domain/LibraryJsonContext.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| using System.Text.Json.Serialization; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| [JsonSerializable(typeof(Library))] | ||||
| public partial class LibraryJsonContext : JsonSerializerContext; | ||||
							
								
								
									
										43
									
								
								CliFx.Demo/Domain/LibraryProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								CliFx.Demo/Domain/LibraryProvider.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Text.Json; | ||||
|  | ||||
| namespace CliFx.Demo.Domain; | ||||
|  | ||||
| public class LibraryProvider | ||||
| { | ||||
|     private static string StorageFilePath { get; } = | ||||
|         Path.Combine(Directory.GetCurrentDirectory(), "Library.json"); | ||||
|  | ||||
|     private void StoreLibrary(Library library) | ||||
|     { | ||||
|         var data = JsonSerializer.Serialize(library, LibraryJsonContext.Default.Library); | ||||
|         File.WriteAllText(StorageFilePath, data); | ||||
|     } | ||||
|  | ||||
|     public Library GetLibrary() | ||||
|     { | ||||
|         if (!File.Exists(StorageFilePath)) | ||||
|             return Library.Empty; | ||||
|  | ||||
|         var data = File.ReadAllText(StorageFilePath); | ||||
|  | ||||
|         return JsonSerializer.Deserialize(data, LibraryJsonContext.Default.Library) | ||||
|             ?? Library.Empty; | ||||
|     } | ||||
|  | ||||
|     public Book? TryGetBook(string title) => | ||||
|         GetLibrary().Books.FirstOrDefault(b => b.Title == title); | ||||
|  | ||||
|     public void AddBook(Book book) | ||||
|     { | ||||
|         var updatedLibrary = GetLibrary().WithBook(book); | ||||
|         StoreLibrary(updatedLibrary); | ||||
|     } | ||||
|  | ||||
|     public void RemoveBook(Book book) | ||||
|     { | ||||
|         var updatedLibrary = GetLibrary().WithoutBook(book); | ||||
|         StoreLibrary(updatedLibrary); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										18
									
								
								CliFx.Demo/Program.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								CliFx.Demo/Program.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| using CliFx; | ||||
| using CliFx.Demo.Domain; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
|  | ||||
| // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands | ||||
| var services = new ServiceCollection(); | ||||
| services.AddSingleton<LibraryProvider>(); | ||||
|  | ||||
| // Register all commands as transient services | ||||
| foreach (var commandType in commandTypes) | ||||
|     services.AddTransient(commandType); | ||||
|  | ||||
| return await new CliApplicationBuilder() | ||||
|     .SetDescription("Demo application showcasing CliFx features.") | ||||
|     .AddCommandsFromThisAssembly() | ||||
|     .UseTypeActivator(services.BuildServiceProvider()) | ||||
|     .Build() | ||||
|     .RunAsync(); | ||||
							
								
								
									
										5
									
								
								CliFx.Demo/Readme.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								CliFx.Demo/Readme.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| # CliFx Demo Project | ||||
|  | ||||
| Sample command-line interface for managing a library of books. | ||||
|  | ||||
| This demo project showcases basic CliFx functionality such as command routing, argument parsing, and autogenerated help text. | ||||
							
								
								
									
										39
									
								
								CliFx.Demo/Utils/ConsoleExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								CliFx.Demo/Utils/ConsoleExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| using System; | ||||
| using CliFx.Demo.Domain; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Demo.Utils; | ||||
|  | ||||
| internal static class ConsoleExtensions | ||||
| { | ||||
|     public static void WriteBook(this ConsoleWriter writer, Book book) | ||||
|     { | ||||
|         // Title | ||||
|         using (writer.Console.WithForegroundColor(ConsoleColor.White)) | ||||
|             writer.WriteLine(book.Title); | ||||
|  | ||||
|         // Author | ||||
|         writer.Write("  "); | ||||
|         writer.Write("Author: "); | ||||
|  | ||||
|         using (writer.Console.WithForegroundColor(ConsoleColor.White)) | ||||
|             writer.WriteLine(book.Author); | ||||
|  | ||||
|         // Published | ||||
|         writer.Write("  "); | ||||
|         writer.Write("Published: "); | ||||
|  | ||||
|         using (writer.Console.WithForegroundColor(ConsoleColor.White)) | ||||
|             writer.WriteLine($"{book.Published:d}"); | ||||
|  | ||||
|         // ISBN | ||||
|         writer.Write("  "); | ||||
|         writer.Write("ISBN: "); | ||||
|  | ||||
|         using (writer.Console.WithForegroundColor(ConsoleColor.White)) | ||||
|             writer.WriteLine(book.Isbn); | ||||
|     } | ||||
|  | ||||
|     public static void WriteBook(this IConsole console, Book book) => | ||||
|         console.Output.WriteBook(book); | ||||
| } | ||||
							
								
								
									
										27
									
								
								CliFx.SourceGeneration/CliFx.SourceGeneration.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								CliFx.SourceGeneration/CliFx.SourceGeneration.csproj
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>netstandard2.0</TargetFramework> | ||||
|     <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> | ||||
|     <GenerateDependencyFile>true</GenerateDependencyFile> | ||||
|     <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> | ||||
|     <NoWarn>$(NoWarn);RS1035</NoWarn> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <!-- | ||||
|       Because this project only has a single target framework, the condition in | ||||
|       Directory.Build.props does not appear to work. This is a workaround for that. | ||||
|     --> | ||||
|     <Nullable>annotations</Nullable> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.28.2" PrivateAssets="all" /> | ||||
|     <!-- Make sure to target the lowest possible version of the compiler for wider support --> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis" Version="4.11.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="PolyShim" Version="1.12.0" PrivateAssets="all" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
							
								
								
									
										130
									
								
								CliFx.SourceGeneration/CommandSchemaGenerator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								CliFx.SourceGeneration/CommandSchemaGenerator.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| using System.Linq; | ||||
| using CliFx.SourceGeneration.SemanticModel; | ||||
| using CliFx.SourceGeneration.Utils.Extensions; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||||
|  | ||||
| namespace CliFx.SourceGeneration; | ||||
|  | ||||
| [Generator] | ||||
| public class CommandSchemaGenerator : IIncrementalGenerator | ||||
| { | ||||
|     public void Initialize(IncrementalGeneratorInitializationContext context) | ||||
|     { | ||||
|         var values = context.SyntaxProvider.ForAttributeWithMetadataName<( | ||||
|             CommandSymbol?, | ||||
|             Diagnostic? | ||||
|         )>( | ||||
|             KnownSymbolNames.CliFxCommandAttribute, | ||||
|             (n, _) => n is TypeDeclarationSyntax, | ||||
|             (x, _) => | ||||
|             { | ||||
|                 // Predicate above ensures that these casts are safe | ||||
|                 var commandTypeSyntax = (TypeDeclarationSyntax)x.TargetNode; | ||||
|                 var commandTypeSymbol = (INamedTypeSymbol)x.TargetSymbol; | ||||
|  | ||||
|                 // Check if the target type and all its containing types are partial | ||||
|                 if ( | ||||
|                     commandTypeSyntax | ||||
|                         .AncestorsAndSelf() | ||||
|                         .Any(a => | ||||
|                             a is TypeDeclarationSyntax t | ||||
|                             && !t.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) | ||||
|                         ) | ||||
|                 ) | ||||
|                 { | ||||
|                     return ( | ||||
|                         null, | ||||
|                         Diagnostic.Create( | ||||
|                             DiagnosticDescriptors.CommandMustBePartial, | ||||
|                             commandTypeSyntax.Identifier.GetLocation() | ||||
|                         ) | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 // Check if the target type implements ICommand | ||||
|                 var hasCommandInterface = commandTypeSymbol.AllInterfaces.Any(i => | ||||
|                     i.DisplayNameMatches(KnownSymbolNames.CliFxCommandInterface) | ||||
|                 ); | ||||
|  | ||||
|                 if (!hasCommandInterface) | ||||
|                 { | ||||
|                     return ( | ||||
|                         null, | ||||
|                         Diagnostic.Create( | ||||
|                             DiagnosticDescriptors.CommandMustImplementInterface, | ||||
|                             commandTypeSymbol.Locations.First() | ||||
|                         ) | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 // Resolve the command | ||||
|                 var commandAttribute = x.Attributes.First(a => | ||||
|                     a.AttributeClass?.DisplayNameMatches(KnownSymbolNames.CliFxCommandAttribute) | ||||
|                     == true | ||||
|                 ); | ||||
|  | ||||
|                 var command = CommandSymbol.FromSymbol(commandTypeSymbol, commandAttribute); | ||||
|  | ||||
|                 // TODO: validate command | ||||
|  | ||||
|                 return (command, null); | ||||
|             } | ||||
|         ); | ||||
|  | ||||
|         // Report diagnostics | ||||
|         var diagnostics = values.Select((v, _) => v.Item2).WhereNotNull(); | ||||
|         context.RegisterSourceOutput(diagnostics, (x, d) => x.ReportDiagnostic(d)); | ||||
|  | ||||
|         // Generate command schemas | ||||
|         var symbols = values.Select((v, _) => v.Item1).WhereNotNull(); | ||||
|         context.RegisterSourceOutput( | ||||
|             symbols, | ||||
|             (x, c) => | ||||
|                 x.AddSource( | ||||
|                     $"{c.Type.FullyQualifiedName}.CommandSchema.Generated.cs", | ||||
|                     // lang=csharp | ||||
|                     $$""" | ||||
|                     namespace {{c.Type.Namespace}}; | ||||
|  | ||||
|                     partial class {{c.Type.Name}} | ||||
|                     { | ||||
|                         public static CliFx.Schema.CommandSchema<{{c.Type.FullyQualifiedName}}> Schema { get; } = {{c.GenerateSchemaInitializationCode()}}; | ||||
|                     } | ||||
|                     """ | ||||
|                 ) | ||||
|         ); | ||||
|  | ||||
|         // Generate extension methods | ||||
|         var symbolsCollected = symbols.Collect(); | ||||
|         context.RegisterSourceOutput( | ||||
|             symbolsCollected, | ||||
|             (x, cs) => | ||||
|                 x.AddSource( | ||||
|                     "CommandSchemaExtensions.Generated.cs", | ||||
|                     // lang=csharp | ||||
|                     $$""" | ||||
|                   namespace CliFx; | ||||
|  | ||||
|                   static partial class GeneratedExtensions | ||||
|                   { | ||||
|                       public static CliFx.CliApplicationBuilder AddCommandsFromThisAssembly(this CliFx.CliApplicationBuilder builder) | ||||
|                       { | ||||
|                           {{ | ||||
|                               cs.Select(c => c.Type.FullyQualifiedName) | ||||
|                                   .Select(t => | ||||
|                                       // lang=csharp | ||||
|                                       $"builder.AddCommand({t}.Schema);" | ||||
|                                   ) | ||||
|                                   .JoinToString("\n") | ||||
|                           }} | ||||
|                            | ||||
|                           return builder; | ||||
|                       } | ||||
|                   } | ||||
|                   """ | ||||
|                 ) | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										27
									
								
								CliFx.SourceGeneration/DiagnosticDescriptors.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								CliFx.SourceGeneration/DiagnosticDescriptors.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| using CliFx.SourceGeneration.SemanticModel; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration; | ||||
|  | ||||
| internal static class DiagnosticDescriptors | ||||
| { | ||||
|     public static DiagnosticDescriptor CommandMustBePartial { get; } = | ||||
|         new( | ||||
|             $"{nameof(CliFx)}_{nameof(CommandMustBePartial)}", | ||||
|             "Command types must be declared as `partial`", | ||||
|             "This type (and all its containing types, if present) must be declared as `partial` in order to be a valid command.", | ||||
|             "CliFx", | ||||
|             DiagnosticSeverity.Error, | ||||
|             true | ||||
|         ); | ||||
|  | ||||
|     public static DiagnosticDescriptor CommandMustImplementInterface { get; } = | ||||
|         new( | ||||
|             $"{nameof(CliFx)}_{nameof(CommandMustImplementInterface)}", | ||||
|             $"Commands must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface", | ||||
|             $"This type must implement the `{KnownSymbolNames.CliFxCommandInterface}` interface in order to be a valid command.", | ||||
|             "CliFx", | ||||
|             DiagnosticSeverity.Error, | ||||
|             true | ||||
|         ); | ||||
| } | ||||
							
								
								
									
										66
									
								
								CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								CliFx.SourceGeneration/SemanticModel/CommandInputSymbol.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.SemanticModel; | ||||
|  | ||||
| internal abstract partial class CommandInputSymbol( | ||||
|     PropertyDescriptor property, | ||||
|     bool isSequence, | ||||
|     string? description, | ||||
|     TypeDescriptor? converterType, | ||||
|     IReadOnlyList<TypeDescriptor> validatorTypes | ||||
| ) | ||||
| { | ||||
|     public PropertyDescriptor Property { get; } = property; | ||||
|  | ||||
|     public bool IsSequence { get; } = isSequence; | ||||
|  | ||||
|     public string? Description { get; } = description; | ||||
|  | ||||
|     public TypeDescriptor? ConverterType { get; } = converterType; | ||||
|  | ||||
|     public IReadOnlyList<TypeDescriptor> ValidatorTypes { get; } = validatorTypes; | ||||
| } | ||||
|  | ||||
| internal partial class CommandInputSymbol : IEquatable<CommandInputSymbol> | ||||
| { | ||||
|     public bool Equals(CommandInputSymbol? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, other)) | ||||
|             return true; | ||||
|  | ||||
|         return Property.Equals(other.Property) | ||||
|             && IsSequence == other.IsSequence | ||||
|             && Description == other.Description | ||||
|             && Equals(ConverterType, other.ConverterType) | ||||
|             && ValidatorTypes.SequenceEqual(other.ValidatorTypes); | ||||
|     } | ||||
|  | ||||
|     public override bool Equals(object? obj) | ||||
|     { | ||||
|         if (ReferenceEquals(null, obj)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, obj)) | ||||
|             return true; | ||||
|         if (obj.GetType() != GetType()) | ||||
|             return false; | ||||
|  | ||||
|         return Equals((CommandInputSymbol)obj); | ||||
|     } | ||||
|  | ||||
|     public override int GetHashCode() => | ||||
|         HashCode.Combine(Property, IsSequence, Description, ConverterType, ValidatorTypes); | ||||
| } | ||||
|  | ||||
| internal partial class CommandInputSymbol | ||||
| { | ||||
|     public static bool IsSequenceType(ITypeSymbol type) => | ||||
|         type.AllInterfaces.Any(i => | ||||
|             i.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T | ||||
|         ) | ||||
|         && type.SpecialType != SpecialType.System_String; | ||||
| } | ||||
							
								
								
									
										90
									
								
								CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								CliFx.SourceGeneration/SemanticModel/CommandOptionSymbol.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using CliFx.SourceGeneration.Utils.Extensions; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.SemanticModel; | ||||
|  | ||||
| internal partial class CommandOptionSymbol( | ||||
|     PropertyDescriptor property, | ||||
|     bool isSequence, | ||||
|     string? name, | ||||
|     char? shortName, | ||||
|     string? environmentVariable, | ||||
|     bool isRequired, | ||||
|     string? description, | ||||
|     TypeDescriptor? converterType, | ||||
|     IReadOnlyList<TypeDescriptor> validatorTypes | ||||
| ) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes) | ||||
| { | ||||
|     public string? Name { get; } = name; | ||||
|  | ||||
|     public char? ShortName { get; } = shortName; | ||||
|  | ||||
|     public string? EnvironmentVariable { get; } = environmentVariable; | ||||
|  | ||||
|     public bool IsRequired { get; } = isRequired; | ||||
| } | ||||
|  | ||||
| internal partial class CommandOptionSymbol : IEquatable<CommandOptionSymbol> | ||||
| { | ||||
|     public bool Equals(CommandOptionSymbol? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, other)) | ||||
|             return true; | ||||
|  | ||||
|         return base.Equals(other) | ||||
|             && Name == other.Name | ||||
|             && ShortName == other.ShortName | ||||
|             && EnvironmentVariable == other.EnvironmentVariable | ||||
|             && IsRequired == other.IsRequired; | ||||
|     } | ||||
|  | ||||
|     public override bool Equals(object? obj) | ||||
|     { | ||||
|         if (ReferenceEquals(null, obj)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, obj)) | ||||
|             return true; | ||||
|         if (obj.GetType() != GetType()) | ||||
|             return false; | ||||
|  | ||||
|         return Equals((CommandOptionSymbol)obj); | ||||
|     } | ||||
|  | ||||
|     public override int GetHashCode() => | ||||
|         HashCode.Combine(base.GetHashCode(), Name, ShortName, EnvironmentVariable, IsRequired); | ||||
| } | ||||
|  | ||||
| internal partial class CommandOptionSymbol | ||||
| { | ||||
|     public static CommandOptionSymbol FromSymbol( | ||||
|         IPropertySymbol property, | ||||
|         AttributeData attribute | ||||
|     ) => | ||||
|         new( | ||||
|             PropertyDescriptor.FromSymbol(property), | ||||
|             IsSequenceType(property.Type), | ||||
|             attribute | ||||
|                 .ConstructorArguments.FirstOrDefault(a => | ||||
|                     a.Type?.SpecialType == SpecialType.System_String | ||||
|                 ) | ||||
|                 .Value as string, | ||||
|             attribute | ||||
|                 .ConstructorArguments.FirstOrDefault(a => | ||||
|                     a.Type?.SpecialType == SpecialType.System_Char | ||||
|                 ) | ||||
|                 .Value as char?, | ||||
|             attribute.GetNamedArgumentValue("EnvironmentVariable", default(string)), | ||||
|             attribute.GetNamedArgumentValue("IsRequired", property.IsRequired), | ||||
|             attribute.GetNamedArgumentValue("Description", default(string)), | ||||
|             TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol?>("Converter")), | ||||
|             attribute | ||||
|                 .GetNamedArgumentValues<ITypeSymbol>("Validators") | ||||
|                 .Select(TypeDescriptor.FromSymbol) | ||||
|                 .ToArray() | ||||
|         ); | ||||
| } | ||||
| @@ -0,0 +1,77 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using CliFx.SourceGeneration.Utils.Extensions; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.SemanticModel; | ||||
|  | ||||
| internal partial class CommandParameterSymbol( | ||||
|     PropertyDescriptor property, | ||||
|     bool isSequence, | ||||
|     int order, | ||||
|     string name, | ||||
|     bool isRequired, | ||||
|     string? description, | ||||
|     TypeDescriptor? converterType, | ||||
|     IReadOnlyList<TypeDescriptor> validatorTypes | ||||
| ) : CommandInputSymbol(property, isSequence, description, converterType, validatorTypes) | ||||
| { | ||||
|     public int Order { get; } = order; | ||||
|  | ||||
|     public string Name { get; } = name; | ||||
|  | ||||
|     public bool IsRequired { get; } = isRequired; | ||||
| } | ||||
|  | ||||
| internal partial class CommandParameterSymbol : IEquatable<CommandParameterSymbol> | ||||
| { | ||||
|     public bool Equals(CommandParameterSymbol? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, other)) | ||||
|             return true; | ||||
|  | ||||
|         return base.Equals(other) | ||||
|             && Order == other.Order | ||||
|             && Name == other.Name | ||||
|             && IsRequired == other.IsRequired; | ||||
|     } | ||||
|  | ||||
|     public override bool Equals(object? obj) | ||||
|     { | ||||
|         if (ReferenceEquals(null, obj)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, obj)) | ||||
|             return true; | ||||
|         if (obj.GetType() != GetType()) | ||||
|             return false; | ||||
|  | ||||
|         return Equals((CommandParameterSymbol)obj); | ||||
|     } | ||||
|  | ||||
|     public override int GetHashCode() => | ||||
|         HashCode.Combine(base.GetHashCode(), Order, Name, IsRequired); | ||||
| } | ||||
|  | ||||
| internal partial class CommandParameterSymbol | ||||
| { | ||||
|     public static CommandParameterSymbol FromSymbol( | ||||
|         IPropertySymbol property, | ||||
|         AttributeData attribute | ||||
|     ) => | ||||
|         new( | ||||
|             PropertyDescriptor.FromSymbol(property), | ||||
|             IsSequenceType(property.Type), | ||||
|             (int)attribute.ConstructorArguments.First().Value!, | ||||
|             attribute.GetNamedArgumentValue("Name", default(string)), | ||||
|             attribute.GetNamedArgumentValue("IsRequired", true), | ||||
|             attribute.GetNamedArgumentValue("Description", default(string)), | ||||
|             TypeDescriptor.FromSymbol(attribute.GetNamedArgumentValue<ITypeSymbol>("Converter")), | ||||
|             attribute | ||||
|                 .GetNamedArgumentValues<ITypeSymbol>("Validators") | ||||
|                 .Select(TypeDescriptor.FromSymbol) | ||||
|                 .ToArray() | ||||
|         ); | ||||
| } | ||||
							
								
								
									
										167
									
								
								CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								CliFx.SourceGeneration/SemanticModel/CommandSymbol.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using CliFx.SourceGeneration.Utils.Extensions; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.SemanticModel; | ||||
|  | ||||
| internal partial class CommandSymbol( | ||||
|     TypeDescriptor type, | ||||
|     string? name, | ||||
|     string? description, | ||||
|     IReadOnlyList<CommandInputSymbol> inputs | ||||
| ) | ||||
| { | ||||
|     public TypeDescriptor Type { get; } = type; | ||||
|  | ||||
|     public string? Name { get; } = name; | ||||
|  | ||||
|     public string? Description { get; } = description; | ||||
|  | ||||
|     public IReadOnlyList<CommandInputSymbol> Inputs { get; } = inputs; | ||||
|  | ||||
|     public IReadOnlyList<CommandParameterSymbol> Parameters => | ||||
|         Inputs.OfType<CommandParameterSymbol>().ToArray(); | ||||
|  | ||||
|     public IReadOnlyList<CommandOptionSymbol> Options => | ||||
|         Inputs.OfType<CommandOptionSymbol>().ToArray(); | ||||
|  | ||||
|     private string GeneratePropertyBindingInitializationCode(PropertyDescriptor property) => | ||||
|         // lang=csharp | ||||
|         $$""" | ||||
|             new CliFx.Schema.PropertyBinding<{{Type.FullyQualifiedName}}, {{property | ||||
|                 .Type | ||||
|                 .FullyQualifiedName}}>( | ||||
|                 (obj) => obj.{{property.Name}}, | ||||
|                 (obj, value) => obj.{{property.Name}} = value | ||||
|             ) | ||||
|             """; | ||||
|  | ||||
|     private string GenerateSchemaInitializationCode(CommandInputSymbol input) => | ||||
|         input switch | ||||
|         { | ||||
|             CommandParameterSymbol parameter | ||||
|                 => | ||||
|                 // lang=csharp | ||||
|                 $$""" | ||||
|                     new CliFx.Schema.CommandParameterSchema<{{Type.FullyQualifiedName}}, {{parameter | ||||
|                         .Property | ||||
|                         .Type | ||||
|                         .FullyQualifiedName}}>( | ||||
|                         {{GeneratePropertyBindingInitializationCode(parameter.Property)}}, | ||||
|                         {{parameter.IsSequence}}, | ||||
|                         {{parameter.Order}}, | ||||
|                         "{{parameter.Name}}", | ||||
|                         {{parameter.IsRequired}}, | ||||
|                         "{{parameter.Description}}", | ||||
|                         // TODO, | ||||
|                         // TODO | ||||
|                     ); | ||||
|                     """, | ||||
|             CommandOptionSymbol option | ||||
|                 => | ||||
|                 // lang=csharp | ||||
|                 $$""" | ||||
|                     new CliFx.Schema.CommandOptionSchema<{{Type.FullyQualifiedName}}, {{option | ||||
|                         .Property | ||||
|                         .Type | ||||
|                         .FullyQualifiedName}}>( | ||||
|                         {{GeneratePropertyBindingInitializationCode(option.Property)}}, | ||||
|                         {{option.IsSequence}}, | ||||
|                         "{{option.Name}}", | ||||
|                         '{{option.ShortName}}', | ||||
|                         "{{option.EnvironmentVariable}}", | ||||
|                         {{option.IsRequired}}, | ||||
|                         "{{option.Description}}", | ||||
|                         // TODO, | ||||
|                         // TODO | ||||
|                     ); | ||||
|                     """, | ||||
|             _ => throw new ArgumentOutOfRangeException(nameof(input), input, null) | ||||
|         }; | ||||
|  | ||||
|     public string GenerateSchemaInitializationCode() => | ||||
|             // lang=csharp | ||||
|             $$""" | ||||
|             new CliFx.Schema.CommandSchema<{{Type.FullyQualifiedName}}>( | ||||
|                 "{{Name}}", | ||||
|                 "{{Description}}", | ||||
|                 new CliFx.Schema.CommandInputSchema[] | ||||
|                 { | ||||
|                     {{Inputs.Select(GenerateSchemaInitializationCode).JoinToString(",\n")}} | ||||
|                 } | ||||
|             ) | ||||
|             """; | ||||
| } | ||||
|  | ||||
| internal partial class CommandSymbol : IEquatable<CommandSymbol> | ||||
| { | ||||
|     public bool Equals(CommandSymbol? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, other)) | ||||
|             return true; | ||||
|  | ||||
|         return Type.Equals(other.Type) | ||||
|             && Name == other.Name | ||||
|             && Description == other.Description | ||||
|             && Inputs.SequenceEqual(other.Inputs); | ||||
|     } | ||||
|  | ||||
|     public override bool Equals(object? obj) | ||||
|     { | ||||
|         if (ReferenceEquals(null, obj)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, obj)) | ||||
|             return true; | ||||
|         if (obj.GetType() != GetType()) | ||||
|             return false; | ||||
|  | ||||
|         return Equals((CommandSymbol)obj); | ||||
|     } | ||||
|  | ||||
|     public override int GetHashCode() => HashCode.Combine(Type, Name, Description, Inputs); | ||||
| } | ||||
|  | ||||
| internal partial class CommandSymbol | ||||
| { | ||||
|     public static CommandSymbol FromSymbol(INamedTypeSymbol symbol, AttributeData attribute) | ||||
|     { | ||||
|         var inputs = new List<CommandInputSymbol>(); | ||||
|         foreach (var property in symbol.GetMembers().OfType<IPropertySymbol>()) | ||||
|         { | ||||
|             var parameterAttribute = property | ||||
|                 .GetAttributes() | ||||
|                 .FirstOrDefault(a => | ||||
|                     a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandParameterAttribute | ||||
|                 ); | ||||
|  | ||||
|             if (parameterAttribute is not null) | ||||
|             { | ||||
|                 inputs.Add(CommandParameterSymbol.FromSymbol(property, parameterAttribute)); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             var optionAttribute = property | ||||
|                 .GetAttributes() | ||||
|                 .FirstOrDefault(a => | ||||
|                     a.AttributeClass?.Name == KnownSymbolNames.CliFxCommandOptionAttribute | ||||
|                 ); | ||||
|  | ||||
|             if (optionAttribute is not null) | ||||
|             { | ||||
|                 inputs.Add(CommandOptionSymbol.FromSymbol(property, optionAttribute)); | ||||
|                 continue; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return new CommandSymbol( | ||||
|             TypeDescriptor.FromSymbol(symbol), | ||||
|             attribute.ConstructorArguments.FirstOrDefault().Value as string, | ||||
|             attribute.GetNamedArgumentValue("Description", default(string)), | ||||
|             inputs | ||||
|         ); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								CliFx.SourceGeneration/SemanticModel/KnownSymbolNames.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								CliFx.SourceGeneration/SemanticModel/KnownSymbolNames.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| namespace CliFx.SourceGeneration.SemanticModel; | ||||
|  | ||||
| internal static class KnownSymbolNames | ||||
| { | ||||
|     public const string CliFxCommandInterface = "CliFx.ICommand"; | ||||
|     public const string CliFxCommandAttribute = "CliFx.Attributes.CommandAttribute"; | ||||
|     public const string CliFxCommandParameterAttribute = | ||||
|         "CliFx.Attributes.CommandParameterAttribute"; | ||||
|     public const string CliFxCommandOptionAttribute = "CliFx.Attributes.CommandOptionAttribute"; | ||||
| } | ||||
							
								
								
									
										44
									
								
								CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								CliFx.SourceGeneration/SemanticModel/PropertyDescriptor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| using System; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.SemanticModel; | ||||
|  | ||||
| internal partial class PropertyDescriptor(TypeDescriptor type, string name) | ||||
| { | ||||
|     public TypeDescriptor Type { get; } = type; | ||||
|  | ||||
|     public string Name { get; } = name; | ||||
| } | ||||
|  | ||||
| internal partial class PropertyDescriptor : IEquatable<PropertyDescriptor> | ||||
| { | ||||
|     public bool Equals(PropertyDescriptor? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, other)) | ||||
|             return true; | ||||
|  | ||||
|         return Type.Equals(other.Type) && Name == other.Name; | ||||
|     } | ||||
|  | ||||
|     public override bool Equals(object? obj) | ||||
|     { | ||||
|         if (ReferenceEquals(null, obj)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, obj)) | ||||
|             return true; | ||||
|         if (obj.GetType() != GetType()) | ||||
|             return false; | ||||
|  | ||||
|         return Equals((PropertyDescriptor)obj); | ||||
|     } | ||||
|  | ||||
|     public override int GetHashCode() => HashCode.Combine(Type, Name); | ||||
| } | ||||
|  | ||||
| internal partial class PropertyDescriptor | ||||
| { | ||||
|     public static PropertyDescriptor FromSymbol(IPropertySymbol symbol) => | ||||
|         new(TypeDescriptor.FromSymbol(symbol.Type), symbol.Name); | ||||
| } | ||||
							
								
								
									
										47
									
								
								CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								CliFx.SourceGeneration/SemanticModel/TypeDescriptor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| using System; | ||||
| using CliFx.SourceGeneration.Utils.Extensions; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.SemanticModel; | ||||
|  | ||||
| internal partial class TypeDescriptor(string fullyQualifiedName) | ||||
| { | ||||
|     public string FullyQualifiedName { get; } = fullyQualifiedName; | ||||
|  | ||||
|     public string Namespace { get; } = fullyQualifiedName.SubstringUntilLast("."); | ||||
|  | ||||
|     public string Name { get; } = fullyQualifiedName.SubstringAfterLast("."); | ||||
| } | ||||
|  | ||||
| internal partial class TypeDescriptor : IEquatable<TypeDescriptor> | ||||
| { | ||||
|     public bool Equals(TypeDescriptor? other) | ||||
|     { | ||||
|         if (ReferenceEquals(null, other)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, other)) | ||||
|             return true; | ||||
|  | ||||
|         return FullyQualifiedName == other.FullyQualifiedName; | ||||
|     } | ||||
|  | ||||
|     public override bool Equals(object? obj) | ||||
|     { | ||||
|         if (ReferenceEquals(null, obj)) | ||||
|             return false; | ||||
|         if (ReferenceEquals(this, obj)) | ||||
|             return true; | ||||
|         if (obj.GetType() != GetType()) | ||||
|             return false; | ||||
|  | ||||
|         return Equals((TypeDescriptor)obj); | ||||
|     } | ||||
|  | ||||
|     public override int GetHashCode() => FullyQualifiedName.GetHashCode(); | ||||
| } | ||||
|  | ||||
| internal partial class TypeDescriptor | ||||
| { | ||||
|     public static TypeDescriptor FromSymbol(ITypeSymbol symbol) => | ||||
|         new(symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.Utils.Extensions; | ||||
|  | ||||
| internal static class CollectionExtensions | ||||
| { | ||||
|     public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) | ||||
|         where T : class | ||||
|     { | ||||
|         foreach (var i in source) | ||||
|         { | ||||
|             if (i is not null) | ||||
|                 yield return i; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.Utils.Extensions; | ||||
|  | ||||
| internal static class GenericExtensions | ||||
| { | ||||
|     public static TOut Pipe<TIn, TOut>(this TIn input, Func<TIn, TOut> transform) => | ||||
|         transform(input); | ||||
| } | ||||
							
								
								
									
										39
									
								
								CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								CliFx.SourceGeneration/Utils/Extensions/RoslynExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Collections.Immutable; | ||||
| using System.Linq; | ||||
| using Microsoft.CodeAnalysis; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.Utils.Extensions; | ||||
|  | ||||
| internal static class RoslynExtensions | ||||
| { | ||||
|     public static bool DisplayNameMatches(this ISymbol symbol, string name) => | ||||
|         string.Equals( | ||||
|             // Fully qualified name, without `global::` | ||||
|             symbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat), | ||||
|             name, | ||||
|             StringComparison.Ordinal | ||||
|         ); | ||||
|  | ||||
|     public static T GetNamedArgumentValue<T>( | ||||
|         this AttributeData attribute, | ||||
|         string name, | ||||
|         T defaultValue = default | ||||
|     ) => | ||||
|         attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Value is T valueAsT | ||||
|             ? valueAsT | ||||
|             : defaultValue; | ||||
|  | ||||
|     public static IReadOnlyList<T> GetNamedArgumentValues<T>( | ||||
|         this AttributeData attribute, | ||||
|         string name | ||||
|     ) | ||||
|         where T : class => | ||||
|         attribute.NamedArguments.FirstOrDefault(i => i.Key == name).Value.Values.CastArray<T>(); | ||||
|  | ||||
|     public static IncrementalValuesProvider<T> WhereNotNull<T>( | ||||
|         this IncrementalValuesProvider<T?> values | ||||
|     ) | ||||
|         where T : class => values.Where(i => i is not null); | ||||
| } | ||||
							
								
								
									
										30
									
								
								CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								CliFx.SourceGeneration/Utils/Extensions/StringExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace CliFx.SourceGeneration.Utils.Extensions; | ||||
|  | ||||
| internal static class StringExtensions | ||||
| { | ||||
|     public static string SubstringUntilLast( | ||||
|         this string str, | ||||
|         string sub, | ||||
|         StringComparison comparison = StringComparison.Ordinal | ||||
|     ) | ||||
|     { | ||||
|         var index = str.LastIndexOf(sub, comparison); | ||||
|         return index < 0 ? str : str[..index]; | ||||
|     } | ||||
|  | ||||
|     public static string SubstringAfterLast( | ||||
|         this string str, | ||||
|         string sub, | ||||
|         StringComparison comparison = StringComparison.Ordinal | ||||
|     ) | ||||
|     { | ||||
|         var index = str.LastIndexOf(sub, comparison); | ||||
|         return index >= 0 ? str.Substring(index + sub.Length, str.Length - index - sub.Length) : ""; | ||||
|     } | ||||
|  | ||||
|     public static string JoinToString<T>(this IEnumerable<T> source, string separator) => | ||||
|         string.Join(separator, source); | ||||
| } | ||||
| @@ -2,12 +2,17 @@ | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <OutputType>Exe</OutputType> | ||||
|     <TargetFramework>net45</TargetFramework> | ||||
|     <LangVersion>latest</LangVersion> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|     <ApplicationIcon>../favicon.ico</ApplicationIcon> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\CliFx\CliFx.csproj" /> | ||||
|     <ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @@ -1,25 +0,0 @@ | ||||
| using System; | ||||
| using System.Globalization; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Models; | ||||
|  | ||||
| namespace CliFx.Tests.Dummy.Commands | ||||
| { | ||||
|     [Command("add")] | ||||
|     public class AddCommand : Command | ||||
|     { | ||||
|         [CommandOption("a", IsRequired = true, Description = "Left operand.")] | ||||
|         public double A { get; set; } | ||||
|  | ||||
|         [CommandOption("b", IsRequired = true, Description = "Right operand.")] | ||||
|         public double B { get; set; } | ||||
|  | ||||
|         public override ExitCode Execute() | ||||
|         { | ||||
|             var result = A + B; | ||||
|             Console.WriteLine(result.ToString(CultureInfo.InvariantCulture)); | ||||
|  | ||||
|             return ExitCode.Success; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										27
									
								
								CliFx.Tests.Dummy/Commands/CancellationTestCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								CliFx.Tests.Dummy/Commands/CancellationTestCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Tests.Dummy.Commands; | ||||
|  | ||||
| [Command("cancel-test")] | ||||
| public class CancellationTestCommand : ICommand | ||||
| { | ||||
|     public async ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         try | ||||
|         { | ||||
|             console.WriteLine("Started."); | ||||
|  | ||||
|             await Task.Delay(TimeSpan.FromSeconds(3), console.RegisterCancellationHandler()); | ||||
|  | ||||
|             console.WriteLine("Completed."); | ||||
|         } | ||||
|         catch (OperationCanceledException) | ||||
|         { | ||||
|             console.WriteLine("Cancelled."); | ||||
|             throw; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										23
									
								
								CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								CliFx.Tests.Dummy/Commands/ConsoleTestCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Tests.Dummy.Commands; | ||||
|  | ||||
| [Command("console-test")] | ||||
| public class ConsoleTestCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         var input = console.Input.ReadToEnd(); | ||||
|  | ||||
|         using (console.WithColors(ConsoleColor.Black, ConsoleColor.White)) | ||||
|         { | ||||
|             console.Output.WriteLine(input); | ||||
|             console.Error.WriteLine(input); | ||||
|         } | ||||
|  | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
| @@ -1,31 +0,0 @@ | ||||
| using System; | ||||
| using System.Text; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Models; | ||||
|  | ||||
| namespace CliFx.Tests.Dummy.Commands | ||||
| { | ||||
|     [DefaultCommand] | ||||
|     public class DefaultCommand : Command | ||||
|     { | ||||
|         [CommandOption("target", ShortName = 't', Description = "Greeting target.")] | ||||
|         public string Target { get; set; } = "world"; | ||||
|  | ||||
|         [CommandOption("enthusiastic", ShortName = 'e', Description = "Whether the greeting should be enthusiastic.")] | ||||
|         public bool IsEnthusiastic { get; set; } | ||||
|  | ||||
|         public override ExitCode Execute() | ||||
|         { | ||||
|             var buffer = new StringBuilder(); | ||||
|  | ||||
|             buffer.Append("Hello ").Append(Target); | ||||
|  | ||||
|             if (IsEnthusiastic) | ||||
|                 buffer.Append("!!!"); | ||||
|  | ||||
|             Console.WriteLine(buffer.ToString()); | ||||
|  | ||||
|             return ExitCode.Success; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										18
									
								
								CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								CliFx.Tests.Dummy/Commands/EnvironmentTestCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Tests.Dummy.Commands; | ||||
|  | ||||
| [Command("env-test")] | ||||
| public class EnvironmentTestCommand : ICommand | ||||
| { | ||||
|     [CommandOption("target", EnvironmentVariable = "ENV_TARGET")] | ||||
|     public string GreetingTarget { get; init; } = "World"; | ||||
|  | ||||
|     public ValueTask ExecuteAsync(IConsole console) | ||||
|     { | ||||
|         console.WriteLine($"Hello {GreetingTarget}!"); | ||||
|         return default; | ||||
|     } | ||||
| } | ||||
| @@ -1,25 +0,0 @@ | ||||
| using System; | ||||
| using System.Globalization; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Models; | ||||
|  | ||||
| namespace CliFx.Tests.Dummy.Commands | ||||
| { | ||||
|     [Command("log")] | ||||
|     public class LogCommand : Command | ||||
|     { | ||||
|         [CommandOption("value", IsRequired = true, Description = "Value whose logarithm is to be found.")] | ||||
|         public double Value { get; set; } | ||||
|  | ||||
|         [CommandOption("base", Description = "Logarithm base.")] | ||||
|         public double Base { get; set; } = 10; | ||||
|  | ||||
|         public override ExitCode Execute() | ||||
|         { | ||||
|             var result = Math.Log(Value, Base); | ||||
|             Console.WriteLine(result.ToString(CultureInfo.InvariantCulture)); | ||||
|  | ||||
|             return ExitCode.Success; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +1,28 @@ | ||||
| using System.Threading.Tasks; | ||||
| using System; | ||||
| using System.IO; | ||||
| using System.Reflection; | ||||
| using System.Threading.Tasks; | ||||
|  | ||||
| namespace CliFx.Tests.Dummy | ||||
| namespace CliFx.Tests.Dummy; | ||||
|  | ||||
| // This dummy application is used in tests for scenarios that require an external process to properly verify | ||||
| public static class Program | ||||
| { | ||||
|     public static class Program | ||||
|     // Path to the apphost | ||||
|     public static string FilePath { get; } = | ||||
|         Path.ChangeExtension( | ||||
|             Assembly.GetExecutingAssembly().Location, | ||||
|             OperatingSystem.IsWindows() ? "exe" : null | ||||
|         ); | ||||
|  | ||||
|     public static async Task Main() | ||||
|     { | ||||
|         public static Task<int> Main(string[] args) => new CliApplication().RunAsync(args); | ||||
|         // Make sure color codes are not produced because we rely on the output in tests | ||||
|         Environment.SetEnvironmentVariable( | ||||
|             "DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION", | ||||
|             "false" | ||||
|         ); | ||||
|  | ||||
|         await new CliApplicationBuilder().AddCommandsFromThisAssembly().Build().RunAsync(); | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										71
									
								
								CliFx.Tests/ApplicationSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								CliFx.Tests/ApplicationSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class ApplicationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_create_an_application_with_the_default_configuration() | ||||
|     { | ||||
|         // Act | ||||
|         var app = new CliApplicationBuilder() | ||||
|             .AddCommandsFromThisAssembly() | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         var exitCode = await app.RunAsync([], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_create_an_application_with_a_custom_configuration() | ||||
|     { | ||||
|         // Act | ||||
|         var app = new CliApplicationBuilder() | ||||
|             .AddCommand<NoOpCommand>() | ||||
|             .AddCommandsFrom(typeof(NoOpCommand).Assembly) | ||||
|             .AddCommands([typeof(NoOpCommand)]) | ||||
|             .AddCommandsFrom([typeof(NoOpCommand).Assembly]) | ||||
|             .AddCommandsFromThisAssembly() | ||||
|             .AllowDebugMode() | ||||
|             .AllowPreviewMode() | ||||
|             .SetTitle("test") | ||||
|             .SetExecutableName("test") | ||||
|             .SetVersion("test") | ||||
|             .SetDescription("test") | ||||
|             .UseConsole(FakeConsole) | ||||
|             .UseTypeActivator(Activator.CreateInstance!) | ||||
|             .Build(); | ||||
|  | ||||
|         var exitCode = await app.RunAsync([], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_create_an_application_and_get_an_error_if_it_has_invalid_commands() | ||||
|     { | ||||
|         // Act | ||||
|         var app = new CliApplicationBuilder() | ||||
|             .AddCommand(typeof(ApplicationSpecs)) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         var exitCode = await app.RunAsync([], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("not a valid command"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										107
									
								
								CliFx.Tests/CancellationSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								CliFx.Tests/CancellationSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using CliWrap; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class CancellationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact(Timeout = 15000)] | ||||
|     public async Task I_can_configure_the_command_to_listen_to_the_interrupt_signal() | ||||
|     { | ||||
|         // Arrange | ||||
|         using var cts = new CancellationTokenSource(); | ||||
|  | ||||
|         // We need to send the cancellation request right after the process has registered | ||||
|         // a handler for the interrupt signal, otherwise the default handler will trigger | ||||
|         // and just kill the process. | ||||
|         void HandleStdOut(string line) | ||||
|         { | ||||
|             if (string.Equals(line, "Started.", StringComparison.OrdinalIgnoreCase)) | ||||
|                 cts.CancelAfter(TimeSpan.FromSeconds(0.2)); | ||||
|         } | ||||
|  | ||||
|         var stdOutBuffer = new StringBuilder(); | ||||
|  | ||||
|         var pipeTarget = PipeTarget.Merge( | ||||
|             PipeTarget.ToDelegate(HandleStdOut), | ||||
|             PipeTarget.ToStringBuilder(stdOutBuffer) | ||||
|         ); | ||||
|  | ||||
|         var command = Cli.Wrap(Dummy.Program.FilePath).WithArguments("cancel-test") | pipeTarget; | ||||
|  | ||||
|         // Act & assert | ||||
|         await Assert.ThrowsAnyAsync<OperationCanceledException>( | ||||
|             async () => | ||||
|                 await command.ExecuteAsync( | ||||
|                     // Forceful cancellation (not required because we have a timeout) | ||||
|                     CancellationToken.None, | ||||
|                     // Graceful cancellation | ||||
|                     cts.Token | ||||
|                 ) | ||||
|         ); | ||||
|  | ||||
|         stdOutBuffer.ToString().Trim().Should().ConsistOfLines("Started.", "Cancelled."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_the_command_to_listen_to_the_interrupt_signal_when_running_in_isolation() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public async ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     try | ||||
|                     { | ||||
|                         console.WriteLine("Started."); | ||||
|  | ||||
|                         await Task.Delay( | ||||
|                             TimeSpan.FromSeconds(3), | ||||
|                             console.RegisterCancellationHandler() | ||||
|                         ); | ||||
|  | ||||
|                         console.WriteLine("Completed."); | ||||
|                     } | ||||
|                     catch (OperationCanceledException) | ||||
|                     { | ||||
|                         console.WriteLine("Cancelled."); | ||||
|                         throw; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         FakeConsole.RequestCancellation(TimeSpan.FromSeconds(0.2)); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().ConsistOfLines("Started.", "Cancelled."); | ||||
|     } | ||||
| } | ||||
| @@ -1,33 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestObjects; | ||||
| using Moq; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CliApplicationTests | ||||
|     { | ||||
|         [Test] | ||||
|         public async Task RunAsync_Test() | ||||
|         { | ||||
|             // Arrange | ||||
|             var command = new TestCommand(); | ||||
|             var expectedExitCode = await command.ExecuteAsync(); | ||||
|  | ||||
|             var commandResolverMock = new Mock<ICommandResolver>(); | ||||
|             commandResolverMock.Setup(m => m.ResolveCommand(It.IsAny<IReadOnlyList<string>>())).Returns(command); | ||||
|             var commandResolver = commandResolverMock.Object; | ||||
|  | ||||
|             var application = new CliApplication(commandResolver); | ||||
|  | ||||
|             // Act | ||||
|             var exitCodeValue = await application.RunAsync(); | ||||
|  | ||||
|             // Assert | ||||
|             Assert.That(exitCodeValue, Is.EqualTo(expectedExitCode.Value)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,18 +1,26 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>net45</TargetFramework> | ||||
|     <IsPackable>false</IsPackable> | ||||
|     <IsTestProject>true</IsTestProject> | ||||
|     <LangVersion>latest</LangVersion> | ||||
|     <TargetFramework>net9.0</TargetFramework> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" /> | ||||
|     <PackageReference Include="NUnit" Version="3.11.0" /> | ||||
|     <PackageReference Include="NUnit3TestAdapter" Version="3.11.0" /> | ||||
|     <PackageReference Include="Moq" Version="4.11.0" /> | ||||
|     <PackageReference Include="CliWrap" Version="2.3.0" /> | ||||
|     <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.0" /> | ||||
|     <PackageReference Include="CliWrap" Version="3.7.1" /> | ||||
|     <PackageReference Include="coverlet.collector" Version="6.0.4" PrivateAssets="all" /> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" /> | ||||
|     <PackageReference Include="FluentAssertions" Version="8.0.1" /> | ||||
|     <PackageReference Include="GitHubActionsTestLogger" Version="2.4.1" PrivateAssets="all" /> | ||||
|     <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" /> | ||||
|     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" /> | ||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> | ||||
|     <PackageReference Include="PolyShim" Version="1.14.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="xunit" Version="2.9.3" /> | ||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" PrivateAssets="all" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|   | ||||
| @@ -1,83 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestObjects; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandOptionConverterTests | ||||
|     { | ||||
|         private static IEnumerable<TestCaseData> GetData_ConvertOption() | ||||
|         { | ||||
|             yield return new TestCaseData("value", typeof(string), "value") | ||||
|                 .SetName("To string"); | ||||
|  | ||||
|             yield return new TestCaseData("value", typeof(object), "value") | ||||
|                 .SetName("To object"); | ||||
|  | ||||
|             yield return new TestCaseData("true", typeof(bool), true) | ||||
|                 .SetName("To bool (true)"); | ||||
|  | ||||
|             yield return new TestCaseData("false", typeof(bool), false) | ||||
|                 .SetName("To bool (false)"); | ||||
|  | ||||
|             yield return new TestCaseData(null, typeof(bool), true) | ||||
|                 .SetName("To bool (switch)"); | ||||
|  | ||||
|             yield return new TestCaseData("123", typeof(int), 123) | ||||
|                 .SetName("To int"); | ||||
|  | ||||
|             yield return new TestCaseData("123.45", typeof(double), 123.45) | ||||
|                 .SetName("To double"); | ||||
|  | ||||
|             yield return new TestCaseData("28 Apr 1995", typeof(DateTime), new DateTime(1995, 04, 28)) | ||||
|                 .SetName("To DateTime"); | ||||
|  | ||||
|             yield return new TestCaseData("28 Apr 1995", typeof(DateTimeOffset), new DateTimeOffset(new DateTime(1995, 04, 28))) | ||||
|                 .SetName("To DateTimeOffset"); | ||||
|  | ||||
|             yield return new TestCaseData("00:14:59", typeof(TimeSpan), new TimeSpan(00, 14, 59)) | ||||
|                 .SetName("To TimeSpan"); | ||||
|  | ||||
|             yield return new TestCaseData("value2", typeof(TestEnum), TestEnum.Value2) | ||||
|                 .SetName("To enum"); | ||||
|  | ||||
|             yield return new TestCaseData("666", typeof(int?), 666) | ||||
|                 .SetName("To int? (with value)"); | ||||
|  | ||||
|             yield return new TestCaseData(null, typeof(int?), null) | ||||
|                 .SetName("To int? (no value)"); | ||||
|  | ||||
|             yield return new TestCaseData("value3", typeof(TestEnum?), TestEnum.Value3) | ||||
|                 .SetName("To enum? (with value)"); | ||||
|  | ||||
|             yield return new TestCaseData(null, typeof(TestEnum?), null) | ||||
|                 .SetName("To enum? (no value)"); | ||||
|  | ||||
|             yield return new TestCaseData("01:00:00", typeof(TimeSpan?), new TimeSpan(01, 00, 00)) | ||||
|                 .SetName("To TimeSpan? (with value)"); | ||||
|  | ||||
|             yield return new TestCaseData(null, typeof(TimeSpan?), null) | ||||
|                 .SetName("To TimeSpan? (no value)"); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetData_ConvertOption))] | ||||
|         public void ConvertOption_Test(string value, Type targetType, object expectedConvertedValue) | ||||
|         { | ||||
|             // Arrange | ||||
|             var converter = new CommandOptionConverter(); | ||||
|  | ||||
|             // Act | ||||
|             var convertedValue = converter.ConvertOption(value, targetType); | ||||
|  | ||||
|             // Assert | ||||
|             Assert.That(convertedValue, Is.EqualTo(expectedConvertedValue)); | ||||
|  | ||||
|             if (convertedValue != null) | ||||
|                 Assert.That(convertedValue, Is.AssignableTo(targetType)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,139 +0,0 @@ | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandOptionParserTests | ||||
|     { | ||||
|         private static IEnumerable<TestCaseData> GetData_ParseOptions() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new string[0], | ||||
|                 CommandOptionSet.Empty | ||||
|             ).SetName("No arguments"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"--argument", "value"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"argument", "value"} | ||||
|                 }) | ||||
|             ).SetName("Single argument"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"--argument1", "value1", "--argument2", "value2", "--argument3", "value3"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"argument1", "value1"}, | ||||
|                     {"argument2", "value2"}, | ||||
|                     {"argument3", "value3"} | ||||
|                 }) | ||||
|             ).SetName("Multiple arguments"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"-a", "value"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"a", "value"} | ||||
|                 }) | ||||
|             ).SetName("Single short argument"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"-a", "value1", "-b", "value2", "-c", "value3"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"a", "value1"}, | ||||
|                     {"b", "value2"}, | ||||
|                     {"c", "value3"} | ||||
|                 }) | ||||
|             ).SetName("Multiple short arguments"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"--argument1", "value1", "-b", "value2", "--argument3", "value3"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"argument1", "value1"}, | ||||
|                     {"b", "value2"}, | ||||
|                     {"argument3", "value3"} | ||||
|                 }) | ||||
|             ).SetName("Multiple mixed arguments"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"--switch"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"switch", null} | ||||
|                 }) | ||||
|             ).SetName("Single switch"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"--switch1", "--switch2", "--switch3"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"switch1", null}, | ||||
|                     {"switch2", null}, | ||||
|                     {"switch3", null} | ||||
|                 }) | ||||
|             ).SetName("Multiple switches"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"-s"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"s", null} | ||||
|                 }) | ||||
|             ).SetName("Single short switch"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"-a", "-b", "-c"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"a", null}, | ||||
|                     {"b", null}, | ||||
|                     {"c", null} | ||||
|                 }) | ||||
|             ).SetName("Multiple short switches"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"-abc"}, | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"a", null}, | ||||
|                     {"b", null}, | ||||
|                     {"c", null} | ||||
|                 }) | ||||
|             ).SetName("Multiple stacked short switches"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"command"}, | ||||
|                 new CommandOptionSet("command") | ||||
|             ).SetName("No arguments (with command name)"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new[] {"command", "--argument", "value"}, | ||||
|                 new CommandOptionSet("command", new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"argument", "value"} | ||||
|                 }) | ||||
|             ).SetName("Single argument (with command name)"); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetData_ParseOptions))] | ||||
|         public void ParseOptions_Test(IReadOnlyList<string> commandLineArguments, CommandOptionSet expectedCommandOptionSet) | ||||
|         { | ||||
|             // Arrange | ||||
|             var parser = new CommandOptionParser(); | ||||
|  | ||||
|             // Act | ||||
|             var optionSet = parser.ParseOptions(commandLineArguments); | ||||
|  | ||||
|             // Assert | ||||
|             Assert.That(optionSet.CommandName, Is.EqualTo(expectedCommandOptionSet.CommandName)); | ||||
|             Assert.That(optionSet.Options, Is.EqualTo(expectedCommandOptionSet.Options)); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,116 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Models; | ||||
| using CliFx.Services; | ||||
| using CliFx.Tests.TestObjects; | ||||
| using Moq; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class CommandResolverTests | ||||
|     { | ||||
|         private static IEnumerable<TestCaseData> GetData_ResolveCommand() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"int", "13"} | ||||
|                 }), | ||||
|                 new TestCommand {IntOption = 13} | ||||
|             ).SetName("Single option"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"int", "13"}, | ||||
|                     {"str", "hello world" } | ||||
|                 }), | ||||
|                 new TestCommand { IntOption = 13, StringOption = "hello world"} | ||||
|             ).SetName("Multiple options"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"i", "13"} | ||||
|                 }), | ||||
|                 new TestCommand { IntOption = 13 } | ||||
|             ).SetName("Single short option"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionSet("command", new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"int", "13"} | ||||
|                 }), | ||||
|                 new TestCommand { IntOption = 13 } | ||||
|             ).SetName("Single option (with command name)"); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetData_ResolveCommand))] | ||||
|         public void ResolveCommand_Test(CommandOptionSet commandOptionSet, TestCommand expectedCommand) | ||||
|         { | ||||
|             // Arrange | ||||
|             var commandTypes = new[] {typeof(TestCommand)}; | ||||
|  | ||||
|             var typeProviderMock = new Mock<ITypeProvider>(); | ||||
|             typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes); | ||||
|             var typeProvider = typeProviderMock.Object; | ||||
|  | ||||
|             var optionParserMock = new Mock<ICommandOptionParser>(); | ||||
|             optionParserMock.Setup(m => m.ParseOptions(It.IsAny<IReadOnlyList<string>>())).Returns(commandOptionSet); | ||||
|             var optionParser = optionParserMock.Object; | ||||
|  | ||||
|             var optionConverter = new CommandOptionConverter(); | ||||
|  | ||||
|             var resolver = new CommandResolver(typeProvider, optionParser, optionConverter); | ||||
|  | ||||
|             // Act | ||||
|             var command = resolver.ResolveCommand() as TestCommand; | ||||
|  | ||||
|             // Assert | ||||
|             Assert.That(command, Is.Not.Null); | ||||
|             Assert.That(command.StringOption, Is.EqualTo(expectedCommand.StringOption), nameof(command.StringOption)); | ||||
|             Assert.That(command.IntOption, Is.EqualTo(expectedCommand.IntOption), nameof(command.IntOption)); | ||||
|         } | ||||
|  | ||||
|         private static IEnumerable<TestCaseData> GetData_ResolveCommand_IsRequired() | ||||
|         { | ||||
|             yield return new TestCaseData( | ||||
|                 CommandOptionSet.Empty | ||||
|             ).SetName("No options"); | ||||
|  | ||||
|             yield return new TestCaseData( | ||||
|                 new CommandOptionSet(new Dictionary<string, string> | ||||
|                 { | ||||
|                     {"str", "hello world"} | ||||
|                 }) | ||||
|             ).SetName("Required option is not set"); | ||||
|         } | ||||
|  | ||||
|         [Test] | ||||
|         [TestCaseSource(nameof(GetData_ResolveCommand_IsRequired))] | ||||
|         public void ResolveCommand_IsRequired_Test(CommandOptionSet commandOptionSet) | ||||
|         { | ||||
|             // Arrange | ||||
|             var commandTypes = new[] { typeof(TestCommand) }; | ||||
|  | ||||
|             var typeProviderMock = new Mock<ITypeProvider>(); | ||||
|             typeProviderMock.Setup(m => m.GetTypes()).Returns(commandTypes); | ||||
|             var typeProvider = typeProviderMock.Object; | ||||
|  | ||||
|             var optionParserMock = new Mock<ICommandOptionParser>(); | ||||
|             optionParserMock.Setup(m => m.ParseOptions(It.IsAny<IReadOnlyList<string>>())).Returns(commandOptionSet); | ||||
|             var optionParser = optionParserMock.Object; | ||||
|  | ||||
|             var optionConverter = new CommandOptionConverter(); | ||||
|  | ||||
|             var resolver = new CommandResolver(typeProvider, optionParser, optionConverter); | ||||
|  | ||||
|             // Act & Assert | ||||
|             Assert.Throws<CommandResolveException>(() => resolver.ResolveCommand()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										204
									
								
								CliFx.Tests/ConsoleSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								CliFx.Tests/ConsoleSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,204 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Infrastructure; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using CliWrap; | ||||
| using CliWrap.Buffered; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class ConsoleSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact(Timeout = 15000)] | ||||
|     public async Task I_can_run_the_application_with_the_default_console_implementation_to_interact_with_the_system_console() | ||||
|     { | ||||
|         // Can't verify our own console output, so using an external process for this test | ||||
|  | ||||
|         // Arrange | ||||
|         var command = | ||||
|             "Hello world" | Cli.Wrap(Dummy.Program.FilePath).WithArguments("console-test"); | ||||
|  | ||||
|         // Act | ||||
|         var result = await command.ExecuteBufferedAsync(); | ||||
|  | ||||
|         // Assert | ||||
|         result.StandardOutput.Trim().Should().Be("Hello world"); | ||||
|         result.StandardError.Trim().Should().Be("Hello world"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public void I_can_run_the_application_on_a_system_with_a_custom_console_encoding_and_not_get_corrupted_output() | ||||
|     { | ||||
|         // Arrange | ||||
|         using var buffer = new MemoryStream(); | ||||
|         using var consoleWriter = new ConsoleWriter(FakeConsole, buffer, Encoding.UTF8); | ||||
|  | ||||
|         // Act | ||||
|         consoleWriter.Write("Hello world"); | ||||
|         consoleWriter.Flush(); | ||||
|  | ||||
|         // Assert | ||||
|         var outputBytes = buffer.ToArray(); | ||||
|         outputBytes.Should().NotContain(Encoding.UTF8.GetPreamble()); | ||||
|  | ||||
|         var output = consoleWriter.Encoding.GetString(outputBytes); | ||||
|         output.Should().Be("Hello world"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_run_the_application_with_the_fake_console_implementation_to_isolate_console_interactions() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.ResetColor(); | ||||
|                     console.ForegroundColor = ConsoleColor.DarkMagenta; | ||||
|                     console.BackgroundColor = ConsoleColor.DarkMagenta; | ||||
|                     console.WindowWidth = 100; | ||||
|                     console.WindowHeight = 25; | ||||
|                     console.CursorLeft = 42; | ||||
|                     console.CursorTop = 24; | ||||
|  | ||||
|                     console.Output.WriteLine("Hello "); | ||||
|                     console.Error.WriteLine("world!"); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         Console.OpenStandardInput().Should().NotBeSameAs(FakeConsole.Input.BaseStream); | ||||
|         Console.OpenStandardOutput().Should().NotBeSameAs(FakeConsole.Output.BaseStream); | ||||
|         Console.OpenStandardError().Should().NotBeSameAs(FakeConsole.Error.BaseStream); | ||||
|  | ||||
|         Console.ForegroundColor.Should().NotBe(ConsoleColor.DarkMagenta); | ||||
|         Console.BackgroundColor.Should().NotBe(ConsoleColor.DarkMagenta); | ||||
|  | ||||
|         // This fails because tests don't spawn a console window | ||||
|         //Console.WindowWidth.Should().Be(100); | ||||
|         //Console.WindowHeight.Should().Be(25); | ||||
|         //Console.CursorLeft.Should().NotBe(42); | ||||
|         //Console.CursorTop.Should().NotBe(24); | ||||
|  | ||||
|         FakeConsole.IsInputRedirected.Should().BeTrue(); | ||||
|         FakeConsole.IsOutputRedirected.Should().BeTrue(); | ||||
|         FakeConsole.IsErrorRedirected.Should().BeTrue(); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_run_the_application_with_the_fake_console_implementation_and_simulate_stream_interactions() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     var input = console.Input.ReadToEnd(); | ||||
|                     console.Output.WriteLine(input); | ||||
|                     console.Error.WriteLine(input); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         FakeConsole.WriteInput("Hello world"); | ||||
|  | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("Hello world"); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Trim().Should().Be("Hello world"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_run_the_application_with_the_fake_console_implementation_and_simulate_key_presses() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(console.ReadKey().Key); | ||||
|                     console.WriteLine(console.ReadKey().Key); | ||||
|                     console.WriteLine(console.ReadKey().Key); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         FakeConsole.EnqueueKey(new ConsoleKeyInfo('0', ConsoleKey.D0, false, false, false)); | ||||
|         FakeConsole.EnqueueKey(new ConsoleKeyInfo('a', ConsoleKey.A, false, false, false)); | ||||
|         FakeConsole.EnqueueKey(new ConsoleKeyInfo('\0', ConsoleKey.Backspace, false, false, false)); | ||||
|  | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().ConsistOfLines("D0", "A", "Backspace"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										941
									
								
								CliFx.Tests/ConversionSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										941
									
								
								CliFx.Tests/ConversionSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,941 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class ConversionSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_string_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("xyz"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_an_object_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public object? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("xyz"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_boolean_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public bool Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption('b')] | ||||
|                 public bool Bar { get; init; } | ||||
|  | ||||
|                 [CommandOption('c')] | ||||
|                 public bool Baz { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|                     console.WriteLine("Baz = " + Baz); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "true", "-b", "false", "-c"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = True", "Bar = False", "Baz = True"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_an_integer_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public int Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "32"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("32"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_double_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public double Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo.ToString(CultureInfo.InvariantCulture)); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "32.14"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("32.14"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_DateTimeOffset_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public DateTimeOffset Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo.ToString("u", CultureInfo.InvariantCulture)); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "1995-04-28Z"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("1995-04-28 00:00:00Z"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_TimeSpan_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public TimeSpan Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo.ToString(null, CultureInfo.InvariantCulture)); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "12:34:56"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("12:34:56"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_an_enum_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One = 1, Two = 2, Three = 3 } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public CustomEnum Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine((int) Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "two"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("2"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_nullable_integer_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public int? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption('b')] | ||||
|                 public int? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-b", "123"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = ", "Bar = 123"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_nullable_enum_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One = 1, Two = 2, Three = 3 } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public CustomEnum? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption('b')] | ||||
|                 public CustomEnum? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + (int?) Foo); | ||||
|                     console.WriteLine("Bar = " + (int?) Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-b", "two"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = ", "Bar = 2"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_string_constructable_object_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public class CustomType | ||||
|             { | ||||
|                 public string Value { get; } | ||||
|  | ||||
|                 public CustomType(string value) => Value = value; | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public CustomType? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo.Value); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("xyz"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_string_parsable_object_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public class CustomTypeA | ||||
|             { | ||||
|                 public string Value { get; } | ||||
|  | ||||
|                 private CustomTypeA(string value) => Value = value; | ||||
|  | ||||
|                 public static CustomTypeA Parse(string value) => | ||||
|                     new CustomTypeA(value); | ||||
|             } | ||||
|  | ||||
|             public class CustomTypeB | ||||
|             { | ||||
|                 public string Value { get; } | ||||
|  | ||||
|                 private CustomTypeB(string value) => Value = value; | ||||
|  | ||||
|                 public static CustomTypeB Parse(string value, IFormatProvider formatProvider) => | ||||
|                     new CustomTypeB(value); | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public CustomTypeA? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption('b')] | ||||
|                 public CustomTypeB? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo.Value); | ||||
|                     console.WriteLine("Bar = " + Bar.Value); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "hello", "-b", "world"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = hello", "Bar = world"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_property_with_a_custom_converter() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public class CustomConverter : BindingConverter<int> | ||||
|             { | ||||
|                 public override int Convert(string rawValue) => | ||||
|                     rawValue.Length; | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f', Converter = typeof(CustomConverter))] | ||||
|                 public int Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "hello world"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("11"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_string_array_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public string[]? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "one", "two", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_read_only_list_of_strings_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public IReadOnlyList<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "one", "two", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_a_string_list_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public List<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "one", "two", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_or_an_option_to_an_integer_array_property() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public int[]? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "1", "13", "27"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("1", "13", "27"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_it_is_of_an_unsupported_type() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public class CustomType | ||||
|             { | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public CustomType? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "xyz"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("has an unsupported underlying property type"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_non_scalar_property_and_get_an_error_if_it_is_of_an_unsupported_type() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public class CustomType : IEnumerable<object> | ||||
|             { | ||||
|                 public IEnumerator<object> GetEnumerator() => Enumerable.Empty<object>().GetEnumerator(); | ||||
|  | ||||
|                 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public CustomType? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "one", "two"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("has an unsupported underlying property type"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_the_user_provides_an_invalid_value() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public int Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "12.34"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().NotBeNullOrWhiteSpace(); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_property_and_get_an_error_if_a_custom_validator_fails() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public class ValidatorA : BindingValidator<int> | ||||
|             { | ||||
|                 public override BindingValidationError Validate(int value) => Ok(); | ||||
|             } | ||||
|  | ||||
|             public class ValidatorB : BindingValidator<int> | ||||
|             { | ||||
|                 public override BindingValidationError Validate(int value) => Error("Hello world"); | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f', Validators = [typeof(ValidatorA), typeof(ValidatorB)])] | ||||
|                 public int Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "12"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Hello world"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_parameter_or_an_option_to_a_string_parsable_property_and_get_an_error_if_the_parsing_fails() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public class CustomType | ||||
|             { | ||||
|                 public string Value { get; } | ||||
|  | ||||
|                 private CustomType(string value) => Value = value; | ||||
|  | ||||
|                 public static CustomType Parse(string value) => throw new Exception("Hello world"); | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public CustomType? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f", "bar"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Hello world"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										91
									
								
								CliFx.Tests/DirectivesSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								CliFx.Tests/DirectivesSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,91 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using CliWrap; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class DirectivesSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact(Timeout = 15000)] | ||||
|     public async Task I_can_use_the_debug_directive_to_make_the_application_wait_for_the_debugger_to_attach() | ||||
|     { | ||||
|         // Arrange | ||||
|         using var cts = new CancellationTokenSource(); | ||||
|  | ||||
|         // We can't actually attach a debugger, but we can ensure that the process is waiting for one | ||||
|         void HandleStdOut(string line) | ||||
|         { | ||||
|             // Kill the process once it writes the output we expect | ||||
|             if (line.Contains("Attach the debugger to", StringComparison.OrdinalIgnoreCase)) | ||||
|                 cts.Cancel(); | ||||
|         } | ||||
|  | ||||
|         var command = Cli.Wrap(Dummy.Program.FilePath).WithArguments("[debug]") | HandleStdOut; | ||||
|  | ||||
|         // Act & assert | ||||
|         try | ||||
|         { | ||||
|             await command.ExecuteAsync(cts.Token); | ||||
|         } | ||||
|         catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token) | ||||
|         { | ||||
|             // This means that the process was killed after it wrote the expected output | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_use_the_preview_directive_to_make_the_application_print_the_parsed_command_input() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd")] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .AllowPreviewMode() | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["[preview]", "cmd", "param", "-abc", "--option", "foo"], | ||||
|             new Dictionary<string, string> { ["ENV_QOP"] = "hello", ["ENV_KIL"] = "world" } | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "cmd", | ||||
|                 "<param>", | ||||
|                 "[-a]", | ||||
|                 "[-b]", | ||||
|                 "[-c]", | ||||
|                 "[--option \"foo\"]", | ||||
|                 "ENV_QOP", | ||||
|                 "=", | ||||
|                 "\"hello\"", | ||||
|                 "ENV_KIL", | ||||
|                 "=", | ||||
|                 "\"world\"" | ||||
|             ); | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +0,0 @@ | ||||
| using System.IO; | ||||
| using System.Threading.Tasks; | ||||
| using CliWrap; | ||||
| using NUnit.Framework; | ||||
|  | ||||
| namespace CliFx.Tests | ||||
| { | ||||
|     [TestFixture] | ||||
|     public class DummyTests | ||||
|     { | ||||
|         private string DummyFilePath => Path.Combine(TestContext.CurrentContext.TestDirectory, "CliFx.Tests.Dummy.exe"); | ||||
|  | ||||
|         [Test] | ||||
|         [TestCase("", "Hello world")] | ||||
|         [TestCase("-t .NET", "Hello .NET")] | ||||
|         [TestCase("-e", "Hello world!!!")] | ||||
|         [TestCase("add --a 1 --b 2", "3")] | ||||
|         [TestCase("add --a 2.75 --b 3.6", "6.35")] | ||||
|         [TestCase("log --value 100", "2")] | ||||
|         [TestCase("log --value 256 --base 2", "8")] | ||||
|         public async Task Execute_Test(string arguments, string expectedOutput) | ||||
|         { | ||||
|             // Act | ||||
|             var result = await Cli.Wrap(DummyFilePath).SetArguments(arguments).ExecuteAsync(); | ||||
|  | ||||
|             // Assert | ||||
|             Assert.That(result.ExitCode, Is.Zero); | ||||
|             Assert.That(result.StandardOutput.Trim(), Is.EqualTo(expectedOutput)); | ||||
|             Assert.That(result.StandardError.Trim(), Is.Empty); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										160
									
								
								CliFx.Tests/EnvironmentSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								CliFx.Tests/EnvironmentSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,160 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using CliWrap; | ||||
| using CliWrap.Buffered; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class EnvironmentSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_an_option_to_fall_back_to_an_environment_variable_if_the_user_does_not_provide_the_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo", EnvironmentVariable = "ENV_FOO")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar", EnvironmentVariable = "ENV_BAR")] | ||||
|                 public string? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     console.WriteLine(Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "42"], | ||||
|             new Dictionary<string, string> { ["ENV_FOO"] = "100", ["ENV_BAR"] = "200" } | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().ConsistOfLines("42", "200"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_an_option_bound_to_a_non_scalar_property_to_fall_back_to_an_environment_variable_if_the_user_does_not_provide_the_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo", EnvironmentVariable = "ENV_FOO")] | ||||
|                 public IReadOnlyList<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" } | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("bar", "baz"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_an_option_bound_to_a_scalar_property_to_fall_back_to_an_environment_variable_while_ignoring_path_separators() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo", EnvironmentVariable = "ENV_FOO")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string> { ["ENV_FOO"] = $"bar{Path.PathSeparator}baz" } | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be($"bar{Path.PathSeparator}baz"); | ||||
|     } | ||||
|  | ||||
|     [Fact(Timeout = 15000)] | ||||
|     public async Task I_can_run_the_application_and_it_will_resolve_all_required_environment_variables_automatically() | ||||
|     { | ||||
|         // Ensures that the environment variables are properly obtained from | ||||
|         // System.Environment when they are not provided explicitly to CliApplication. | ||||
|  | ||||
|         // Arrange | ||||
|         var command = Cli.Wrap(Dummy.Program.FilePath) | ||||
|             .WithArguments("env-test") | ||||
|             .WithEnvironmentVariables(e => e.Set("ENV_TARGET", "Mars")); | ||||
|  | ||||
|         // Act | ||||
|         var result = await command.ExecuteBufferedAsync(); | ||||
|  | ||||
|         // Assert | ||||
|         result.StandardOutput.Trim().Should().Be("Hello Mars!"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										209
									
								
								CliFx.Tests/ErrorReportingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								CliFx.Tests/ErrorReportingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class ErrorReportingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_with_a_stacktrace() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => | ||||
|                     throw new Exception("Something went wrong"); | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().BeEmpty(); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr | ||||
|             .Should() | ||||
|             .ContainAllInOrder("System.Exception", "Something went wrong", "at", "CliFx."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_throw_an_exception_with_an_inner_exception_in_a_command_to_report_an_error_with_a_stacktrace() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => | ||||
|                     throw new Exception("Something went wrong", new Exception("Another exception")); | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().BeEmpty(); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "System.Exception", | ||||
|                 "Something went wrong", | ||||
|                 "System.Exception", | ||||
|                 "Another exception", | ||||
|                 "at", | ||||
|                 "CliFx." | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_and_exit_with_the_specified_code() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => | ||||
|                     throw new CommandException("Something went wrong", 69); | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(69); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().BeEmpty(); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Trim().Should().Be("Something went wrong"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_throw_an_exception_without_a_message_in_a_command_to_report_an_error_with_a_stacktrace() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => | ||||
|                     throw new CommandException("", 69); | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(69); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().BeEmpty(); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().ContainAllInOrder("CliFx.Exceptions.CommandException", "at", "CliFx."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_throw_an_exception_in_a_command_to_report_an_error_and_print_the_help_text() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => | ||||
|                     throw new CommandException("Something went wrong", 69, true); | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(69); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Trim().Should().Be("Something went wrong"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										994
									
								
								CliFx.Tests/HelpTextSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										994
									
								
								CliFx.Tests/HelpTextSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,994 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class HelpTextSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_without_arguments_if_the_default_command_is_not_defined() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_with_the_help_option_even_if_the_default_command_is_not_defined() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_for_a_specific_command_by_running_the_application_and_specifying_its_name_with_the_help_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd", Description = "Description of a named command.")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["cmd", "--help"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("Description of a named command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_for_a_specific_nested_command_by_running_the_application_and_specifying_its_name_with_the_help_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child", Description = "Description of a named child command.")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["cmd", "sub", "--help"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("Description of a named child command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_by_running_the_application_with_invalid_arguments() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand<NoOpCommand>() | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetDescription("This will be in help text") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["invalid-command", "--invalid-option"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().Contain("This will be in help text"); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().NotBeNullOrWhiteSpace(); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_application_title_description_and_version() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .UseConsole(FakeConsole) | ||||
|             .SetTitle("App title") | ||||
|             .SetDescription("App description") | ||||
|             .SetVersion("App version") | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAll("App title", "App description", "App version"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_command_description() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command(Description = "Description of the default command.")] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("DESCRIPTION", "Description of the default command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_a_named_command() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("USAGE", "[command]", "[...]"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_all_parameters() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required string Bar { get; init; } | ||||
|  | ||||
|                 [CommandParameter(2)] | ||||
|                 public required IReadOnlyList<string> Baz { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("USAGE", "<foo>", "<bar>", "<baz...>"); | ||||
|     } | ||||
|  | ||||
|     // https://github.com/Tyrrrz/CliFx/issues/117 | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_all_parameters_in_the_correct_order() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             // Base members appear last in reflection order | ||||
|             public abstract class CommandBase : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 public abstract ValueTask ExecuteAsync(IConsole console); | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : CommandBase | ||||
|             { | ||||
|                 [CommandParameter(2)] | ||||
|                 public required IReadOnlyList<string> Baz { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required string Bar { get; init; } | ||||
|  | ||||
|                 public override ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ContainAllInOrder("USAGE", "<foo>", "<bar>", "<baz...>"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_usage_format_for_all_required_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public string? Bar { get; init; } | ||||
|  | ||||
|                 [CommandOption("baz")] | ||||
|                 public required IReadOnlyList<string> Baz { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder("USAGE", "--foo <value>", "--baz <values...>", "[options]"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_all_parameters_and_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0, Name = "foo", Description = "Description of foo.")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar", Description = "Description of bar.")] | ||||
|                 public string? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Description of foo.", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Description of bar." | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_help_and_version_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "OPTIONS", | ||||
|                 "-h", | ||||
|                 "--help", | ||||
|                 "Shows help text", | ||||
|                 "--version", | ||||
|                 "Shows version information" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_on_a_named_command_to_see_the_help_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd")] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["cmd", "--help"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|  | ||||
|         stdOut.Should().ContainAllInOrder("OPTIONS", "-h", "--help", "Shows help text"); | ||||
|  | ||||
|         stdOut.Should().NotContainAny("--version", "Shows version information"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_valid_values_for_all_parameters_and_options_bound_to_enum_properties() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public CustomEnum Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public CustomEnum Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_valid_values_for_all_parameters_and_options_bound_to_non_scalar_enum_properties() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required IReadOnlyList<CustomEnum> Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public required IReadOnlyList<CustomEnum> Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_valid_values_for_all_parameters_and_options_bound_to_nullable_enum_properties() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public CustomEnum? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public IReadOnlyList<CustomEnum?>? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "PARAMETERS", | ||||
|                 "foo", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three", | ||||
|                 "OPTIONS", | ||||
|                 "--bar", | ||||
|                 "Choices:", | ||||
|                 "One", | ||||
|                 "Two", | ||||
|                 "Three" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_environment_variables_of_options_that_use_them_as_fallback() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo", EnvironmentVariable = "ENV_FOO")] | ||||
|                 public CustomEnum Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar", EnvironmentVariable = "ENV_BAR")] | ||||
|                 public CustomEnum Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "OPTIONS", | ||||
|                 "--foo", | ||||
|                 "Environment variable:", | ||||
|                 "ENV_FOO", | ||||
|                 "--bar", | ||||
|                 "Environment variable:", | ||||
|                 "ENV_BAR" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_default_values_of_non_required_options() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public enum CustomEnum { One, Two, Three } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public object? Foo { get; init; } = 42; | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public string? Bar { get; init; } = "hello"; | ||||
|  | ||||
|                 [CommandOption("baz")] | ||||
|                 public IReadOnlyList<string>? Baz { get; init; } = new[] {"one", "two", "three"}; | ||||
|  | ||||
|                 [CommandOption("qwe")] | ||||
|                 public bool Qwe { get; init; } = true; | ||||
|  | ||||
|                 [CommandOption("qop")] | ||||
|                 public int? Qop { get; init; } = 1337; | ||||
|  | ||||
|                 [CommandOption("zor")] | ||||
|                 public TimeSpan Zor { get; init; } = TimeSpan.FromMinutes(123); | ||||
|  | ||||
|                 [CommandOption("lol")] | ||||
|                 public CustomEnum Lol { get; init; } = CustomEnum.Two; | ||||
|  | ||||
|                 [CommandOption("hmm")] | ||||
|                 public required string Hmm { get; init; } = "not printed"; | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|  | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "OPTIONS", | ||||
|                 "--foo", | ||||
|                 "Default:", | ||||
|                 "42", | ||||
|                 "--bar", | ||||
|                 "Default:", | ||||
|                 "hello", | ||||
|                 "--baz", | ||||
|                 "Default:", | ||||
|                 "one", | ||||
|                 "two", | ||||
|                 "three", | ||||
|                 "--qwe", | ||||
|                 "Default:", | ||||
|                 "True", | ||||
|                 "--qop", | ||||
|                 "Default:", | ||||
|                 "1337", | ||||
|                 "--zor", | ||||
|                 "Default:", | ||||
|                 "02:03:00", | ||||
|                 "--lol", | ||||
|                 "Default:", | ||||
|                 "Two" | ||||
|             ); | ||||
|  | ||||
|         stdOut.Should().NotContain("not printed"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_all_immediate_child_commands() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd1", Description = "Description of one command.")] | ||||
|             public class FirstCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2", Description = "Description of another command.")] | ||||
|             public class SecondCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2 child", Description = "Description of another command's child command.")] | ||||
|             public class SecondCommandChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd3 child", Description = "Description of an orphaned command.")] | ||||
|             public class ThirdCommandChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|  | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "COMMANDS", | ||||
|                 "cmd1", | ||||
|                 "Description of one command.", | ||||
|                 "cmd2", | ||||
|                 "Description of another command.", | ||||
|                 // `cmd2 child` will appear as an immediate command because it does not | ||||
|                 // have a more specific parent. | ||||
|                 "cmd3 child", | ||||
|                 "Description of an orphaned command." | ||||
|             ); | ||||
|  | ||||
|         // `cmd2 child` will still appear in the list of `cmd2` subcommands, | ||||
|         // but its description will not be visible. | ||||
|         stdOut.Should().NotContain("Description of another command's child command."); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_help_text_to_see_the_list_of_all_immediate_grand_child_commands() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command("cmd1")] | ||||
|             public class FirstCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd1 child1")] | ||||
|             public class FirstCommandFirstChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2")] | ||||
|             public class SecondCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2 child11")] | ||||
|             public class SecondCommandFirstChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|  | ||||
|             [Command("cmd2 child2")] | ||||
|             public class SecondCommandSecondChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--help"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ContainAllInOrder( | ||||
|                 "COMMANDS", | ||||
|                 "cmd1", | ||||
|                 "Subcommands:", | ||||
|                 "cmd1 child1", | ||||
|                 "cmd2", | ||||
|                 "Subcommands:", | ||||
|                 "cmd2 child1", | ||||
|                 "cmd2 child2" | ||||
|             ); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_request_the_version_text_by_running_the_application_with_the_version_option() | ||||
|     { | ||||
|         // Arrange | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand<NoOpCommand>() | ||||
|             .SetVersion("v6.9") | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--version"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("v6.9"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										762
									
								
								CliFx.Tests/OptionBindingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										762
									
								
								CliFx.Tests/OptionBindingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,762 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class OptionBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public bool Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--foo"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("True"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_by_short_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public bool Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["-f"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("True"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public string? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "one", "--bar", "two"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = one", "Bar = two"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_set_by_short_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption('b')] | ||||
|                 public string? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "one", "-b", "two"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = one", "Bar = two"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_and_get_the_value_from_the_corresponding_argument_stack_by_short_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption('b')] | ||||
|                 public string? Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-fb", "value"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = ", "Bar = value"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("Foo")] | ||||
|                 public IReadOnlyList<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "one", "two", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_arguments_by_short_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public IReadOnlyList<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "one", "two", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public IReadOnlyList<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "one", "--foo", "two", "--foo", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_short_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption('f')] | ||||
|                 public IReadOnlyList<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["-f", "one", "-f", "two", "-f", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_non_scalar_property_and_get_the_values_from_the_corresponding_argument_sets_by_name_or_short_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo", 'f')] | ||||
|                 public IReadOnlyList<string>? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     foreach (var i in Foo) | ||||
|                         console.WriteLine(i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "one", "-f", "two", "--foo", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("one", "two", "three"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_and_get_no_value_if_the_user_does_not_provide_the_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 [CommandOption("bar")] | ||||
|                 public string? Bar { get; init; } = "hello"; | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "one"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = one", "Bar = hello"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_through_multiple_inheritance() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             public static class SharedContext | ||||
|             { | ||||
|                 public static int Foo { get; set; } | ||||
|  | ||||
|                 public static bool Bar { get; set; } | ||||
|             } | ||||
|  | ||||
|             public interface IHasFoo : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public int Foo | ||||
|                 { | ||||
|                     get => SharedContext.Foo; | ||||
|                     init => SharedContext.Foo = value; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             public interface IHasBar : ICommand | ||||
|             { | ||||
|                 [CommandOption("bar")] | ||||
|                 public bool Bar | ||||
|                 { | ||||
|                     get => SharedContext.Bar; | ||||
|                     init => SharedContext.Bar = value; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             public interface IHasBaz : ICommand | ||||
|             { | ||||
|                 public string? Baz { get; init; } | ||||
|             } | ||||
|  | ||||
|             [Command] | ||||
|             public class Command : IHasFoo, IHasBar, IHasBaz | ||||
|             { | ||||
|                 [CommandOption("baz")] | ||||
|                 public string? Baz { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + SharedContext.Foo); | ||||
|                     console.WriteLine("Bar = " + SharedContext.Bar); | ||||
|                     console.WriteLine("Baz = " + Baz); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--foo", "42", "--bar", "--baz", "xyz"]); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = 42", "Bar = True", "Baz = xyz"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_an_option_to_a_property_and_get_the_correct_value_if_the_user_provides_an_argument_containing_a_negative_number() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(Foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "-13"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("-13"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Missing required option(s)"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_required_option_to_a_property_and_get_an_error_if_the_user_provides_an_empty_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--foo"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Missing required option(s)"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_an_option_to_a_non_scalar_property_and_get_an_error_if_the_user_does_not_provide_at_least_one_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public required IReadOnlyList<string> Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["--foo"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Missing required option(s)"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_options_and_get_an_error_if_the_user_provides_unrecognized_arguments() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "one", "--bar", "two"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Unrecognized option(s)"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_an_option_to_a_scalar_property_and_get_an_error_if_the_user_provides_too_many_arguments() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandOption("foo")] | ||||
|                 public string? Foo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["--foo", "one", "two", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("expects a single argument, but provided with multiple"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										263
									
								
								CliFx.Tests/ParameterBindingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										263
									
								
								CliFx.Tests/ParameterBindingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,263 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class ParameterBindingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_to_a_property_and_get_the_value_from_the_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required string Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["one", "two"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = one", "Bar = two"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_parameter_to_a_non_scalar_property_and_get_values_from_the_remaining_non_option_arguments() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required string Bar { get; init; } | ||||
|  | ||||
|                 [CommandParameter(2)] | ||||
|                 public required IReadOnlyList<string> Baz { get; init; } | ||||
|  | ||||
|                 [CommandOption("boo")] | ||||
|                 public string? Boo { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     foreach (var i in Baz) | ||||
|                         console.WriteLine("Baz = " + i); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["one", "two", "three", "four", "five", "--boo", "xxx"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut | ||||
|             .Should() | ||||
|             .ConsistOfLines("Foo = one", "Bar = two", "Baz = three", "Baz = four", "Baz = five"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_parameter_to_a_property_and_get_an_error_if_the_user_does_not_provide_the_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required string Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["one"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Missing required parameter(s)"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_a_parameter_to_a_non_scalar_property_and_get_an_error_if_the_user_does_not_provide_at_least_one_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required IReadOnlyList<string> Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["one"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Missing required parameter(s)"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_bind_a_non_required_parameter_to_a_property_and_get_no_value_if_the_user_does_not_provide_the_corresponding_argument() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1, IsRequired = false)] | ||||
|                 public string? Bar { get; init; } = "xyz"; | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("Foo = " + Foo); | ||||
|                     console.WriteLine("Bar = " + Bar); | ||||
|  | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["abc"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Should().ConsistOfLines("Foo = abc", "Bar = xyz"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_bind_parameters_and_get_an_error_if_the_user_provides_too_many_arguments() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 [CommandParameter(0)] | ||||
|                 public required string Foo { get; init; } | ||||
|  | ||||
|                 [CommandParameter(1)] | ||||
|                 public required string Bar { get; init; } | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["one", "two", "three"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Unexpected parameter(s)"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										179
									
								
								CliFx.Tests/RoutingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								CliFx.Tests/RoutingSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Tests.Utils; | ||||
| using FluentAssertions; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class RoutingSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_a_command_to_be_executed_by_default_when_the_user_does_not_specify_a_command_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("default"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("cmd"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("cmd child"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("default"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_a_command_to_be_executed_when_the_user_specifies_its_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("default"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("cmd"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("cmd child"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync(["cmd"], new Dictionary<string, string>()); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("cmd"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_a_nested_command_to_be_executed_when_the_user_specifies_its_name() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandTypes = DynamicCommandBuilder.CompileMany( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class DefaultCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("default"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             [Command("cmd")] | ||||
|             public class NamedCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("cmd"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             [Command("cmd child")] | ||||
|             public class NamedChildCommand : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("cmd child"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommands(commandTypes) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             ["cmd", "child"], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("cmd child"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								CliFx.Tests/SpecsBase.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								CliFx.Tests/SpecsBase.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| using System; | ||||
| using CliFx.Infrastructure; | ||||
| using CliFx.Tests.Utils.Extensions; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public abstract class SpecsBase(ITestOutputHelper testOutput) : IDisposable | ||||
| { | ||||
|     public ITestOutputHelper TestOutput { get; } = testOutput; | ||||
|  | ||||
|     public FakeInMemoryConsole FakeConsole { get; } = new(); | ||||
|  | ||||
|     public void Dispose() | ||||
|     { | ||||
|         FakeConsole.DumpToTestOutput(TestOutput); | ||||
|         FakeConsole.Dispose(); | ||||
|     } | ||||
| } | ||||
| @@ -1,18 +0,0 @@ | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Models; | ||||
|  | ||||
| namespace CliFx.Tests.TestObjects | ||||
| { | ||||
|     [DefaultCommand] | ||||
|     [Command("command")] | ||||
|     public class TestCommand : Command | ||||
|     { | ||||
|         [CommandOption("int", ShortName = 'i', IsRequired = true)] | ||||
|         public int IntOption { get; set; } = 24; | ||||
|  | ||||
|         [CommandOption("str", ShortName = 's')] | ||||
|         public string StringOption { get; set; } = "foo bar"; | ||||
|  | ||||
|         public override ExitCode Execute() => new ExitCode(IntOption, StringOption); | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| namespace CliFx.Tests.TestObjects | ||||
| { | ||||
|     public enum TestEnum | ||||
|     { | ||||
|         Value1, | ||||
|         Value2, | ||||
|         Value3 | ||||
|     } | ||||
| } | ||||
							
								
								
									
										223
									
								
								CliFx.Tests/TypeActivationSpecs.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								CliFx.Tests/TypeActivationSpecs.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Infrastructure; | ||||
| using CliFx.Tests.Utils; | ||||
| using FluentAssertions; | ||||
| using Microsoft.Extensions.DependencyInjection; | ||||
| using Xunit; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests; | ||||
|  | ||||
| public class TypeActivationSpecs(ITestOutputHelper testOutput) : SpecsBase(testOutput) | ||||
| { | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_the_application_to_use_the_default_type_activator_to_initialize_types_through_parameterless_constructors() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("foo"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .UseTypeActivator(new DefaultTypeActivator()) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("foo"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_configure_the_application_to_use_the_default_type_activator_and_get_an_error_if_the_requested_type_does_not_have_a_parameterless_constructor() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public Command(string foo) {} | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) => default; | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .UseTypeActivator(new DefaultTypeActivator()) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Failed to create an instance of type"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_the_application_to_use_a_custom_type_activator_to_initialize_types_using_a_delegate() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 private readonly string _foo; | ||||
|  | ||||
|                 public Command(string foo) => _foo = foo; | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(_foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .UseTypeActivator(type => Activator.CreateInstance(type, "Hello world")!) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("Hello world"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_configure_the_application_to_use_a_custom_type_activator_to_initialize_types_using_a_service_provider() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 private readonly string _foo; | ||||
|  | ||||
|                 public Command(string foo) => _foo = foo; | ||||
|  | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine(_foo); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .UseTypeActivator(commandTypes => | ||||
|             { | ||||
|                 var services = new ServiceCollection(); | ||||
|  | ||||
|                 foreach (var serviceType in commandTypes) | ||||
|                 { | ||||
|                     services.AddSingleton( | ||||
|                         serviceType, | ||||
|                         Activator.CreateInstance(serviceType, "Hello world")! | ||||
|                     ); | ||||
|                 } | ||||
|  | ||||
|                 return services.BuildServiceProvider(); | ||||
|             }) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().Be(0); | ||||
|  | ||||
|         var stdOut = FakeConsole.ReadOutputString(); | ||||
|         stdOut.Trim().Should().Be("Hello world"); | ||||
|     } | ||||
|  | ||||
|     [Fact] | ||||
|     public async Task I_can_try_to_configure_the_application_to_use_a_custom_type_activator_and_get_an_error_if_the_requested_type_cannot_be_initialized() | ||||
|     { | ||||
|         // Arrange | ||||
|         var commandType = DynamicCommandBuilder.Compile( | ||||
|             // lang=csharp | ||||
|             """ | ||||
|             [Command] | ||||
|             public class Command : ICommand | ||||
|             { | ||||
|                 public ValueTask ExecuteAsync(IConsole console) | ||||
|                 { | ||||
|                     console.WriteLine("foo"); | ||||
|                     return default; | ||||
|                 } | ||||
|             } | ||||
|             """ | ||||
|         ); | ||||
|  | ||||
|         var application = new CliApplicationBuilder() | ||||
|             .AddCommand(commandType) | ||||
|             .UseConsole(FakeConsole) | ||||
|             .UseTypeActivator((Type _) => null!) | ||||
|             .Build(); | ||||
|  | ||||
|         // Act | ||||
|         var exitCode = await application.RunAsync( | ||||
|             [], | ||||
|             new Dictionary<string, string>() | ||||
|         ); | ||||
|  | ||||
|         // Assert | ||||
|         exitCode.Should().NotBe(0); | ||||
|  | ||||
|         var stdErr = FakeConsole.ReadErrorString(); | ||||
|         stdErr.Should().Contain("Failed to create an instance of type"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										135
									
								
								CliFx.Tests/Utils/DynamicCommandBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								CliFx.Tests/Utils/DynamicCommandBuilder.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using System.Reflection; | ||||
| using Basic.Reference.Assemblies; | ||||
| using Microsoft.CodeAnalysis; | ||||
| using Microsoft.CodeAnalysis.CSharp; | ||||
| using Microsoft.CodeAnalysis.Text; | ||||
|  | ||||
| namespace CliFx.Tests.Utils; | ||||
|  | ||||
| // This class uses Roslyn to compile commands dynamically. | ||||
| // It allows us to collocate commands with tests more easily, which helps a lot when reasoning about them. | ||||
| // Unfortunately, this comes at a cost of static typing, but this is still a worthwhile trade off. | ||||
| // Maybe one day C# will allow declaring classes inside methods and doing this will no longer be necessary. | ||||
| // Language proposal: https://github.com/dotnet/csharplang/discussions/130 | ||||
| internal static class DynamicCommandBuilder | ||||
| { | ||||
|     public static IReadOnlyList<Type> CompileMany(string sourceCode) | ||||
|     { | ||||
|         // Get default system namespaces | ||||
|         var defaultSystemNamespaces = new[] | ||||
|         { | ||||
|             "System", | ||||
|             "System.Collections", | ||||
|             "System.Collections.Generic", | ||||
|             "System.Linq", | ||||
|             "System.Threading.Tasks", | ||||
|             "System.Globalization", | ||||
|         }; | ||||
|  | ||||
|         // Get default CliFx namespaces | ||||
|         var defaultCliFxNamespaces = typeof(ICommand) | ||||
|             .Assembly.GetTypes() | ||||
|             .Where(t => t.IsPublic) | ||||
|             .Select(t => t.Namespace) | ||||
|             .Distinct() | ||||
|             .ToArray(); | ||||
|  | ||||
|         // Append default imports to the source code | ||||
|         var sourceCodeWithUsings = | ||||
|             string.Join(Environment.NewLine, defaultSystemNamespaces.Select(n => $"using {n};")) | ||||
|             + string.Join(Environment.NewLine, defaultCliFxNamespaces.Select(n => $"using {n};")) | ||||
|             + Environment.NewLine | ||||
|             + sourceCode; | ||||
|  | ||||
|         // Parse the source code | ||||
|         var ast = SyntaxFactory.ParseSyntaxTree( | ||||
|             SourceText.From(sourceCodeWithUsings), | ||||
|             CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Preview) | ||||
|         ); | ||||
|  | ||||
|         // Compile the code to IL | ||||
|         var compilation = CSharpCompilation.Create( | ||||
|             "CliFxTests_DynamicAssembly_" + Guid.NewGuid(), | ||||
|             [ast], | ||||
|             Net80 | ||||
|                 .References.All.Append( | ||||
|                     MetadataReference.CreateFromFile(typeof(ICommand).Assembly.Location) | ||||
|                 ) | ||||
|                 .Append( | ||||
|                     MetadataReference.CreateFromFile( | ||||
|                         typeof(DynamicCommandBuilder).Assembly.Location | ||||
|                     ) | ||||
|                 ), | ||||
|             // DLL to avoid having to define the Main() method | ||||
|             new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) | ||||
|         ); | ||||
|  | ||||
|         var compilationErrors = compilation | ||||
|             .GetDiagnostics() | ||||
|             .Where(d => d.Severity >= DiagnosticSeverity.Error) | ||||
|             .ToArray(); | ||||
|  | ||||
|         if (compilationErrors.Any()) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 $""" | ||||
|                 Failed to compile code. | ||||
|                 {string.Join(Environment.NewLine, compilationErrors.Select(e => e.ToString()))} | ||||
|                 """ | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Emit the code to an in-memory buffer | ||||
|         using var buffer = new MemoryStream(); | ||||
|         var emit = compilation.Emit(buffer); | ||||
|  | ||||
|         var emitErrors = emit | ||||
|             .Diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Error) | ||||
|             .ToArray(); | ||||
|  | ||||
|         if (emitErrors.Any()) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 $""" | ||||
|                 Failed to emit code. | ||||
|                 {string.Join(Environment.NewLine, emitErrors.Select(e => e.ToString()))} | ||||
|                 """ | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         // Load the generated assembly | ||||
|         var generatedAssembly = Assembly.Load(buffer.ToArray()); | ||||
|  | ||||
|         // Return all defined commands | ||||
|         var commandTypes = generatedAssembly | ||||
|             .GetTypes() | ||||
|             .Where(t => t.IsAssignableTo(typeof(ICommand)) && !t.IsAbstract) | ||||
|             .ToArray(); | ||||
|  | ||||
|         if (commandTypes.Length <= 0) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 "There are no command definitions in the provided source code." | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return commandTypes; | ||||
|     } | ||||
|  | ||||
|     public static Type Compile(string sourceCode) | ||||
|     { | ||||
|         var commandTypes = CompileMany(sourceCode); | ||||
|         if (commandTypes.Count > 1) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 "There are more than one command definitions in the provided source code." | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return commandTypes.Single(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								CliFx.Tests/Utils/Extensions/AssertionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								CliFx.Tests/Utils/Extensions/AssertionExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using FluentAssertions; | ||||
| using FluentAssertions.Primitives; | ||||
|  | ||||
| namespace CliFx.Tests.Utils.Extensions; | ||||
|  | ||||
| internal static class AssertionExtensions | ||||
| { | ||||
|     public static void ConsistOfLines( | ||||
|         this StringAssertions assertions, | ||||
|         params IEnumerable<string> lines | ||||
|     ) => | ||||
|         assertions | ||||
|             .Subject.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries) | ||||
|             .Should() | ||||
|             .Equal(lines); | ||||
|  | ||||
|     public static AndConstraint<StringAssertions> ContainAllInOrder( | ||||
|         this StringAssertions assertions, | ||||
|         IEnumerable<string> values | ||||
|     ) | ||||
|     { | ||||
|         var lastIndex = 0; | ||||
|  | ||||
|         foreach (var value in values) | ||||
|         { | ||||
|             var index = assertions.Subject.IndexOf(value, lastIndex, StringComparison.Ordinal); | ||||
|  | ||||
|             if (index < 0) | ||||
|             { | ||||
|                 assertions.CurrentAssertionChain.FailWith( | ||||
|                     $"Expected string '{assertions.Subject}' to contain '{value}' after position {lastIndex}." | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
|             lastIndex = index; | ||||
|         } | ||||
|  | ||||
|         return new AndConstraint<StringAssertions>(assertions); | ||||
|     } | ||||
|  | ||||
|     public static AndConstraint<StringAssertions> ContainAllInOrder( | ||||
|         this StringAssertions assertions, | ||||
|         params string[] values | ||||
|     ) => assertions.ContainAllInOrder((IEnumerable<string>)values); | ||||
| } | ||||
							
								
								
									
										19
									
								
								CliFx.Tests/Utils/Extensions/ConsoleExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								CliFx.Tests/Utils/Extensions/ConsoleExtensions.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| using CliFx.Infrastructure; | ||||
| using Xunit.Abstractions; | ||||
|  | ||||
| namespace CliFx.Tests.Utils.Extensions; | ||||
|  | ||||
| internal static class ConsoleExtensions | ||||
| { | ||||
|     public static void DumpToTestOutput( | ||||
|         this FakeInMemoryConsole console, | ||||
|         ITestOutputHelper testOutput | ||||
|     ) | ||||
|     { | ||||
|         testOutput.WriteLine("[*] Captured standard output:"); | ||||
|         testOutput.WriteLine(console.ReadOutputString()); | ||||
|  | ||||
|         testOutput.WriteLine("[*] Captured standard error:"); | ||||
|         testOutput.WriteLine(console.ReadErrorString()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										11
									
								
								CliFx.Tests/Utils/NoOpCommand.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								CliFx.Tests/Utils/NoOpCommand.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Attributes; | ||||
| using CliFx.Infrastructure; | ||||
|  | ||||
| namespace CliFx.Tests.Utils; | ||||
|  | ||||
| [Command] | ||||
| internal class NoOpCommand : ICommand | ||||
| { | ||||
|     public ValueTask ExecuteAsync(IConsole console) => default; | ||||
| } | ||||
							
								
								
									
										5
									
								
								CliFx.Tests/xunit.runner.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								CliFx.Tests/xunit.runner.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
|   "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", | ||||
|   "methodDisplayOptions": "all", | ||||
|   "methodDisplay": "method" | ||||
| } | ||||
							
								
								
									
										72
									
								
								CliFx.sln
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								CliFx.sln
									
									
									
									
									
								
							| @@ -7,15 +7,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx", "CliFx\CliFx.csproj | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{4904B3EB-3286-4F1B-8B74-6FF051C8E787}" | ||||
| EndProject | ||||
| Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AAE8166-BB8E-49DA-844C-3A0EE6BD40A0}" | ||||
| 	ProjectSection(SolutionItems) = preProject | ||||
| 		Changelog.md = Changelog.md | ||||
| 		Directory.Build.props = Directory.Build.props | ||||
| 		License.txt = License.txt | ||||
| 		Readme.md = Readme.md | ||||
| 	EndProjectSection | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Benchmarks", "CliFx.Benchmarks\CliFx.Benchmarks.csproj", "{8ACD6DC2-D768-4850-9223-5B7C83A78513}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Demo", "CliFx.Demo\CliFx.Demo.csproj", "{AAB6844C-BF71-448F-A11B-89AEE459AB15}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{F717347D-8656-44DA-A4A2-BE515E8C4655}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.SourceGeneration", "CliFx.SourceGeneration\CliFx.SourceGeneration.csproj", "{F8460D69-F8CF-405C-A6ED-BED02A21DB42}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
| @@ -50,18 +56,54 @@ Global | ||||
| 		{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{AAB6844C-BF71-448F-A11B-89AEE459AB15}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{F717347D-8656-44DA-A4A2-BE515E8C4655}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{F8460D69-F8CF-405C-A6ED-BED02A21DB42}.Release|x86.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
|   | ||||
							
								
								
									
										28
									
								
								CliFx/ApplicationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								CliFx/ApplicationConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| using CliFx.Schema; | ||||
|  | ||||
| namespace CliFx; | ||||
|  | ||||
| /// <summary> | ||||
| /// Configuration of an application. | ||||
| /// </summary> | ||||
| public class ApplicationConfiguration( | ||||
|     ApplicationSchema schema, | ||||
|     bool isDebugModeAllowed, | ||||
|     bool isPreviewModeAllowed | ||||
| ) | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Application schema. | ||||
|     /// </summary> | ||||
|     public ApplicationSchema Schema { get; } = schema; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Whether debug mode is allowed in the application. | ||||
|     /// </summary> | ||||
|     public bool IsDebugModeAllowed { get; } = isDebugModeAllowed; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Whether preview mode is allowed in the application. | ||||
|     /// </summary> | ||||
|     public bool IsPreviewModeAllowed { get; } = isPreviewModeAllowed; | ||||
| } | ||||
							
								
								
									
										32
									
								
								CliFx/ApplicationMetadata.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								CliFx/ApplicationMetadata.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| namespace CliFx; | ||||
|  | ||||
| /// <summary> | ||||
| /// Metadata associated with an application. | ||||
| /// </summary> | ||||
| public class ApplicationMetadata( | ||||
|     string title, | ||||
|     string executableName, | ||||
|     string version, | ||||
|     string? description | ||||
| ) | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Application title. | ||||
|     /// </summary> | ||||
|     public string Title { get; } = title; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Application executable name. | ||||
|     /// </summary> | ||||
|     public string ExecutableName { get; } = executableName; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Application version. | ||||
|     /// </summary> | ||||
|     public string Version { get; } = version; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Application description. | ||||
|     /// </summary> | ||||
|     public string? Description { get; } = description; | ||||
| } | ||||
| @@ -1,15 +1,29 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Attributes | ||||
| { | ||||
|     [AttributeUsage(AttributeTargets.Class, Inherited = false)] | ||||
|     public class CommandAttribute : Attribute | ||||
|     { | ||||
|         public string Name { get; } | ||||
| namespace CliFx.Attributes; | ||||
|  | ||||
|         public CommandAttribute(string name) | ||||
|         { | ||||
|             Name = name; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| /// <summary> | ||||
| /// Annotates a type that defines a command. | ||||
| /// If the command is named, then the user must provide its name through the | ||||
| /// command-line arguments in order to execute it. | ||||
| /// If the command is not named, then it is treated as the application's | ||||
| /// default command and is executed whenever the user does not provide a command name. | ||||
| /// </summary> | ||||
| /// <remarks> | ||||
| /// Only one default command is allowed per application. | ||||
| /// All commands registered in an application must have unique names (comparison IS NOT case-sensitive). | ||||
| /// </remarks> | ||||
| [AttributeUsage(AttributeTargets.Class, Inherited = false)] | ||||
| public class CommandAttribute(string? name = null) : Attribute | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Command name. | ||||
|     /// </summary> | ||||
|     public string? Name { get; } = name; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Command description. | ||||
|     /// This is shown to the user in the help text. | ||||
|     /// </summary> | ||||
|     public string? Description { get; set; } | ||||
| } | ||||
|   | ||||
							
								
								
									
										19
									
								
								CliFx/Attributes/CommandHelpOptionAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								CliFx/Attributes/CommandHelpOptionAttribute.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| namespace CliFx.Attributes; | ||||
|  | ||||
| /// <summary> | ||||
| /// Binds a property to the help option of a command. | ||||
| /// </summary> | ||||
| /// <remarks> | ||||
| /// This attribute is applied automatically by the framework and should not need to be used explicitly. | ||||
| /// </remarks> | ||||
| public class CommandHelpOptionAttribute : CommandOptionAttribute | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Initializes an instance of <see cref="CommandHelpOptionAttribute" />. | ||||
|     /// </summary> | ||||
|     public CommandHelpOptionAttribute() | ||||
|         : base("help", 'h') | ||||
|     { | ||||
|         Description = "Show help for this command."; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										34
									
								
								CliFx/Attributes/CommandInputAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								CliFx/Attributes/CommandInputAttribute.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| using System; | ||||
| using CliFx.Extensibility; | ||||
|  | ||||
| namespace CliFx.Attributes; | ||||
|  | ||||
| /// <summary> | ||||
| /// Binds a property to a command-line input. | ||||
| /// </summary> | ||||
| [AttributeUsage(AttributeTargets.Property)] | ||||
| public abstract class CommandInputAttribute : Attribute | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Input description, as shown in the help text. | ||||
|     /// </summary> | ||||
|     public string? Description { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Custom converter used for mapping the raw command-line argument into | ||||
|     /// the type and shape expected by the underlying property. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// Converter must derive from <see cref="BindingConverter{T}" />. | ||||
|     /// </remarks> | ||||
|     public Type? Converter { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Custom validators used for verifying the value of the underlying | ||||
|     /// property, after it has been set. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// Validators must derive from <see cref="BindingValidator{T}" />. | ||||
|     /// </remarks> | ||||
|     public Type[] Validators { get; set; } = []; | ||||
| } | ||||
| @@ -1,21 +1,67 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Attributes | ||||
| namespace CliFx.Attributes; | ||||
|  | ||||
| /// <summary> | ||||
| /// Binds a property to a command option — a command-line input that is identified by a name and/or a short name. | ||||
| /// </summary> | ||||
| /// <remarks> | ||||
| /// All options in a command must have unique names (comparison IS NOT case-sensitive) | ||||
| /// and short names (comparison IS case-sensitive). | ||||
| /// </remarks> | ||||
| [AttributeUsage(AttributeTargets.Property)] | ||||
| public class CommandOptionAttribute : CommandInputAttribute | ||||
| { | ||||
|     [AttributeUsage(AttributeTargets.Property)] | ||||
|     public class CommandOptionAttribute : Attribute | ||||
|     /// <summary> | ||||
|     /// Initializes an instance of <see cref="CommandOptionAttribute" />. | ||||
|     /// </summary> | ||||
|     private CommandOptionAttribute(string? name, char? shortName) | ||||
|     { | ||||
|         public string Name { get; } | ||||
|  | ||||
|         public char ShortName { get; set; } | ||||
|  | ||||
|         public bool IsRequired { get; set; } | ||||
|  | ||||
|         public string Description { get; set; } | ||||
|  | ||||
|         public CommandOptionAttribute(string name) | ||||
|         { | ||||
|             Name = name; | ||||
|         } | ||||
|         Name = name; | ||||
|         ShortName = shortName; | ||||
|     } | ||||
| } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Initializes an instance of <see cref="CommandOptionAttribute" />. | ||||
|     /// </summary> | ||||
|     public CommandOptionAttribute(string name, char shortName) | ||||
|         : this(name, (char?)shortName) { } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Initializes an instance of <see cref="CommandOptionAttribute" />. | ||||
|     /// </summary> | ||||
|     public CommandOptionAttribute(string name) | ||||
|         : this(name, null) { } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Initializes an instance of <see cref="CommandOptionAttribute" />. | ||||
|     /// </summary> | ||||
|     public CommandOptionAttribute(char shortName) | ||||
|         : this(null, (char?)shortName) { } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Option name. | ||||
|     /// </summary> | ||||
|     public string? Name { get; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Option short name. | ||||
|     /// </summary> | ||||
|     public char? ShortName { get; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Whether this option is required (default: <c>false</c>). | ||||
|     /// If an option is required, the user will get an error when they don't set it. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// You can use the <c>required</c> keyword on the property (introduced in C# 11) to implicitly | ||||
|     /// set <see cref="IsRequired" /> to <c>true</c>. | ||||
|     /// </remarks> | ||||
|     public bool IsRequired { get; set; } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Environment variable whose value will be used as a fallback if the option | ||||
|     /// has not been explicitly set through command-line arguments. | ||||
|     /// </summary> | ||||
|     public string? EnvironmentVariable { get; set; } | ||||
| } | ||||
|   | ||||
							
								
								
									
										41
									
								
								CliFx/Attributes/CommandParameterAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								CliFx/Attributes/CommandParameterAttribute.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace CliFx.Attributes; | ||||
|  | ||||
| /// <summary> | ||||
| /// Binds a property to a command parameter — a command-line input that is identified by its relative position (order). | ||||
| /// Higher order means that the parameter appears later, lower order means that it appears earlier. | ||||
| /// </summary> | ||||
| /// <remarks> | ||||
| /// All parameters in a command must have unique order values. | ||||
| /// If a parameter is bound to a property whose type is a sequence (i.e. implements <see cref="IEnumerable{T}"/>; except <see cref="string" />), | ||||
| /// then it must have the highest order in the command. | ||||
| /// Only one sequential parameter is allowed per command. | ||||
| /// </remarks> | ||||
| [AttributeUsage(AttributeTargets.Property)] | ||||
| public class CommandParameterAttribute(int order) : CommandInputAttribute | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Parameter order. | ||||
|     /// </summary> | ||||
|     public int Order { get; } = order; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Whether this parameter is required (default: <c>true</c>). | ||||
|     /// If a parameter is required, the user will get an error when they don't set it. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// Parameter marked as non-required must have the highest order in the command. | ||||
|     /// Only one non-required parameter is allowed per command. | ||||
|     /// </remarks> | ||||
|     public bool IsRequired { get; set; } = true; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Parameter name, as shown in the help text. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// If this isn't specified, parameter name is inferred from the property name. | ||||
|     /// </remarks> | ||||
|     public string? Name { get; set; } | ||||
| } | ||||
							
								
								
									
										19
									
								
								CliFx/Attributes/CommandVersionOptionAttribute.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								CliFx/Attributes/CommandVersionOptionAttribute.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| namespace CliFx.Attributes; | ||||
|  | ||||
| /// <summary> | ||||
| /// Binds a property to the version option of a command. | ||||
| /// </summary> | ||||
| /// <remarks> | ||||
| /// This attribute is applied automatically by the framework and should not need to be used explicitly. | ||||
| /// </remarks> | ||||
| public class CommandVersionOptionAttribute : CommandOptionAttribute | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Initializes an instance of <see cref="CommandVersionOptionAttribute" />. | ||||
|     /// </summary> | ||||
|     public CommandVersionOptionAttribute() | ||||
|         : base("version") | ||||
|     { | ||||
|         Description = "Show application version."; | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Attributes | ||||
| { | ||||
|     [AttributeUsage(AttributeTargets.Class, Inherited = false)] | ||||
|     public class DefaultCommandAttribute : Attribute | ||||
|     { | ||||
|     } | ||||
| } | ||||
| @@ -1,45 +1,226 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Reflection; | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics; | ||||
| using System.Linq; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Services; | ||||
| using CliFx.Exceptions; | ||||
| using CliFx.Formatting; | ||||
| using CliFx.Infrastructure; | ||||
| using CliFx.Parsing; | ||||
| using CliFx.Schema; | ||||
| using CliFx.Utils; | ||||
| using CliFx.Utils.Extensions; | ||||
|  | ||||
| namespace CliFx | ||||
| namespace CliFx; | ||||
|  | ||||
| /// <summary> | ||||
| /// Command-line application facade. | ||||
| /// </summary> | ||||
| public class CliApplication( | ||||
|     ApplicationMetadata metadata, | ||||
|     ApplicationConfiguration configuration, | ||||
|     IConsole console, | ||||
|     ITypeActivator typeActivator | ||||
| ) | ||||
| { | ||||
|     public partial class CliApplication : ICliApplication | ||||
|     /// <summary> | ||||
|     /// Application metadata. | ||||
|     /// </summary> | ||||
|     public ApplicationMetadata Metadata { get; } = metadata; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Application configuration. | ||||
|     /// </summary> | ||||
|     public ApplicationConfiguration Configuration { get; } = configuration; | ||||
|  | ||||
|     private bool IsDebugModeEnabled(CommandArguments commandArguments) => | ||||
|         Configuration.IsDebugModeAllowed && commandArguments.IsDebugDirectiveSpecified; | ||||
|  | ||||
|     private bool IsPreviewModeEnabled(CommandArguments commandArguments) => | ||||
|         Configuration.IsPreviewModeAllowed && commandArguments.IsPreviewDirectiveSpecified; | ||||
|  | ||||
|     private async ValueTask PromptDebuggerAsync() | ||||
|     { | ||||
|         private readonly ICommandResolver _commandResolver; | ||||
|  | ||||
|         public CliApplication(ICommandResolver commandResolver) | ||||
|         using (console.WithForegroundColor(ConsoleColor.Green)) | ||||
|         { | ||||
|             _commandResolver = commandResolver; | ||||
|             console.Output.WriteLine( | ||||
|                 $"Attach the debugger to process with ID {ProcessEx.GetCurrentProcessId()} to continue." | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         public CliApplication() | ||||
|             : this(GetDefaultCommandResolver(Assembly.GetCallingAssembly())) | ||||
|         // Try to also launch the debugger ourselves (only works with Visual Studio) | ||||
|         Debugger.Launch(); | ||||
|  | ||||
|         while (!Debugger.IsAttached) | ||||
|             await Task.Delay(100); | ||||
|     } | ||||
|  | ||||
|     private async ValueTask<int> RunAsync( | ||||
|         ApplicationSchema applicationSchema, | ||||
|         CommandArguments commandArguments, | ||||
|         IReadOnlyDictionary<string, string?> environmentVariables | ||||
|     ) | ||||
|     { | ||||
|         // Console colors may have already been overridden by the parent process, | ||||
|         // so we need to reset it to make sure that everything we write looks properly. | ||||
|         console.ResetColor(); | ||||
|  | ||||
|         // Handle the debug directive | ||||
|         if (IsDebugModeEnabled(commandArguments)) | ||||
|         { | ||||
|             await PromptDebuggerAsync(); | ||||
|         } | ||||
|  | ||||
|         public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments) | ||||
|         // Handle the preview directive | ||||
|         if (IsPreviewModeEnabled(commandArguments)) | ||||
|         { | ||||
|             // Resolve and execute command | ||||
|             var command = _commandResolver.ResolveCommand(commandLineArguments); | ||||
|             var exitCode = await command.ExecuteAsync(); | ||||
|             console.WriteCommandInput(commandArguments); | ||||
|             return 0; | ||||
|         } | ||||
|  | ||||
|             // TODO: print message if error? | ||||
|         // Try to get the command schema that matches the input | ||||
|         var command = | ||||
|             ( | ||||
|                 !string.IsNullOrWhiteSpace(commandArguments.CommandName) | ||||
|                     // If the command name is specified, try to find the command by name. | ||||
|                     // This should always succeed, because the input parsing relies on | ||||
|                     // the list of available command names. | ||||
|                     ? applicationSchema.TryFindCommand(commandArguments.CommandName) | ||||
|                     // Otherwise, try to find the default command | ||||
|                     : applicationSchema.TryFindDefaultCommand() | ||||
|             ) | ||||
|             ?? | ||||
|             // If a valid command was not found, use the fallback default command. | ||||
|             // This is only used as a stub to show the help text. | ||||
|             FallbackDefaultCommand.Schema; | ||||
|  | ||||
|             return exitCode.Value; | ||||
|         // Initialize an instance of the command type | ||||
|         var commandInstance = | ||||
|             command == FallbackDefaultCommand.Schema | ||||
|                 ? new FallbackDefaultCommand() // bypass the activator | ||||
|                 : typeActivator.CreateInstance<ICommand>(command.Type); | ||||
|  | ||||
|         // Assemble the help context | ||||
|         var helpContext = new HelpContext( | ||||
|             Metadata, | ||||
|             applicationSchema, | ||||
|             command, | ||||
|             command.GetValues(commandInstance) | ||||
|         ); | ||||
|  | ||||
|         // Starting from this point, we may produce exceptions that are meant for the | ||||
|         // end-user of the application (i.e. invalid input, command exception, etc). | ||||
|         // Catch these exceptions here, print them to the console, and don't let them | ||||
|         // propagate further. | ||||
|         try | ||||
|         { | ||||
|             // Activate the command instance with the provided user input | ||||
|             command.Activate(commandInstance, commandArguments, environmentVariables); | ||||
|  | ||||
|             // Handle the version option | ||||
|             if (commandInstance is ICommandWithVersionOption { IsVersionRequested: true }) | ||||
|             { | ||||
|                 console.WriteLine(Metadata.Version); | ||||
|                 return 0; | ||||
|             } | ||||
|  | ||||
|             // Handle the help option | ||||
|             if ( | ||||
|                 commandInstance | ||||
|                 is ICommandWithHelpOption { IsHelpRequested: true } | ||||
|                     // Fallback default command always shows help, even if the option is not specified | ||||
|                     or FallbackDefaultCommand | ||||
|             ) | ||||
|             { | ||||
|                 console.WriteHelpText(helpContext); | ||||
|                 return 0; | ||||
|             } | ||||
|  | ||||
|             // Execute the command | ||||
|             await commandInstance.ExecuteAsync(console); | ||||
|             return 0; | ||||
|         } | ||||
|         catch (CliFxException ex) | ||||
|         { | ||||
|             console.WriteException(ex); | ||||
|  | ||||
|             if (ex.ShowHelp) | ||||
|             { | ||||
|                 console.WriteLine(); | ||||
|                 console.WriteHelpText(helpContext); | ||||
|             } | ||||
|  | ||||
|             return ex.ExitCode; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public partial class CliApplication | ||||
|     /// <summary> | ||||
|     /// Runs the application with the specified command-line arguments and environment variables. | ||||
|     /// Returns the exit code which indicates whether the application completed successfully. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// When running WITHOUT the debugger attached (i.e. in production), this method swallows | ||||
|     /// all exceptions and reports them to the console. | ||||
|     /// </remarks> | ||||
|     public async ValueTask<int> RunAsync( | ||||
|         IReadOnlyList<string> commandLineArguments, | ||||
|         IReadOnlyDictionary<string, string?> environmentVariables | ||||
|     ) | ||||
|     { | ||||
|         private static ICommandResolver GetDefaultCommandResolver(Assembly assembly) | ||||
|         try | ||||
|         { | ||||
|             var typeProvider = TypeProvider.FromAssembly(assembly); | ||||
|             var commandOptionParser = new CommandOptionParser(); | ||||
|             var commandOptionConverter = new CommandOptionConverter(); | ||||
|  | ||||
|             return new CommandResolver(typeProvider, commandOptionParser, commandOptionConverter); | ||||
|             return await RunAsync( | ||||
|                 Configuration.Schema, | ||||
|                 CommandArguments.Parse( | ||||
|                     commandLineArguments, | ||||
|                     Configuration.Schema.GetCommandNames() | ||||
|                 ), | ||||
|                 environmentVariables | ||||
|             ); | ||||
|         } | ||||
|         // To prevent the app from showing the annoying troubleshooting dialog on Windows, | ||||
|         // we handle all exceptions ourselves and print them to the console. | ||||
|         // We only want to do that if the app is running in production, which we infer | ||||
|         // based on whether the debugger is attached to the process. | ||||
|         // When not running in production, we want the IDE to show exceptions to the | ||||
|         // developer, so we don't swallow them in that case. | ||||
|         catch (Exception ex) when (!Debugger.IsAttached) | ||||
|         { | ||||
|             console.WriteException(ex); | ||||
|             return 1; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Runs the application with the specified command-line arguments. | ||||
|     /// Returns the exit code which indicates whether the application completed successfully. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// When running WITHOUT the debugger attached (i.e. in production), this method swallows | ||||
|     /// all exceptions and reports them to the console. | ||||
|     /// </remarks> | ||||
|     public async ValueTask<int> RunAsync(IReadOnlyList<string> commandLineArguments) => | ||||
|         await RunAsync( | ||||
|             commandLineArguments, | ||||
|             Environment | ||||
|                 .GetEnvironmentVariables() | ||||
|                 .ToDictionary<string, string?>(StringComparer.Ordinal) | ||||
|         ); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Runs the application. | ||||
|     /// Command-line arguments and environment variables are resolved automatically. | ||||
|     /// Returns the exit code which indicates whether the application completed successfully. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// When running WITHOUT the debugger attached (i.e. in production), this method swallows | ||||
|     /// all exceptions and reports them to the console. | ||||
|     /// </remarks> | ||||
|     public async ValueTask<int> RunAsync() => | ||||
|         await RunAsync( | ||||
|             Environment | ||||
|                 .GetCommandLineArgs() | ||||
|                 .Skip(1) // first element is the file path | ||||
|                 .ToArray() | ||||
|         ); | ||||
| } | ||||
|   | ||||
							
								
								
									
										252
									
								
								CliFx/CliApplicationBuilder.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								CliFx/CliApplicationBuilder.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,252 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.IO; | ||||
| using System.Linq; | ||||
| using CliFx.Infrastructure; | ||||
| using CliFx.Schema; | ||||
| using CliFx.Utils; | ||||
| using CliFx.Utils.Extensions; | ||||
|  | ||||
| namespace CliFx; | ||||
|  | ||||
| /// <summary> | ||||
| /// Builder for <see cref="CliApplication" />. | ||||
| /// </summary> | ||||
| public partial class CliApplicationBuilder | ||||
| { | ||||
|     private readonly HashSet<CommandSchema> _commands = []; | ||||
|  | ||||
|     private bool _isDebugModeAllowed = true; | ||||
|     private bool _isPreviewModeAllowed = true; | ||||
|     private string? _title; | ||||
|     private string? _executableName; | ||||
|     private string? _version; | ||||
|     private string? _description; | ||||
|     private IConsole? _console; | ||||
|     private ITypeActivator? _typeActivator; | ||||
|  | ||||
|     // TODO: | ||||
|     // The source generator should generate an internal extension method for the builder called | ||||
|     // AddCommandsFromThisAssembly() that would add all command types from the assembly where the builder is used. | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Adds a command to the application. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder AddCommand(CommandSchema command) | ||||
|     { | ||||
|         _commands.Add(command); | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Adds multiple commands to the application. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder AddCommands(IReadOnlyList<CommandSchema> commands) | ||||
|     { | ||||
|         foreach (var command in commands) | ||||
|             AddCommand(command); | ||||
|  | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Specifies whether debug mode (enabled with the [debug] directive) is allowed in the application. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder AllowDebugMode(bool isAllowed = true) | ||||
|     { | ||||
|         _isDebugModeAllowed = isAllowed; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Specifies whether preview mode (enabled with the [preview] directive) is allowed in the application. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder AllowPreviewMode(bool isAllowed = true) | ||||
|     { | ||||
|         _isPreviewModeAllowed = isAllowed; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Sets the application title, which is shown in the help text. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// By default, application title is inferred from the assembly name. | ||||
|     /// </remarks> | ||||
|     public CliApplicationBuilder SetTitle(string title) | ||||
|     { | ||||
|         _title = title; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Sets the application executable name, which is shown in the help text. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// By default, application executable name is inferred from the assembly file name. | ||||
|     /// </remarks> | ||||
|     public CliApplicationBuilder SetExecutableName(string executableName) | ||||
|     { | ||||
|         _executableName = executableName; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Sets the application version, which is shown in the help text or when the user specifies the version option. | ||||
|     /// </summary> | ||||
|     /// <remarks> | ||||
|     /// By default, application version is inferred from the assembly version. | ||||
|     /// </remarks> | ||||
|     public CliApplicationBuilder SetVersion(string version) | ||||
|     { | ||||
|         _version = version; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Sets the application description, which is shown in the help text. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder SetDescription(string? description) | ||||
|     { | ||||
|         _description = description; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Configures the application to use the specified implementation of <see cref="IConsole" />. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder UseConsole(IConsole console) | ||||
|     { | ||||
|         _console = console; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Configures the application to use the specified implementation of <see cref="ITypeActivator" />. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder UseTypeActivator(ITypeActivator typeActivator) | ||||
|     { | ||||
|         _typeActivator = typeActivator; | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Configures the application to use the specified delegate for activating types. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder UseTypeActivator(Func<Type, object> createInstance) => | ||||
|         UseTypeActivator(new DelegateTypeActivator(createInstance)); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Configures the application to use the specified service provider for activating types. | ||||
|     /// </summary> | ||||
|     public CliApplicationBuilder UseTypeActivator(IServiceProvider serviceProvider) => | ||||
|         // Null returns are handled by DelegateTypeActivator | ||||
|         UseTypeActivator(serviceProvider.GetService!); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Creates a configured instance of <see cref="CliApplication" />. | ||||
|     /// </summary> | ||||
|     public CliApplication Build() | ||||
|     { | ||||
|         var metadata = new ApplicationMetadata( | ||||
|             _title ?? GetDefaultTitle(), | ||||
|             _executableName ?? GetDefaultExecutableName(), | ||||
|             _version ?? GetDefaultVersionText(), | ||||
|             _description | ||||
|         ); | ||||
|  | ||||
|         var configuration = new ApplicationConfiguration( | ||||
|             new ApplicationSchema(_commands.ToArray()), | ||||
|             _isDebugModeAllowed, | ||||
|             _isPreviewModeAllowed | ||||
|         ); | ||||
|  | ||||
|         return new CliApplication( | ||||
|             metadata, | ||||
|             configuration, | ||||
|             _console ?? new SystemConsole(), | ||||
|             _typeActivator ?? new DefaultTypeActivator() | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public partial class CliApplicationBuilder | ||||
| { | ||||
|     private static string GetDefaultTitle() | ||||
|     { | ||||
|         var entryAssemblyName = EnvironmentEx.EntryAssembly?.GetName().Name; | ||||
|         if (string.IsNullOrWhiteSpace(entryAssemblyName)) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 "Failed to infer the default application title. " | ||||
|                     + $"Please specify it explicitly using `{nameof(SetTitle)}()`." | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return entryAssemblyName; | ||||
|     } | ||||
|  | ||||
|     [UnconditionalSuppressMessage( | ||||
|         "SingleFile", | ||||
|         "IL3000:Avoid accessing Assembly file path when publishing as a single file", | ||||
|         Justification = "The file path is checked to ensure the assembly location is available." | ||||
|     )] | ||||
|     private static string GetDefaultExecutableName() | ||||
|     { | ||||
|         var processFilePath = EnvironmentEx.ProcessPath; | ||||
|  | ||||
|         // Process file path should generally always be available | ||||
|         if (string.IsNullOrWhiteSpace(processFilePath)) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 "Failed to infer the default application executable name. " | ||||
|                     + $"Please specify it explicitly using `{nameof(SetExecutableName)}()`." | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         var entryAssemblyFilePath = EnvironmentEx.EntryAssembly?.Location; | ||||
|  | ||||
|         // Single file application: entry assembly is not on disk and doesn't have a file path | ||||
|         if (string.IsNullOrWhiteSpace(entryAssemblyFilePath)) | ||||
|         { | ||||
|             return Path.GetFileNameWithoutExtension(processFilePath); | ||||
|         } | ||||
|  | ||||
|         // Legacy .NET Framework application: entry assembly has the same file path as the process | ||||
|         if (PathEx.AreEqual(entryAssemblyFilePath, processFilePath)) | ||||
|         { | ||||
|             return Path.GetFileNameWithoutExtension(entryAssemblyFilePath); | ||||
|         } | ||||
|  | ||||
|         // .NET Core application launched through the native application host: | ||||
|         // entry assembly has the same file path as the process, but with a different extension. | ||||
|         if ( | ||||
|             PathEx.AreEqual(Path.ChangeExtension(entryAssemblyFilePath, "exe"), processFilePath) | ||||
|             || PathEx.AreEqual( | ||||
|                 Path.GetFileNameWithoutExtension(entryAssemblyFilePath), | ||||
|                 processFilePath | ||||
|             ) | ||||
|         ) | ||||
|         { | ||||
|             return Path.GetFileNameWithoutExtension(entryAssemblyFilePath); | ||||
|         } | ||||
|  | ||||
|         // .NET Core application launched through the .NET CLI | ||||
|         return "dotnet " + Path.GetFileName(entryAssemblyFilePath); | ||||
|     } | ||||
|  | ||||
|     private static string GetDefaultVersionText() | ||||
|     { | ||||
|         var entryAssemblyVersion = EnvironmentEx.EntryAssembly?.GetName().Version; | ||||
|         if (entryAssemblyVersion is null) | ||||
|         { | ||||
|             throw new InvalidOperationException( | ||||
|                 "Failed to infer the default application version. " | ||||
|                     + $"Please specify it explicitly using `{nameof(SetVersion)}()`." | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return "v" + entryAssemblyVersion.ToSemanticString(); | ||||
|     } | ||||
| } | ||||
| @@ -1,23 +1,37 @@ | ||||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFrameworks>net45;netstandard2.0</TargetFrameworks> | ||||
|     <LangVersion>latest</LangVersion> | ||||
|     <Version>0.0.1</Version> | ||||
|     <Company>Tyrrrz</Company> | ||||
|     <Authors>$(Company)</Authors> | ||||
|     <Copyright>Copyright (C) Alexey Golub</Copyright> | ||||
|     <Description>Declarative framework for CLI applications</Description> | ||||
|     <PackageTags>command line executable interface framework parser arguments net core</PackageTags> | ||||
|     <PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl> | ||||
|     <PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/blob/master/Changelog.md</PackageReleaseNotes> | ||||
|     <PackageIconUrl>https://raw.githubusercontent.com/Tyrrrz/CliFx/master/favicon.png</PackageIconUrl> | ||||
|     <PackageLicenseExpression>LGPL-3.0-only</PackageLicenseExpression> | ||||
|     <RepositoryUrl>https://github.com/Tyrrrz/CliFx</RepositoryUrl> | ||||
|     <RepositoryType>git</RepositoryType> | ||||
|     <PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance> | ||||
|     <GeneratePackageOnBuild>True</GeneratePackageOnBuild> | ||||
|     <DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile> | ||||
|     <TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks> | ||||
|     <IsPackable>true</IsPackable> | ||||
|     <IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable> | ||||
|     <IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <Authors>$(Company)</Authors> | ||||
|     <Description>Class-first framework for building command-line interfaces</Description> | ||||
|     <PackageTags>command line executable interface framework parser arguments cli app application net core</PackageTags> | ||||
|     <PackageProjectUrl>https://github.com/Tyrrrz/CliFx</PackageProjectUrl> | ||||
|     <PackageReleaseNotes>https://github.com/Tyrrrz/CliFx/releases</PackageReleaseNotes> | ||||
|     <PackageIcon>favicon.png</PackageIcon> | ||||
|     <PackageLicenseExpression>MIT</PackageLicenseExpression> | ||||
|     <GenerateDocumentationFile>true</GenerateDocumentationFile> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <None Include="../favicon.png" Pack="true" PackagePath="" Visible="false" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" /> | ||||
|     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="PolyShim" Version="1.14.0" PrivateAssets="all" /> | ||||
|     <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <!-- Embed the analyzer inside the package --> | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
| </Project> | ||||
| @@ -1,15 +0,0 @@ | ||||
| using System; | ||||
| using System.Threading.Tasks; | ||||
| using CliFx.Models; | ||||
|  | ||||
| namespace CliFx | ||||
| { | ||||
|     public abstract class Command | ||||
|     { | ||||
|         public virtual ExitCode Execute() => throw new InvalidOperationException( | ||||
|             "Can't execute command because its execution method is not defined. " + | ||||
|             $"Override Execute or ExecuteAsync on {GetType().Name} in order to make it executable."); | ||||
|  | ||||
|         public virtual Task<ExitCode> ExecuteAsync() => Task.FromResult(Execute()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								CliFx/Exceptions/CliFxException.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								CliFx/Exceptions/CliFxException.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Exceptions; | ||||
|  | ||||
| /// <summary> | ||||
| /// Exception thrown within <see cref="CliFx" />. | ||||
| /// </summary> | ||||
| public partial class CliFxException( | ||||
|     string message, | ||||
|     int exitCode = CliFxException.DefaultExitCode, | ||||
|     bool showHelp = false, | ||||
|     Exception? innerException = null | ||||
| ) : Exception(message, innerException) | ||||
| { | ||||
|     internal const int DefaultExitCode = 1; | ||||
|  | ||||
|     // When an exception is created without a message, the base Exception class | ||||
|     // provides a default message that is not very useful. | ||||
|     // This property is used to identify whether this instance was created with | ||||
|     // a custom message, so that we can avoid printing the default message. | ||||
|     internal bool HasCustomMessage { get; } = !string.IsNullOrWhiteSpace(message); | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Returned exit code. | ||||
|     /// </summary> | ||||
|     public int ExitCode { get; } = exitCode; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Whether to show the help text before exiting. | ||||
|     /// </summary> | ||||
|     public bool ShowHelp { get; } = showHelp; | ||||
| } | ||||
|  | ||||
| public partial class CliFxException | ||||
| { | ||||
|     // Internal errors don't show help because they're meant for the developer and | ||||
|     // not the end-user of the application. | ||||
|     internal static CliFxException InternalError( | ||||
|         string message, | ||||
|         Exception? innerException = null | ||||
|     ) => new(message, DefaultExitCode, false, innerException); | ||||
|  | ||||
|     // User errors are typically caused by invalid input and are meant for the end-user, | ||||
|     // so we want to show help. | ||||
|     internal static CliFxException UserError(string message, Exception? innerException = null) => | ||||
|         new(message, DefaultExitCode, true, innerException); | ||||
| } | ||||
							
								
								
									
										14
									
								
								CliFx/Exceptions/CommandException.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								CliFx/Exceptions/CommandException.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Exceptions; | ||||
|  | ||||
| /// <summary> | ||||
| /// Exception thrown when a command cannot proceed with its normal execution due to an error. | ||||
| /// Use this exception to report an error to the console and return a specific exit code. | ||||
| /// </summary> | ||||
| public class CommandException( | ||||
|     string message, | ||||
|     int exitCode = CliFxException.DefaultExitCode, | ||||
|     bool showHelp = false, | ||||
|     Exception? innerException = null | ||||
| ) : CliFxException(message, exitCode, showHelp, innerException); | ||||
| @@ -1,21 +0,0 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Exceptions | ||||
| { | ||||
|     public class CommandResolveException : Exception | ||||
|     { | ||||
|         public CommandResolveException() | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public CommandResolveException(string message) | ||||
|             : base(message) | ||||
|         { | ||||
|         } | ||||
|  | ||||
|         public CommandResolveException(string message, Exception innerException) | ||||
|             : base(message, innerException) | ||||
|         { | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								CliFx/Extensibility/BindingConverter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								CliFx/Extensibility/BindingConverter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Extensibility; | ||||
|  | ||||
| /// <summary> | ||||
| /// Defines custom conversion logic for activating command inputs from the corresponding raw command-line arguments. | ||||
| /// </summary> | ||||
| public abstract class BindingConverter<T> : IBindingConverter | ||||
| { | ||||
|     /// <inheritdoc cref="IBindingConverter.Convert" /> | ||||
|     public abstract T? Convert(string? rawValue, IFormatProvider? formatProvider); | ||||
|  | ||||
|     object? IBindingConverter.Convert(string? rawValue, IFormatProvider? formatProvider) => | ||||
|         Convert(rawValue, formatProvider); | ||||
| } | ||||
							
								
								
									
										12
									
								
								CliFx/Extensibility/BindingValidationError.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								CliFx/Extensibility/BindingValidationError.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| namespace CliFx.Extensibility; | ||||
|  | ||||
| /// <summary> | ||||
| /// Represents a validation error. | ||||
| /// </summary> | ||||
| public class BindingValidationError(string message) | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Error message shown to the user. | ||||
|     /// </summary> | ||||
|     public string Message { get; } = message; | ||||
| } | ||||
							
								
								
									
										26
									
								
								CliFx/Extensibility/BindingValidator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								CliFx/Extensibility/BindingValidator.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| namespace CliFx.Extensibility; | ||||
|  | ||||
| /// <summary> | ||||
| /// Defines custom validation logic for activated command inputs. | ||||
| /// </summary> | ||||
| public abstract class BindingValidator<T> : IBindingValidator | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Returns a successful validation result. | ||||
|     /// </summary> | ||||
|     protected BindingValidationError? Ok() => null; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Returns a non-successful validation result. | ||||
|     /// </summary> | ||||
|     protected BindingValidationError Error(string message) => new(message); | ||||
|  | ||||
|     /// <inheritdoc cref="IBindingValidator.Validate" /> | ||||
|     /// <remarks> | ||||
|     /// You can use the utility methods <see cref="Ok" /> and <see cref="Error" /> to | ||||
|     /// create an appropriate result. | ||||
|     /// </remarks> | ||||
|     public abstract BindingValidationError? Validate(T? value); | ||||
|  | ||||
|     BindingValidationError? IBindingValidator.Validate(object? value) => Validate((T?)value); | ||||
| } | ||||
							
								
								
									
										13
									
								
								CliFx/Extensibility/BoolBindingConverter.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								CliFx/Extensibility/BoolBindingConverter.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| using System; | ||||
|  | ||||
| namespace CliFx.Extensibility; | ||||
|  | ||||
| /// <summary> | ||||
| /// Converter for activating command inputs bound to properties of type <see cref="bool" />. | ||||
| /// </summary> | ||||
| public class BoolBindingConverter : BindingConverter<bool> | ||||
| { | ||||
|     /// <inheritdoc /> | ||||
|     public override bool Convert(string? rawValue, IFormatProvider? formatProvider) => | ||||
|         string.IsNullOrWhiteSpace(rawValue) || bool.Parse(rawValue); | ||||
| } | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user