mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
544 Commits
0.0.1
...
25083eff44
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25083eff44 | ||
|
|
7f2c00fe3a | ||
|
|
7638b997ff | ||
|
|
d80d012938 | ||
|
|
2a02d39dba | ||
|
|
c40b4f3501 | ||
|
|
3fb2a2319b | ||
|
|
1a5a0374c7 | ||
|
|
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
|
# User-specific files
|
||||||
*.rsuser
|
.vs/
|
||||||
|
.idea/
|
||||||
*.suo
|
*.suo
|
||||||
*.user
|
*.user
|
||||||
*.userosscache
|
|
||||||
*.sln.docstates
|
|
||||||
|
|
||||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
|
||||||
*.userprefs
|
|
||||||
|
|
||||||
# Build results
|
# Build results
|
||||||
[Dd]ebug/
|
bin/
|
||||||
[Dd]ebugPublic/
|
obj/
|
||||||
[Rr]elease/
|
|
||||||
[Rr]eleases/
|
|
||||||
x64/
|
|
||||||
x86/
|
|
||||||
[Aa][Rr][Mm]/
|
|
||||||
[Aa][Rr][Mm]64/
|
|
||||||
bld/
|
|
||||||
[Bb]in/
|
|
||||||
[Oo]bj/
|
|
||||||
[Ll]og/
|
|
||||||
|
|
||||||
# Visual Studio 2015/2017 cache/options directory
|
# Test results
|
||||||
.vs/
|
TestResults/
|
||||||
# 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
|
|
||||||
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.15.2" />
|
||||||
|
<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-beta5.25306.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.6" />
|
||||||
|
</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>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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
|
||||||
{
|
{
|
||||||
public static Task<int> Main(string[] args) => new CliApplication().RunAsync(args);
|
// Path to the apphost
|
||||||
|
public static string FilePath { get; } =
|
||||||
|
Path.ChangeExtension(
|
||||||
|
Assembly.GetExecutingAssembly().Location,
|
||||||
|
OperatingSystem.IsWindows() ? "exe" : null
|
||||||
|
);
|
||||||
|
|
||||||
|
public static async Task Main()
|
||||||
|
{
|
||||||
|
// 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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net45</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
|
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<PackageReference Include="NUnit" Version="3.11.0" />
|
</ItemGroup>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.11.0" />
|
|
||||||
<PackageReference Include="Moq" Version="4.11.0" />
|
<ItemGroup>
|
||||||
<PackageReference Include="CliWrap" Version="2.3.0" />
|
<PackageReference Include="Basic.Reference.Assemblies.Net80" Version="1.8.2" />
|
||||||
|
<PackageReference Include="CliWrap" Version="3.9.0" />
|
||||||
|
<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.4.0" />
|
||||||
|
<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.6" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="PolyShim" Version="1.15.0" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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");
|
||||||
|
}
|
||||||
|
}
|
||||||
1062
CliFx.Tests/HelpTextSpecs.cs
Normal file
1062
CliFx.Tests/HelpTextSpecs.cs
Normal file
File diff suppressed because it is too large
Load Diff
842
CliFx.Tests/OptionBindingSpecs.cs
Normal file
842
CliFx.Tests/OptionBindingSpecs.cs
Normal file
@@ -0,0 +1,842 @@
|
|||||||
|
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_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_help_option_and_get_the_correct_value()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
|
// lang=csharp
|
||||||
|
"""
|
||||||
|
[Command]
|
||||||
|
public class Command : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("help", 'h')]
|
||||||
|
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(
|
||||||
|
["--help", "me"],
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
var stdOut = FakeConsole.ReadOutputString();
|
||||||
|
stdOut.Trim().Should().Be("me");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task I_can_bind_an_option_to_a_property_with_the_same_identifier_as_the_implicit_version_option_and_get_the_correct_value()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var commandType = DynamicCommandBuilder.Compile(
|
||||||
|
// lang=csharp
|
||||||
|
"""
|
||||||
|
[Command]
|
||||||
|
public class Command : ICommand
|
||||||
|
{
|
||||||
|
[CommandOption("version")]
|
||||||
|
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(
|
||||||
|
["--version", "1.2.0"],
|
||||||
|
new Dictionary<string, string>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
var stdOut = FakeConsole.ReadOutputString();
|
||||||
|
stdOut.Trim().Should().Be("1.2.0");
|
||||||
|
}
|
||||||
|
|
||||||
|
[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"
|
||||||
|
}
|
||||||
74
CliFx.sln
74
CliFx.sln
@@ -7,15 +7,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx", "CliFx\CliFx.csproj
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests", "CliFx.Tests\CliFx.Tests.csproj", "{268CF863-65A5-49BB-93CF-08972B7756DC}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliFx.Tests.Dummy", "CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj", "{4904B3EB-3286-4F1B-8B74-6FF051C8E787}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Misc", "Misc", "{3AAE8166-BB8E-49DA-844C-3A0EE6BD40A0}"
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3AAE8166-BB8E-49DA-844C-3A0EE6BD40A0}"
|
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
Changelog.md = Changelog.md
|
Directory.Build.props = Directory.Build.props
|
||||||
License.txt = License.txt
|
License.txt = License.txt
|
||||||
Readme.md = Readme.md
|
Readme.md = Readme.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.ActiveCfg = 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
|
{268CF863-65A5-49BB-93CF-08972B7756DC}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x64.Build.0 = Debug|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.ActiveCfg = Debug|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Debug|x86.Build.0 = Debug|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.ActiveCfg = Release|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x64.Build.0 = Release|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.ActiveCfg = Release|Any CPU
|
{8ACD6DC2-D768-4850-9223-5B7C83A78513}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{4904B3EB-3286-4F1B-8B74-6FF051C8E787}.Release|x86.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
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;
|
using System;
|
||||||
|
|
||||||
namespace CliFx.Attributes
|
namespace CliFx.Attributes;
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
|
|
||||||
public class CommandAttribute : Attribute
|
|
||||||
{
|
|
||||||
public string Name { get; }
|
|
||||||
|
|
||||||
public CommandAttribute(string 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
|
||||||
{
|
{
|
||||||
Name = name;
|
/// <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;
|
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)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandOptionAttribute : Attribute
|
public class CommandOptionAttribute : CommandInputAttribute
|
||||||
{
|
{
|
||||||
public string Name { get; }
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="CommandOptionAttribute" />.
|
||||||
public char ShortName { get; set; }
|
/// </summary>
|
||||||
|
private CommandOptionAttribute(string? name, char? shortName)
|
||||||
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;
|
||||||
using System.Reflection;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
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;
|
||||||
{
|
|
||||||
public partial class CliApplication : ICliApplication
|
|
||||||
{
|
|
||||||
private readonly ICommandResolver _commandResolver;
|
|
||||||
|
|
||||||
public CliApplication(ICommandResolver commandResolver)
|
/// <summary>
|
||||||
|
/// Command-line application facade.
|
||||||
|
/// </summary>
|
||||||
|
public class CliApplication(
|
||||||
|
ApplicationMetadata metadata,
|
||||||
|
ApplicationConfiguration configuration,
|
||||||
|
IConsole console,
|
||||||
|
ITypeActivator typeActivator
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_commandResolver = commandResolver;
|
/// <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()
|
||||||
|
{
|
||||||
|
using (console.WithForegroundColor(ConsoleColor.Green))
|
||||||
|
{
|
||||||
|
console.Output.WriteLine(
|
||||||
|
$"Attach the debugger to process with ID {ProcessEx.GetCurrentProcessId()} to continue."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CliApplication()
|
// Try to also launch the debugger ourselves (only works with Visual Studio)
|
||||||
: this(GetDefaultCommandResolver(Assembly.GetCallingAssembly()))
|
Debugger.Launch();
|
||||||
{
|
|
||||||
|
while (!Debugger.IsAttached)
|
||||||
|
await Task.Delay(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
private async ValueTask<int> RunAsync(
|
||||||
|
ApplicationSchema applicationSchema,
|
||||||
|
CommandArguments commandArguments,
|
||||||
|
IReadOnlyDictionary<string, string?> environmentVariables
|
||||||
|
)
|
||||||
{
|
{
|
||||||
// Resolve and execute command
|
// Console colors may have already been overridden by the parent process,
|
||||||
var command = _commandResolver.ResolveCommand(commandLineArguments);
|
// so we need to reset it to make sure that everything we write looks properly.
|
||||||
var exitCode = await command.ExecuteAsync();
|
console.ResetColor();
|
||||||
|
|
||||||
// TODO: print message if error?
|
// Handle the debug directive
|
||||||
|
if (IsDebugModeEnabled(commandArguments))
|
||||||
|
{
|
||||||
|
await PromptDebuggerAsync();
|
||||||
|
}
|
||||||
|
|
||||||
return exitCode.Value;
|
// Handle the preview directive
|
||||||
|
if (IsPreviewModeEnabled(commandArguments))
|
||||||
|
{
|
||||||
|
console.WriteCommandInput(commandArguments);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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);
|
return await RunAsync(
|
||||||
var commandOptionParser = new CommandOptionParser();
|
Configuration.Schema,
|
||||||
var commandOptionConverter = new CommandOptionConverter();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new CommandResolver(typeProvider, commandOptionParser, commandOptionConverter);
|
/// <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">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net45;netstandard2.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||||
<LangVersion>latest</LangVersion>
|
<IsPackable>true</IsPackable>
|
||||||
<Version>0.0.1</Version>
|
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
|
||||||
<Company>Tyrrrz</Company>
|
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
|
||||||
<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>
|
|
||||||
</PropertyGroup>
|
</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.15.0" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.6.0" Condition="!$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'netstandard2.1'))" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Embed the analyzer inside the package -->
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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