mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
426 Commits
0.0.5
...
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 |
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
|
|
||||||
4
.github/FUNDING.yml
vendored
4
.github/FUNDING.yml
vendored
@@ -1,4 +0,0 @@
|
|||||||
github: Tyrrrz
|
|
||||||
patreon: Tyrrrz
|
|
||||||
open_collective: Tyrrrz
|
|
||||||
custom: ['buymeacoffee.com/Tyrrrz']
|
|
||||||
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
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using BenchmarkDotNet.Attributes;
|
|
||||||
using CliFx.Benchmarks.Commands;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks
|
|
||||||
{
|
|
||||||
[CoreJob]
|
|
||||||
[RankColumn]
|
|
||||||
public class Benchmark
|
|
||||||
{
|
|
||||||
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
|
|
||||||
|
|
||||||
[Benchmark(Description = "CliFx", Baseline = true)]
|
|
||||||
public Task<int> ExecuteWithCliFx() => new CliApplicationBuilder().AddCommand(typeof(CliFxCommand)).Build().RunAsync(Arguments);
|
|
||||||
|
|
||||||
[Benchmark(Description = "System.CommandLine")]
|
|
||||||
public Task<int> ExecuteWithSystemCommandLine() => new SystemCommandLineCommand().ExecuteAsync(Arguments);
|
|
||||||
|
|
||||||
[Benchmark(Description = "McMaster.Extensions.CommandLineUtils")]
|
|
||||||
public int ExecuteWithMcMaster() => McMaster.Extensions.CommandLineUtils.CommandLineApplication.Execute<McMasterCommand>(Arguments);
|
|
||||||
|
|
||||||
[Benchmark(Description = "CommandLineParser")]
|
|
||||||
public void ExecuteWithCommandLineParser()
|
|
||||||
{
|
|
||||||
var parsed = new CommandLine.Parser().ParseArguments(Arguments, typeof(CommandLineParserCommand));
|
|
||||||
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Benchmark(Description = "PowerArgs")]
|
|
||||||
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
|
|
||||||
|
|
||||||
[Benchmark(Description = "Clipr")]
|
|
||||||
public void ExecuteWithClipr() => clipr.CliParser.Parse<CliprCommand>(Arguments).Execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<NuGetAudit>false</NuGetAudit>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
|
||||||
<PackageReference Include="clipr" Version="1.6.1" />
|
<PackageReference Include="clipr" Version="1.6.1" />
|
||||||
<PackageReference Include="CommandLineParser" Version="2.6.0" />
|
<PackageReference Include="Cocona" Version="2.2.0" />
|
||||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" />
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
<PackageReference Include="PowerArgs" Version="3.6.0" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
[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 Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using clipr;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using CommandLine;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using McMaster.Extensions.CommandLineUtils;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using PowerArgs;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
using System.CommandLine;
|
|
||||||
using System.CommandLine.Invocation;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks.Commands
|
|
||||||
{
|
|
||||||
public class SystemCommandLineCommand
|
|
||||||
{
|
|
||||||
public static int ExecuteHandler(string s, int i, bool b) => 0;
|
|
||||||
|
|
||||||
public Task<int> ExecuteAsync(string[] args)
|
|
||||||
{
|
|
||||||
var command = new RootCommand
|
|
||||||
{
|
|
||||||
new Option(new[] {"--str", "-s"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<string>()
|
|
||||||
},
|
|
||||||
new Option(new[] {"--int", "-i"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<int>()
|
|
||||||
},
|
|
||||||
new Option(new[] {"--bool", "-b"})
|
|
||||||
{
|
|
||||||
Argument = new Argument<bool>()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
command.Handler = CommandHandler.Create(typeof(SystemCommandLineCommand).GetMethod(nameof(ExecuteHandler)));
|
|
||||||
|
|
||||||
return command.InvokeAsync(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using BenchmarkDotNet.Configs;
|
|
||||||
using BenchmarkDotNet.Running;
|
|
||||||
|
|
||||||
namespace CliFx.Benchmarks
|
|
||||||
{
|
|
||||||
public static class Program
|
|
||||||
{
|
|
||||||
public static void Main() =>
|
|
||||||
BenchmarkRunner.Run(typeof(Program).Assembly, DefaultConfig.Instance
|
|
||||||
.With(ConfigOptions.DisableOptimizationsValidator));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 |
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<LangVersion>latest</LangVersion>
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
|
<PublishAot>true</PublishAot>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
|
||||||
</ItemGroup>
|
</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,75 +1,55 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Models;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Demo.Services;
|
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book add", Description = "Adds a book to the library.")]
|
||||||
|
public class BookAddCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book add", Description = "Add a book to the library.")]
|
[CommandParameter(0, Name = "title", Description = "Book title.")]
|
||||||
public partial class BookAddCommand : ICommand
|
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)
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
if (libraryProvider.TryGetBook(Title) is not null)
|
||||||
|
throw new CommandException($"Book '{Title}' already exists.", 10);
|
||||||
|
|
||||||
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
var book = new Book(Title, Author, Published, Isbn);
|
||||||
public string Title { get; set; }
|
libraryProvider.AddBook(book);
|
||||||
|
|
||||||
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
|
console.WriteLine($"Book '{Title}' added.");
|
||||||
public string Author { get; set; }
|
console.WriteBook(book);
|
||||||
|
|
||||||
[CommandOption("published", 'p', Description = "Book publish date.")]
|
return default;
|
||||||
public DateTimeOffset Published { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("isbn", 'n', Description = "Book ISBN.")]
|
|
||||||
public Isbn Isbn { get; set; }
|
|
||||||
|
|
||||||
public BookAddCommand(LibraryService libraryService)
|
|
||||||
{
|
|
||||||
_libraryService = libraryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
// To make the demo simpler, we will just generate random publish date and ISBN if they were not set
|
|
||||||
if (Published == default)
|
|
||||||
Published = CreateRandomDate();
|
|
||||||
if (Isbn == default)
|
|
||||||
Isbn = CreateRandomIsbn();
|
|
||||||
|
|
||||||
if (_libraryService.GetBook(Title) != null)
|
|
||||||
throw new CommandException("Book already exists.", 1);
|
|
||||||
|
|
||||||
var book = new Book(Title, Author, Published, Isbn);
|
|
||||||
_libraryService.AddBook(book);
|
|
||||||
|
|
||||||
console.Output.WriteLine("Book added.");
|
|
||||||
console.RenderBook(book);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public partial class BookAddCommand
|
|
||||||
{
|
|
||||||
private static readonly Random Random = new Random();
|
|
||||||
|
|
||||||
private static DateTimeOffset CreateRandomDate() => new DateTimeOffset(
|
|
||||||
Random.Next(1800, 2020),
|
|
||||||
Random.Next(1, 12),
|
|
||||||
Random.Next(1, 28),
|
|
||||||
Random.Next(1, 23),
|
|
||||||
Random.Next(1, 59),
|
|
||||||
Random.Next(1, 59),
|
|
||||||
TimeSpan.Zero);
|
|
||||||
|
|
||||||
public static Isbn CreateRandomIsbn() => new Isbn(
|
|
||||||
Random.Next(0, 999),
|
|
||||||
Random.Next(0, 99),
|
|
||||||
Random.Next(0, 99999),
|
|
||||||
Random.Next(0, 99),
|
|
||||||
Random.Next(0, 9));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,35 +1,27 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book", Description = "Retrieves a book from the library.")]
|
||||||
|
public class BookCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book", Description = "View, list, add or remove books.")]
|
[CommandParameter(0, Name = "title", Description = "Title of the book to retrieve.")]
|
||||||
public class BookCommand : ICommand
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
var book = libraryProvider.TryGetBook(Title);
|
||||||
|
|
||||||
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
if (book is null)
|
||||||
public string Title { get; set; }
|
throw new CommandException($"Book '{Title}' not found.", 10);
|
||||||
|
|
||||||
public BookCommand(LibraryService libraryService)
|
console.WriteBook(book);
|
||||||
{
|
|
||||||
_libraryService = libraryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
return default;
|
||||||
{
|
|
||||||
var book = _libraryService.GetBook(Title);
|
|
||||||
|
|
||||||
if (book == null)
|
|
||||||
throw new CommandException("Book not found.", 1);
|
|
||||||
|
|
||||||
console.RenderBook(book);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,29 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Internal;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Utils;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book list", Description = "Lists all books in the library.")]
|
||||||
|
public class BookListCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book list", Description = "List all books in the library.")]
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
public class BookListCommand : ICommand
|
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
var library = libraryProvider.GetLibrary();
|
||||||
|
|
||||||
public BookListCommand(LibraryService libraryService)
|
for (var i = 0; i < library.Books.Count; i++)
|
||||||
{
|
{
|
||||||
_libraryService = libraryService;
|
// Add margin
|
||||||
|
if (i != 0)
|
||||||
|
console.WriteLine();
|
||||||
|
|
||||||
|
// Render book
|
||||||
|
var book = library.Books[i];
|
||||||
|
console.WriteBook(book);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
return default;
|
||||||
{
|
|
||||||
var library = _libraryService.GetLibrary();
|
|
||||||
|
|
||||||
var isFirst = true;
|
|
||||||
foreach (var book in library.Books)
|
|
||||||
{
|
|
||||||
// Margin
|
|
||||||
if (!isFirst)
|
|
||||||
console.Output.WriteLine();
|
|
||||||
isFirst = false;
|
|
||||||
|
|
||||||
// Render book
|
|
||||||
console.RenderBook(book);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using CliFx.Demo.Services;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
using CliFx.Services;
|
using CliFx.Infrastructure;
|
||||||
|
|
||||||
namespace CliFx.Demo.Commands
|
namespace CliFx.Demo.Commands;
|
||||||
|
|
||||||
|
[Command("book remove", Description = "Removes a book from the library.")]
|
||||||
|
public class BookRemoveCommand(LibraryProvider libraryProvider) : ICommand
|
||||||
{
|
{
|
||||||
[Command("book remove", Description = "Remove a book from the library.")]
|
[CommandParameter(0, Name = "title", Description = "Title of the book to remove.")]
|
||||||
public class BookRemoveCommand : ICommand
|
public required string Title { get; init; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
private readonly LibraryService _libraryService;
|
var book = libraryProvider.TryGetBook(Title);
|
||||||
|
|
||||||
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")]
|
if (book is null)
|
||||||
public string Title { get; set; }
|
throw new CommandException($"Book '{Title}' not found.", 10);
|
||||||
|
|
||||||
public BookRemoveCommand(LibraryService libraryService)
|
libraryProvider.RemoveBook(book);
|
||||||
{
|
|
||||||
_libraryService = libraryService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
console.WriteLine($"Book '{Title}' removed.");
|
||||||
{
|
|
||||||
var book = _libraryService.GetBook(Title);
|
|
||||||
|
|
||||||
if (book == null)
|
return default;
|
||||||
throw new CommandException("Book not found.", 1);
|
|
||||||
|
|
||||||
_libraryService.RemoveBook(book);
|
|
||||||
|
|
||||||
console.Output.WriteLine($"Book {Title} removed.");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using CliFx.Demo.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Internal
|
|
||||||
{
|
|
||||||
internal static class Extensions
|
|
||||||
{
|
|
||||||
public static void RenderBook(this IConsole console, Book book)
|
|
||||||
{
|
|
||||||
// Title
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Title));
|
|
||||||
|
|
||||||
// Author
|
|
||||||
console.Output.Write(" ");
|
|
||||||
console.Output.Write("Author: ");
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Author));
|
|
||||||
|
|
||||||
// Published
|
|
||||||
console.Output.Write(" ");
|
|
||||||
console.Output.Write("Published: ");
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine($"{book.Published:d}"));
|
|
||||||
|
|
||||||
// ISBN
|
|
||||||
console.Output.Write(" ");
|
|
||||||
console.Output.Write("ISBN: ");
|
|
||||||
console.WithForegroundColor(ConsoleColor.White, () => console.Output.WriteLine(book.Isbn));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public class Book
|
|
||||||
{
|
|
||||||
public string Title { get; }
|
|
||||||
|
|
||||||
public string Author { get; }
|
|
||||||
|
|
||||||
public DateTimeOffset Published { get; }
|
|
||||||
|
|
||||||
public Isbn Isbn { get; }
|
|
||||||
|
|
||||||
public Book(string title, string author, DateTimeOffset published, Isbn isbn)
|
|
||||||
{
|
|
||||||
Title = title;
|
|
||||||
Author = author;
|
|
||||||
Published = published;
|
|
||||||
Isbn = isbn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public static class Extensions
|
|
||||||
{
|
|
||||||
public static Library WithBook(this Library library, Book book)
|
|
||||||
{
|
|
||||||
var books = library.Books.ToList();
|
|
||||||
books.Add(book);
|
|
||||||
|
|
||||||
return new Library(books);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Library WithoutBook(this Library library, Book book)
|
|
||||||
{
|
|
||||||
var books = library.Books.Where(b => b != book).ToArray();
|
|
||||||
|
|
||||||
return new Library(books);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public partial class Isbn
|
|
||||||
{
|
|
||||||
public int EanPrefix { get; }
|
|
||||||
|
|
||||||
public int RegistrationGroup { get; }
|
|
||||||
|
|
||||||
public int Registrant { get; }
|
|
||||||
|
|
||||||
public int Publication { get; }
|
|
||||||
|
|
||||||
public int CheckDigit { get; }
|
|
||||||
|
|
||||||
public Isbn(int eanPrefix, int registrationGroup, int registrant, int publication, int checkDigit)
|
|
||||||
{
|
|
||||||
EanPrefix = eanPrefix;
|
|
||||||
RegistrationGroup = registrationGroup;
|
|
||||||
Registrant = registrant;
|
|
||||||
Publication = publication;
|
|
||||||
CheckDigit = checkDigit;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ToString() => $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Isbn
|
|
||||||
{
|
|
||||||
public static Isbn Parse(string value)
|
|
||||||
{
|
|
||||||
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
|
|
||||||
|
|
||||||
return new Isbn(
|
|
||||||
int.Parse(components[0], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[1], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[2], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[3], CultureInfo.InvariantCulture),
|
|
||||||
int.Parse(components[4], CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Models
|
|
||||||
{
|
|
||||||
public partial class Library
|
|
||||||
{
|
|
||||||
public IReadOnlyList<Book> Books { get; }
|
|
||||||
|
|
||||||
public Library(IReadOnlyList<Book> books)
|
|
||||||
{
|
|
||||||
Books = books;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public partial class Library
|
|
||||||
{
|
|
||||||
public static Library Empty { get; } = new Library(Array.Empty<Book>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,18 @@
|
|||||||
using System.Threading.Tasks;
|
using CliFx;
|
||||||
using CliFx.Demo.Commands;
|
using CliFx.Demo.Domain;
|
||||||
using CliFx.Demo.Services;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace CliFx.Demo
|
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
||||||
{
|
var services = new ServiceCollection();
|
||||||
public static class Program
|
services.AddSingleton<LibraryProvider>();
|
||||||
{
|
|
||||||
public static Task<int> Main(string[] args)
|
|
||||||
{
|
|
||||||
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
|
|
||||||
// Register services
|
// Register all commands as transient services
|
||||||
services.AddSingleton<LibraryService>();
|
foreach (var commandType in commandTypes)
|
||||||
|
services.AddTransient(commandType);
|
||||||
|
|
||||||
// Register commands
|
return await new CliApplicationBuilder()
|
||||||
services.AddTransient<BookCommand>();
|
.SetDescription("Demo application showcasing CliFx features.")
|
||||||
services.AddTransient<BookAddCommand>();
|
.AddCommandsFromThisAssembly()
|
||||||
services.AddTransient<BookRemoveCommand>();
|
.UseTypeActivator(services.BuildServiceProvider())
|
||||||
services.AddTransient<BookListCommand>();
|
.Build()
|
||||||
|
.RunAsync();
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
|
||||||
|
|
||||||
return new CliApplicationBuilder()
|
|
||||||
.AddCommandsFromThisAssembly()
|
|
||||||
.UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type))
|
|
||||||
.Build()
|
|
||||||
.RunAsync(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# CliFx Demo Project
|
# CliFx Demo Project
|
||||||
|
|
||||||
Sample command line interface for managing a library of books.
|
Sample command-line interface for managing a library of books.
|
||||||
|
|
||||||
This demo project shows basic CliFx functionality such as command routing, option parsing, autogenerated help text, and some other things.
|
This demo project showcases basic CliFx functionality such as command routing, argument parsing, and autogenerated help text.
|
||||||
|
|
||||||
You can get a list of available commands by running `CliFx.Demo --help`.
|
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Demo.Models;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CliFx.Demo.Services
|
|
||||||
{
|
|
||||||
public class LibraryService
|
|
||||||
{
|
|
||||||
private string StorageFilePath => Path.Combine(Directory.GetCurrentDirectory(), "Data.json");
|
|
||||||
|
|
||||||
private void StoreLibrary(Library library)
|
|
||||||
{
|
|
||||||
var data = JsonConvert.SerializeObject(library);
|
|
||||||
File.WriteAllText(StorageFilePath, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Library GetLibrary()
|
|
||||||
{
|
|
||||||
if (!File.Exists(StorageFilePath))
|
|
||||||
return Library.Empty;
|
|
||||||
|
|
||||||
var data = File.ReadAllText(StorageFilePath);
|
|
||||||
|
|
||||||
return JsonConvert.DeserializeObject<Library>(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Book GetBook(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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);
|
||||||
|
}
|
||||||
18
CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
Normal file
18
CliFx.Tests.Dummy/CliFx.Tests.Dummy.csproj
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ApplicationIcon>../favicon.ico</ApplicationIcon>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CSharpier.MsBuild" Version="0.30.6" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
|
<ProjectReference Include="..\CliFx.SourceGeneration\CliFx.SourceGeneration.csproj" ReferenceOutputAssembly="false" OutputItemType="analyzer" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
CliFx.Tests.Dummy/Program.cs
Normal file
28
CliFx.Tests.Dummy/Program.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
// 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,49 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCommands;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CliApplicationBuilderTests
|
|
||||||
{
|
|
||||||
// Make sure all builder methods work
|
|
||||||
[Test]
|
|
||||||
public void All_Smoke_Test()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var builder = new CliApplicationBuilder();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
builder
|
|
||||||
.AddCommand(typeof(HelloWorldDefaultCommand))
|
|
||||||
.AddCommandsFrom(typeof(HelloWorldDefaultCommand).Assembly)
|
|
||||||
.AddCommands(new[] {typeof(HelloWorldDefaultCommand)})
|
|
||||||
.AddCommandsFrom(new[] {typeof(HelloWorldDefaultCommand).Assembly})
|
|
||||||
.AddCommandsFromThisAssembly()
|
|
||||||
.AllowDebugMode()
|
|
||||||
.AllowPreviewMode()
|
|
||||||
.UseTitle("test")
|
|
||||||
.UseExecutableName("test")
|
|
||||||
.UseVersionText("test")
|
|
||||||
.UseDescription("test")
|
|
||||||
.UseConsole(new VirtualConsole(TextWriter.Null))
|
|
||||||
.UseCommandFactory(schema => (ICommand) Activator.CreateInstance(schema.Type))
|
|
||||||
.UseCommandOptionInputConverter(new CommandOptionInputConverter())
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure builder can produce an application with no parameters specified
|
|
||||||
[Test]
|
|
||||||
public void Build_Test()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var builder = new CliApplicationBuilder();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
builder.Build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,230 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCommands;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CliApplicationTests
|
|
||||||
{
|
|
||||||
private const string TestVersionText = "v1.0";
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(HelloWorldDefaultCommand)},
|
|
||||||
new string[0],
|
|
||||||
"Hello world."
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"concat", "-i", "foo", "-i", "bar", "-s", " "},
|
|
||||||
"foo bar"
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"concat", "-i", "one", "two", "three", "-s", ", "},
|
|
||||||
"one, two, three"
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(DivideCommand)},
|
|
||||||
new[] {"div", "-D", "24", "-d", "8"},
|
|
||||||
"3"
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(HelloWorldDefaultCommand)},
|
|
||||||
new[] {"--version"},
|
|
||||||
TestVersionText
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"--version"},
|
|
||||||
TestVersionText
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(HelloWorldDefaultCommand)},
|
|
||||||
new[] {"-h"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(HelloWorldDefaultCommand)},
|
|
||||||
new[] {"--help"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new string[0],
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"-h"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"--help"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"concat", "-h"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ExceptionCommand)},
|
|
||||||
new[] {"exc", "-h"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(CommandExceptionCommand)},
|
|
||||||
new[] {"exc", "-h"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"[preview]"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ExceptionCommand)},
|
|
||||||
new[] {"exc", "[preview]"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"concat", "[preview]", "-o", "value"},
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new Type[0],
|
|
||||||
new string[0],
|
|
||||||
null, null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ConcatCommand)},
|
|
||||||
new[] {"non-existing"},
|
|
||||||
null, null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(ExceptionCommand)},
|
|
||||||
new[] {"exc"},
|
|
||||||
null, null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(CommandExceptionCommand)},
|
|
||||||
new[] {"exc"},
|
|
||||||
null, null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(CommandExceptionCommand)},
|
|
||||||
new[] {"exc"},
|
|
||||||
null, null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(CommandExceptionCommand)},
|
|
||||||
new[] {"exc", "-m", "foo bar"},
|
|
||||||
"foo bar", null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(CommandExceptionCommand)},
|
|
||||||
new[] {"exc", "-m", "foo bar", "-c", "666"},
|
|
||||||
"foo bar", 666
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_RunAsync))]
|
|
||||||
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
|
|
||||||
string expectedStdOut = null)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
using (var stdoutStream = new StringWriter())
|
|
||||||
{
|
|
||||||
var console = new VirtualConsole(stdoutStream);
|
|
||||||
|
|
||||||
var application = new CliApplicationBuilder()
|
|
||||||
.AddCommands(commandTypes)
|
|
||||||
.UseVersionText(TestVersionText)
|
|
||||||
.UseConsole(console)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var exitCode = await application.RunAsync(commandLineArguments);
|
|
||||||
var stdOut = stdoutStream.ToString().Trim();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
exitCode.Should().Be(0);
|
|
||||||
|
|
||||||
if (expectedStdOut != null)
|
|
||||||
stdOut.Should().Be(expectedStdOut);
|
|
||||||
else
|
|
||||||
stdOut.Should().NotBeNullOrWhiteSpace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
|
|
||||||
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments,
|
|
||||||
string expectedStdErr = null, int? expectedExitCode = null)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
using (var stderrStream = new StringWriter())
|
|
||||||
{
|
|
||||||
var console = new VirtualConsole(TextWriter.Null, stderrStream);
|
|
||||||
|
|
||||||
var application = new CliApplicationBuilder()
|
|
||||||
.AddCommands(commandTypes)
|
|
||||||
.UseVersionText(TestVersionText)
|
|
||||||
.UseConsole(console)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var exitCode = await application.RunAsync(commandLineArguments);
|
|
||||||
var stderr = stderrStream.ToString().Trim();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
if (expectedExitCode != null)
|
|
||||||
exitCode.Should().Be(expectedExitCode);
|
|
||||||
else
|
|
||||||
exitCode.Should().NotBe(0);
|
|
||||||
|
|
||||||
if (expectedStdErr != null)
|
|
||||||
stderr.Should().Be(expectedStdErr);
|
|
||||||
else
|
|
||||||
stderr.Should().NotBeNullOrWhiteSpace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,30 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net46</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
<IsTestProject>true</IsTestProject>
|
|
||||||
<CollectCoverage>true</CollectCoverage>
|
|
||||||
<CoverletOutputFormat>opencover</CoverletOutputFormat>
|
|
||||||
<CoverletOutput>bin/$(Configuration)/Coverage.xml</CoverletOutput>
|
|
||||||
<LangVersion>latest</LangVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="5.8.0" />
|
<Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
|
|
||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" />
|
|
||||||
<PackageReference Include="coverlet.msbuild" Version="2.6.3">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<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>
|
||||||
|
<ProjectReference Include="..\CliFx.Tests.Dummy\CliFx.Tests.Dummy.csproj" />
|
||||||
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
<ProjectReference Include="..\CliFx\CliFx.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
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\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCommands;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandFactoryTests
|
|
||||||
{
|
|
||||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
|
||||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(GetCommandSchema(typeof(HelloWorldDefaultCommand)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_CreateCommand))]
|
|
||||||
public void CreateCommand_Test(CommandSchema commandSchema)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var factory = new CommandFactory();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var command = factory.CreateCommand(commandSchema);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
command.Should().BeOfType(commandSchema.Type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCommands;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandInitializerTests
|
|
||||||
{
|
|
||||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
|
||||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new DivideCommand(),
|
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
|
||||||
new CommandInput("div", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("dividend", "13"),
|
|
||||||
new CommandOptionInput("divisor", "8")
|
|
||||||
}),
|
|
||||||
new DivideCommand {Dividend = 13, Divisor = 8}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new DivideCommand(),
|
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
|
||||||
new CommandInput("div", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("dividend", "13"),
|
|
||||||
new CommandOptionInput("d", "8")
|
|
||||||
}),
|
|
||||||
new DivideCommand {Dividend = 13, Divisor = 8}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new DivideCommand(),
|
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
|
||||||
new CommandInput("div", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("D", "13"),
|
|
||||||
new CommandOptionInput("d", "8")
|
|
||||||
}),
|
|
||||||
new DivideCommand {Dividend = 13, Divisor = 8}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new ConcatCommand(),
|
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
|
||||||
new CommandInput("concat", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("i", new[] {"foo", " ", "bar"})
|
|
||||||
}),
|
|
||||||
new ConcatCommand {Inputs = new[] {"foo", " ", "bar"}}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new ConcatCommand(),
|
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
|
||||||
new CommandInput("concat", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("i", new[] {"foo", "bar"}),
|
|
||||||
new CommandOptionInput("s", " ")
|
|
||||||
}),
|
|
||||||
new ConcatCommand {Inputs = new[] {"foo", "bar"}, Separator = " "}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new DivideCommand(),
|
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
|
||||||
new CommandInput("div")
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new DivideCommand(),
|
|
||||||
GetCommandSchema(typeof(DivideCommand)),
|
|
||||||
new CommandInput("div", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("D", "13")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new ConcatCommand(),
|
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
|
||||||
new CommandInput("concat")
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new ConcatCommand(),
|
|
||||||
GetCommandSchema(typeof(ConcatCommand)),
|
|
||||||
new CommandInput("concat", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("s", "_")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand))]
|
|
||||||
public void InitializeCommand_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput,
|
|
||||||
ICommand expectedCommand)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var initializer = new CommandInitializer();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
initializer.InitializeCommand(command, commandSchema, commandInput);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
command.Should().BeEquivalentTo(expectedCommand, o => o.RespectingRuntimeTypes());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_InitializeCommand_Negative))]
|
|
||||||
public void InitializeCommand_Negative_Test(ICommand command, CommandSchema commandSchema, CommandInput commandInput)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var initializer = new CommandInitializer();
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
initializer.Invoking(i => i.InitializeCommand(command, commandSchema, commandInput))
|
|
||||||
.Should().ThrowExactly<CliFxException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,219 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandInputParserTests
|
|
||||||
{
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_ParseCommandInput()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(new string[0], CommandInput.Empty);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--option", "value"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("option", "value")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--option1", "value1", "--option2", "value2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("option1", "value1"),
|
|
||||||
new CommandOptionInput("option2", "value2")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--option", "value1", "value2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--option", "value1", "--option", "value2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "value"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("a", "value")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "value1", "-b", "value2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("a", "value1"),
|
|
||||||
new CommandOptionInput("b", "value2")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "value1", "value2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("a", new[] {"value1", "value2"})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "value1", "-a", "value2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("a", new[] {"value1", "value2"})
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--option1", "value1", "-b", "value2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("option1", "value1"),
|
|
||||||
new CommandOptionInput("b", "value2")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--switch"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("switch")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"--switch1", "--switch2"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("switch1"),
|
|
||||||
new CommandOptionInput("switch2")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-s"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("s")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-a", "-b"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("a"),
|
|
||||||
new CommandOptionInput("b")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-ab"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("a"),
|
|
||||||
new CommandOptionInput("b")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"-ab", "value"},
|
|
||||||
new CommandInput(new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("a"),
|
|
||||||
new CommandOptionInput("b", "value")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"command"},
|
|
||||||
new CommandInput("command")
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"command", "--option", "value"},
|
|
||||||
new CommandInput("command", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("option", "value")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"long", "command", "name"},
|
|
||||||
new CommandInput("long command name")
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"long", "command", "name", "--option", "value"},
|
|
||||||
new CommandInput("long command name", new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("option", "value")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"[debug]"},
|
|
||||||
new CommandInput(null,
|
|
||||||
new[] {"debug"},
|
|
||||||
new CommandOptionInput[0])
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"[debug]", "[preview]"},
|
|
||||||
new CommandInput(null,
|
|
||||||
new[] {"debug", "preview"},
|
|
||||||
new CommandOptionInput[0])
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"[debug]", "[preview]", "-o", "value"},
|
|
||||||
new CommandInput(null,
|
|
||||||
new[] {"debug", "preview"},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("o", "value")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {"command", "[debug]", "[preview]", "-o", "value"},
|
|
||||||
new CommandInput("command",
|
|
||||||
new[] {"debug", "preview"},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new CommandOptionInput("o", "value")
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_ParseCommandInput))]
|
|
||||||
public void ParseCommandInput_Test(IReadOnlyList<string> commandLineArguments,
|
|
||||||
CommandInput expectedCommandInput)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var parser = new CommandInputParser();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var commandInput = parser.ParseCommandInput(commandLineArguments);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
commandInput.Should().BeEquivalentTo(expectedCommandInput);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCustomTypes;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandOptionInputConverterTests
|
|
||||||
{
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "value"),
|
|
||||||
typeof(string),
|
|
||||||
"value"
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "value"),
|
|
||||||
typeof(object),
|
|
||||||
"value"
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "true"),
|
|
||||||
typeof(bool),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "false"),
|
|
||||||
typeof(bool),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option"),
|
|
||||||
typeof(bool),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "a"),
|
|
||||||
typeof(char),
|
|
||||||
'a'
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "15"),
|
|
||||||
typeof(sbyte),
|
|
||||||
(sbyte) 15
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "15"),
|
|
||||||
typeof(byte),
|
|
||||||
(byte) 15
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "15"),
|
|
||||||
typeof(short),
|
|
||||||
(short) 15
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "15"),
|
|
||||||
typeof(ushort),
|
|
||||||
(ushort) 15
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123"),
|
|
||||||
typeof(int),
|
|
||||||
123
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123"),
|
|
||||||
typeof(uint),
|
|
||||||
123u
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123"),
|
|
||||||
typeof(long),
|
|
||||||
123L
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123"),
|
|
||||||
typeof(ulong),
|
|
||||||
123UL
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123.45"),
|
|
||||||
typeof(float),
|
|
||||||
123.45f
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123.45"),
|
|
||||||
typeof(double),
|
|
||||||
123.45
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123.45"),
|
|
||||||
typeof(decimal),
|
|
||||||
123.45m
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "28 Apr 1995"),
|
|
||||||
typeof(DateTime),
|
|
||||||
new DateTime(1995, 04, 28)
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "28 Apr 1995"),
|
|
||||||
typeof(DateTimeOffset),
|
|
||||||
new DateTimeOffset(new DateTime(1995, 04, 28))
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "00:14:59"),
|
|
||||||
typeof(TimeSpan),
|
|
||||||
new TimeSpan(00, 14, 59)
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "value2"),
|
|
||||||
typeof(TestEnum),
|
|
||||||
TestEnum.Value2
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "666"),
|
|
||||||
typeof(int?),
|
|
||||||
666
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option"),
|
|
||||||
typeof(int?),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "value3"),
|
|
||||||
typeof(TestEnum?),
|
|
||||||
TestEnum.Value3
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option"),
|
|
||||||
typeof(TestEnum?),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "01:00:00"),
|
|
||||||
typeof(TimeSpan?),
|
|
||||||
new TimeSpan(01, 00, 00)
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option"),
|
|
||||||
typeof(TimeSpan?),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "value"),
|
|
||||||
typeof(TestStringConstructable),
|
|
||||||
new TestStringConstructable("value")
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "value"),
|
|
||||||
typeof(TestStringParseable),
|
|
||||||
TestStringParseable.Parse("value")
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "value"),
|
|
||||||
typeof(TestStringParseableWithFormatProvider),
|
|
||||||
TestStringParseableWithFormatProvider.Parse("value", CultureInfo.InvariantCulture)
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(string[]),
|
|
||||||
new[] {"value1", "value2"}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(object[]),
|
|
||||||
new[] {"value1", "value2"}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"47", "69"}),
|
|
||||||
typeof(int[]),
|
|
||||||
new[] {47, 69}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"47"}),
|
|
||||||
typeof(int[]),
|
|
||||||
new[] {47}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value3"}),
|
|
||||||
typeof(TestEnum[]),
|
|
||||||
new[] {TestEnum.Value1, TestEnum.Value3}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"1337", "2441"}),
|
|
||||||
typeof(int?[]),
|
|
||||||
new int?[] {1337, 2441}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(TestStringConstructable[]),
|
|
||||||
new[] {new TestStringConstructable("value1"), new TestStringConstructable("value2")}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(IEnumerable),
|
|
||||||
new[] {"value1", "value2"}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(IEnumerable<string>),
|
|
||||||
new[] {"value1", "value2"}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(IReadOnlyList<string>),
|
|
||||||
new[] {"value1", "value2"}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(List<string>),
|
|
||||||
new List<string> {"value1", "value2"}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"value1", "value2"}),
|
|
||||||
typeof(HashSet<string>),
|
|
||||||
new HashSet<string> {"value1", "value2"}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_ConvertOptionInput_Negative()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "1234.5"),
|
|
||||||
typeof(int)
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", new[] {"123", "456"}),
|
|
||||||
typeof(int)
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option"),
|
|
||||||
typeof(int)
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new CommandOptionInput("option", "123"),
|
|
||||||
typeof(TestNonStringParseable)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_ConvertOptionInput))]
|
|
||||||
public void ConvertOptionInput_Test(CommandOptionInput optionInput, Type targetType,
|
|
||||||
object expectedConvertedValue)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var converter = new CommandOptionInputConverter();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var convertedValue = converter.ConvertOptionInput(optionInput, targetType);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
convertedValue.Should().BeEquivalentTo(expectedConvertedValue);
|
|
||||||
convertedValue?.Should().BeAssignableTo(targetType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_ConvertOptionInput_Negative))]
|
|
||||||
public void ConvertOptionInput_Negative_Test(CommandOptionInput optionInput, Type targetType)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var converter = new CommandOptionInputConverter();
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
converter.Invoking(c => c.ConvertOptionInput(optionInput, targetType))
|
|
||||||
.Should().ThrowExactly<CliFxException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCommands;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class CommandSchemaResolverTests
|
|
||||||
{
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(DivideCommand), typeof(ConcatCommand)},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new CommandSchema(typeof(DivideCommand), "div", "Divide one number by another.",
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Dividend)),
|
|
||||||
"dividend", 'D', true, "The number to divide."),
|
|
||||||
new CommandOptionSchema(typeof(DivideCommand).GetProperty(nameof(DivideCommand.Divisor)),
|
|
||||||
"divisor", 'd', true, "The number to divide by.")
|
|
||||||
}),
|
|
||||||
new CommandSchema(typeof(ConcatCommand), "concat", "Concatenate strings.",
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Inputs)),
|
|
||||||
null, 'i', true, "Input strings."),
|
|
||||||
new CommandOptionSchema(typeof(ConcatCommand).GetProperty(nameof(ConcatCommand.Separator)),
|
|
||||||
null, 's', false, "String separator.")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new[] {typeof(HelloWorldDefaultCommand)},
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
new CommandSchema(typeof(HelloWorldDefaultCommand), null, null, new CommandOptionSchema[0])
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas_Negative()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(new object[]
|
|
||||||
{
|
|
||||||
new Type[0]
|
|
||||||
});
|
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
|
||||||
{
|
|
||||||
new[] {typeof(NonImplementedCommand)}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
|
||||||
{
|
|
||||||
new[] {typeof(NonAnnotatedCommand)}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
|
||||||
{
|
|
||||||
new[] {typeof(DuplicateOptionNamesCommand)}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
|
||||||
{
|
|
||||||
new[] {typeof(DuplicateOptionShortNamesCommand)}
|
|
||||||
});
|
|
||||||
|
|
||||||
yield return new TestCaseData(new object[]
|
|
||||||
{
|
|
||||||
new[] {typeof(ExceptionCommand), typeof(CommandExceptionCommand)}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas))]
|
|
||||||
public void GetCommandSchemas_Test(IReadOnlyList<Type> commandTypes,
|
|
||||||
IReadOnlyList<CommandSchema> expectedCommandSchemas)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var commandSchemaResolver = new CommandSchemaResolver();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var commandSchemas = commandSchemaResolver.GetCommandSchemas(commandTypes);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
commandSchemas.Should().BeEquivalentTo(expectedCommandSchemas);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_GetCommandSchemas_Negative))]
|
|
||||||
public void GetCommandSchemas_Negative_Test(IReadOnlyList<Type> commandTypes)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var resolver = new CommandSchemaResolver();
|
|
||||||
|
|
||||||
// Act & Assert
|
|
||||||
resolver.Invoking(r => r.GetCommandSchemas(commandTypes))
|
|
||||||
.Should().ThrowExactly<CliFxException>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCommands;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class DelegateCommandFactoryTests
|
|
||||||
{
|
|
||||||
private static CommandSchema GetCommandSchema(Type commandType) =>
|
|
||||||
new CommandSchemaResolver().GetCommandSchemas(new[] {commandType}).Single();
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_CreateCommand()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
new Func<CommandSchema, ICommand>(schema => (ICommand) Activator.CreateInstance(schema.Type)),
|
|
||||||
GetCommandSchema(typeof(HelloWorldDefaultCommand))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_CreateCommand))]
|
|
||||||
public void CreateCommand_Test(Func<CommandSchema, ICommand> factoryMethod, CommandSchema commandSchema)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var factory = new DelegateCommandFactory(factoryMethod);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var command = factory.CreateCommand(commandSchema);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
command.Should().BeOfType(commandSchema.Type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using CliFx.Models;
|
|
||||||
using CliFx.Services;
|
|
||||||
using CliFx.Tests.TestCommands;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class HelpTextRendererTests
|
|
||||||
{
|
|
||||||
private static HelpTextSource CreateHelpTextSource(IReadOnlyList<Type> availableCommandTypes, Type targetCommandType)
|
|
||||||
{
|
|
||||||
var commandSchemaResolver = new CommandSchemaResolver();
|
|
||||||
|
|
||||||
var applicationMetadata = new ApplicationMetadata("TestApp", "testapp", "1.0", null);
|
|
||||||
var availableCommandSchemas = commandSchemaResolver.GetCommandSchemas(availableCommandTypes);
|
|
||||||
var targetCommandSchema = availableCommandSchemas.Single(s => s.Type == targetCommandType);
|
|
||||||
|
|
||||||
return new HelpTextSource(applicationMetadata, availableCommandSchemas, targetCommandSchema);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<TestCaseData> GetTestCases_RenderHelpText()
|
|
||||||
{
|
|
||||||
yield return new TestCaseData(
|
|
||||||
CreateHelpTextSource(
|
|
||||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
|
||||||
typeof(HelpDefaultCommand)),
|
|
||||||
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
"Description",
|
|
||||||
"HelpDefaultCommand description.",
|
|
||||||
"Usage",
|
|
||||||
"[command]", "[options]",
|
|
||||||
"Options",
|
|
||||||
"-a|--option-a", "OptionA description.",
|
|
||||||
"-b|--option-b", "OptionB description.",
|
|
||||||
"-h|--help", "Shows help text.",
|
|
||||||
"--version", "Shows version information.",
|
|
||||||
"Commands",
|
|
||||||
"cmd", "HelpNamedCommand description.",
|
|
||||||
"You can run", "to show help on a specific command."
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
CreateHelpTextSource(
|
|
||||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
|
||||||
typeof(HelpNamedCommand)),
|
|
||||||
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
"Description",
|
|
||||||
"HelpNamedCommand description.",
|
|
||||||
"Usage",
|
|
||||||
"cmd", "[command]", "[options]",
|
|
||||||
"Options",
|
|
||||||
"-c|--option-c", "OptionC description.",
|
|
||||||
"-d|--option-d", "OptionD description.",
|
|
||||||
"-h|--help", "Shows help text.",
|
|
||||||
"Commands",
|
|
||||||
"sub", "HelpSubCommand description.",
|
|
||||||
"You can run", "to show help on a specific command."
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
yield return new TestCaseData(
|
|
||||||
CreateHelpTextSource(
|
|
||||||
new[] {typeof(HelpDefaultCommand), typeof(HelpNamedCommand), typeof(HelpSubCommand)},
|
|
||||||
typeof(HelpSubCommand)),
|
|
||||||
|
|
||||||
new[]
|
|
||||||
{
|
|
||||||
"Description",
|
|
||||||
"HelpSubCommand description.",
|
|
||||||
"Usage",
|
|
||||||
"cmd sub", "[options]",
|
|
||||||
"Options",
|
|
||||||
"-e|--option-e", "OptionE description.",
|
|
||||||
"-h|--help", "Shows help text."
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCaseSource(nameof(GetTestCases_RenderHelpText))]
|
|
||||||
public void RenderHelpText_Test(HelpTextSource source,
|
|
||||||
IReadOnlyList<string> expectedSubstrings)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
using (var stdout = new StringWriter())
|
|
||||||
{
|
|
||||||
var console = new VirtualConsole(stdout);
|
|
||||||
var renderer = new HelpTextRenderer();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
renderer.RenderHelpText(console, source);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
stdout.ToString().Should().ContainAll(expectedSubstrings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
using System;
|
|
||||||
using CliFx.Services;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class SystemConsoleTests
|
|
||||||
{
|
|
||||||
[TearDown]
|
|
||||||
public void TearDown()
|
|
||||||
{
|
|
||||||
// Reset console color so it doesn't carry on into next tests
|
|
||||||
Console.ResetColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure console correctly wraps around System.Console
|
|
||||||
[Test]
|
|
||||||
public void All_Smoke_Test()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var console = new SystemConsole();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
console.ResetColor();
|
|
||||||
console.ForegroundColor = ConsoleColor.DarkMagenta;
|
|
||||||
console.BackgroundColor = ConsoleColor.DarkMagenta;
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
console.Input.Should().BeSameAs(Console.In);
|
|
||||||
console.IsInputRedirected.Should().Be(Console.IsInputRedirected);
|
|
||||||
console.Output.Should().BeSameAs(Console.Out);
|
|
||||||
console.IsOutputRedirected.Should().Be(Console.IsOutputRedirected);
|
|
||||||
console.Error.Should().BeSameAs(Console.Error);
|
|
||||||
console.IsErrorRedirected.Should().Be(Console.IsErrorRedirected);
|
|
||||||
console.ForegroundColor.Should().Be(Console.ForegroundColor);
|
|
||||||
console.BackgroundColor.Should().Be(Console.BackgroundColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using CliFx.Services;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.Services
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class VirtualConsoleTests
|
|
||||||
{
|
|
||||||
// Make sure console uses specified streams and doesn't leak to System.Console
|
|
||||||
[Test]
|
|
||||||
public void All_Smoke_Test()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
using (var stdin = new StringReader("hello world"))
|
|
||||||
using (var stdout = new StringWriter())
|
|
||||||
using (var stderr = new StringWriter())
|
|
||||||
{
|
|
||||||
var console = new VirtualConsole(stdin, stdout, stderr);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
console.ResetColor();
|
|
||||||
console.ForegroundColor = ConsoleColor.DarkMagenta;
|
|
||||||
console.BackgroundColor = ConsoleColor.DarkMagenta;
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
console.Input.Should().BeSameAs(stdin);
|
|
||||||
console.Input.Should().NotBeSameAs(Console.In);
|
|
||||||
console.IsInputRedirected.Should().BeTrue();
|
|
||||||
console.Output.Should().BeSameAs(stdout);
|
|
||||||
console.Output.Should().NotBeSameAs(Console.Out);
|
|
||||||
console.IsOutputRedirected.Should().BeTrue();
|
|
||||||
console.Error.Should().BeSameAs(stderr);
|
|
||||||
console.Error.Should().NotBeSameAs(Console.Error);
|
|
||||||
console.IsErrorRedirected.Should().BeTrue();
|
|
||||||
console.ForegroundColor.Should().NotBe(Console.ForegroundColor);
|
|
||||||
console.BackgroundColor.Should().NotBe(Console.BackgroundColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,19 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Exceptions;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command("exc")]
|
|
||||||
public class CommandExceptionCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("code", 'c')]
|
|
||||||
public int ExitCode { get; set; } = 1337;
|
|
||||||
|
|
||||||
[CommandOption("msg", 'm')]
|
|
||||||
public string Message { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command("concat", Description = "Concatenate strings.")]
|
|
||||||
public class ConcatCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('i', IsRequired = true, Description = "Input strings.")]
|
|
||||||
public IReadOnlyList<string> Inputs { get; set; }
|
|
||||||
|
|
||||||
[CommandOption('s', Description = "String separator.")]
|
|
||||||
public string Separator { get; set; } = "";
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
console.Output.WriteLine(string.Join(Separator, Inputs));
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command("div", Description = "Divide one number by another.")]
|
|
||||||
public class DivideCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("dividend", 'D', IsRequired = true, Description = "The number to divide.")]
|
|
||||||
public double Dividend { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("divisor", 'd', IsRequired = true, Description = "The number to divide by.")]
|
|
||||||
public double Divisor { get; set; }
|
|
||||||
|
|
||||||
// This property should be ignored by resolver
|
|
||||||
public bool NotAnOption { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
console.Output.WriteLine(Dividend / Divisor);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command]
|
|
||||||
public class DuplicateOptionNamesCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("fruits")]
|
|
||||||
public string Apples { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("fruits")]
|
|
||||||
public string Oranges { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command]
|
|
||||||
public class DuplicateOptionShortNamesCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string Apples { get; set; }
|
|
||||||
|
|
||||||
[CommandOption('f')]
|
|
||||||
public string Oranges { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command("exc")]
|
|
||||||
public class ExceptionCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("msg", 'm')]
|
|
||||||
public string Message { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => throw new Exception(Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command]
|
|
||||||
public class HelloWorldDefaultCommand : ICommand
|
|
||||||
{
|
|
||||||
public Task ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
console.Output.WriteLine("Hello world.");
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command(Description = "HelpDefaultCommand description.")]
|
|
||||||
public class HelpDefaultCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("option-a", 'a', Description = "OptionA description.")]
|
|
||||||
public string OptionA { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("option-b", 'b', Description = "OptionB description.")]
|
|
||||||
public string OptionB { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command("cmd", Description = "HelpNamedCommand description.")]
|
|
||||||
public class HelpNamedCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("option-c", 'c', Description = "OptionC description.")]
|
|
||||||
public string OptionC { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("option-d", 'd', Description = "OptionD description.")]
|
|
||||||
public string OptionD { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
[Command("cmd sub", Description = "HelpSubCommand description.")]
|
|
||||||
public class HelpSubCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandOption("option-e", 'e', Description = "OptionE description.")]
|
|
||||||
public string OptionE { get; set; }
|
|
||||||
|
|
||||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx.Services;
|
|
||||||
|
|
||||||
namespace CliFx.Tests.TestCommands
|
|
||||||
{
|
|
||||||
public class NonAnnotatedCommand : ICommand
|
|
||||||
{
|
|
||||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user