159 Commits
0.0.2 ... 1.6

Author SHA1 Message Date
Tyrrrz
90a01e729b Update version 2020-12-06 18:42:58 +02:00
Tyrrrz
ac01c2aecb Allow dashes to appear in parameter values 2020-12-06 18:39:03 +02:00
Tyrrrz
4acffe925c Revert help text changes in the command section
Not happy with how it looks right now. Confusing to the user.
2020-12-06 18:24:30 +02:00
Tyrrrz
18f53eeeef Simplify gitignore and remove gitattributes 2020-12-06 17:58:02 +02:00
Tyrrrz
03d6942540 Require options to begin with a letter character
Fixes #88
2020-11-29 17:14:23 +02:00
Tyrrrz
9be811a89a Refactor ArgumentValueConverter into a class 2020-11-18 18:37:20 +02:00
Tyrrrz
f9f5a4696b Add analyzers for invalid validators 2020-11-18 18:26:04 +02:00
Tyrrrz
d6da687170 Refactor recent PRs 2020-11-18 18:00:43 +02:00
Tyrrrz
eba66d0878 Simplify coverage collection 2020-11-11 01:20:08 +02:00
Tyrrrz
8c682766bd Bump target frameworks on peripheral projects 2020-11-10 23:44:51 +02:00
Tyrrrz
39d626c8d8 Update build infra 2020-11-10 23:30:36 +02:00
Alexey Golub
a338ac8ce2 Custom value validators (#87) 2020-11-09 17:21:18 +02:00
Oleksandr Shustov
11637127cb remove redundant space 2020-11-08 20:01:47 +02:00
Oleksandr Shustov
4e12aefafb add tests 2020-11-07 21:46:32 +02:00
Oleksandr Shustov
144d3592fb cleanup after merge 2020-11-07 21:34:12 +02:00
Oleksandr Shustov
6f82c2f0f9 merge branch master into feature/custom-validators 2020-11-07 21:18:34 +02:00
Oleksandr Shustov
b8c60717d5 add a base type for custom validators 2020-11-06 20:37:46 +02:00
Rene Escalante
fec6850c39 Change format for the command section in help text (#83) 2020-10-29 20:31:03 +02:00
Tyrrrz
6a378ad946 Update nuget packages 2020-10-27 17:20:07 +02:00
Tyrrrz
11579f11b1 Update readme 2020-10-25 01:55:12 +03:00
Tyrrrz
60a3b26fd1 Update version 2020-10-23 23:46:57 +03:00
Tyrrrz
3abdfb1acf Improve child command usage info in help text 2020-10-23 23:36:36 +03:00
Tyrrrz
9557d386e2 Better success check in stack trace parsing 2020-10-23 23:00:29 +03:00
Tyrrrz
d0d024c427 Improve stack trace parsing 2020-10-23 22:57:48 +03:00
Tyrrrz
f765af6061 Update readme with info about custom converters 2020-10-23 22:03:20 +03:00
Tyrrrz
7f2202e869 Cleanup 2020-10-23 21:40:53 +03:00
Tyrrrz
14ad9d5738 Improve tests 2020-10-23 21:18:57 +03:00
Tyrrrz
b120138de3 Update github actions 2020-10-23 20:53:27 +03:00
Tyrrrz
8df1d607c1 Refactor & improve argument conversion feature 2020-10-23 20:52:26 +03:00
Tyrrrz
c06f2810b9 Cleanup analyzers 2020-10-23 18:23:58 +03:00
Tyrrrz
d52a205f13 Improve coverage slightly 2020-10-23 18:17:17 +03:00
Tyrrrz
0ec12e57c1 Refactor pretty stack traces 2020-10-23 18:01:40 +03:00
Nikiforov Alexey
c322b7029c Add child command usage in help text (#77) 2020-10-22 16:02:58 +03:00
Oleksandr Shustov
6a38c04c11 Custom value converters (#81) 2020-10-16 14:22:42 +03:00
Alexey Golub
5e53107def Treat nullability warnings as errors 2020-10-07 15:20:35 +03:00
Alexey Golub
36cea937de Update nuget packages 2020-10-07 15:11:37 +03:00
Mårten Åsberg
438d6b98ac Pretty printing of exception messages (#79) 2020-10-06 20:43:19 +03:00
Alexey Golub
8e1488c395 Update readme 2020-09-14 22:31:25 +03:00
Alexey Golub
65d321b476 Update readme 2020-08-20 19:31:26 +03:00
Alexey Golub
c6d2359d6b Update version 2020-08-20 17:02:19 +03:00
Alexey Golub
0d32876bad Add VirtualConsole.CreateBuffered() for easier testing 2020-08-20 16:27:23 +03:00
Alexey Golub
c063251d89 Exclude some ToString() methods from coverage
These are only used for debug information and I'm okayish with them failing at runtime.
2020-08-19 22:39:43 +03:00
Alexey Golub
3831cfc7c0 Get rid of internal tests
Move all tests to e2e+ level
2020-08-19 22:31:09 +03:00
Alexey Golub
b17341b56c Update version 2020-07-31 15:41:47 +03:00
Alexey Golub
5bda964fb5 Cleanup 2020-07-31 15:34:38 +03:00
Daniel Hix
432430489a Add error for non-scalar parameters bound without any values (#71) 2020-07-31 15:08:13 +03:00
Ron Myers
9a20101f30 Fix application crashes if there are two environment variables with same name, differing only in case (#67) 2020-07-28 14:20:02 +03:00
Alexey Golub
b491818779 Update version 2020-07-19 18:20:42 +03:00
Alexey Golub
69c24c8dfc Refactor 2020-07-19 18:11:54 +03:00
Ihor Nechyporuk
004f906148 Fix exit code overflow for unhandled exceptions on Unix systems (#62) 2020-07-19 16:50:37 +03:00
Volodymyr Shkolka
ac83233dc2 Add ability to specify active debugger attachment instead of passive (#61) 2020-07-10 13:54:09 +03:00
Alexey Golub
082910c968 Update readme 2020-05-24 12:46:16 +03:00
Alexey Golub
11e3e0f85d Update version 2020-05-23 19:02:48 +03:00
Alexey Golub
42f4d7d5a7 Use Stream.Synchronized 2020-05-23 18:48:46 +03:00
Alexey Golub
bed22b6500 Refactor (#56) 2020-05-23 18:45:07 +03:00
Alexey Golub
17449e0794 Remove unused dummy commands 2020-05-16 22:16:42 +03:00
Alexey Golub
4732166f5f Refactor 2020-05-16 21:54:16 +03:00
Alexey Golub
f5e37b96fc Default to semantic representation of assembly version in help text 2020-05-16 14:49:25 +03:00
Domn Werner
4cef596fe8 Show default values in help (#54) 2020-05-16 14:11:23 +03:00
Alexey Golub
19b87717c1 [Analyzers] Switch from warnings to errors where relevant 2020-05-13 23:15:46 +03:00
Alexey Golub
7e4c6b20ff Update readme 2020-05-12 20:36:38 +03:00
Alexey Golub
fb2071ed2b Update readme 2020-05-11 21:53:22 +03:00
Alexey Golub
7d2f934310 Update version 2020-05-11 21:29:26 +03:00
Alexey Golub
95a00b0952 Improve error messages 2020-05-11 21:28:49 +03:00
Alexey Golub
cb3fee65f3 [Analyzers] Update descriptors 2020-05-11 18:30:19 +03:00
Alexey Golub
65628b145a Move extensions closer to the actual classes 2020-05-11 16:51:36 +03:00
Alexey Golub
802bbfccc6 Add CursorLeft and CursorTop to IConsole
Closes #25
2020-05-11 16:29:54 +03:00
Alexey Golub
6e7742a4f3 Show valid values for parameters too 2020-05-08 16:40:19 +03:00
Alexey Golub
f6a1a40471 Cleanup 2020-05-08 16:33:28 +03:00
Domn Werner
33ca4da260 Show valid values of an enum option in help (#53) 2020-05-08 12:40:23 +03:00
Alexey Golub
cbb72b16ae Refactor a bit 2020-05-05 22:23:27 +03:00
Alexey Golub
c58629e999 Pack the analyzers together with CliFx in the main nupkg 2020-04-25 22:25:16 +03:00
Domn Werner
387fb72718 Print help text on specific domain exceptions (#51) 2020-04-25 21:59:03 +03:00
Alexey Golub
e04f0da318 [Analyzers] Remove redundant parameter check 2020-04-25 18:07:28 +03:00
Alexey Golub
d25873ee10 Add CliFx.Analyzers (#50) 2020-04-25 18:03:21 +03:00
Domn Werner
a28223fc8b Show help text on demand (#49) 2020-04-23 10:33:12 +03:00
Alexey Golub
1dab27de55 Fix warnings in tests 2020-04-20 17:20:17 +03:00
Alexey Golub
698629b153 Disable nullability checks for older target frameworks 2020-04-20 17:11:15 +03:00
Alexey Golub
65b66b0d27 Improve exceptions 2020-04-20 16:43:43 +03:00
Alexey Golub
7d3ba612c4 Validate option name length
Closes #40
2020-04-16 16:51:51 +03:00
Alexey Golub
8c3b8d1f49 Throw when a required option is set but doesn't have a value
Closes #47
2020-04-16 16:02:21 +03:00
Alexey Golub
fdd39855ad Use GitHub Actions test logger 2020-03-23 18:15:04 +02:00
Alexey Golub
671532efce Update version 2020-03-16 20:37:43 +02:00
Alexey Golub
5b124345b0 Update readme 2020-03-16 20:29:56 +02:00
Alexey Golub
b812bd1423 Allow mixed naming when setting an option to multiple values 2020-03-16 19:47:51 +02:00
Alexey Golub
c854f5fb8d Throw errors on unrecognized input
Closes #38
Closes #24
2020-03-16 14:48:48 +02:00
Alexey Golub
f38bd32510 Run CI on multiple platforms 2020-03-16 01:21:22 +02:00
Alexey Golub
765fa5503e Update benchmarks 2020-03-16 01:13:38 +02:00
Alexey Golub
57f168723b Rework tests from 1-to-1 mapping into specifications (#46) 2020-03-16 01:03:03 +02:00
Alexey Golub
79e1a2e3d7 Expose raw streams in IConsole to allow writing/reading binary data 2020-03-11 23:23:01 +02:00
Alexey Golub
f4f6d04857 Update GitHub Actions workflows 2020-02-16 20:35:46 +02:00
Alexey Golub
015ede0d15 Update readme 2020-02-12 00:52:05 +02:00
Alexey Golub
4fd7f7c3ca Update benchmark results 2020-02-04 20:34:17 +02:00
Alexey Golub
896dd49eb4 Cleanup benchmarks 2020-02-04 19:45:33 +02:00
Alexey Golub
4365ad457a Update version 2020-01-30 12:31:37 +02:00
Alexey Golub
fb3617980e Add argument syntax info to readme 2020-01-30 12:28:56 +02:00
Alexey Golub
7690aae456 Update readme 2020-01-30 11:48:31 +02:00
Alexey Golub
076678a08c Change how headers are rendered in help text 2020-01-30 11:44:36 +02:00
Alexey Golub
104279d6e9 Change how non-scalar arguments are displayed in usage 2020-01-30 10:58:36 +02:00
Alexey Golub
515d51a91d Add dummy tests to cover difficult scenarios 2020-01-29 23:35:45 +02:00
Alexey Golub
4fdf543190 Ensure delegate type activator doesn't return null 2020-01-29 20:03:06 +02:00
Alexey Golub
4e1ab096c9 Add info about environment variables 2020-01-29 19:40:58 +02:00
Alexey Golub
8aa6911cca Update readme 2020-01-29 10:59:45 +02:00
Alexey Golub
f0362019ed Use lowercase default display name for parameters 2020-01-28 22:24:38 +02:00
Alexey Golub
82895f2e42 Improve preview directive 2020-01-28 14:48:21 +02:00
Alexey Golub
4cf622abe5 Add Cocona to benchmarks 2020-01-27 21:50:25 +02:00
Alexey Golub
d4e22a78d6 Change to a more permissive license 2020-01-27 21:35:14 +02:00
Alexey Golub
3883c831e9 Rework (#36) 2020-01-27 21:10:14 +02:00
dgarcia202
63441688fe Add required options to the usage help text (#35) 2020-01-17 23:05:01 +02:00
Thorkil Holm-Jacobsen
e48839b938 Add positional arguments (#32) 2020-01-13 13:31:05 +02:00
Alexey Golub
ed87373dc3 Make exceptions slightly more friendly 2019-12-16 22:58:31 +02:00
Alexey Golub
6ce52c70f7 Use ValueTask 2019-12-16 22:16:16 +02:00
Alexey Golub
d2b0b16121 Use shared props files 2019-12-01 16:08:41 +02:00
Alexey Golub
d67a9fe762 Update readme 2019-11-30 23:32:06 +02:00
Alexey Golub
ce2a3153e6 Update donation info 2019-11-19 21:14:15 +02:00
Alexey Golub
d4b54231fb Remove unnecessary CD step 2019-11-13 20:41:57 +02:00
Alexey Golub
70bfe0bf91 Update version 2019-11-13 20:34:11 +02:00
Alexey Golub
9690c380d3 Use C#8 features and cleanup 2019-11-13 20:31:48 +02:00
Alexey Golub
85caa275ae Add source link 2019-11-12 22:26:29 +02:00
Federico Paolillo
32026e59c0 Use Path.Separator in environment variables tests (#31) 2019-11-09 13:06:00 +02:00
Alexey Golub
486ccb9685 Update csproj 2019-11-08 13:21:53 +02:00
Alexey Golub
7b766f70f3 Use GitHub actions 2019-11-06 15:08:51 +02:00
Alexey Golub
f73e96488f Update version 2019-10-31 14:42:30 +02:00
Moophic
af63fa5a1f Refactor cancellation (#30) 2019-10-31 14:39:56 +02:00
Moophic
e8f53c9463 Updated readme with cancellation info (#29) 2019-10-30 19:49:43 +02:00
Alexey Golub
9564cd5d30 Update version 2019-10-30 18:41:24 +02:00
Moophic
ed458c3980 Cancellation support (#28) 2019-10-30 18:37:32 +02:00
Alexey Golub
25538f99db Migrate from PackageIconUrl to PackageIcon 2019-10-08 16:59:13 +03:00
Federico Paolillo
36436e7a4b Environment variables (#27) 2019-09-29 20:44:24 +03:00
Alexey Golub
a6070332c9 Migrate to .NET Core 3 where applicable 2019-09-25 22:52:33 +03:00
Alexey Golub
25cbfdb4b8 Move screenshots to repository 2019-09-06 20:24:28 +03:00
Alexey Golub
d1b5107c2c Update version 2019-08-26 20:48:43 +03:00
Alexey Golub
03873d63cd Fix exception when converting option values to array when there's only one value 2019-08-26 20:47:23 +03:00
Alexey Golub
89aba39964 Add extensibility point for injecting custom option converters
Closes #19
2019-08-26 20:10:37 +03:00
Alexey Golub
ab57a103d1 Update benchmarks 2019-08-26 17:20:14 +03:00
Alexey Golub
d0b2ebc061 Update readme 2019-08-25 23:27:19 +03:00
Alexey Golub
857257ca73 Update version 2019-08-25 23:19:10 +03:00
Alexey Golub
3587155c7e Update readme 2019-08-25 23:17:58 +03:00
Alexey Golub
ae05e0db96 Refactor 2019-08-25 22:08:34 +03:00
Alexey Golub
41c0493e66 Refactor tests again 2019-08-25 18:26:40 +03:00
Alexey Golub
43a304bb26 Refactor tests 2019-08-25 17:28:54 +03:00
Alexey Golub
cd3892bf83 Refactor CliApplication.RunAsync using chain of responsibility 2019-08-25 14:54:29 +03:00
Alexey Golub
3f7c02342d Add smoke tests for VirtualConsole 2019-08-25 11:30:06 +03:00
Alexey Golub
c65cdf465e Remove dummy tests 2019-08-24 23:25:41 +03:00
Alexey Golub
b5d67ecf24 Fix not printing version when requested if used with stub default command 2019-08-24 22:46:10 +03:00
Alexey Golub
a94b2296e1 Add tests for CommandInitializer that verify that short name comparison is case sensitive 2019-08-24 22:44:11 +03:00
Alexey Golub
fa05e4df3f Rework schema validation in CommandSchemaResolver 2019-08-24 22:23:12 +03:00
Alexey Golub
b70b25076e Add smoke tests for CliApplicationBuilder 2019-08-24 18:31:17 +03:00
Alexey Golub
0662f341e6 Rename some methods 2019-08-24 18:25:56 +03:00
Alexey Golub
80bf477f3b Add support for directives (debug and preview)
Closes #7
Closes #8
2019-08-24 18:22:54 +03:00
Alexey Golub
e4a502d9d6 Rename ProgressReporter to ProgressTicker 2019-08-24 13:00:13 +03:00
Alexey Golub
13b15b98ed Add ProgressReporter
Closes #14
2019-08-23 22:50:43 +03:00
Alexey Golub
80465e0e51 Move tests into corresponding namespaces 2019-08-23 17:01:49 +03:00
Alexey Golub
9a1ce7e7e5 Add 1 more negative test for CommandSchemaResolver 2019-08-22 12:08:08 +03:00
Alexey Golub
b45da64664 Make CommandAttribute non-optional on command types 2019-08-21 21:04:42 +03:00
Alexey Golub
df01dc055e Prepend 'v' to default version text 2019-08-21 15:55:05 +03:00
Alexey Golub
31dd24d189 Sort options when rendering help 2019-08-21 14:37:53 +03:00
Alexey Golub
2a76dfe1c8 Update version 2019-08-20 18:12:33 +03:00
Alexey Golub
59ee2e34d8 Don't add abstract and interface types that implement ICommand 2019-08-20 18:12:22 +03:00
192 changed files with 10593 additions and 5301 deletions

63
.gitattributes vendored
View File

@@ -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

3
.github/FUNDING.yml vendored
View File

@@ -1,4 +1,3 @@
github: Tyrrrz github: Tyrrrz
patreon: Tyrrrz patreon: Tyrrrz
open_collective: Tyrrrz custom: ['buymeacoffee.com/Tyrrrz', 'tyrrrz.me/donate']
custom: ['buymeacoffee.com/Tyrrrz']

25
.github/workflows/CD.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: CD
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2.3.3
- name: Install .NET
uses: actions/setup-dotnet@v1.7.2
with:
dotnet-version: 5.0.100
- name: Pack
run: dotnet pack CliFx --configuration Release
- name: Deploy
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }}

28
.github/workflows/CI.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@v2.3.3
- name: Install .NET
uses: actions/setup-dotnet@v1.7.2
with:
dotnet-version: 5.0.100
- name: Build & test
run: dotnet test --configuration Release --logger GitHubActions
- name: Upload coverage
uses: codecov/codecov-action@v1.0.5
with:
token: ${{ secrets.CODECOV_TOKEN }}

331
.gitignore vendored
View File

@@ -1,340 +1,21 @@
## 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
*.suo *.suo
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
.idea/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results # Build results
[Dd]ebug/ [Dd]ebug/
[Dd]ebugPublic/ [Dd]ebugPublic/
[Rr]elease/ [Rr]elease/
[Rr]eleases/ [Rr]eleases/
x64/ [Xx]64/
x86/ [Xx]86/
[Aa][Rr][Mm]/ [Bb]uild/
[Aa][Rr][Mm]64/
bld/ bld/
[Bb]in/ [Bb]in/
[Oo]bj/ [Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory # Coverage
.vs/ *.opencover.xml
# 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

BIN
.screenshots/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,51 @@
### v1.6 (06-Dec-2020)
- Added support for custom value validators. You can now create a type that inherits from `CliFx.ArgumentValueValidator<T>` to implement reusable validation logic for command arguments. To use a validator, include it in the `Validators` property on the `CommandOption` or `CommandParameter` attribute. (Thanks [@Oleksandr Shustov](https://github.com/AlexandrShustov))
- Added `CliFx.ArgumentValueConverter<T>` class that you can inherit from to implement custom value converters. `CliFx.IArgumentValueConverter` interface is still available, but it is recommended to inherit from the generic class instead, due to the type safety it provides. The interface may become internal or get removed in one of the future major versions.
- Updated requirements for option names and short names: short names now must be letter characters (lowercase or uppercase), while names must now start with a letter character. This means option names can no longer start with a digit or a special character. This change makes it possible to pass negative number values without the need to quote them, i.e. `--my-number -5`.
### v1.5 (23-Oct-2020)
- Added pretty-printing for unhandled exceptions thrown from within the application. This makes the errors easier to parse visually and should help in troubleshooting. This change does not affect `CommandException`, as it already has special treatment. (Thanks [@Mårten Åsberg](https://github.com/89netraM))
- Added support for custom value converters. You can now create a type that implements `CliFx.IArgumentValueConverter` and specify it as a converter for your parameters or options via the `Converter` named property. This should enable conversion between raw argument values and custom types which are not string-initializable. (Thanks [@Oleksandr Shustov](https://github.com/AlexandrShustov))
- Improved help text so that it also shows minimal usage examples for child and descendant commands, besides the actual command it was requested on. This should improve user experience for applications with many nested commands. (Thanks [@Nikiforov Alexey](https://github.com/NikiforovAll))
### v1.4 (20-Aug-2020)
- Added `VirtualConsole.CreateBuffered()` method to simplify test setup when using in-memory backing stores for output and error streams. Please refer to the readme for updated recommendations on how to test applications built with CliFx.
- Added generic `CliApplicationBuilder.AddCommand<TCommand>()`. This overload simplifies adding commands one-by-one as it also checks that the type implements `ICommand`.
### v1.3.2 (31-Jul-2020)
- Fixed an issue where a command was incorrectly allowed to execute when the user did not specify any value for a non-scalar parameter. Since they are always required, a parameter needs to be bound to (at least) one value. (Thanks [@Daniel Hix](https://github.com/ADustyOldMuffin))
- Fixed an issue where `CliApplication.RunAsync(...)` threw `ArgumentException` if there were two environment variables, whose names differed only in case. Environment variable names are now treated case-sensitively. (Thanks [@Ron Myers](https://github.com/ron-myers))
### v1.3.1 (19-Jul-2020)
- Running the application with the debug directive (`myapp [debug]`) will now also try to launch a debugger instance. In most cases it will save time as you won't need to attach the debugger manually. (Thanks [@Volodymyr Shkolka](https://github.com/BlackGad))
- Fixed an issue where unhandled generic exceptions (i.e. not `CommandException`) sometimes caused the application to incorrectly return successful exit code due to an overflow issue on Unix systems. Starting from this version, all unhandled generic exceptions will produce `1` as the exit code when thrown. Instances of `CommandException` can still be configured to return any specified exit code, but it's recommended to constrain the values between `1` and `255` to avoid overflow issues. (Thanks [@Ihor Nechyporuk](https://github.com/inech))
### v1.3 (23-May-2020)
- Changed analyzers to report errors instead of warnings. If you find that some analyzer works incorrectly, please report it on GitHub. You can also configure inspection severity overrides in your project if you need to.
- Improved help text by showing default values for non-required options. This only works on types that have a custom override for `ToString()` method. Additionally, if the type implements `IFormattable`, the overload with a format provider will be used instead. (Thanks [@Domn Werner](https://github.com/domn1995))
- Changed default version text to only show 3 version components instead of 4, if the last component (revision) is not specified or is zero. This makes the default version text compliant with semantic versioning.
- Fixed an issue where it was possible to define a command with an option that has the same name or short name as built-in help or version options. Previously it would lead to the user-defined option being ignored in favor of the built-in option. Now this will throw an exception instead.
- Changed the underlying representation of `StreamReader`/`StreamWriter` objects used in `SystemConsole` and `VirtualConsole` to be thread-safe.
### v1.2 (11-May-2020)
- Added built-in Roslyn analyzers that help catch incorrect usage of the library. Currently, all analyzers report issues as warnings so as to not prevent the project from building. In the future that may change.
- Added an optional parameter to `new CommandException(...)` called `showHelp` which can be used to instruct CliFx to show help for the current command after printing the error. (Thanks [@Domn Werner](https://github.com/domn1995))
- Improved help text shown for enum options and parameters by providing the list of valid values that the enum can accept. (Thanks [@Domn Werner](https://github.com/domn1995))
- Fixed an issue where it was possible to set an option without providing a value, while the option was marked as required.
- Fixed an issue where it was possible to configure an option with an empty name or a name consisting of a single character. If you want to use a single character as a name, you should set the option's short name instead.
- Added `CursorLeft` and `CursorTop` properties to `IConsole` and its implementations. In `VirtualConsole`, these are just auto-properties.
- Improved exception messages.
- Improved exceptions related to user input by also showing help text after the error message. (Thanks [@Domn Werner](https://github.com/domn1995))
### v1.1 (16-Mar-2020)
- Changed `IConsole` interface (and as a result, `SystemConsole` and `VirtualConsole`) to support writing binary data. Instead of `TextReader`/`TextWriter` instances, the streams are now exposed as `StreamReader`/`StreamWriter` which provide the `BaseStream` property that allows raw access. Existing usages inside commands should remain the same because `StreamReader`/`StreamWriter` are compatible with their base classes `TextReader`/`TextWriter`, but if you were using `VirtualConsole` in tests, you may have to update it to the new API. Refer to the readme for more info.
- Changed argument binding behavior so that an error is produced if the user provides an argument that doesn't match with any parameter or option. This is done in order to improve user experience, as otherwise the user may make a typo without knowing that their input wasn't taken into account.
- Changed argument binding behavior so that options can be set to multiple argument values while specifying them with mixed naming. For example, `--option value1 -o value2 --option value3` would result in the option being set to corresponding three values, assuming `--option` and `-o` match with the same option.

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers.Tests
{
public class AnalyzerTestCase
{
public string Name { get; }
public IReadOnlyList<DiagnosticDescriptor> TestedDiagnostics { get; }
public IReadOnlyList<string> SourceCodes { get; }
public AnalyzerTestCase(
string name,
IReadOnlyList<DiagnosticDescriptor> testedDiagnostics,
IReadOnlyList<string> sourceCodes)
{
Name = name;
TestedDiagnostics = testedDiagnostics;
SourceCodes = sourceCodes;
}
public AnalyzerTestCase(
string name,
IReadOnlyList<DiagnosticDescriptor> testedDiagnostics,
string sourceCode)
: this(name, testedDiagnostics, new[] {sourceCode})
{
}
public AnalyzerTestCase(
string name,
DiagnosticDescriptor testedDiagnostic,
string sourceCode)
: this(name, new[] {testedDiagnostic}, sourceCode)
{
}
public override string ToString() => $"{Name} [{string.Join(", ", TestedDiagnostics.Select(d => d.Id))}]";
}
}

View File

@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Gu.Roslyn.Asserts" Version="3.3.1" />
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
<PackageReference Include="coverlet.msbuild" Version="2.9.0" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" />
<ProjectReference Include="..\CliFx\CliFx.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,719 @@
using System.Collections.Generic;
using CliFx.Analyzers.Tests.Internal;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class CommandSchemaAnalyzerTests
{
private static DiagnosticAnalyzer Analyzer { get; } = new CommandSchemaAnalyzer();
public static IEnumerable<object[]> GetValidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Non-command type",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class Foo
{
public int Bar { get; set; } = 5;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Command implements interface and has attribute",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Command doesn't have an attribute but is an abstract type",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public abstract class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with unique order",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13)]
public string ParamA { get; set; }
[CommandParameter(15)]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with unique names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13, Name = ""foo"")]
public string ParamA { get; set; }
[CommandParameter(15, Name = ""bar"")]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Single non-scalar parameter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public string ParamA { get; set; }
[CommandParameter(2)]
public HashSet<string> ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Non-scalar parameter is last in order",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public string ParamA { get; set; }
[CommandParameter(2)]
public IReadOnlyList<string> ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with valid converter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyConverter : ArgumentValueConverter<string>
{
public string ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with valid validator",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyValidator : ArgumentValueValidator<string>
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a proper name",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a proper name and short name",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"", 'f')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with unique names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string OptionA { get; set; }
[CommandOption(""bar"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with unique short names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string OptionA { get; set; }
[CommandOption('x')]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with unique environment variable names",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('a', EnvironmentVariableName = ""env_var_a"")]
public string OptionA { get; set; }
[CommandOption('b', EnvironmentVariableName = ""env_var_b"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with valid converter",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyConverter : ArgumentValueConverter<string>
{
public string ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Converter = typeof(MyConverter))]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with valid validator",
Analyzer.SupportedDiagnostics,
// language=cs
@"
public class MyValidator : ArgumentValueValidator<string>
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Validators = new[] {typeof(MyValidator)})]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
public static IEnumerable<object[]> GetInvalidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Command is missing the attribute",
DiagnosticDescriptors.CliFx0002,
// language=cs
@"
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Command doesn't implement the interface",
DiagnosticDescriptors.CliFx0001,
// language=cs
@"
[Command]
public class MyCommand
{
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with duplicate order",
DiagnosticDescriptors.CliFx0021,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13)]
public string ParamA { get; set; }
[CommandParameter(13)]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameters with duplicate names",
DiagnosticDescriptors.CliFx0022,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(13, Name = ""foo"")]
public string ParamA { get; set; }
[CommandParameter(15, Name = ""foo"")]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Multiple non-scalar parameters",
DiagnosticDescriptors.CliFx0023,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public IReadOnlyList<string> ParamA { get; set; }
[CommandParameter(2)]
public HashSet<string> ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Non-last non-scalar parameter",
DiagnosticDescriptors.CliFx0024,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandParameter(1)]
public IReadOnlyList<string> ParamA { get; set; }
[CommandParameter(2)]
public string ParamB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with invalid converter",
DiagnosticDescriptors.CliFx0025,
// language=cs
@"
public class MyConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Converter = typeof(MyConverter))]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Parameter with invalid validator",
DiagnosticDescriptors.CliFx0026,
// language=cs
@"
public class MyValidator
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandParameter(0, Validators = new[] {typeof(MyValidator)})]
public string Param { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with an empty name",
DiagnosticDescriptors.CliFx0041,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption("""")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a name which is too short",
DiagnosticDescriptors.CliFx0042,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""a"")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with duplicate names",
DiagnosticDescriptors.CliFx0043,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""foo"")]
public string OptionA { get; set; }
[CommandOption(""foo"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with duplicate short names",
DiagnosticDescriptors.CliFx0044,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('f')]
public string OptionA { get; set; }
[CommandOption('f')]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Options with duplicate environment variable names",
DiagnosticDescriptors.CliFx0045,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('a', EnvironmentVariableName = ""env_var"")]
public string OptionA { get; set; }
[CommandOption('b', EnvironmentVariableName = ""env_var"")]
public string OptionB { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with invalid converter",
DiagnosticDescriptors.CliFx0046,
// language=cs
@"
public class MyConverter
{
public object ConvertFrom(string value) => value;
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Converter = typeof(MyConverter))]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with invalid validator",
DiagnosticDescriptors.CliFx0047,
// language=cs
@"
public class MyValidator
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
[Command]
public class MyCommand : ICommand
{
[CommandOption('o', Validators = new[] {typeof(MyValidator)})]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a name that doesn't start with a letter character",
DiagnosticDescriptors.CliFx0048,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption(""0foo"")]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Option with a short name that isn't a letter character",
DiagnosticDescriptors.CliFx0049,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
[CommandOption('0')]
public string Option { get; set; }
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
[Theory]
[MemberData(nameof(GetValidCases))]
public void Valid(AnalyzerTestCase testCase) =>
Analyzer.Should().NotProduceDiagnostics(testCase);
[Theory]
[MemberData(nameof(GetInvalidCases))]
public void Invalid(AnalyzerTestCase testCase) =>
Analyzer.Should().ProduceDiagnostics(testCase);
}
}

View File

@@ -0,0 +1,144 @@
using System.Collections.Generic;
using CliFx.Analyzers.Tests.Internal;
using Microsoft.CodeAnalysis.Diagnostics;
using Xunit;
namespace CliFx.Analyzers.Tests
{
public class ConsoleUsageAnalyzerTests
{
private static DiagnosticAnalyzer Analyzer { get; } = new ConsoleUsageAnalyzer();
public static IEnumerable<object[]> GetValidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Using console abstraction",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Console abstraction is not available in scope",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public void SomeOtherMethod() => Console.WriteLine(""Test"");
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
public static IEnumerable<object[]> GetInvalidCases()
{
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction in the ExecuteAsync method",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction in the ExecuteAsync method when writing stderr",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
Console.Error.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction while referencing System.Console by full name",
Analyzer.SupportedDiagnostics,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
System.Console.Error.WriteLine(""Hello world"");
return default;
}
}"
)
};
yield return new object[]
{
new AnalyzerTestCase(
"Not using available console abstraction in another method",
DiagnosticDescriptors.CliFx0100,
// language=cs
@"
[Command]
public class MyCommand : ICommand
{
public void SomeOtherMethod(IConsole console) => Console.WriteLine(""Test"");
public ValueTask ExecuteAsync(IConsole console) => default;
}"
)
};
}
[Theory]
[MemberData(nameof(GetValidCases))]
public void Valid(AnalyzerTestCase testCase) =>
Analyzer.Should().NotProduceDiagnostics(testCase);
[Theory]
[MemberData(nameof(GetInvalidCases))]
public void Invalid(AnalyzerTestCase testCase) =>
Analyzer.Should().ProduceDiagnostics(testCase);
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FluentAssertions.Execution;
using FluentAssertions.Primitives;
using Gu.Roslyn.Asserts;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers.Tests.Internal
{
internal partial class AnalyzerAssertions : ReferenceTypeAssertions<DiagnosticAnalyzer, AnalyzerAssertions>
{
protected override string Identifier { get; } = "analyzer";
public AnalyzerAssertions(DiagnosticAnalyzer analyzer)
: base(analyzer)
{
}
public void ProduceDiagnostics(
IReadOnlyList<DiagnosticDescriptor> diagnostics,
IReadOnlyList<string> sourceCodes)
{
var producedDiagnostics = GetProducedDiagnostics(Subject, sourceCodes);
var expectedIds = diagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var producedIds = producedDiagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var result = expectedIds.Intersect(producedIds).Count() == expectedIds.Length;
Execute.Assertion.ForCondition(result).FailWith($@"
Expected and produced diagnostics do not match.
Expected: {string.Join(", ", expectedIds)}
Produced: {(producedIds.Any() ? string.Join(", ", producedIds) : "<none>")}
".Trim());
}
public void ProduceDiagnostics(AnalyzerTestCase testCase) =>
ProduceDiagnostics(testCase.TestedDiagnostics, testCase.SourceCodes);
public void NotProduceDiagnostics(
IReadOnlyList<DiagnosticDescriptor> diagnostics,
IReadOnlyList<string> sourceCodes)
{
var producedDiagnostics = GetProducedDiagnostics(Subject, sourceCodes);
var expectedIds = diagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var producedIds = producedDiagnostics.Select(d => d.Id).Distinct().OrderBy(d => d).ToArray();
var result = !expectedIds.Intersect(producedIds).Any();
Execute.Assertion.ForCondition(result).FailWith($@"
Expected no produced diagnostics.
Produced: {string.Join(", ", producedIds)}
".Trim());
}
public void NotProduceDiagnostics(AnalyzerTestCase testCase) =>
NotProduceDiagnostics(testCase.TestedDiagnostics, testCase.SourceCodes);
}
internal partial class AnalyzerAssertions
{
private static IReadOnlyList<MetadataReference> DefaultMetadataReferences { get; } =
MetadataReferences.Transitive(typeof(CliApplication).Assembly).ToArray();
private static string WrapCodeWithUsingDirectives(string code)
{
var usingDirectives = new[]
{
"using System;",
"using System.Collections.Generic;",
"using System.Threading.Tasks;",
"using CliFx;",
"using CliFx.Attributes;",
"using CliFx.Exceptions;",
"using CliFx.Utilities;"
};
return
string.Join(Environment.NewLine, usingDirectives) +
Environment.NewLine +
code;
}
private static IReadOnlyList<Diagnostic> GetProducedDiagnostics(
DiagnosticAnalyzer analyzer,
IReadOnlyList<string> sourceCodes)
{
var compilationOptions = new CSharpCompilationOptions(OutputKind.ConsoleApplication);
var wrappedSourceCodes = sourceCodes.Select(WrapCodeWithUsingDirectives).ToArray();
return Analyze.GetDiagnostics(analyzer, wrappedSourceCodes, compilationOptions, DefaultMetadataReferences)
.SelectMany(d => d)
.ToArray();
}
}
internal static class AnalyzerAssertionsExtensions
{
public static AnalyzerAssertions Should(this DiagnosticAnalyzer analyzer) => new AnalyzerAssertions(analyzer);
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>annotations</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" PrivateAssets="all" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,423 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
{
// TODO: split into multiple analyzers
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CommandSchemaAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
DiagnosticDescriptors.CliFx0001,
DiagnosticDescriptors.CliFx0002,
DiagnosticDescriptors.CliFx0021,
DiagnosticDescriptors.CliFx0022,
DiagnosticDescriptors.CliFx0023,
DiagnosticDescriptors.CliFx0024,
DiagnosticDescriptors.CliFx0025,
DiagnosticDescriptors.CliFx0026,
DiagnosticDescriptors.CliFx0041,
DiagnosticDescriptors.CliFx0042,
DiagnosticDescriptors.CliFx0043,
DiagnosticDescriptors.CliFx0044,
DiagnosticDescriptors.CliFx0045,
DiagnosticDescriptors.CliFx0046,
DiagnosticDescriptors.CliFx0047,
DiagnosticDescriptors.CliFx0048,
DiagnosticDescriptors.CliFx0049
);
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
KnownSymbols.IsSystemString(typeSymbol) ||
!typeSymbol.AllInterfaces.Select(i => i.ConstructedFrom)
.Any(KnownSymbols.IsSystemCollectionsGenericIEnumerable);
private static void CheckCommandParameterProperties(
SymbolAnalysisContext context,
IReadOnlyList<IPropertySymbol> properties)
{
var parameters = properties
.Select(p =>
{
var attribute = p
.GetAttributes()
.First(a => KnownSymbols.IsCommandParameterAttribute(a.AttributeClass));
var order = attribute
.ConstructorArguments
.Select(a => a.Value)
.FirstOrDefault() as int?;
var name = attribute
.NamedArguments
.Where(a => a.Key == "Name")
.Select(a => a.Value.Value)
.FirstOrDefault() as string;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.Cast<ITypeSymbol?>()
.FirstOrDefault();
var validators = attribute
.NamedArguments
.Where(a => a.Key == "Validators")
.SelectMany(a => a.Value.Values)
.Select(c => c.Value)
.Cast<ITypeSymbol>()
.ToArray();
return new
{
Property = p,
Order = order,
Name = name,
Converter = converter,
Validators = validators
};
})
.ToArray();
// Duplicate order
var duplicateOrderParameters = parameters
.Where(p => p.Order != null)
.GroupBy(p => p.Order)
.Where(g => g.Count() > 1)
.SelectMany(g => g.AsEnumerable())
.ToArray();
foreach (var parameter in duplicateOrderParameters)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0021, parameter.Property.Locations.First()
));
}
// Duplicate name
var duplicateNameParameters = parameters
.Where(p => !string.IsNullOrWhiteSpace(p.Name))
.GroupBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
.Where(g => g.Count() > 1)
.SelectMany(g => g.AsEnumerable())
.ToArray();
foreach (var parameter in duplicateNameParameters)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0022, parameter.Property.Locations.First()
));
}
// Multiple non-scalar
var nonScalarParameters = parameters
.Where(p => !IsScalarType(p.Property.Type))
.ToArray();
if (nonScalarParameters.Length > 1)
{
foreach (var parameter in nonScalarParameters)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0023, parameter.Property.Locations.First()
));
}
}
// Non-last non-scalar
var nonLastNonScalarParameter = parameters
.OrderByDescending(a => a.Order)
.Skip(1)
.LastOrDefault(p => !IsScalarType(p.Property.Type));
if (nonLastNonScalarParameter != null)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0024, nonLastNonScalarParameter.Property.Locations.First()
));
}
// Invalid converter
var invalidConverterParameters = parameters
.Where(p =>
p.Converter != null &&
!p.Converter.AllInterfaces.Any(KnownSymbols.IsArgumentValueConverterInterface))
.ToArray();
foreach (var parameter in invalidConverterParameters)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0025, parameter.Property.Locations.First()
));
}
// Invalid validators
var invalidValidatorsParameters = parameters
.Where(p => !p.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface)))
.ToArray();
foreach (var parameter in invalidValidatorsParameters)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0026, parameter.Property.Locations.First()
));
}
}
private static void CheckCommandOptionProperties(
SymbolAnalysisContext context,
IReadOnlyList<IPropertySymbol> properties)
{
var options = properties
.Select(p =>
{
var attribute = p
.GetAttributes()
.First(a => KnownSymbols.IsCommandOptionAttribute(a.AttributeClass));
var name = attribute
.ConstructorArguments
.Where(a => KnownSymbols.IsSystemString(a.Type))
.Select(a => a.Value)
.FirstOrDefault() as string;
var shortName = attribute
.ConstructorArguments
.Where(a => KnownSymbols.IsSystemChar(a.Type))
.Select(a => a.Value)
.FirstOrDefault() as char?;
var envVarName = attribute
.NamedArguments
.Where(a => a.Key == "EnvironmentVariableName")
.Select(a => a.Value.Value)
.FirstOrDefault() as string;
var converter = attribute
.NamedArguments
.Where(a => a.Key == "Converter")
.Select(a => a.Value.Value)
.Cast<ITypeSymbol>()
.FirstOrDefault();
var validators = attribute
.NamedArguments
.Where(a => a.Key == "Validators")
.SelectMany(a => a.Value.Values)
.Select(c => c.Value)
.Cast<ITypeSymbol>()
.ToArray();
return new
{
Property = p,
Name = name,
ShortName = shortName,
EnvironmentVariableName = envVarName,
Converter = converter,
Validators = validators
};
})
.ToArray();
// No name
var noNameOptions = options
.Where(o => string.IsNullOrWhiteSpace(o.Name) && o.ShortName == null)
.ToArray();
foreach (var option in noNameOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0041, option.Property.Locations.First()
));
}
// Too short name
var invalidNameLengthOptions = options
.Where(o => !string.IsNullOrWhiteSpace(o.Name) && o.Name.Length <= 1)
.ToArray();
foreach (var option in invalidNameLengthOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0042, option.Property.Locations.First()
));
}
// Duplicate name
var duplicateNameOptions = options
.Where(p => !string.IsNullOrWhiteSpace(p.Name))
.GroupBy(p => p.Name, StringComparer.OrdinalIgnoreCase)
.Where(g => g.Count() > 1)
.SelectMany(g => g.AsEnumerable())
.ToArray();
foreach (var option in duplicateNameOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0043, option.Property.Locations.First()
));
}
// Duplicate name
var duplicateShortNameOptions = options
.Where(p => p.ShortName != null)
.GroupBy(p => p.ShortName)
.Where(g => g.Count() > 1)
.SelectMany(g => g.AsEnumerable())
.ToArray();
foreach (var option in duplicateShortNameOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0044, option.Property.Locations.First()
));
}
// Duplicate environment variable name
var duplicateEnvironmentVariableNameOptions = options
.Where(p => !string.IsNullOrWhiteSpace(p.EnvironmentVariableName))
.GroupBy(p => p.EnvironmentVariableName, StringComparer.Ordinal)
.Where(g => g.Count() > 1)
.SelectMany(g => g.AsEnumerable())
.ToArray();
foreach (var option in duplicateEnvironmentVariableNameOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0045, option.Property.Locations.First()
));
}
// Invalid converter
var invalidConverterOptions = options
.Where(o =>
o.Converter != null &&
!o.Converter.AllInterfaces.Any(KnownSymbols.IsArgumentValueConverterInterface))
.ToArray();
foreach (var option in invalidConverterOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0046, option.Property.Locations.First()
));
}
// Invalid validators
var invalidValidatorsOptions = options
.Where(o => !o.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface)))
.ToArray();
foreach (var option in invalidValidatorsOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0047, option.Property.Locations.First()
));
}
// Non-letter first character in name
var nonLetterFirstCharacterInNameOptions = options
.Where(o => !string.IsNullOrWhiteSpace(o.Name) && !char.IsLetter(o.Name[0]))
.ToArray();
foreach (var option in nonLetterFirstCharacterInNameOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0048, option.Property.Locations.First()
));
}
// Non-letter short name
var nonLetterShortNameOptions = options
.Where(o => o.ShortName != null && !char.IsLetter(o.ShortName.Value))
.ToArray();
foreach (var option in nonLetterShortNameOptions)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0049, option.Property.Locations.First()
));
}
}
private static void CheckCommandType(SymbolAnalysisContext context)
{
// Named type: MyCommand
if (!(context.Symbol is INamedTypeSymbol namedTypeSymbol) ||
namedTypeSymbol.TypeKind != TypeKind.Class)
return;
// Implements ICommand?
var implementsCommandInterface = namedTypeSymbol
.AllInterfaces
.Any(KnownSymbols.IsCommandInterface);
// Has CommandAttribute?
var hasCommandAttribute = namedTypeSymbol
.GetAttributes()
.Select(a => a.AttributeClass)
.Any(KnownSymbols.IsCommandAttribute);
var isValidCommandType =
// implements interface
implementsCommandInterface && (
// and either abstract class or has attribute
namedTypeSymbol.IsAbstract || hasCommandAttribute
);
if (!isValidCommandType)
{
// See if this was meant to be a command type (either interface or attribute present)
var isAlmostValidCommandType = implementsCommandInterface ^ hasCommandAttribute;
if (isAlmostValidCommandType && !implementsCommandInterface)
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0001,
namedTypeSymbol.Locations.First()));
if (isAlmostValidCommandType && !hasCommandAttribute)
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0002,
namedTypeSymbol.Locations.First()));
return;
}
var properties = namedTypeSymbol
.GetMembers()
.Where(m => m.Kind == SymbolKind.Property)
.OfType<IPropertySymbol>().ToArray();
// Check parameters
var parameterProperties = properties
.Where(p => p
.GetAttributes()
.Select(a => a.AttributeClass)
.Any(KnownSymbols.IsCommandParameterAttribute))
.ToArray();
CheckCommandParameterProperties(context, parameterProperties);
// Check options
var optionsProperties = properties
.Where(p => p
.GetAttributes()
.Select(a => a.AttributeClass)
.Any(KnownSymbols.IsCommandOptionAttribute))
.ToArray();
CheckCommandOptionProperties(context, optionsProperties);
}
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSymbolAction(CheckCommandType, SymbolKind.NamedType);
}
}
}

View File

@@ -0,0 +1,74 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace CliFx.Analyzers
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ConsoleUsageAnalyzer : DiagnosticAnalyzer
{
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(
DiagnosticDescriptors.CliFx0100
);
private static bool IsSystemConsoleInvocation(
SyntaxNodeAnalysisContext context,
InvocationExpressionSyntax invocationSyntax)
{
if (invocationSyntax.Expression is MemberAccessExpressionSyntax memberAccessSyntax &&
context.SemanticModel.GetSymbolInfo(memberAccessSyntax).Symbol is IMethodSymbol methodSymbol)
{
// Direct call to System.Console (e.g. System.Console.WriteLine())
if (KnownSymbols.IsSystemConsole(methodSymbol.ContainingType))
{
return true;
}
// Indirect call to System.Console (e.g. System.Console.Error.WriteLine())
if (memberAccessSyntax.Expression is MemberAccessExpressionSyntax parentMemberAccessSyntax &&
context.SemanticModel.GetSymbolInfo(parentMemberAccessSyntax).Symbol is IPropertySymbol propertySymbol)
{
return KnownSymbols.IsSystemConsole(propertySymbol.ContainingType);
}
}
return false;
}
private static void CheckSystemConsoleUsage(SyntaxNodeAnalysisContext context)
{
if (context.Node is InvocationExpressionSyntax invocationSyntax &&
IsSystemConsoleInvocation(context, invocationSyntax))
{
// Check if IConsole is available in scope as alternative to System.Console
var isConsoleInterfaceAvailable = invocationSyntax
.Ancestors()
.OfType<MethodDeclarationSyntax>()
.SelectMany(m => m.ParameterList.Parameters)
.Select(p => p.Type)
.Select(t => context.SemanticModel.GetSymbolInfo(t).Symbol)
.Where(s => s != null)
.Any(KnownSymbols.IsConsoleInterface!);
if (isConsoleInterfaceAvailable)
{
context.ReportDiagnostic(Diagnostic.Create(
DiagnosticDescriptors.CliFx0100,
invocationSyntax.GetLocation()
));
}
}
}
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.RegisterSyntaxNodeAction(CheckSystemConsoleUsage, SyntaxKind.InvocationExpression);
}
}
}

View File

@@ -0,0 +1,133 @@
using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers
{
public static class DiagnosticDescriptors
{
public static readonly DiagnosticDescriptor CliFx0001 =
new DiagnosticDescriptor(nameof(CliFx0001),
"Type must implement the 'CliFx.ICommand' interface in order to be a valid command",
"Type must implement the 'CliFx.ICommand' interface in order to be a valid command",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0002 =
new DiagnosticDescriptor(nameof(CliFx0002),
"Type must be annotated with the 'CliFx.Attributes.CommandAttribute' in order to be a valid command",
"Type must be annotated with the 'CliFx.Attributes.CommandAttribute' in order to be a valid command",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0021 =
new DiagnosticDescriptor(nameof(CliFx0021),
"Parameter order must be unique within its command",
"Parameter order must be unique within its command",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0022 =
new DiagnosticDescriptor(nameof(CliFx0022),
"Parameter order must have unique name within its command",
"Parameter order must have unique name within its command",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0023 =
new DiagnosticDescriptor(nameof(CliFx0023),
"Only one non-scalar parameter per command is allowed",
"Only one non-scalar parameter per command is allowed",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0024 =
new DiagnosticDescriptor(nameof(CliFx0024),
"Non-scalar parameter must be last in order",
"Non-scalar parameter must be last in order",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0025 =
new DiagnosticDescriptor(nameof(CliFx0025),
"Parameter converter must implement 'CliFx.IArgumentValueConverter'",
"Parameter converter must implement 'CliFx.IArgumentValueConverter'",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0026 =
new DiagnosticDescriptor(nameof(CliFx0026),
"Parameter validator must implement 'CliFx.ArgumentValueValidator<T>'",
"Parameter validator must implement 'CliFx.ArgumentValueValidator<T>'",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0041 =
new DiagnosticDescriptor(nameof(CliFx0041),
"Option must have a name or short name specified",
"Option must have a name or short name specified",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0042 =
new DiagnosticDescriptor(nameof(CliFx0042),
"Option name must be at least 2 characters long",
"Option name must be at least 2 characters long",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0043 =
new DiagnosticDescriptor(nameof(CliFx0043),
"Option name must be unique within its command",
"Option name must be unique within its command",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0044 =
new DiagnosticDescriptor(nameof(CliFx0044),
"Option short name must be unique within its command",
"Option short name must be unique within its command",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0045 =
new DiagnosticDescriptor(nameof(CliFx0045),
"Option environment variable name must be unique within its command",
"Option environment variable name must be unique within its command",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0046 =
new DiagnosticDescriptor(nameof(CliFx0046),
"Option converter must implement 'CliFx.IArgumentValueConverter'",
"Option converter must implement 'CliFx.IArgumentValueConverter'",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0047 =
new DiagnosticDescriptor(nameof(CliFx0047),
"Option validator must implement 'CliFx.ArgumentValueValidator<T>'",
"Option validator must implement 'CliFx.ArgumentValueValidator<T>'",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0048 =
new DiagnosticDescriptor(nameof(CliFx0048),
"Option name must begin with a letter character.",
"Option name must begin with a letter character.",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0049 =
new DiagnosticDescriptor(nameof(CliFx0049),
"Option short name must be a letter character.",
"Option short name must be a letter character.",
"Usage", DiagnosticSeverity.Error, true
);
public static readonly DiagnosticDescriptor CliFx0100 =
new DiagnosticDescriptor(nameof(CliFx0100),
"Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation",
"Use the provided IConsole abstraction instead of System.Console to ensure that the command can be tested in isolation",
"Usage", DiagnosticSeverity.Warning, true
);
}
}

View File

@@ -0,0 +1,11 @@
using System;
using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers.Internal
{
internal static class RoslynExtensions
{
public static bool DisplayNameMatches(this ISymbol symbol, string name) =>
string.Equals(symbol.ToDisplayString(), name, StringComparison.Ordinal);
}
}

View File

@@ -0,0 +1,43 @@
using CliFx.Analyzers.Internal;
using Microsoft.CodeAnalysis;
namespace CliFx.Analyzers
{
internal static class KnownSymbols
{
public static bool IsSystemString(ISymbol symbol) =>
symbol.DisplayNameMatches("string") ||
symbol.DisplayNameMatches("System.String");
public static bool IsSystemChar(ISymbol symbol) =>
symbol.DisplayNameMatches("char") ||
symbol.DisplayNameMatches("System.Char");
public static bool IsSystemCollectionsGenericIEnumerable(ISymbol symbol) =>
symbol.DisplayNameMatches("System.Collections.Generic.IEnumerable<T>");
public static bool IsSystemConsole(ISymbol symbol) =>
symbol.DisplayNameMatches("System.Console");
public static bool IsConsoleInterface(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.IConsole");
public static bool IsCommandInterface(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.ICommand");
public static bool IsArgumentValueConverterInterface(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.IArgumentValueConverter");
public static bool IsArgumentValueValidatorInterface(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.IArgumentValueValidator");
public static bool IsCommandAttribute(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.Attributes.CommandAttribute");
public static bool IsCommandParameterAttribute(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.Attributes.CommandParameterAttribute");
public static bool IsCommandOptionAttribute(ISymbol symbol) =>
symbol.DisplayNameMatches("CliFx.Attributes.CommandOptionAttribute");
}
}

View File

@@ -1,34 +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);
// Skipped because this benchmark freezes after a couple of iterations
// Probably wasn't designed to run multiple times in single process execution
//[Benchmark(Description = "CommandLineParser")]
public void ExecuteWithCommandLineParser()
{
var parsed = CommandLine.Parser.Default.ParseArguments(Arguments, typeof(CommandLineParserCommand));
CommandLine.ParserResultExtensions.WithParsed<CommandLineParserCommand>(parsed, c => c.Execute());
}
[Benchmark(Description = "PowerArgs")]
public void ExecuteWithPowerArgs() => PowerArgs.Args.InvokeMain<PowerArgsCommand>(Arguments);
}
}

View File

@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
using CliFx.Benchmarks.Commands;
using CommandLine;
namespace CliFx.Benchmarks
{
[SimpleJob]
[RankColumn]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class Benchmarks
{
private static readonly string[] Arguments = {"--str", "hello world", "-i", "13", "-b"};
[Benchmark(Description = "CliFx", Baseline = true)]
public async ValueTask<int> ExecuteWithCliFx() =>
await new CliApplicationBuilder().AddCommand<CliFxCommand>().Build().RunAsync(Arguments, new Dictionary<string, string>());
[Benchmark(Description = "System.CommandLine")]
public async Task<int> ExecuteWithSystemCommandLine() =>
await 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() =>
new Parser()
.ParseArguments(Arguments, typeof(CommandLineParserCommand))
.WithParsed<CommandLineParserCommand>(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();
[Benchmark(Description = "Cocona")]
public void ExecuteWithCocona() =>
Cocona.CoconaApp.Run<CoconaCommand>(Arguments);
public static void Main() =>
BenchmarkRunner.Run<Benchmarks>(DefaultConfig.Instance.With(ConfigOptions.DisableOptimizationsValidator));
}
}

View File

@@ -1,15 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" /> <PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
<PackageReference Include="CommandLineParser" Version="2.6.0" /> <PackageReference Include="clipr" Version="1.6.1" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.3.4" /> <PackageReference Include="Cocona" Version="1.5.0" />
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="3.0.0" />
<PackageReference Include="PowerArgs" Version="3.6.0" /> <PackageReference Include="PowerArgs" Version="3.6.0" />
<PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" /> <PackageReference Include="System.CommandLine.Experimental" Version="0.3.0-alpha.19317.1" />
</ItemGroup> </ItemGroup>

View File

@@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Benchmarks.Commands namespace CliFx.Benchmarks.Commands
{ {
@@ -8,7 +7,7 @@ namespace CliFx.Benchmarks.Commands
public class CliFxCommand : ICommand public class CliFxCommand : ICommand
{ {
[CommandOption("str", 's')] [CommandOption("str", 's')]
public string StrOption { get; set; } public string? StrOption { get; set; }
[CommandOption("int", 'i')] [CommandOption("int", 'i')]
public int IntOption { get; set; } public int IntOption { get; set; }
@@ -16,6 +15,6 @@ namespace CliFx.Benchmarks.Commands
[CommandOption("bool", 'b')] [CommandOption("bool", 'b')]
public bool BoolOption { get; set; } public bool BoolOption { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask; public ValueTask ExecuteAsync(IConsole console) => default;
} }
} }

View File

@@ -0,0 +1,20 @@
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()
{
}
}
}

View File

@@ -0,0 +1,17 @@
using Cocona;
namespace CliFx.Benchmarks.Commands
{
public class CoconaCommand
{
public void Execute(
[Option("str", new []{'s'})]
string? strOption,
[Option("int", new []{'i'})]
int intOption,
[Option("bool", new []{'b'})]
bool boolOption)
{
}
}
}

View File

@@ -5,7 +5,7 @@ namespace CliFx.Benchmarks.Commands
public class CommandLineParserCommand public class CommandLineParserCommand
{ {
[Option('s', "str")] [Option('s', "str")]
public string StrOption { get; set; } public string? StrOption { get; set; }
[Option('i', "int")] [Option('i', "int")]
public int IntOption { get; set; } public int IntOption { get; set; }

View File

@@ -5,7 +5,7 @@ namespace CliFx.Benchmarks.Commands
public class McMasterCommand public class McMasterCommand
{ {
[Option("--str|-s")] [Option("--str|-s")]
public string StrOption { get; set; } public string? StrOption { get; set; }
[Option("--int|-i")] [Option("--int|-i")]
public int IntOption { get; set; } public int IntOption { get; set; }

View File

@@ -5,7 +5,7 @@ namespace CliFx.Benchmarks.Commands
public class PowerArgsCommand public class PowerArgsCommand
{ {
[ArgShortcut("--str"), ArgShortcut("-s")] [ArgShortcut("--str"), ArgShortcut("-s")]
public string StrOption { get; set; } public string? StrOption { get; set; }
[ArgShortcut("--int"), ArgShortcut("-i")] [ArgShortcut("--int"), ArgShortcut("-i")]
public int IntOption { get; set; } public int IntOption { get; set; }

View File

@@ -14,7 +14,7 @@ namespace CliFx.Benchmarks.Commands
{ {
new Option(new[] {"--str", "-s"}) new Option(new[] {"--str", "-s"})
{ {
Argument = new Argument<string>() Argument = new Argument<string?>()
}, },
new Option(new[] {"--int", "-i"}) new Option(new[] {"--int", "-i"})
{ {

View File

@@ -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));
}
}

View File

@@ -1,18 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CliFx\CliFx.csproj" /> <ProjectReference Include="..\CliFx\CliFx.csproj" />
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,7 +5,6 @@ using CliFx.Demo.Internal;
using CliFx.Demo.Models; using CliFx.Demo.Models;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -14,31 +13,25 @@ namespace CliFx.Demo.Commands
{ {
private readonly LibraryService _libraryService; private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")] [CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } public string Title { get; set; } = "";
[CommandOption("author", 'a', IsRequired = true, Description = "Book author.")] [CommandOption("author", 'a', IsRequired = true, Description = "Book author.")]
public string Author { get; set; } public string Author { get; set; } = "";
[CommandOption("published", 'p', Description = "Book publish date.")] [CommandOption("published", 'p', Description = "Book publish date.")]
public DateTimeOffset Published { get; set; } public DateTimeOffset Published { get; set; } = CreateRandomDate();
[CommandOption("isbn", 'n', Description = "Book ISBN.")] [CommandOption("isbn", 'n', Description = "Book ISBN.")]
public Isbn Isbn { get; set; } public Isbn Isbn { get; set; } = CreateRandomIsbn();
public BookAddCommand(LibraryService libraryService) public BookAddCommand(LibraryService libraryService)
{ {
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public ValueTask 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) if (_libraryService.GetBook(Title) != null)
throw new CommandException("Book already exists.", 1); throw new CommandException("Book already exists.", 1);
@@ -48,7 +41,7 @@ namespace CliFx.Demo.Commands
console.Output.WriteLine("Book added."); console.Output.WriteLine("Book added.");
console.RenderBook(book); console.RenderBook(book);
return Task.CompletedTask; return default;
} }
} }
@@ -65,7 +58,7 @@ namespace CliFx.Demo.Commands
Random.Next(1, 59), Random.Next(1, 59),
TimeSpan.Zero); TimeSpan.Zero);
public static Isbn CreateRandomIsbn() => new Isbn( private static Isbn CreateRandomIsbn() => new Isbn(
Random.Next(0, 999), Random.Next(0, 999),
Random.Next(0, 99), Random.Next(0, 99),
Random.Next(0, 99999), Random.Next(0, 99999),

View File

@@ -3,7 +3,6 @@ using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -12,15 +11,15 @@ namespace CliFx.Demo.Commands
{ {
private readonly LibraryService _libraryService; private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")] [CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } public string Title { get; set; } = "";
public BookCommand(LibraryService libraryService) public BookCommand(LibraryService libraryService)
{ {
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
var book = _libraryService.GetBook(Title); var book = _libraryService.GetBook(Title);
@@ -29,7 +28,7 @@ namespace CliFx.Demo.Commands
console.RenderBook(book); console.RenderBook(book);
return Task.CompletedTask; return default;
} }
} }
} }

View File

@@ -2,7 +2,6 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Internal; using CliFx.Demo.Internal;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -16,7 +15,7 @@ namespace CliFx.Demo.Commands
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
var library = _libraryService.GetLibrary(); var library = _libraryService.GetLibrary();
@@ -32,7 +31,7 @@ namespace CliFx.Demo.Commands
console.RenderBook(book); console.RenderBook(book);
} }
return Task.CompletedTask; return default;
} }
} }
} }

View File

@@ -2,7 +2,6 @@
using CliFx.Attributes; using CliFx.Attributes;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using CliFx.Exceptions; using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Demo.Commands namespace CliFx.Demo.Commands
{ {
@@ -11,15 +10,15 @@ namespace CliFx.Demo.Commands
{ {
private readonly LibraryService _libraryService; private readonly LibraryService _libraryService;
[CommandOption("title", 't', IsRequired = true, Description = "Book title.")] [CommandParameter(0, Name = "title", Description = "Book title.")]
public string Title { get; set; } public string Title { get; set; } = "";
public BookRemoveCommand(LibraryService libraryService) public BookRemoveCommand(LibraryService libraryService)
{ {
_libraryService = libraryService; _libraryService = libraryService;
} }
public Task ExecuteAsync(IConsole console) public ValueTask ExecuteAsync(IConsole console)
{ {
var book = _libraryService.GetBook(Title); var book = _libraryService.GetBook(Title);
@@ -30,7 +29,7 @@ namespace CliFx.Demo.Commands
console.Output.WriteLine($"Book {Title} removed."); console.Output.WriteLine($"Book {Title} removed.");
return Task.CompletedTask; return default;
} }
} }
} }

View File

@@ -1,6 +1,5 @@
using System; using System;
using CliFx.Demo.Models; using CliFx.Demo.Models;
using CliFx.Services;
namespace CliFx.Demo.Internal namespace CliFx.Demo.Internal
{ {

View File

@@ -1,5 +1,4 @@
using System; using System;
using System.Globalization;
namespace CliFx.Demo.Models namespace CliFx.Demo.Models
{ {
@@ -24,21 +23,23 @@ namespace CliFx.Demo.Models
CheckDigit = checkDigit; CheckDigit = checkDigit;
} }
public override string ToString() => $"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}"; public override string ToString() =>
$"{EanPrefix:000}-{RegistrationGroup:00}-{Registrant:00000}-{Publication:00}-{CheckDigit:0}";
} }
public partial class Isbn public partial class Isbn
{ {
public static Isbn Parse(string value) public static Isbn Parse(string value, IFormatProvider formatProvider)
{ {
var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries); var components = value.Split('-', 5, StringSplitOptions.RemoveEmptyEntries);
return new Isbn( return new Isbn(
int.Parse(components[0], CultureInfo.InvariantCulture), int.Parse(components[0], formatProvider),
int.Parse(components[1], CultureInfo.InvariantCulture), int.Parse(components[1], formatProvider),
int.Parse(components[2], CultureInfo.InvariantCulture), int.Parse(components[2], formatProvider),
int.Parse(components[3], CultureInfo.InvariantCulture), int.Parse(components[3], formatProvider),
int.Parse(components[4], CultureInfo.InvariantCulture)); int.Parse(components[4], formatProvider)
);
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using System.Threading.Tasks; using System;
using System.Threading.Tasks;
using CliFx.Demo.Commands; using CliFx.Demo.Commands;
using CliFx.Demo.Services; using CliFx.Demo.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -7,7 +8,7 @@ namespace CliFx.Demo
{ {
public static class Program public static class Program
{ {
public static Task<int> Main(string[] args) private static IServiceProvider GetServiceProvider()
{ {
// We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands // We use Microsoft.Extensions.DependencyInjection for injecting dependencies in commands
var services = new ServiceCollection(); var services = new ServiceCollection();
@@ -21,13 +22,14 @@ namespace CliFx.Demo
services.AddTransient<BookRemoveCommand>(); services.AddTransient<BookRemoveCommand>();
services.AddTransient<BookListCommand>(); services.AddTransient<BookListCommand>();
var serviceProvider = services.BuildServiceProvider(); return services.BuildServiceProvider();
return new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseCommandFactory(schema => (ICommand) serviceProvider.GetRequiredService(schema.Type))
.Build()
.RunAsync(args);
} }
public static async Task<int> Main() =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.UseTypeActivator(GetServiceProvider().GetRequiredService)
.Build()
.RunAsync();
} }
} }

View File

@@ -2,6 +2,6 @@
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 shows basic CliFx functionality such as command routing, argument parsing, autogenerated help text, and some other things.
You can get a list of available commands by running `CliFx.Demo --help`. You can get a list of available commands by running `CliFx.Demo --help`.

View File

@@ -25,7 +25,7 @@ namespace CliFx.Demo.Services
return JsonConvert.DeserializeObject<Library>(data); return JsonConvert.DeserializeObject<Library>(data);
} }
public Book GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title); public Book? GetBook(string title) => GetLibrary().Books.FirstOrDefault(b => b.Title == title);
public void AddBook(Book book) public void AddBook(Book book)
{ {

View File

@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net46</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<Version>1.2.3.4</Version>
<LangVersion>latest</LangVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\CliFx\CliFx.csproj" /> <ProjectReference Include="..\CliFx\CliFx.csproj" />
<ProjectReference Include="..\CliFx.Analyzers\CliFx.Analyzers.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,23 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Dummy.Commands
{
[Command("console-test")]
public class ConsoleTestCommand : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
var input = console.Input.ReadToEnd();
console.WithColors(ConsoleColor.Black, ConsoleColor.White, () =>
{
console.Output.WriteLine(input);
console.Error.WriteLine(input);
});
return default;
}
}
}

View File

@@ -1,31 +0,0 @@
using System.Text;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
[Command]
public class GreeterCommand : ICommand
{
[CommandOption("target", 't', Description = "Greeting target.")]
public string Target { get; set; } = "world";
[CommandOption('e', Description = "Whether the greeting should be exclaimed.")]
public bool IsExclaimed { get; set; }
public Task ExecuteAsync(IConsole console)
{
var buffer = new StringBuilder();
buffer.Append("Hello").Append(' ').Append(Target);
if (IsExclaimed)
buffer.Append('!');
console.Output.WriteLine(buffer.ToString());
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Dummy.Commands
{
[Command]
public class HelloWorldCommand : ICommand
{
[CommandOption("target", EnvironmentVariableName = "ENV_TARGET")]
public string Target { get; set; } = "World";
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine($"Hello {Target}!");
return default;
}
}
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
[Command("log", Description = "Calculate the logarithm of a value.")]
public class LogCommand : ICommand
{
[CommandOption("value", 'v', IsRequired = true, Description = "Value whose logarithm is to be found.")]
public double Value { get; set; }
[CommandOption("base", 'b', Description = "Logarithm base.")]
public double Base { get; set; } = 10;
public Task ExecuteAsync(IConsole console)
{
var result = Math.Log(Value, Base);
console.Output.WriteLine(result);
return Task.CompletedTask;
}
}
}

View File

@@ -1,23 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests.Dummy.Commands
{
[Command("sum", Description = "Calculate the sum of all input values.")]
public class SumCommand : ICommand
{
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
public IReadOnlyList<double> Values { get; set; }
public Task ExecuteAsync(IConsole console)
{
var result = Values.Sum();
console.Output.WriteLine(result);
return Task.CompletedTask;
}
}
}

View File

@@ -1,21 +1,21 @@
using System.Globalization; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CliFx.Tests.Dummy namespace CliFx.Tests.Dummy
{ {
public static class Program public static partial class Program
{ {
public static Task<int> Main(string[] args) public static Assembly Assembly { get; } = typeof(Program).Assembly;
{
// Set culture to invariant to maintain consistent format because we rely on it in tests
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.InvariantCulture;
return new CliApplicationBuilder() public static string Location { get; } = Assembly.Location;
}
public static partial class Program
{
public static async Task Main() =>
await new CliApplicationBuilder()
.AddCommandsFromThisAssembly() .AddCommandsFromThisAssembly()
.UseDescription("Dummy program used for E2E tests.")
.Build() .Build()
.RunAsync(args); .RunAsync();
}
} }
} }

View File

@@ -0,0 +1,495 @@
using System;
using System.IO;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using CliFx.Tests.Commands.Invalid;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public class ApplicationSpecs
{
private readonly ITestOutputHelper _output;
public ApplicationSpecs(ITestOutputHelper output) => _output = output;
[Fact]
public void Application_can_be_created_with_a_default_configuration()
{
// Act
var app = new CliApplicationBuilder()
.AddCommandsFromThisAssembly()
.Build();
// Assert
app.Should().NotBeNull();
}
[Fact]
public void Application_can_be_created_with_a_custom_configuration()
{
// Act
var app = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommandsFrom(typeof(DefaultCommand).Assembly)
.AddCommands(new[] {typeof(DefaultCommand)})
.AddCommandsFrom(new[] {typeof(DefaultCommand).Assembly})
.AddCommandsFromThisAssembly()
.AllowDebugMode()
.AllowPreviewMode()
.UseTitle("test")
.UseExecutableName("test")
.UseVersionText("test")
.UseDescription("test")
.UseConsole(new VirtualConsole(Stream.Null))
.UseTypeActivator(Activator.CreateInstance!)
.Build();
// Assert
app.Should().NotBeNull();
}
[Fact]
public async Task At_least_one_command_must_be_defined_in_an_application()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Commands_must_implement_the_corresponding_interface()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand(typeof(NonImplementedCommand))
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Commands_must_be_annotated_by_an_attribute()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<NonAnnotatedCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Commands_must_have_unique_names()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<GenericExceptionCommand>()
.AddCommand<CommandExceptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_can_be_default_but_only_if_it_is_the_only_such_command()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<DefaultCommand>()
.AddCommand<OtherDefaultCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_parameters_must_have_unique_order()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<DuplicateParameterOrderCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_parameters_must_have_unique_names()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<DuplicateParameterNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_parameter_can_be_non_scalar_only_if_no_other_such_parameter_is_present()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<MultipleNonScalarParametersCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_parameter_can_be_non_scalar_only_if_it_is_the_last_in_order()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<NonLastNonScalarParameterCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_parameter_custom_converter_must_implement_the_corresponding_interface()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<InvalidCustomConverterParameterCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_parameter_custom_validator_must_implement_the_corresponding_interface()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<InvalidCustomValidatorParameterCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_names_that_are_not_empty()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<EmptyOptionNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_names_that_are_longer_than_one_character()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<SingleCharacterOptionNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_unique_names()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<DuplicateOptionNamesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_unique_short_names()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<DuplicateOptionShortNamesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_unique_environment_variable_names()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<DuplicateOptionEnvironmentVariableNamesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_not_have_conflicts_with_the_implicit_help_option()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<ConflictWithHelpOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_not_have_conflicts_with_the_implicit_version_option()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<ConflictWithVersionOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_option_custom_converter_must_implement_the_corresponding_interface()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<InvalidCustomConverterOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_option_custom_validator_must_implement_the_corresponding_interface()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<InvalidCustomValidatorOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_names_that_start_with_a_letter_character()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<NonLetterCharacterNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Command_options_must_have_short_names_that_are_letter_characters()
{
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<NonLetterCharacterShortNameCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(Array.Empty<string>());
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
}
}

View File

@@ -0,0 +1,270 @@
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using CliFx.Tests.Internal;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;
namespace CliFx.Tests
{
public class ArgumentBindingSpecs
{
private readonly ITestOutputHelper _output;
public ArgumentBindingSpecs(ITestOutputHelper output) => _output = output;
[Fact]
public async Task Property_annotated_as_an_option_can_be_bound_from_multiple_values_even_if_the_inputs_use_mixed_naming()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<WithStringArrayOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--opt", "foo", "-o", "bar", "--opt", "baz"
});
var commandInstance = stdOut.GetString().DeserializeJson<WithStringArrayOptionCommand>();
// Assert
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new WithStringArrayOptionCommand
{
Opt = new[] {"foo", "bar", "baz"}
});
}
[Fact]
public async Task Property_annotated_as_a_required_option_must_always_be_set()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<WithSingleRequiredOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--opt-a", "foo"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Property_annotated_as_a_required_option_must_always_be_bound_to_some_value()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<WithSingleRequiredOptionCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--opt-a"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Property_annotated_as_a_required_option_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<WithRequiredOptionsCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--opt-a", "foo"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Property_annotated_as_parameter_is_bound_directly_from_argument_value_according_to_the_order()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<WithParametersCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "foo", "13", "bar", "baz"
});
var commandInstance = stdOut.GetString().DeserializeJson<WithParametersCommand>();
// Assert
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new WithParametersCommand
{
ParamA = "foo",
ParamB = 13,
ParamC = new[] {"bar", "baz"}
});
}
[Fact]
public async Task Property_annotated_as_parameter_must_always_be_bound_to_some_value()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<WithSingleParameterCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Property_annotated_as_parameter_must_always_be_bound_to_at_least_one_value_if_it_expects_multiple_values()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<WithParametersCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "foo", "13"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task Argument_that_begins_with_a_dash_is_not_parsed_as_option_name_if_it_does_not_start_with_a_letter_character()
{
// Arrange
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<SupportedArgumentTypesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--int", "-13"
});
var commandInstance = stdOut.GetString().DeserializeJson<SupportedArgumentTypesCommand>();
// Assert
exitCode.Should().Be(0);
commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand
{
Int = -13
});
}
[Fact]
public async Task All_provided_option_arguments_must_be_bound_to_corresponding_properties()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<SupportedArgumentTypesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cmd", "--non-existing-option", "13"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
[Fact]
public async Task All_provided_parameter_arguments_must_be_bound_to_corresponding_properties()
{
// Arrange
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
var application = new CliApplicationBuilder()
.AddCommand<SupportedArgumentTypesCommand>()
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(new[]
{
"cnd", "non-existing-parameter"
});
// Assert
exitCode.Should().NotBe(0);
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
_output.WriteLine(stdErr.GetString());
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CliFx.Tests.Commands;
using FluentAssertions;
using Xunit;
namespace CliFx.Tests
{
public class CancellationSpecs
{
[Fact]
public async Task Command_can_perform_additional_cleanup_if_cancellation_is_requested()
{
// Can't test it with a real console because CliWrap can't send Ctrl+C
// Arrange
using var cts = new CancellationTokenSource();
var (console, stdOut, _) = VirtualConsole.CreateBuffered(cts.Token);
var application = new CliApplicationBuilder()
.AddCommand<CancellableCommand>()
.UseConsole(console)
.Build();
// Act
cts.CancelAfter(TimeSpan.FromSeconds(0.2));
var exitCode = await application.RunAsync(new[] {"cmd"});
// Assert
exitCode.Should().NotBe(0);
stdOut.GetString().Trim().Should().Be(CancellableCommand.CancellationOutputText);
}
}
}

View File

@@ -1,53 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CliApplicationTests
{
[Command]
private class DefaultCommand : ICommand
{
public Task ExecuteAsync(IConsole console)
{
console.Output.WriteLine("DefaultCommand executed.");
return Task.CompletedTask;
}
}
[Command("cmd")]
private class NamedCommand : ICommand
{
public Task ExecuteAsync(IConsole console)
{
console.Output.WriteLine("NamedCommand executed.");
return Task.CompletedTask;
}
}
}
// Negative
public partial class CliApplicationTests
{
[Command("faulty1")]
private class FaultyCommand1 : ICommand
{
public Task ExecuteAsync(IConsole console) => throw new CommandException(150);
}
[Command("faulty2")]
private class FaultyCommand2 : ICommand
{
public Task ExecuteAsync(IConsole console) => throw new CommandException("FaultyCommand2 error message.", 150);
}
[Command("faulty3")]
private class FaultyCommand3 : ICommand
{
public Task ExecuteAsync(IConsole console) => throw new Exception("FaultyCommand3 error message.");
}
}
}

View File

@@ -1,174 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial class CliApplicationTests
{
private static IEnumerable<TestCaseData> GetTestCases_RunAsync()
{
yield return new TestCaseData(
new[] {typeof(DefaultCommand)},
new string[0],
"DefaultCommand executed."
);
yield return new TestCaseData(
new[] {typeof(NamedCommand)},
new[] {"cmd"},
"NamedCommand executed."
);
}
private static IEnumerable<TestCaseData> GetTestCases_HelpAndVersion_RunAsync()
{
yield return new TestCaseData(
new[] {typeof(DefaultCommand)},
new string[0]
);
yield return new TestCaseData(
new[] {typeof(DefaultCommand)},
new[] {"-h"}
);
yield return new TestCaseData(
new[] {typeof(DefaultCommand)},
new[] {"--help"}
);
yield return new TestCaseData(
new[] {typeof(DefaultCommand)},
new[] {"--version"}
);
yield return new TestCaseData(
new[] {typeof(NamedCommand)},
new string[0]
);
yield return new TestCaseData(
new[] {typeof(NamedCommand)},
new[] {"cmd", "-h"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand1)},
new[] {"faulty1", "-h"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand2)},
new[] {"faulty2", "-h"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand3)},
new[] {"faulty3", "-h"}
);
}
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
{
yield return new TestCaseData(
new Type[0],
new string[0]
);
yield return new TestCaseData(
new[] {typeof(DefaultCommand)},
new[] {"non-existing"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand1)},
new[] {"faulty1"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand2)},
new[] {"faulty2"}
);
yield return new TestCaseData(
new[] {typeof(FaultyCommand3)},
new[] {"faulty3"}
);
}
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync))]
public async Task RunAsync_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments, string expectedStdOut)
{
// Arrange
using (var stdout = new StringWriter())
{
var console = new VirtualConsole(stdout);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments);
// Assert
exitCode.Should().Be(0);
stdout.ToString().Trim().Should().Be(expectedStdOut);
}
}
[Test]
[TestCaseSource(nameof(GetTestCases_HelpAndVersion_RunAsync))]
public async Task RunAsync_HelpAndVersion_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
{
// Arrange
using (var stdout = new StringWriter())
{
var console = new VirtualConsole(stdout);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments);
// Assert
exitCode.Should().Be(0);
stdout.ToString().Should().NotBeNullOrWhiteSpace();
}
}
[Test]
[TestCaseSource(nameof(GetTestCases_RunAsync_Negative))]
public async Task RunAsync_Negative_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
{
// Arrange
using (var stderr = new StringWriter())
{
var console = new VirtualConsole(TextWriter.Null, stderr);
var application = new CliApplicationBuilder()
.AddCommands(commandTypes)
.UseConsole(console)
.Build();
// Act
var exitCode = await application.RunAsync(commandLineArguments);
// Assert
exitCode.Should().NotBe(0);
stderr.ToString().Should().NotBeNullOrWhiteSpace();
}
}
}
}

View File

@@ -1,25 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="../CliFx.props" />
<PropertyGroup> <PropertyGroup>
<TargetFramework>net46</TargetFramework> <TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<CollectCoverage>true</CollectCoverage> <CollectCoverage>true</CollectCoverage>
<CoverletOutputFormat>opencover</CoverletOutputFormat> <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" /> </ItemGroup>
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.14.0" /> <ItemGroup>
<PackageReference Include="CliWrap" Version="2.3.1" /> <PackageReference Include="CliWrap" Version="3.2.2" />
<PackageReference Include="coverlet.msbuild" Version="2.6.3"> <PackageReference Include="FluentAssertions" Version="5.10.3" />
<PrivateAssets>all</PrivateAssets> <PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
</PackageReference> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
<PackageReference Include="coverlet.msbuild" Version="2.9.0" PrivateAssets="all" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -27,4 +29,12 @@
<ProjectReference Include="..\CliFx\CliFx.csproj" /> <ProjectReference Include="..\CliFx\CliFx.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="../CliFx.Tests.Dummy/bin/$(Configuration)/$(TargetFramework)/CliFx.Tests.Dummy.runtimeconfig.json">
<Link>CliFx.Tests.Dummy.runtimeconfig.json</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>False</Visible>
</None>
</ItemGroup>
</Project> </Project>

View File

@@ -1,15 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CommandFactoryTests
{
[Command]
private class TestCommand : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial 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(TestCommand)));
}
[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);
}
}
}

View File

@@ -1,21 +0,0 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CommandInitializerTests
{
[Command]
private class TestCommand : ICommand
{
[CommandOption("int", 'i', IsRequired = true)]
public int IntOption { get; set; } = 24;
[CommandOption("str", 's')]
public string StringOption { get; set; } = "foo bar";
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,96 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial 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 TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("int", "13")
}),
new TestCommand {IntOption = 13}
);
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("int", "13"),
new CommandOptionInput("str", "hello world")
}),
new TestCommand {IntOption = 13, StringOption = "hello world"}
);
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("i", "13")
}),
new TestCommand {IntOption = 13}
);
}
private static IEnumerable<TestCaseData> GetTestCases_InitializeCommand_Negative()
{
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
CommandInput.Empty
);
yield return new TestCaseData(
new TestCommand(),
GetCommandSchema(typeof(TestCommand)),
new CommandInput(new[]
{
new CommandOptionInput("str", "hello world")
})
);
}
[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<MissingCommandOptionInputException>();
}
}
}

View File

@@ -1,184 +0,0 @@
using System.Collections.Generic;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[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")
})
);
}
[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);
}
}
}

View File

@@ -1,63 +0,0 @@
using System;
namespace CliFx.Tests
{
public partial class CommandOptionInputConverterTests
{
private enum TestEnum
{
Value1,
Value2,
Value3
}
private class TestStringConstructable
{
public string Value { get; }
public TestStringConstructable(string value)
{
Value = value;
}
}
private class TestStringParseable
{
public string Value { get; }
private TestStringParseable(string value)
{
Value = value;
}
public static TestStringParseable Parse(string value) => new TestStringParseable(value);
}
private class TestStringParseableWithFormatProvider
{
public string Value { get; }
private TestStringParseableWithFormatProvider(string value)
{
Value = value;
}
public static TestStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new TestStringParseableWithFormatProvider(value + " " + formatProvider);
}
}
// Negative
public partial class CommandOptionInputConverterTests
{
private class NonStringParseable
{
public int Value { get; }
public NonStringParseable(int value)
{
Value = value;
}
}
}
}

View File

@@ -1,305 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial 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[] {"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", "123"),
typeof(NonStringParseable)
);
}
[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<InvalidCommandOptionInputException>();
}
}
}

View File

@@ -1,81 +0,0 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Services;
namespace CliFx.Tests
{
public partial class CommandSchemaResolverTests
{
[Command("cmd", Description = "NormalCommand1 description.")]
private class NormalCommand1 : ICommand
{
[CommandOption("option-a", 'a')]
public int OptionA { get; set; }
[CommandOption("option-b", IsRequired = true)]
public string OptionB { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command(Description = "NormalCommand2 description.")]
private class NormalCommand2 : ICommand
{
[CommandOption("option-c", Description = "OptionC description.")]
public bool OptionC { get; set; }
[CommandOption("option-d", 'd')]
public DateTimeOffset OptionD { get; set; }
public string NotAnOption { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
// Negative
public partial class CommandSchemaResolverTests
{
[Command("conflict")]
private class ConflictingCommand1 : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command("conflict")]
private class ConflictingCommand2 : ICommand
{
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command]
private class InvalidCommand1
{
}
[Command]
private class InvalidCommand2 : ICommand
{
[CommandOption("conflict")]
public string ConflictingOption1 { get; set; }
[CommandOption("conflict")]
public string ConflictingOption2 { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
[Command]
private class InvalidCommand3 : ICommand
{
[CommandOption('c')]
public string ConflictingOption1 { get; set; }
[CommandOption('c')]
public string ConflictingOption2 { get; set; }
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
}
}
}

View File

@@ -1,94 +0,0 @@
using System;
using System.Collections.Generic;
using CliFx.Exceptions;
using CliFx.Models;
using CliFx.Services;
using FluentAssertions;
using NUnit.Framework;
namespace CliFx.Tests
{
[TestFixture]
public partial class CommandSchemaResolverTests
{
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas()
{
yield return new TestCaseData(
new[] {typeof(NormalCommand1), typeof(NormalCommand2)},
new[]
{
new CommandSchema(typeof(NormalCommand1), "cmd", "NormalCommand1 description.",
new[]
{
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionA)),
"option-a", 'a', false, null),
new CommandOptionSchema(typeof(NormalCommand1).GetProperty(nameof(NormalCommand1.OptionB)),
"option-b", null, true, null)
}),
new CommandSchema(typeof(NormalCommand2), null, "NormalCommand2 description.",
new[]
{
new CommandOptionSchema(typeof(NormalCommand2).GetProperty(nameof(NormalCommand2.OptionC)),
"option-c", null, false, "OptionC description."),
new CommandOptionSchema(typeof(NormalCommand2).GetProperty(nameof(NormalCommand2.OptionD)),
"option-d", 'd', false, null)
})
}
);
}
private static IEnumerable<TestCaseData> GetTestCases_GetCommandSchemas_Negative()
{
yield return new TestCaseData(new object[]
{
new Type[0]
});
yield return new TestCaseData(new object[]
{
new[] {typeof(ConflictingCommand1), typeof(ConflictingCommand2)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(InvalidCommand1)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(InvalidCommand2)}
});
yield return new TestCaseData(new object[]
{
new[] {typeof(InvalidCommand3)}
});
}
[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<InvalidCommandSchemaException>();
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class CancellableCommand : ICommand
{
public const string CompletionOutputText = "Finished";
public const string CancellationOutputText = "Canceled";
public async ValueTask ExecuteAsync(IConsole console)
{
try
{
await Task.Delay(
TimeSpan.FromSeconds(3),
console.GetCancellationToken()
);
console.Output.WriteLine(CompletionOutputText);
}
catch (OperationCanceledException)
{
console.Output.WriteLine(CancellationOutputText);
throw;
}
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using CliFx.Attributes;
using CliFx.Exceptions;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class CommandExceptionCommand : ICommand
{
[CommandOption("code", 'c')]
public int ExitCode { get; set; } = 133;
[CommandOption("msg", 'm')]
public string? Message { get; set; }
[CommandOption("show-help")]
public bool ShowHelp { get; set; }
public ValueTask ExecuteAsync(IConsole console) => throw new CommandException(Message, ExitCode, ShowHelp);
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command(Description = "Default command description")]
public class DefaultCommand : ICommand
{
public const string ExpectedOutputText = nameof(DefaultCommand);
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(ExpectedOutputText);
return default;
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class GenericExceptionCommand : ICommand
{
[CommandOption("msg", 'm')]
public string? Message { get; set; }
public ValueTask ExecuteAsync(IConsole console) => throw new Exception(Message);
}
}

View File

@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class GenericInnerExceptionCommand : ICommand
{
[CommandOption("msg", 'm')]
public string? Message { get; set; }
[CommandOption("inner-msg", 'i')]
public string? InnerMessage { get; set; }
public ValueTask ExecuteAsync(IConsole console) =>
throw new Exception(Message, new Exception(InnerMessage));
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class ConflictWithHelpOptionCommand : SelfSerializeCommandBase
{
[CommandOption("option-h", 'h')]
public string? OptionH { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
// Must be default because version option is available only on default commands
[Command]
public class ConflictWithVersionOptionCommand : SelfSerializeCommandBase
{
[CommandOption("version")]
public string? Version { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateOptionEnvironmentVariableNamesCommand : SelfSerializeCommandBase
{
[CommandOption("option-a", EnvironmentVariableName = "ENV_VAR")]
public string? OptionA { get; set; }
[CommandOption("option-b", EnvironmentVariableName = "ENV_VAR")]
public string? OptionB { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateOptionNamesCommand : SelfSerializeCommandBase
{
[CommandOption("fruits")]
public string? Apples { get; set; }
[CommandOption("fruits")]
public string? Oranges { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateOptionShortNamesCommand : SelfSerializeCommandBase
{
[CommandOption('x')]
public string? OptionA { get; set; }
[CommandOption('x')]
public string? OptionB { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateParameterNameCommand : SelfSerializeCommandBase
{
[CommandParameter(0, Name = "param")]
public string? ParamA { get; set; }
[CommandParameter(1, Name = "param")]
public string? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class DuplicateParameterOrderCommand : SelfSerializeCommandBase
{
[CommandParameter(13)]
public string? ParamA { get; set; }
[CommandParameter(13)]
public string? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class EmptyOptionNameCommand : SelfSerializeCommandBase
{
[CommandOption("")]
public string? Apples { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class InvalidCustomConverterOptionCommand : SelfSerializeCommandBase
{
[CommandOption('f', Converter = typeof(Converter))]
public string? Option { get; set; }
public class Converter
{
public object ConvertFrom(string value) => value;
}
}
}

View File

@@ -0,0 +1,16 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class InvalidCustomConverterParameterCommand : SelfSerializeCommandBase
{
[CommandParameter(0, Converter = typeof(Converter))]
public string? Param { get; set; }
public class Converter
{
public object ConvertFrom(string value) => value;
}
}
}

View File

@@ -0,0 +1,16 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class InvalidCustomValidatorOptionCommand : SelfSerializeCommandBase
{
[CommandOption('f', Validators = new[] { typeof(Validator) })]
public string? Option { get; set; }
public class Validator
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
}
}

View File

@@ -0,0 +1,16 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class InvalidCustomValidatorParameterCommand : SelfSerializeCommandBase
{
[CommandParameter(0, Validators = new[] { typeof(Validator) })]
public string? Param { get; set; }
public class Validator
{
public ValidationResult Validate(string value) => ValidationResult.Ok();
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class MultipleNonScalarParametersCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParamA { get; set; }
[CommandParameter(1)]
public IReadOnlyList<string>? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,6 @@
namespace CliFx.Tests.Commands.Invalid
{
public class NonAnnotatedCommand : SelfSerializeCommandBase
{
}
}

View File

@@ -0,0 +1,9 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class NonImplementedCommand
{
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class NonLastNonScalarParameterCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public IReadOnlyList<string>? ParamA { get; set; }
[CommandParameter(1)]
public string? ParamB { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class NonLetterCharacterNameCommand : SelfSerializeCommandBase
{
[CommandOption("0foo")]
public string? Apples { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class NonLetterCharacterShortNameCommand : SelfSerializeCommandBase
{
[CommandOption('0')]
public string? Apples { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command]
public class OtherDefaultCommand : SelfSerializeCommandBase
{
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands.Invalid
{
[Command("cmd")]
public class SingleCharacterOptionNameCommand : SelfSerializeCommandBase
{
[CommandOption("a")]
public string? Apples { get; set; }
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("named", Description = "Named command description")]
public class NamedCommand : ICommand
{
public const string ExpectedOutputText = nameof(NamedCommand);
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(ExpectedOutputText);
return default;
}
}
}

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("named sub", Description = "Named sub command description")]
public class NamedSubCommand : ICommand
{
public const string ExpectedOutputText = nameof(NamedSubCommand);
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(ExpectedOutputText);
return default;
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace CliFx.Tests.Commands
{
public abstract class SelfSerializeCommandBase : ICommand
{
public ValueTask ExecuteAsync(IConsole console)
{
console.Output.WriteLine(JsonConvert.SerializeObject(this));
return default;
}
}
}

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using CliFx.Attributes;
using Newtonsoft.Json;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public partial class SupportedArgumentTypesCommand : SelfSerializeCommandBase
{
[CommandOption("obj")]
public object? Object { get; set; } = 42;
[CommandOption("str")]
public string? String { get; set; } = "foo bar";
[CommandOption("bool")]
public bool Bool { get; set; }
[CommandOption("char")]
public char Char { get; set; }
[CommandOption("sbyte")]
public sbyte Sbyte { get; set; }
[CommandOption("byte")]
public byte Byte { get; set; }
[CommandOption("short")]
public short Short { get; set; }
[CommandOption("ushort")]
public ushort Ushort { get; set; }
[CommandOption("int")]
public int Int { get; set; }
[CommandOption("uint")]
public uint Uint { get; set; }
[CommandOption("long")]
public long Long { get; set; }
[CommandOption("ulong")]
public ulong Ulong { get; set; }
[CommandOption("float")]
public float Float { get; set; }
[CommandOption("double")]
public double Double { get; set; }
[CommandOption("decimal")]
public decimal Decimal { get; set; }
[CommandOption("datetime")]
public DateTime DateTime { get; set; }
[CommandOption("datetime-offset")]
public DateTimeOffset DateTimeOffset { get; set; }
[CommandOption("timespan")]
public TimeSpan TimeSpan { get; set; }
[CommandOption("enum")]
public CustomEnum Enum { get; set; }
[CommandOption("int-nullable")]
public int? IntNullable { get; set; }
[CommandOption("enum-nullable")]
public CustomEnum? EnumNullable { get; set; }
[CommandOption("timespan-nullable")]
public TimeSpan? TimeSpanNullable { get; set; }
[CommandOption("str-constructible")]
public CustomStringConstructible? StringConstructible { get; set; }
[CommandOption("str-parseable")]
public CustomStringParseable? StringParseable { get; set; }
[CommandOption("str-parseable-format")]
public CustomStringParseableWithFormatProvider? StringParseableWithFormatProvider { get; set; }
[CommandOption("convertible", Converter = typeof(CustomConvertibleConverter))]
public CustomConvertible? Convertible { get; set; }
[CommandOption("obj-array")]
public object[]? ObjectArray { get; set; }
[CommandOption("str-array")]
public string[]? StringArray { get; set; }
[CommandOption("int-array")]
public int[]? IntArray { get; set; }
[CommandOption("enum-array")]
public CustomEnum[]? EnumArray { get; set; }
[CommandOption("int-nullable-array")]
public int?[]? IntNullableArray { get; set; }
[CommandOption("str-constructible-array")]
public CustomStringConstructible[]? StringConstructibleArray { get; set; }
[CommandOption("convertible-array", Converter = typeof(CustomConvertibleConverter))]
public CustomConvertible[]? ConvertibleArray { get; set; }
[CommandOption("str-enumerable")]
public IEnumerable<string>? StringEnumerable { get; set; }
[CommandOption("str-read-only-list")]
public IReadOnlyList<string>? StringReadOnlyList { get; set; }
[CommandOption("str-list")]
public List<string>? StringList { get; set; }
[CommandOption("str-set")]
public HashSet<string>? StringHashSet { get; set; }
}
public partial class SupportedArgumentTypesCommand
{
public enum CustomEnum
{
Value1 = 1,
Value2 = 2,
Value3 = 3
}
public class CustomStringConstructible
{
public string Value { get; }
public CustomStringConstructible(string value) => Value = value;
}
public class CustomStringParseable
{
public string Value { get; }
[JsonConstructor]
private CustomStringParseable(string value) => Value = value;
public static CustomStringParseable Parse(string value) => new CustomStringParseable(value);
}
public class CustomStringParseableWithFormatProvider
{
public string Value { get; }
[JsonConstructor]
private CustomStringParseableWithFormatProvider(string value) => Value = value;
public static CustomStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
new CustomStringParseableWithFormatProvider(value + " " + formatProvider);
}
public class CustomConvertible
{
public int Value { get; }
public CustomConvertible(int value) => Value = value;
}
public class CustomConvertibleConverter : ArgumentValueConverter<CustomConvertible>
{
public override CustomConvertible ConvertFrom(string value) =>
new CustomConvertible(int.Parse(value, CultureInfo.InvariantCulture));
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections;
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public partial class UnsupportedArgumentTypesCommand : SelfSerializeCommandBase
{
[CommandOption("custom")]
public CustomType? CustomNonConvertible { get; set; }
[CommandOption("custom-enumerable")]
public CustomEnumerable<string>? CustomEnumerableNonConvertible { get; set; }
}
public partial class UnsupportedArgumentTypesCommand
{
public class CustomType
{
}
public class CustomEnumerable<T> : IEnumerable<T>
{
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) Array.Empty<T>()).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithDefaultValuesCommand : SelfSerializeCommandBase
{
public enum CustomEnum { Value1, Value2, Value3 };
[CommandOption("obj")]
public object? Object { get; set; } = 42;
[CommandOption("str")]
public string? String { get; set; } = "foo";
[CommandOption("str-empty")]
public string StringEmpty { get; set; } = "";
[CommandOption("str-array")]
public string[]? StringArray { get; set; } = { "foo", "bar", "baz" };
[CommandOption("bool")]
public bool Bool { get; set; } = true;
[CommandOption("char")]
public char Char { get; set; } = 't';
[CommandOption("int")]
public int Int { get; set; } = 1337;
[CommandOption("int-nullable")]
public int? IntNullable { get; set; } = 1337;
[CommandOption("int-array")]
public int[]? IntArray { get; set; } = { 1, 2, 3 };
[CommandOption("timespan")]
public TimeSpan TimeSpan { get; set; } = TimeSpan.FromMinutes(123);
[CommandOption("enum")]
public CustomEnum Enum { get; set; } = CustomEnum.Value2;
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithDependenciesCommand : ICommand
{
public class DependencyA
{
}
public class DependencyB
{
}
private readonly DependencyA _dependencyA;
private readonly DependencyB _dependencyB;
public WithDependenciesCommand(DependencyA dependencyA, DependencyB dependencyB)
{
_dependencyA = dependencyA;
_dependencyB = dependencyB;
}
public ValueTask ExecuteAsync(IConsole console) => default;
}
}

View File

@@ -0,0 +1,19 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithEnumArgumentsCommand : SelfSerializeCommandBase
{
public enum CustomEnum { Value1, Value2, Value3 };
[CommandParameter(0, Name = "enum")]
public CustomEnum EnumParameter { get; set; }
[CommandOption("enum")]
public CustomEnum? EnumOption { get; set; }
[CommandOption("required-enum", IsRequired = true)]
public CustomEnum RequiredEnumOption { get; set; }
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithEnvironmentVariablesCommand : SelfSerializeCommandBase
{
[CommandOption("opt-a", 'a', EnvironmentVariableName = "ENV_OPT_A")]
public string? OptA { get; set; }
[CommandOption("opt-b", 'b', EnvironmentVariableName = "ENV_OPT_B")]
public IReadOnlyList<string>? OptB { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithParametersCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public string? ParamA { get; set; }
[CommandParameter(1)]
public int? ParamB { get; set; }
[CommandParameter(2)]
public IReadOnlyList<string>? ParamC { get; set; }
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithRequiredOptionsCommand : SelfSerializeCommandBase
{
[CommandOption("opt-a", 'a', IsRequired = true)]
public string? OptA { get; set; }
[CommandOption("opt-b", 'b')]
public int? OptB { get; set; }
[CommandOption("opt-c", 'c', IsRequired = true)]
public IReadOnlyList<char>? OptC { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithSingleParameterCommand : SelfSerializeCommandBase
{
[CommandParameter(0)]
public string? ParamA { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using CliFx.Attributes;
namespace CliFx.Tests.Commands
{
[Command("cmd")]
public class WithSingleRequiredOptionCommand : SelfSerializeCommandBase
{
[CommandOption("opt-a")]
public string? OptA { get; set; }
[CommandOption("opt-b", IsRequired = true)]
public string? OptB { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More