mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90a01e729b | ||
|
|
ac01c2aecb | ||
|
|
4acffe925c | ||
|
|
18f53eeeef | ||
|
|
03d6942540 | ||
|
|
9be811a89a | ||
|
|
f9f5a4696b | ||
|
|
d6da687170 | ||
|
|
eba66d0878 | ||
|
|
8c682766bd | ||
|
|
39d626c8d8 | ||
|
|
a338ac8ce2 | ||
|
|
11637127cb | ||
|
|
4e12aefafb | ||
|
|
144d3592fb | ||
|
|
6f82c2f0f9 | ||
|
|
b8c60717d5 | ||
|
|
fec6850c39 | ||
|
|
6a378ad946 | ||
|
|
11579f11b1 | ||
|
|
60a3b26fd1 | ||
|
|
3abdfb1acf | ||
|
|
9557d386e2 | ||
|
|
d0d024c427 | ||
|
|
f765af6061 | ||
|
|
7f2202e869 | ||
|
|
14ad9d5738 | ||
|
|
b120138de3 | ||
|
|
8df1d607c1 | ||
|
|
c06f2810b9 | ||
|
|
d52a205f13 | ||
|
|
0ec12e57c1 | ||
|
|
c322b7029c | ||
|
|
6a38c04c11 | ||
|
|
5e53107def | ||
|
|
36cea937de | ||
|
|
438d6b98ac | ||
|
|
8e1488c395 | ||
|
|
65d321b476 |
63
.gitattributes
vendored
63
.gitattributes
vendored
@@ -1,63 +0,0 @@
|
|||||||
###############################################################################
|
|
||||||
# Set default behavior to automatically normalize line endings.
|
|
||||||
###############################################################################
|
|
||||||
* text=auto
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set default behavior for command prompt diff.
|
|
||||||
#
|
|
||||||
# This is need for earlier builds of msysgit that does not have it on by
|
|
||||||
# default for csharp files.
|
|
||||||
# Note: This is only used by command line
|
|
||||||
###############################################################################
|
|
||||||
#*.cs diff=csharp
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Set the merge driver for project and solution files
|
|
||||||
#
|
|
||||||
# Merging from the command prompt will add diff markers to the files if there
|
|
||||||
# are conflicts (Merging from VS is not affected by the settings below, in VS
|
|
||||||
# the diff markers are never inserted). Diff markers may cause the following
|
|
||||||
# file extensions to fail to load in VS. An alternative would be to treat
|
|
||||||
# these files as binary and thus will always conflict and require user
|
|
||||||
# intervention with every merge. To do so, just uncomment the entries below
|
|
||||||
###############################################################################
|
|
||||||
#*.sln merge=binary
|
|
||||||
#*.csproj merge=binary
|
|
||||||
#*.vbproj merge=binary
|
|
||||||
#*.vcxproj merge=binary
|
|
||||||
#*.vcproj merge=binary
|
|
||||||
#*.dbproj merge=binary
|
|
||||||
#*.fsproj merge=binary
|
|
||||||
#*.lsproj merge=binary
|
|
||||||
#*.wixproj merge=binary
|
|
||||||
#*.modelproj merge=binary
|
|
||||||
#*.sqlproj merge=binary
|
|
||||||
#*.wwaproj merge=binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# behavior for image files
|
|
||||||
#
|
|
||||||
# image files are treated as binary by default.
|
|
||||||
###############################################################################
|
|
||||||
#*.jpg binary
|
|
||||||
#*.png binary
|
|
||||||
#*.gif binary
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# diff behavior for common document formats
|
|
||||||
#
|
|
||||||
# Convert binary document formats to text before diffing them. This feature
|
|
||||||
# is only available from the command line. Turn it on by uncommenting the
|
|
||||||
# entries below.
|
|
||||||
###############################################################################
|
|
||||||
#*.doc diff=astextplain
|
|
||||||
#*.DOC diff=astextplain
|
|
||||||
#*.docx diff=astextplain
|
|
||||||
#*.DOCX diff=astextplain
|
|
||||||
#*.dot diff=astextplain
|
|
||||||
#*.DOT diff=astextplain
|
|
||||||
#*.pdf diff=astextplain
|
|
||||||
#*.PDF diff=astextplain
|
|
||||||
#*.rtf diff=astextplain
|
|
||||||
#*.RTF diff=astextplain
|
|
||||||
22
.github/workflows/CD.yml
vendored
22
.github/workflows/CD.yml
vendored
@@ -3,23 +3,23 @@ name: CD
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*'
|
- "*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.3
|
||||||
|
|
||||||
- name: Install .NET Core
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@v1.4.0
|
uses: actions/setup-dotnet@v1.7.2
|
||||||
with:
|
with:
|
||||||
dotnet-version: 3.1.100
|
dotnet-version: 5.0.100
|
||||||
|
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: dotnet pack CliFx --configuration Release
|
run: dotnet pack CliFx --configuration Release
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }}
|
run: dotnet nuget push CliFx/bin/Release/*.nupkg -s nuget.org -k ${{ secrets.NUGET_TOKEN }}
|
||||||
|
|||||||
31
.github/workflows/CI.yml
vendored
31
.github/workflows/CI.yml
vendored
@@ -11,25 +11,18 @@ jobs:
|
|||||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2.3.3
|
||||||
|
|
||||||
- name: Install .NET Core
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@v1.4.0
|
uses: actions/setup-dotnet@v1.7.2
|
||||||
with:
|
with:
|
||||||
dotnet-version: 3.1.100
|
dotnet-version: 5.0.100
|
||||||
|
|
||||||
- name: Build & test
|
- name: Build & test
|
||||||
run: dotnet test --configuration Release --logger GitHubActions
|
run: dotnet test --configuration Release --logger GitHubActions
|
||||||
|
|
||||||
- name: Upload coverage
|
- name: Upload coverage
|
||||||
uses: codecov/codecov-action@v1.0.5
|
uses: codecov/codecov-action@v1.0.5
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
file: CliFx.Tests/bin/Release/Coverage.xml
|
|
||||||
|
|
||||||
- name: Upload coverage (analyzers)
|
|
||||||
uses: codecov/codecov-action@v1.0.5
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
file: CliFx.Analyzers.Tests/bin/Release/Coverage.xml
|
|
||||||
|
|||||||
332
.gitignore
vendored
332
.gitignore
vendored
@@ -1,341 +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_*
|
|
||||||
.ncrunchsolution
|
|
||||||
|
|
||||||
# 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
|
|
||||||
12
Changelog.md
12
Changelog.md
@@ -1,3 +1,15 @@
|
|||||||
|
### 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)
|
### 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 `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.
|
||||||
|
|||||||
@@ -2,23 +2,22 @@
|
|||||||
<Import Project="../CliFx.props" />
|
<Import Project="../CliFx.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Gu.Roslyn.Asserts" Version="3.3.0" />
|
<PackageReference Include="Gu.Roslyn.Asserts" Version="3.3.1" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.0.0" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.2" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||||
<PackageReference Include="xunit" Version="2.4.0" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
|
||||||
<PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
|
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -140,6 +140,54 @@ public class MyCommand : ICommand
|
|||||||
[CommandParameter(2)]
|
[CommandParameter(2)]
|
||||||
public IReadOnlyList<string> ParamB { get; set; }
|
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;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
@@ -157,7 +205,7 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption(""foo"")]
|
[CommandOption(""foo"")]
|
||||||
public string Param { get; set; }
|
public string Option { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -176,7 +224,7 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption(""foo"", 'f')]
|
[CommandOption(""foo"", 'f')]
|
||||||
public string Param { get; set; }
|
public string Option { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -195,10 +243,10 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption(""foo"")]
|
[CommandOption(""foo"")]
|
||||||
public string ParamA { get; set; }
|
public string OptionA { get; set; }
|
||||||
|
|
||||||
[CommandOption(""bar"")]
|
[CommandOption(""bar"")]
|
||||||
public string ParamB { get; set; }
|
public string OptionB { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -217,10 +265,10 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption('f')]
|
[CommandOption('f')]
|
||||||
public string ParamA { get; set; }
|
public string OptionA { get; set; }
|
||||||
|
|
||||||
[CommandOption('x')]
|
[CommandOption('x')]
|
||||||
public string ParamB { get; set; }
|
public string OptionB { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -239,11 +287,59 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption('a', EnvironmentVariableName = ""env_var_a"")]
|
[CommandOption('a', EnvironmentVariableName = ""env_var_a"")]
|
||||||
public string ParamA { get; set; }
|
public string OptionA { get; set; }
|
||||||
|
|
||||||
[CommandOption('b', EnvironmentVariableName = ""env_var_b"")]
|
[CommandOption('b', EnvironmentVariableName = ""env_var_b"")]
|
||||||
public string ParamB { get; set; }
|
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 ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
@@ -366,6 +462,54 @@ public class MyCommand : ICommand
|
|||||||
[CommandParameter(2)]
|
[CommandParameter(2)]
|
||||||
public string ParamB { get; set; }
|
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;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
@@ -383,7 +527,7 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption("""")]
|
[CommandOption("""")]
|
||||||
public string Param { get; set; }
|
public string Option { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -402,7 +546,7 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption(""a"")]
|
[CommandOption(""a"")]
|
||||||
public string Param { get; set; }
|
public string Option { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -421,10 +565,10 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption(""foo"")]
|
[CommandOption(""foo"")]
|
||||||
public string ParamA { get; set; }
|
public string OptionA { get; set; }
|
||||||
|
|
||||||
[CommandOption(""foo"")]
|
[CommandOption(""foo"")]
|
||||||
public string ParamB { get; set; }
|
public string OptionB { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -443,10 +587,10 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption('f')]
|
[CommandOption('f')]
|
||||||
public string ParamA { get; set; }
|
public string OptionA { get; set; }
|
||||||
|
|
||||||
[CommandOption('f')]
|
[CommandOption('f')]
|
||||||
public string ParamB { get; set; }
|
public string OptionB { get; set; }
|
||||||
|
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
@@ -465,10 +609,96 @@ public class MyCommand : ICommand
|
|||||||
public class MyCommand : ICommand
|
public class MyCommand : ICommand
|
||||||
{
|
{
|
||||||
[CommandOption('a', EnvironmentVariableName = ""env_var"")]
|
[CommandOption('a', EnvironmentVariableName = ""env_var"")]
|
||||||
public string ParamA { get; set; }
|
public string OptionA { get; set; }
|
||||||
|
|
||||||
[CommandOption('b', EnvironmentVariableName = ""env_var"")]
|
[CommandOption('b', EnvironmentVariableName = ""env_var"")]
|
||||||
public string ParamB { get; set; }
|
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;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis.Diagnostics;
|
|||||||
|
|
||||||
namespace CliFx.Analyzers
|
namespace CliFx.Analyzers
|
||||||
{
|
{
|
||||||
|
// TODO: split into multiple analyzers
|
||||||
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
[DiagnosticAnalyzer(LanguageNames.CSharp)]
|
||||||
public class CommandSchemaAnalyzer : DiagnosticAnalyzer
|
public class CommandSchemaAnalyzer : DiagnosticAnalyzer
|
||||||
{
|
{
|
||||||
@@ -17,16 +18,23 @@ namespace CliFx.Analyzers
|
|||||||
DiagnosticDescriptors.CliFx0022,
|
DiagnosticDescriptors.CliFx0022,
|
||||||
DiagnosticDescriptors.CliFx0023,
|
DiagnosticDescriptors.CliFx0023,
|
||||||
DiagnosticDescriptors.CliFx0024,
|
DiagnosticDescriptors.CliFx0024,
|
||||||
|
DiagnosticDescriptors.CliFx0025,
|
||||||
|
DiagnosticDescriptors.CliFx0026,
|
||||||
DiagnosticDescriptors.CliFx0041,
|
DiagnosticDescriptors.CliFx0041,
|
||||||
DiagnosticDescriptors.CliFx0042,
|
DiagnosticDescriptors.CliFx0042,
|
||||||
DiagnosticDescriptors.CliFx0043,
|
DiagnosticDescriptors.CliFx0043,
|
||||||
DiagnosticDescriptors.CliFx0044,
|
DiagnosticDescriptors.CliFx0044,
|
||||||
DiagnosticDescriptors.CliFx0045
|
DiagnosticDescriptors.CliFx0045,
|
||||||
|
DiagnosticDescriptors.CliFx0046,
|
||||||
|
DiagnosticDescriptors.CliFx0047,
|
||||||
|
DiagnosticDescriptors.CliFx0048,
|
||||||
|
DiagnosticDescriptors.CliFx0049
|
||||||
);
|
);
|
||||||
|
|
||||||
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
|
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
|
||||||
KnownSymbols.IsSystemString(typeSymbol) ||
|
KnownSymbols.IsSystemString(typeSymbol) ||
|
||||||
!typeSymbol.AllInterfaces.Select(i => i.ConstructedFrom).Any(KnownSymbols.IsSystemCollectionsGenericIEnumerable);
|
!typeSymbol.AllInterfaces.Select(i => i.ConstructedFrom)
|
||||||
|
.Any(KnownSymbols.IsSystemCollectionsGenericIEnumerable);
|
||||||
|
|
||||||
private static void CheckCommandParameterProperties(
|
private static void CheckCommandParameterProperties(
|
||||||
SymbolAnalysisContext context,
|
SymbolAnalysisContext context,
|
||||||
@@ -50,11 +58,28 @@ namespace CliFx.Analyzers
|
|||||||
.Select(a => a.Value.Value)
|
.Select(a => a.Value.Value)
|
||||||
.FirstOrDefault() as string;
|
.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
|
return new
|
||||||
{
|
{
|
||||||
Property = p,
|
Property = p,
|
||||||
Order = order,
|
Order = order,
|
||||||
Name = name
|
Name = name,
|
||||||
|
Converter = converter,
|
||||||
|
Validators = validators
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -69,8 +94,9 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
foreach (var parameter in duplicateOrderParameters)
|
foreach (var parameter in duplicateOrderParameters)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0021, parameter.Property.Locations.First()));
|
DiagnosticDescriptors.CliFx0021, parameter.Property.Locations.First()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicate name
|
// Duplicate name
|
||||||
@@ -83,8 +109,9 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
foreach (var parameter in duplicateNameParameters)
|
foreach (var parameter in duplicateNameParameters)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0022, parameter.Property.Locations.First()));
|
DiagnosticDescriptors.CliFx0022, parameter.Property.Locations.First()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple non-scalar
|
// Multiple non-scalar
|
||||||
@@ -96,8 +123,9 @@ namespace CliFx.Analyzers
|
|||||||
{
|
{
|
||||||
foreach (var parameter in nonScalarParameters)
|
foreach (var parameter in nonScalarParameters)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0023, parameter.Property.Locations.First()));
|
DiagnosticDescriptors.CliFx0023, parameter.Property.Locations.First()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,8 +137,35 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
if (nonLastNonScalarParameter != null)
|
if (nonLastNonScalarParameter != null)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0024, nonLastNonScalarParameter.Property.Locations.First()));
|
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()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,12 +198,29 @@ namespace CliFx.Analyzers
|
|||||||
.Select(a => a.Value.Value)
|
.Select(a => a.Value.Value)
|
||||||
.FirstOrDefault() as string;
|
.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
|
return new
|
||||||
{
|
{
|
||||||
Property = p,
|
Property = p,
|
||||||
Name = name,
|
Name = name,
|
||||||
ShortName = shortName,
|
ShortName = shortName,
|
||||||
EnvironmentVariableName = envVarName
|
EnvironmentVariableName = envVarName,
|
||||||
|
Converter = converter,
|
||||||
|
Validators = validators
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -160,8 +232,9 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
foreach (var option in noNameOptions)
|
foreach (var option in noNameOptions)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0041, option.Property.Locations.First()));
|
DiagnosticDescriptors.CliFx0041, option.Property.Locations.First()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Too short name
|
// Too short name
|
||||||
@@ -171,8 +244,9 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
foreach (var option in invalidNameLengthOptions)
|
foreach (var option in invalidNameLengthOptions)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0042, option.Property.Locations.First()));
|
DiagnosticDescriptors.CliFx0042, option.Property.Locations.First()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicate name
|
// Duplicate name
|
||||||
@@ -185,8 +259,9 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
foreach (var option in duplicateNameOptions)
|
foreach (var option in duplicateNameOptions)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0043, option.Property.Locations.First()));
|
DiagnosticDescriptors.CliFx0043, option.Property.Locations.First()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicate name
|
// Duplicate name
|
||||||
@@ -199,8 +274,9 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
foreach (var option in duplicateShortNameOptions)
|
foreach (var option in duplicateShortNameOptions)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0044, option.Property.Locations.First()));
|
DiagnosticDescriptors.CliFx0044, option.Property.Locations.First()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duplicate environment variable name
|
// Duplicate environment variable name
|
||||||
@@ -213,19 +289,67 @@ namespace CliFx.Analyzers
|
|||||||
|
|
||||||
foreach (var option in duplicateEnvironmentVariableNameOptions)
|
foreach (var option in duplicateEnvironmentVariableNameOptions)
|
||||||
{
|
{
|
||||||
context.ReportDiagnostic(
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
Diagnostic.Create(DiagnosticDescriptors.CliFx0045, option.Property.Locations.First()));
|
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)
|
private static void CheckCommandType(SymbolAnalysisContext context)
|
||||||
{
|
{
|
||||||
// Named type: MyCommand
|
// Named type: MyCommand
|
||||||
if (!(context.Symbol is INamedTypeSymbol namedTypeSymbol))
|
if (!(context.Symbol is INamedTypeSymbol namedTypeSymbol) ||
|
||||||
return;
|
namedTypeSymbol.TypeKind != TypeKind.Class)
|
||||||
|
|
||||||
// Only classes
|
|
||||||
if (namedTypeSymbol.TypeKind != TypeKind.Class)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Implements ICommand?
|
// Implements ICommand?
|
||||||
@@ -252,10 +376,12 @@ namespace CliFx.Analyzers
|
|||||||
var isAlmostValidCommandType = implementsCommandInterface ^ hasCommandAttribute;
|
var isAlmostValidCommandType = implementsCommandInterface ^ hasCommandAttribute;
|
||||||
|
|
||||||
if (isAlmostValidCommandType && !implementsCommandInterface)
|
if (isAlmostValidCommandType && !implementsCommandInterface)
|
||||||
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0001, namedTypeSymbol.Locations.First()));
|
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0001,
|
||||||
|
namedTypeSymbol.Locations.First()));
|
||||||
|
|
||||||
if (isAlmostValidCommandType && !hasCommandAttribute)
|
if (isAlmostValidCommandType && !hasCommandAttribute)
|
||||||
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0002, namedTypeSymbol.Locations.First()));
|
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0002,
|
||||||
|
namedTypeSymbol.Locations.First()));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,55 +18,49 @@ namespace CliFx.Analyzers
|
|||||||
SyntaxNodeAnalysisContext context,
|
SyntaxNodeAnalysisContext context,
|
||||||
InvocationExpressionSyntax invocationSyntax)
|
InvocationExpressionSyntax invocationSyntax)
|
||||||
{
|
{
|
||||||
// Get the method member access (Console.WriteLine or Console.Error.WriteLine)
|
if (invocationSyntax.Expression is MemberAccessExpressionSyntax memberAccessSyntax &&
|
||||||
if (!(invocationSyntax.Expression is MemberAccessExpressionSyntax memberAccessSyntax))
|
context.SemanticModel.GetSymbolInfo(memberAccessSyntax).Symbol is IMethodSymbol methodSymbol)
|
||||||
return false;
|
{
|
||||||
|
// Direct call to System.Console (e.g. System.Console.WriteLine())
|
||||||
|
if (KnownSymbols.IsSystemConsole(methodSymbol.ContainingType))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the semantic model for the invoked method
|
// Indirect call to System.Console (e.g. System.Console.Error.WriteLine())
|
||||||
if (!(context.SemanticModel.GetSymbolInfo(memberAccessSyntax).Symbol is IMethodSymbol methodSymbol))
|
if (memberAccessSyntax.Expression is MemberAccessExpressionSyntax parentMemberAccessSyntax &&
|
||||||
return false;
|
context.SemanticModel.GetSymbolInfo(parentMemberAccessSyntax).Symbol is IPropertySymbol propertySymbol)
|
||||||
|
{
|
||||||
// Check if contained within System.Console
|
return KnownSymbols.IsSystemConsole(propertySymbol.ContainingType);
|
||||||
if (KnownSymbols.IsSystemConsole(methodSymbol.ContainingType))
|
}
|
||||||
return true;
|
}
|
||||||
|
|
||||||
// In case with Console.Error.WriteLine that wouldn't work, we need to check parent member access too
|
|
||||||
if (!(memberAccessSyntax.Expression is MemberAccessExpressionSyntax parentMemberAccessSyntax))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Get the semantic model for the parent member
|
|
||||||
if (!(context.SemanticModel.GetSymbolInfo(parentMemberAccessSyntax).Symbol is IPropertySymbol propertySymbol))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if contained within System.Console
|
|
||||||
if (KnownSymbols.IsSystemConsole(propertySymbol.ContainingType))
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckSystemConsoleUsage(SyntaxNodeAnalysisContext context)
|
private static void CheckSystemConsoleUsage(SyntaxNodeAnalysisContext context)
|
||||||
{
|
{
|
||||||
if (!(context.Node is InvocationExpressionSyntax invocationSyntax))
|
if (context.Node is InvocationExpressionSyntax invocationSyntax &&
|
||||||
return;
|
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 (!IsSystemConsoleInvocation(context, invocationSyntax))
|
if (isConsoleInterfaceAvailable)
|
||||||
return;
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
// Check if IConsole is available in the scope as a viable alternative
|
DiagnosticDescriptors.CliFx0100,
|
||||||
var isConsoleInterfaceAvailable = invocationSyntax
|
invocationSyntax.GetLocation()
|
||||||
.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)
|
|
||||||
return;
|
|
||||||
|
|
||||||
context.ReportDiagnostic(Diagnostic.Create(DiagnosticDescriptors.CliFx0100, invocationSyntax.GetLocation()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Initialize(AnalysisContext context)
|
public override void Initialize(AnalysisContext context)
|
||||||
|
|||||||
@@ -8,72 +8,126 @@ namespace CliFx.Analyzers
|
|||||||
new DiagnosticDescriptor(nameof(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",
|
||||||
"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);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0002 =
|
public static readonly DiagnosticDescriptor CliFx0002 =
|
||||||
new DiagnosticDescriptor(nameof(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",
|
||||||
"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);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0021 =
|
public static readonly DiagnosticDescriptor CliFx0021 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0021),
|
new DiagnosticDescriptor(nameof(CliFx0021),
|
||||||
"Parameter order must be unique within its command",
|
"Parameter order must be unique within its command",
|
||||||
"Parameter order must be unique within its command",
|
"Parameter order must be unique within its command",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0022 =
|
public static readonly DiagnosticDescriptor CliFx0022 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0022),
|
new DiagnosticDescriptor(nameof(CliFx0022),
|
||||||
"Parameter order must have unique name within its command",
|
"Parameter order must have unique name within its command",
|
||||||
"Parameter order must have unique name within its command",
|
"Parameter order must have unique name within its command",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0023 =
|
public static readonly DiagnosticDescriptor CliFx0023 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0023),
|
new DiagnosticDescriptor(nameof(CliFx0023),
|
||||||
"Only one non-scalar parameter per command is allowed",
|
"Only one non-scalar parameter per command is allowed",
|
||||||
"Only one non-scalar parameter per command is allowed",
|
"Only one non-scalar parameter per command is allowed",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0024 =
|
public static readonly DiagnosticDescriptor CliFx0024 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0024),
|
new DiagnosticDescriptor(nameof(CliFx0024),
|
||||||
"Non-scalar parameter must be last in order",
|
"Non-scalar parameter must be last in order",
|
||||||
"Non-scalar parameter must be last in order",
|
"Non-scalar parameter must be last in order",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"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 =
|
public static readonly DiagnosticDescriptor CliFx0041 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0041),
|
new DiagnosticDescriptor(nameof(CliFx0041),
|
||||||
"Option must have a name or short name specified",
|
"Option must have a name or short name specified",
|
||||||
"Option must have a name or short name specified",
|
"Option must have a name or short name specified",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0042 =
|
public static readonly DiagnosticDescriptor CliFx0042 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0042),
|
new DiagnosticDescriptor(nameof(CliFx0042),
|
||||||
"Option name must be at least 2 characters long",
|
"Option name must be at least 2 characters long",
|
||||||
"Option name must be at least 2 characters long",
|
"Option name must be at least 2 characters long",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0043 =
|
public static readonly DiagnosticDescriptor CliFx0043 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0043),
|
new DiagnosticDescriptor(nameof(CliFx0043),
|
||||||
"Option name must be unique within its command",
|
"Option name must be unique within its command",
|
||||||
"Option name must be unique within its command",
|
"Option name must be unique within its command",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0044 =
|
public static readonly DiagnosticDescriptor CliFx0044 =
|
||||||
new DiagnosticDescriptor(nameof(CliFx0044),
|
new DiagnosticDescriptor(nameof(CliFx0044),
|
||||||
"Option short name must be unique within its command",
|
"Option short name must be unique within its command",
|
||||||
"Option short name must be unique within its command",
|
"Option short name must be unique within its command",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"Usage", DiagnosticSeverity.Error, true
|
||||||
|
);
|
||||||
|
|
||||||
public static readonly DiagnosticDescriptor CliFx0045 =
|
public static readonly DiagnosticDescriptor CliFx0045 =
|
||||||
new DiagnosticDescriptor(nameof(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",
|
||||||
"Option environment variable name must be unique within its command",
|
"Option environment variable name must be unique within its command",
|
||||||
"Usage", DiagnosticSeverity.Error, true);
|
"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 =
|
public static readonly DiagnosticDescriptor CliFx0100 =
|
||||||
new DiagnosticDescriptor(nameof(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",
|
||||||
"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);
|
"Usage", DiagnosticSeverity.Warning, true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis;
|
|||||||
|
|
||||||
namespace CliFx.Analyzers
|
namespace CliFx.Analyzers
|
||||||
{
|
{
|
||||||
public static class KnownSymbols
|
internal static class KnownSymbols
|
||||||
{
|
{
|
||||||
public static bool IsSystemString(ISymbol symbol) =>
|
public static bool IsSystemString(ISymbol symbol) =>
|
||||||
symbol.DisplayNameMatches("string") ||
|
symbol.DisplayNameMatches("string") ||
|
||||||
@@ -25,6 +25,12 @@ namespace CliFx.Analyzers
|
|||||||
public static bool IsCommandInterface(ISymbol symbol) =>
|
public static bool IsCommandInterface(ISymbol symbol) =>
|
||||||
symbol.DisplayNameMatches("CliFx.ICommand");
|
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) =>
|
public static bool IsCommandAttribute(ISymbol symbol) =>
|
||||||
symbol.DisplayNameMatches("CliFx.Attributes.CommandAttribute");
|
symbol.DisplayNameMatches("CliFx.Attributes.CommandAttribute");
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.12.0" />
|
||||||
<PackageReference Include="clipr" Version="1.6.1" />
|
<PackageReference Include="clipr" Version="1.6.1" />
|
||||||
<PackageReference Include="Cocona" Version="1.3.0" />
|
<PackageReference Include="Cocona" Version="1.5.0" />
|
||||||
<PackageReference Include="CommandLineParser" Version="2.7.82" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="2.6.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>
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace CliFx.Demo
|
|||||||
public static async Task<int> Main() =>
|
public static async Task<int> Main() =>
|
||||||
await new CliApplicationBuilder()
|
await new CliApplicationBuilder()
|
||||||
.AddCommandsFromThisAssembly()
|
.AddCommandsFromThisAssembly()
|
||||||
.UseTypeActivator(GetServiceProvider().GetService)
|
.UseTypeActivator(GetServiceProvider().GetRequiredService)
|
||||||
.Build()
|
.Build()
|
||||||
.RunAsync();
|
.RunAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -232,6 +232,46 @@ namespace CliFx.Tests
|
|||||||
_output.WriteLine(stdErr.GetString());
|
_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]
|
[Fact]
|
||||||
public async Task Command_options_must_have_names_that_are_not_empty()
|
public async Task Command_options_must_have_names_that_are_not_empty()
|
||||||
{
|
{
|
||||||
@@ -371,5 +411,85 @@ namespace CliFx.Tests
|
|||||||
|
|
||||||
_output.WriteLine(stdErr.GetString());
|
_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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +191,34 @@ namespace CliFx.Tests
|
|||||||
_output.WriteLine(stdErr.GetString());
|
_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]
|
[Fact]
|
||||||
public async Task All_provided_option_arguments_must_be_bound_to_corresponding_properties()
|
public async Task All_provided_option_arguments_must_be_bound_to_corresponding_properties()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace CliFx.Tests
|
|||||||
public ArgumentConversionSpecs(ITestOutputHelper output) => _output = output;
|
public ArgumentConversionSpecs(ITestOutputHelper output) => _output = output;
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_object_is_bound_directly_from_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_object()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -45,7 +45,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_object_array_is_bound_directly_from_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_array_of_object()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -73,7 +73,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_string_is_bound_directly_from_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -101,7 +101,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_string_array_is_bound_directly_from_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_array_of_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -129,7 +129,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_string_IEnumerable_is_bound_directly_from_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_IEnumerable_of_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -157,7 +157,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_string_IReadOnlyList_is_bound_directly_from_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_IReadOnlyList_of_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -185,7 +185,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_string_List_is_bound_directly_from_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_List_of_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -213,7 +213,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_string_HashSet_is_bound_directly_from_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_HashSet_of_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -241,7 +241,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_true()
|
public async Task Argument_value_can_be_bound_to_boolean_as_true_if_the_value_is_true()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -269,7 +269,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_bool_is_bound_as_false_if_the_argument_value_is_false()
|
public async Task Argument_value_can_be_bound_to_boolean_as_false_if_the_value_is_false()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -297,7 +297,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_bool_is_bound_as_true_if_the_argument_value_is_not_set()
|
public async Task Argument_value_can_be_bound_to_boolean_as_true_if_the_value_is_not_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -325,7 +325,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_char_is_bound_directly_from_the_argument_value_if_it_contains_only_one_character()
|
public async Task Argument_value_can_be_bound_to_char_if_the_value_contains_a_single_character()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -353,7 +353,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_sbyte_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_sbyte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -381,7 +381,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_byte_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_byte()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -409,7 +409,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_short_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_short()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -437,7 +437,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_ushort_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_ushort()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -465,7 +465,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_int_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_int()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -493,7 +493,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_nullable_int_is_bound_by_parsing_the_argument_value_if_it_is_set()
|
public async Task Argument_value_can_be_bound_to_nullable_of_int_as_actual_value_if_it_is_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -521,7 +521,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_nullable_int_is_bound_as_null_if_the_argument_value_is_not_set()
|
public async Task Argument_value_can_be_bound_to_nullable_of_int_as_null_if_it_is_not_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -549,7 +549,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_int_array_is_bound_by_parsing_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_array_of_int()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -577,7 +577,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_nullable_int_array_is_bound_by_parsing_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_array_of_nullable_of_int()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -605,7 +605,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_uint_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_uint()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -633,7 +633,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_long_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_long()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -661,7 +661,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_ulong_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_ulong()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -689,7 +689,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_float_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_float()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -717,7 +717,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_double_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_double()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -745,7 +745,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_decimal_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_decimal()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -773,7 +773,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_DateTime_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_DateTime()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -801,7 +801,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_DateTimeOffset_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_DateTimeOffset()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -829,7 +829,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_TimeSpan_is_bound_by_parsing_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_TimeSpan()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -857,7 +857,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_nullable_TimeSpan_is_bound_by_parsing_the_argument_value_if_it_is_set()
|
public async Task Argument_value_can_be_bound_to_nullable_of_TimeSpan_as_actual_value_if_it_is_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -885,7 +885,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_type_nullable_TimeSpan_is_bound_as_null_if_the_argument_value_is_not_set()
|
public async Task Argument_value_can_be_bound_to_nullable_of_TimeSpan_as_null_if_it_is_not_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -913,7 +913,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_name()
|
public async Task Argument_value_can_be_bound_to_enum_type_by_name()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -941,7 +941,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_an_enum_type_is_bound_by_parsing_the_argument_value_as_id()
|
public async Task Argument_value_can_be_bound_to_enum_type_by_id()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -969,7 +969,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_name_if_it_is_set()
|
public async Task Argument_value_can_be_bound_to_nullable_of_enum_type_by_name_if_it_is_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -997,7 +997,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_a_nullable_enum_type_is_bound_by_parsing_the_argument_value_as_id_if_it_is_set()
|
public async Task Argument_value_can_be_bound_to_nullable_of_enum_type_by_id_if_it_is_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1025,7 +1025,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_a_nullable_enum_type_is_bound_as_null_if_the_argument_value_is_not_set()
|
public async Task Argument_value_can_be_bound_to_nullable_of_enum_type_as_null_if_it_is_not_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1053,7 +1053,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_names()
|
public async Task Argument_values_can_be_bound_to_array_of_enum_type_by_names()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1081,7 +1081,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_ids()
|
public async Task Argument_values_can_be_bound_to_array_of_enum_type_by_ids()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1109,7 +1109,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_an_enum_array_type_is_bound_by_parsing_the_argument_values_as_either_names_or_ids()
|
public async Task Argument_values_can_be_bound_to_array_of_enum_type_by_either_names_or_ids()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1137,7 +1137,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_a_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_value()
|
public async Task Argument_value_can_be_bound_to_a_custom_type_if_it_has_a_constructor_accepting_a_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1165,7 +1165,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_an_array_of_type_that_has_a_constructor_accepting_a_string_is_bound_by_invoking_the_constructor_with_the_argument_values()
|
public async Task Argument_values_can_be_bound_to_array_of_custom_type_if_it_has_a_constructor_accepting_a_string()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1197,7 +1197,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_is_bound_by_invoking_the_method()
|
public async Task Argument_value_can_be_bound_to_a_custom_type_if_it_has_a_static_Parse_method()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1225,7 +1225,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_a_type_that_has_a_static_Parse_method_accepting_a_string_and_format_provider_is_bound_by_invoking_the_method()
|
public async Task Argument_value_can_be_bound_to_a_custom_type_if_it_has_a_static_Parse_method_with_format_provider()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1254,7 +1254,72 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_custom_type_must_be_string_initializable_in_order_to_be_bound()
|
public async Task Argument_value_can_be_bound_to_a_custom_type_if_a_converter_has_been_specified()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<SupportedArgumentTypesCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(new[]
|
||||||
|
{
|
||||||
|
"cmd", "--convertible", "13"
|
||||||
|
});
|
||||||
|
|
||||||
|
var commandInstance = stdOut.GetString().DeserializeJson<SupportedArgumentTypesCommand>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand
|
||||||
|
{
|
||||||
|
Convertible =
|
||||||
|
(SupportedArgumentTypesCommand.CustomConvertible)
|
||||||
|
new SupportedArgumentTypesCommand.CustomConvertibleConverter().ConvertFrom("13")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Argument_values_can_be_bound_to_array_of_custom_type_if_a_converter_has_been_specified()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<SupportedArgumentTypesCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(new[]
|
||||||
|
{
|
||||||
|
"cmd", "--convertible-array", "13", "42"
|
||||||
|
});
|
||||||
|
|
||||||
|
var commandInstance = stdOut.GetString().DeserializeJson<SupportedArgumentTypesCommand>();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
|
||||||
|
commandInstance.Should().BeEquivalentTo(new SupportedArgumentTypesCommand
|
||||||
|
{
|
||||||
|
ConvertibleArray = new[]
|
||||||
|
{
|
||||||
|
(SupportedArgumentTypesCommand.CustomConvertible)
|
||||||
|
new SupportedArgumentTypesCommand.CustomConvertibleConverter().ConvertFrom("13"),
|
||||||
|
|
||||||
|
(SupportedArgumentTypesCommand.CustomConvertible)
|
||||||
|
new SupportedArgumentTypesCommand.CustomConvertibleConverter().ConvertFrom("42")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Argument_value_can_only_be_bound_if_the_target_type_is_supported()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1267,7 +1332,7 @@ namespace CliFx.Tests
|
|||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(new[]
|
var exitCode = await application.RunAsync(new[]
|
||||||
{
|
{
|
||||||
"cmd", "--str-non-initializable", "foobar"
|
"cmd", "--custom"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -1278,20 +1343,20 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_custom_type_that_implements_IEnumerable_can_only_be_bound_if_that_type_has_a_constructor_accepting_an_array()
|
public async Task Argument_value_can_only_be_bound_if_the_provided_value_can_be_converted_to_the_target_type()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
var application = new CliApplicationBuilder()
|
var application = new CliApplicationBuilder()
|
||||||
.AddCommand<UnsupportedArgumentTypesCommand>()
|
.AddCommand<SupportedArgumentTypesCommand>()
|
||||||
.UseConsole(console)
|
.UseConsole(console)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(new[]
|
var exitCode = await application.RunAsync(new[]
|
||||||
{
|
{
|
||||||
"cmd", "--str-enumerable-non-initializable", "foobar"
|
"cmd", "--int", "foo"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
@@ -1302,7 +1367,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_of_non_nullable_type_can_only_be_bound_if_the_argument_value_is_set()
|
public async Task Argument_value_can_only_be_bound_to_non_nullable_type_if_it_is_set()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1326,7 +1391,7 @@ namespace CliFx.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Property_must_have_a_type_that_implements_IEnumerable_in_order_to_be_bound_from_multiple_argument_values()
|
public async Task Argument_values_can_only_be_bound_to_a_type_that_implements_IEnumerable()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
@@ -1348,5 +1413,29 @@ namespace CliFx.Tests
|
|||||||
|
|
||||||
_output.WriteLine(stdErr.GetString());
|
_output.WriteLine(stdErr.GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Argument_values_can_only_be_bound_to_a_type_that_implements_IEnumerable_and_can_be_converted_from_an_array()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (console, _, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<UnsupportedArgumentTypesCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(new[]
|
||||||
|
{
|
||||||
|
"cmd", "--custom-enumerable"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().NotBe(0);
|
||||||
|
stdErr.GetString().Should().NotBeNullOrWhiteSpace();
|
||||||
|
|
||||||
|
_output.WriteLine(stdErr.GetString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,11 @@
|
|||||||
<Import Project="../CliFx.props" />
|
<Import Project="../CliFx.props" />
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</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>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -15,14 +14,14 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliWrap" Version="3.0.0" />
|
<PackageReference Include="CliWrap" Version="3.2.2" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.2" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.0.0" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" PrivateAssets="all" />
|
||||||
<PackageReference Include="coverlet.msbuild" Version="2.8.0" PrivateAssets="all" />
|
<PackageReference Include="coverlet.msbuild" Version="2.9.0" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
19
CliFx.Tests/Commands/GenericInnerExceptionCommand.cs
Normal file
19
CliFx.Tests/Commands/GenericInnerExceptionCommand.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
@@ -84,6 +84,9 @@ namespace CliFx.Tests.Commands
|
|||||||
[CommandOption("str-parseable-format")]
|
[CommandOption("str-parseable-format")]
|
||||||
public CustomStringParseableWithFormatProvider? StringParseableWithFormatProvider { get; set; }
|
public CustomStringParseableWithFormatProvider? StringParseableWithFormatProvider { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("convertible", Converter = typeof(CustomConvertibleConverter))]
|
||||||
|
public CustomConvertible? Convertible { get; set; }
|
||||||
|
|
||||||
[CommandOption("obj-array")]
|
[CommandOption("obj-array")]
|
||||||
public object[]? ObjectArray { get; set; }
|
public object[]? ObjectArray { get; set; }
|
||||||
|
|
||||||
@@ -102,6 +105,9 @@ namespace CliFx.Tests.Commands
|
|||||||
[CommandOption("str-constructible-array")]
|
[CommandOption("str-constructible-array")]
|
||||||
public CustomStringConstructible[]? StringConstructibleArray { get; set; }
|
public CustomStringConstructible[]? StringConstructibleArray { get; set; }
|
||||||
|
|
||||||
|
[CommandOption("convertible-array", Converter = typeof(CustomConvertibleConverter))]
|
||||||
|
public CustomConvertible[]? ConvertibleArray { get; set; }
|
||||||
|
|
||||||
[CommandOption("str-enumerable")]
|
[CommandOption("str-enumerable")]
|
||||||
public IEnumerable<string>? StringEnumerable { get; set; }
|
public IEnumerable<string>? StringEnumerable { get; set; }
|
||||||
|
|
||||||
@@ -151,5 +157,18 @@ namespace CliFx.Tests.Commands
|
|||||||
public static CustomStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
|
public static CustomStringParseableWithFormatProvider Parse(string value, IFormatProvider formatProvider) =>
|
||||||
new CustomStringParseableWithFormatProvider(value + " " + 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,11 @@ namespace CliFx.Tests.Commands
|
|||||||
[Command("cmd")]
|
[Command("cmd")]
|
||||||
public partial class UnsupportedArgumentTypesCommand : SelfSerializeCommandBase
|
public partial class UnsupportedArgumentTypesCommand : SelfSerializeCommandBase
|
||||||
{
|
{
|
||||||
[CommandOption("str-non-initializable")]
|
[CommandOption("custom")]
|
||||||
public CustomType? StringNonInitializable { get; set; }
|
public CustomType? CustomNonConvertible { get; set; }
|
||||||
|
|
||||||
[CommandOption("str-enumerable-non-initializable")]
|
[CommandOption("custom-enumerable")]
|
||||||
public CustomEnumerable<string>? StringEnumerableNonInitializable { get; set; }
|
public CustomEnumerable<string>? CustomEnumerableNonConvertible { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial class UnsupportedArgumentTypesCommand
|
public partial class UnsupportedArgumentTypesCommand
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ namespace CliFx.Tests
|
|||||||
// Act
|
// Act
|
||||||
var exitCode = await application.RunAsync(
|
var exitCode = await application.RunAsync(
|
||||||
new[] {"[preview]", "named", "param", "-abc", "--option", "foo"},
|
new[] {"[preview]", "named", "param", "-abc", "--option", "foo"},
|
||||||
new Dictionary<string, string>());
|
new Dictionary<string, string>()
|
||||||
|
);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
exitCode.Should().Be(0);
|
exitCode.Should().Be(0);
|
||||||
|
|||||||
@@ -39,6 +39,34 @@ namespace CliFx.Tests
|
|||||||
_output.WriteLine(stdErr.GetString());
|
_output.WriteLine(stdErr.GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Command_may_throw_a_generic_exception_with_inner_exception_which_exits_and_prints_error_message_and_stack_trace()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<GenericInnerExceptionCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(new[] {"cmd", "-m", "Kaput", "-i", "FooBar"});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().NotBe(0);
|
||||||
|
stdOut.GetString().Should().BeEmpty();
|
||||||
|
stdErr.GetString().Should().ContainAll(
|
||||||
|
"System.Exception:",
|
||||||
|
"FooBar",
|
||||||
|
"Kaput", "at",
|
||||||
|
"CliFx.Tests"
|
||||||
|
);
|
||||||
|
|
||||||
|
_output.WriteLine(stdOut.GetString());
|
||||||
|
_output.WriteLine(stdErr.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Command_may_throw_a_specialized_exception_which_exits_with_custom_code_and_prints_minimal_error_details()
|
public async Task Command_may_throw_a_specialized_exception_which_exits_with_custom_code_and_prints_minimal_error_details()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -64,6 +64,33 @@ namespace CliFx.Tests
|
|||||||
_output.WriteLine(stdOut.GetString());
|
_output.WriteLine(stdOut.GetString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Help_text_shows_usage_format_which_lists_available_sub_commands()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (console, stdOut, _) = VirtualConsole.CreateBuffered();
|
||||||
|
|
||||||
|
var application = new CliApplicationBuilder()
|
||||||
|
.AddCommand<DefaultCommand>()
|
||||||
|
.AddCommand<NamedCommand>()
|
||||||
|
.AddCommand<NamedSubCommand>()
|
||||||
|
.UseConsole(console)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exitCode = await application.RunAsync(new[] {"--help"});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
exitCode.Should().Be(0);
|
||||||
|
stdOut.GetString().Should().ContainAll(
|
||||||
|
"Usage",
|
||||||
|
"... named",
|
||||||
|
"... named sub"
|
||||||
|
);
|
||||||
|
|
||||||
|
_output.WriteLine(stdOut.GetString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Help_text_shows_all_valid_values_for_enum_arguments()
|
public async Task Help_text_shows_all_valid_values_for_enum_arguments()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.4</Version>
|
<Version>1.6</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
<Copyright>Copyright (C) Alexey Golub</Copyright>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -28,7 +28,11 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="ApplicationMetadata"/>.
|
/// Initializes an instance of <see cref="ApplicationMetadata"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApplicationMetadata(string title, string executableName, string versionText, string? description)
|
public ApplicationMetadata(
|
||||||
|
string title,
|
||||||
|
string executableName,
|
||||||
|
string versionText,
|
||||||
|
string? description)
|
||||||
{
|
{
|
||||||
Title = title;
|
Title = title;
|
||||||
ExecutableName = executableName;
|
ExecutableName = executableName;
|
||||||
|
|||||||
30
CliFx/ArgumentValueConverter.cs
Normal file
30
CliFx/ArgumentValueConverter.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
namespace CliFx
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Implements custom conversion logic that maps an argument value to a domain type.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This type is public for legacy reasons.
|
||||||
|
/// Please derive from <see cref="ArgumentValueConverter{T}"/> instead.
|
||||||
|
/// </remarks>
|
||||||
|
public interface IArgumentValueConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an input value to object of required type.
|
||||||
|
/// </summary>
|
||||||
|
public object ConvertFrom(string value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A base type for custom argument converters.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ArgumentValueConverter<T> : IArgumentValueConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an input value to object of required type.
|
||||||
|
/// </summary>
|
||||||
|
public abstract T ConvertFrom(string value);
|
||||||
|
|
||||||
|
object IArgumentValueConverter.ConvertFrom(string value) => ConvertFrom(value)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
55
CliFx/ArgumentValueValidator.cs
Normal file
55
CliFx/ArgumentValueValidator.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
namespace CliFx
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a result of a validation.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ValidationResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether validation was successful.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsValid => ErrorMessage == null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If validation has failed, contains the associated error, otherwise null.
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes an instance of <see cref="ValidationResult"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ValidationResult(string? errorMessage = null) =>
|
||||||
|
ErrorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public partial class ValidationResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates successful result, meaning that the validation has passed.
|
||||||
|
/// </summary>
|
||||||
|
public static ValidationResult Ok() => new ValidationResult();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an error result, meaning that the validation has failed.
|
||||||
|
/// </summary>
|
||||||
|
public static ValidationResult Error(string message) => new ValidationResult(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IArgumentValueValidator
|
||||||
|
{
|
||||||
|
ValidationResult Validate(object value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A base type for custom argument validators.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class ArgumentValueValidator<T> : IArgumentValueValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the value.
|
||||||
|
/// </summary>
|
||||||
|
public abstract ValidationResult Validate(T value);
|
||||||
|
|
||||||
|
ValidationResult IArgumentValueValidator.Validate(object value) => Validate((T) value);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
CliFx/Attributes/CommandArgumentAttribute.cs
Normal file
28
CliFx/Attributes/CommandArgumentAttribute.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CliFx.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Properties shared by parameter and option arguments.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public abstract class CommandArgumentAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Option description, which is used in help text.
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of converter to use when mapping the argument value.
|
||||||
|
/// Converter must derive from <see cref="ArgumentValueConverter{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Type? Converter { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Types of validators to use when mapping the argument value.
|
||||||
|
/// Validators must derive from <see cref="ArgumentValueValidator{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Type[] Validators { get; set; } = Array.Empty<Type>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace CliFx.Attributes
|
|||||||
/// Annotates a property that defines a command option.
|
/// Annotates a property that defines a command option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandOptionAttribute : Attribute
|
public class CommandOptionAttribute : CommandArgumentAttribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Option name (must be longer than a single character).
|
/// Option name (must be longer than a single character).
|
||||||
@@ -27,11 +27,6 @@ namespace CliFx.Attributes
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsRequired { get; set; }
|
public bool IsRequired { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Option description, which is used in help text.
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Environment variable that will be used as fallback if no option value is specified.
|
/// Environment variable that will be used as fallback if no option value is specified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace CliFx.Attributes
|
|||||||
/// Annotates a property that defines a command parameter.
|
/// Annotates a property that defines a command parameter.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
public class CommandParameterAttribute : Attribute
|
public class CommandParameterAttribute : CommandArgumentAttribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Order of this parameter compared to other parameters.
|
/// Order of this parameter compared to other parameters.
|
||||||
@@ -21,11 +21,6 @@ namespace CliFx.Attributes
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parameter description, which is used in help text.
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
|
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
@@ -38,9 +39,6 @@ namespace CliFx
|
|||||||
_helpTextWriter = new HelpTextWriter(metadata, console);
|
_helpTextWriter = new HelpTextWriter(metadata, console);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteError(string message) => _console.WithForegroundColor(ConsoleColor.Red, () =>
|
|
||||||
_console.Error.WriteLine(message));
|
|
||||||
|
|
||||||
private async ValueTask LaunchAndWaitForDebuggerAsync()
|
private async ValueTask LaunchAndWaitForDebuggerAsync()
|
||||||
{
|
{
|
||||||
var processId = ProcessEx.GetCurrentProcessId();
|
var processId = ProcessEx.GetCurrentProcessId();
|
||||||
@@ -51,7 +49,9 @@ namespace CliFx
|
|||||||
Debugger.Launch();
|
Debugger.Launch();
|
||||||
|
|
||||||
while (!Debugger.IsAttached)
|
while (!Debugger.IsAttached)
|
||||||
|
{
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteCommandLineInput(CommandInput input)
|
private void WriteCommandLineInput(CommandInput input)
|
||||||
@@ -103,9 +103,9 @@ namespace CliFx
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ICommand GetCommandInstance(CommandSchema command) =>
|
private ICommand GetCommandInstance(CommandSchema command) =>
|
||||||
command != StubDefaultCommand.Schema
|
command != FallbackDefaultCommand.Schema
|
||||||
? (ICommand) _typeActivator.CreateInstance(command.Type)
|
? (ICommand) _typeActivator.CreateInstance(command.Type)
|
||||||
: new StubDefaultCommand();
|
: new FallbackDefaultCommand();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
|
/// Runs the application with specified command line arguments and environment variables, and returns the exit code.
|
||||||
@@ -141,7 +141,7 @@ namespace CliFx
|
|||||||
var command =
|
var command =
|
||||||
root.TryFindCommand(input.CommandName) ??
|
root.TryFindCommand(input.CommandName) ??
|
||||||
root.TryFindDefaultCommand() ??
|
root.TryFindDefaultCommand() ??
|
||||||
StubDefaultCommand.Schema;
|
FallbackDefaultCommand.Schema;
|
||||||
|
|
||||||
// Version option
|
// Version option
|
||||||
if (command.IsVersionOptionAvailable && input.IsVersionOptionSpecified)
|
if (command.IsVersionOptionAvailable && input.IsVersionOptionSpecified)
|
||||||
@@ -159,7 +159,7 @@ namespace CliFx
|
|||||||
|
|
||||||
// Help option
|
// Help option
|
||||||
if (command.IsHelpOptionAvailable && input.IsHelpOptionSpecified ||
|
if (command.IsHelpOptionAvailable && input.IsHelpOptionSpecified ||
|
||||||
command == StubDefaultCommand.Schema && !input.Parameters.Any() && !input.Options.Any())
|
command == FallbackDefaultCommand.Schema && !input.Parameters.Any() && !input.Options.Any())
|
||||||
{
|
{
|
||||||
_helpTextWriter.Write(root, command, defaultValues);
|
_helpTextWriter.Write(root, command, defaultValues);
|
||||||
return ExitCode.Success;
|
return ExitCode.Success;
|
||||||
@@ -173,7 +173,10 @@ namespace CliFx
|
|||||||
// This may throw exceptions which are useful only to the end-user
|
// This may throw exceptions which are useful only to the end-user
|
||||||
catch (CliFxException ex)
|
catch (CliFxException ex)
|
||||||
{
|
{
|
||||||
WriteError(ex.ToString());
|
_console.WithForegroundColor(ConsoleColor.Red, () =>
|
||||||
|
_console.Error.WriteLine(ex.ToString())
|
||||||
|
);
|
||||||
|
|
||||||
_helpTextWriter.Write(root, command, defaultValues);
|
_helpTextWriter.Write(root, command, defaultValues);
|
||||||
|
|
||||||
return ExitCode.FromException(ex);
|
return ExitCode.FromException(ex);
|
||||||
@@ -188,10 +191,14 @@ namespace CliFx
|
|||||||
// Swallow command exceptions and route them to the console
|
// Swallow command exceptions and route them to the console
|
||||||
catch (CommandException ex)
|
catch (CommandException ex)
|
||||||
{
|
{
|
||||||
WriteError(ex.ToString());
|
_console.WithForegroundColor(ConsoleColor.Red, () =>
|
||||||
|
_console.Error.WriteLine(ex.ToString())
|
||||||
|
);
|
||||||
|
|
||||||
if (ex.ShowHelp)
|
if (ex.ShowHelp)
|
||||||
|
{
|
||||||
_helpTextWriter.Write(root, command, defaultValues);
|
_helpTextWriter.Write(root, command, defaultValues);
|
||||||
|
}
|
||||||
|
|
||||||
return ex.ExitCode;
|
return ex.ExitCode;
|
||||||
}
|
}
|
||||||
@@ -202,7 +209,13 @@ namespace CliFx
|
|||||||
// because we still want the IDE to show them to the developer.
|
// because we still want the IDE to show them to the developer.
|
||||||
catch (Exception ex) when (!Debugger.IsAttached)
|
catch (Exception ex) when (!Debugger.IsAttached)
|
||||||
{
|
{
|
||||||
WriteError(ex.ToString());
|
_console.WithColors(ConsoleColor.White, ConsoleColor.DarkRed, () =>
|
||||||
|
_console.Error.Write("ERROR:")
|
||||||
|
);
|
||||||
|
|
||||||
|
_console.Error.Write(" ");
|
||||||
|
_console.WriteException(ex);
|
||||||
|
|
||||||
return ExitCode.FromException(ex);
|
return ExitCode.FromException(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,11 +270,14 @@ namespace CliFx
|
|||||||
: 1;
|
: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback default command used when none is defined in the application
|
||||||
[Command]
|
[Command]
|
||||||
private class StubDefaultCommand : ICommand
|
private class FallbackDefaultCommand : ICommand
|
||||||
{
|
{
|
||||||
public static CommandSchema Schema { get; } = CommandSchema.TryResolve(typeof(StubDefaultCommand))!;
|
public static CommandSchema Schema { get; } = CommandSchema.TryResolve(typeof(FallbackDefaultCommand))!;
|
||||||
|
|
||||||
|
// Never actually executed
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
public ValueTask ExecuteAsync(IConsole console) => default;
|
public ValueTask ExecuteAsync(IConsole console) => default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,9 +165,9 @@ namespace CliFx
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public CliApplication Build()
|
public CliApplication Build()
|
||||||
{
|
{
|
||||||
_title ??= TryGetDefaultTitle() ?? "App";
|
_title ??= GetDefaultTitle();
|
||||||
_executableName ??= TryGetDefaultExecutableName() ?? "app";
|
_executableName ??= GetDefaultExecutableName();
|
||||||
_versionText ??= TryGetDefaultVersionText() ?? "v1.0";
|
_versionText ??= GetDefaultVersionText();
|
||||||
_console ??= new SystemConsole();
|
_console ??= new SystemConsole();
|
||||||
_typeActivator ??= new DefaultTypeActivator();
|
_typeActivator ??= new DefaultTypeActivator();
|
||||||
|
|
||||||
@@ -185,23 +185,29 @@ namespace CliFx
|
|||||||
// Entry assembly is null in tests
|
// Entry assembly is null in tests
|
||||||
private static Assembly? EntryAssembly => LazyEntryAssembly.Value;
|
private static Assembly? EntryAssembly => LazyEntryAssembly.Value;
|
||||||
|
|
||||||
private static string? TryGetDefaultTitle() => EntryAssembly?.GetName().Name;
|
private static string GetDefaultTitle() => EntryAssembly?.GetName().Name?? "App";
|
||||||
|
|
||||||
private static string? TryGetDefaultExecutableName()
|
private static string GetDefaultExecutableName()
|
||||||
{
|
{
|
||||||
var entryAssemblyLocation = EntryAssembly?.Location;
|
var entryAssemblyLocation = EntryAssembly?.Location;
|
||||||
|
|
||||||
// The assembly can be an executable or a dll, depending on how it was packaged
|
// The assembly can be an executable or a dll, depending on how it was packaged
|
||||||
var isDll = string.Equals(Path.GetExtension(entryAssemblyLocation), ".dll", StringComparison.OrdinalIgnoreCase);
|
var isDll = string.Equals(
|
||||||
|
Path.GetExtension(entryAssemblyLocation),
|
||||||
|
".dll",
|
||||||
|
StringComparison.OrdinalIgnoreCase
|
||||||
|
);
|
||||||
|
|
||||||
return isDll
|
var name = isDll
|
||||||
? "dotnet " + Path.GetFileName(entryAssemblyLocation)
|
? "dotnet " + Path.GetFileName(entryAssemblyLocation)
|
||||||
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
|
: Path.GetFileNameWithoutExtension(entryAssemblyLocation);
|
||||||
|
|
||||||
|
return name ?? "app";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? TryGetDefaultVersionText() =>
|
private static string GetDefaultVersionText() =>
|
||||||
EntryAssembly != null
|
EntryAssembly != null
|
||||||
? $"v{EntryAssembly.GetName().Version.ToSemanticString()}"
|
? $"v{EntryAssembly.GetName().Version.ToSemanticString()}"
|
||||||
: null;
|
: "v1.0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,17 +24,16 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
|
<None Include="../favicon.png" Pack="true" PackagePath="" />
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="Nullable" Version="1.2.1" PrivateAssets="all" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
|
||||||
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="../favicon.png" Pack="true" PackagePath="" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
|
||||||
|
<PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||||
|
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- The following item group and target ensure that the analyzer project is copied into the output NuGet package -->
|
<!-- The following item group and target ensure that the analyzer project is copied into the output NuGet package -->
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using CliFx.Exceptions;
|
using CliFx.Exceptions;
|
||||||
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
namespace CliFx
|
namespace CliFx
|
||||||
{
|
{
|
||||||
@@ -13,7 +14,7 @@ namespace CliFx
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Activator.CreateInstance(type);
|
return type.CreateInstance();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,49 +17,71 @@ namespace CliFx.Domain
|
|||||||
|
|
||||||
public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null;
|
public bool IsScalar => TryGetEnumerableArgumentUnderlyingType() == null;
|
||||||
|
|
||||||
protected CommandArgumentSchema(PropertyInfo? property, string? description)
|
public Type? ConverterType { get; }
|
||||||
|
|
||||||
|
public Type[] ValidatorTypes { get; }
|
||||||
|
|
||||||
|
protected CommandArgumentSchema(
|
||||||
|
PropertyInfo? property,
|
||||||
|
string? description,
|
||||||
|
Type? converterType,
|
||||||
|
Type[] validatorTypes)
|
||||||
{
|
{
|
||||||
Property = property;
|
Property = property;
|
||||||
Description = description;
|
Description = description;
|
||||||
|
ConverterType = converterType;
|
||||||
|
ValidatorTypes = validatorTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type? TryGetEnumerableArgumentUnderlyingType() =>
|
private Type? TryGetEnumerableArgumentUnderlyingType() =>
|
||||||
Property != null && Property.PropertyType != typeof(string)
|
Property != null && Property.PropertyType != typeof(string)
|
||||||
? Property.PropertyType.GetEnumerableUnderlyingType()
|
? Property.PropertyType.TryGetEnumerableUnderlyingType()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
private object? ConvertScalar(string? value, Type targetType)
|
private object? ConvertScalar(string? value, Type targetType)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Primitive
|
// Custom conversion (always takes highest priority)
|
||||||
|
if (ConverterType != null)
|
||||||
|
return ConverterType.CreateInstance<IArgumentValueConverter>().ConvertFrom(value!);
|
||||||
|
|
||||||
|
// No conversion necessary
|
||||||
|
if (targetType == typeof(object) || targetType == typeof(string))
|
||||||
|
return value;
|
||||||
|
|
||||||
|
// Bool conversion (special case)
|
||||||
|
if (targetType == typeof(bool))
|
||||||
|
return string.IsNullOrWhiteSpace(value) || bool.Parse(value);
|
||||||
|
|
||||||
|
// Primitive conversion
|
||||||
var primitiveConverter = PrimitiveConverters.GetValueOrDefault(targetType);
|
var primitiveConverter = PrimitiveConverters.GetValueOrDefault(targetType);
|
||||||
if (primitiveConverter != null)
|
if (primitiveConverter != null && !string.IsNullOrWhiteSpace(value))
|
||||||
return primitiveConverter(value);
|
return primitiveConverter(value);
|
||||||
|
|
||||||
// Enum
|
// Enum conversion
|
||||||
if (targetType.IsEnum)
|
if (targetType.IsEnum && !string.IsNullOrWhiteSpace(value))
|
||||||
return Enum.Parse(targetType, value, true);
|
return Enum.Parse(targetType, value, true);
|
||||||
|
|
||||||
// Nullable
|
// Nullable<T> conversion
|
||||||
var nullableUnderlyingType = targetType.GetNullableUnderlyingType();
|
var nullableUnderlyingType = targetType.TryGetNullableUnderlyingType();
|
||||||
if (nullableUnderlyingType != null)
|
if (nullableUnderlyingType != null)
|
||||||
return !string.IsNullOrWhiteSpace(value)
|
return !string.IsNullOrWhiteSpace(value)
|
||||||
? ConvertScalar(value, nullableUnderlyingType)
|
? ConvertScalar(value, nullableUnderlyingType)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// String-constructible
|
// String-constructible conversion
|
||||||
var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
|
var stringConstructor = targetType.GetConstructor(new[] {typeof(string)});
|
||||||
if (stringConstructor != null)
|
if (stringConstructor != null)
|
||||||
return stringConstructor.Invoke(new object[] {value!});
|
return stringConstructor.Invoke(new object[] {value!});
|
||||||
|
|
||||||
// String-parseable (with format provider)
|
// String-parseable conversion (with format provider)
|
||||||
var parseMethodWithFormatProvider = targetType.GetStaticParseMethod(true);
|
var parseMethodWithFormatProvider = targetType.TryGetStaticParseMethod(true);
|
||||||
if (parseMethodWithFormatProvider != null)
|
if (parseMethodWithFormatProvider != null)
|
||||||
return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, FormatProvider});
|
return parseMethodWithFormatProvider.Invoke(null, new object[] {value!, FormatProvider});
|
||||||
|
|
||||||
// String-parseable (without format provider)
|
// String-parseable conversion (without format provider)
|
||||||
var parseMethod = targetType.GetStaticParseMethod();
|
var parseMethod = targetType.TryGetStaticParseMethod();
|
||||||
if (parseMethod != null)
|
if (parseMethod != null)
|
||||||
return parseMethod.Invoke(null, new object[] {value!});
|
return parseMethod.Invoke(null, new object[] {value!});
|
||||||
}
|
}
|
||||||
@@ -71,7 +93,10 @@ namespace CliFx.Domain
|
|||||||
throw CliFxException.CannotConvertToType(this, value, targetType);
|
throw CliFxException.CannotConvertToType(this, value, targetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private object ConvertNonScalar(IReadOnlyList<string> values, Type targetEnumerableType, Type targetElementType)
|
private object ConvertNonScalar(
|
||||||
|
IReadOnlyList<string> values,
|
||||||
|
Type targetEnumerableType,
|
||||||
|
Type targetElementType)
|
||||||
{
|
{
|
||||||
var array = values
|
var array = values
|
||||||
.Select(v => ConvertScalar(v, targetElementType))
|
.Select(v => ConvertScalar(v, targetElementType))
|
||||||
@@ -114,8 +139,31 @@ namespace CliFx.Domain
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BindOn(ICommand command, IReadOnlyList<string> values) =>
|
private void Validate(object? value)
|
||||||
Property?.SetValue(command, Convert(values));
|
{
|
||||||
|
if (value is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var validators = ValidatorTypes
|
||||||
|
.Select(t => t.CreateInstance<IArgumentValueValidator>())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var failedValidations = validators
|
||||||
|
.Select(v => v.Validate(value))
|
||||||
|
.Where(result => !result.IsValid)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (failedValidations.Any())
|
||||||
|
throw CliFxException.ValidationFailed(this, failedValidations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BindOn(ICommand command, IReadOnlyList<string> values)
|
||||||
|
{
|
||||||
|
var value = Convert(values);
|
||||||
|
Validate(value);
|
||||||
|
|
||||||
|
Property?.SetValue(command, value);
|
||||||
|
}
|
||||||
|
|
||||||
public void BindOn(ICommand command, params string[] values) =>
|
public void BindOn(ICommand command, params string[] values) =>
|
||||||
BindOn(command, (IReadOnlyList<string>) values);
|
BindOn(command, (IReadOnlyList<string>) values);
|
||||||
@@ -126,7 +174,7 @@ namespace CliFx.Domain
|
|||||||
return Array.Empty<string>();
|
return Array.Empty<string>();
|
||||||
|
|
||||||
var underlyingType =
|
var underlyingType =
|
||||||
Property.PropertyType.GetNullableUnderlyingType() ??
|
Property.PropertyType.TryGetNullableUnderlyingType() ??
|
||||||
Property.PropertyType;
|
Property.PropertyType;
|
||||||
|
|
||||||
// Enum
|
// Enum
|
||||||
@@ -141,12 +189,9 @@ namespace CliFx.Domain
|
|||||||
{
|
{
|
||||||
private static readonly IFormatProvider FormatProvider = CultureInfo.InvariantCulture;
|
private static readonly IFormatProvider FormatProvider = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
private static readonly IReadOnlyDictionary<Type, Func<string?, object?>> PrimitiveConverters =
|
private static readonly IReadOnlyDictionary<Type, Func<string, object?>> PrimitiveConverters =
|
||||||
new Dictionary<Type, Func<string?, object?>>
|
new Dictionary<Type, Func<string, object?>>
|
||||||
{
|
{
|
||||||
[typeof(object)] = v => v,
|
|
||||||
[typeof(string)] = v => v,
|
|
||||||
[typeof(bool)] = v => string.IsNullOrWhiteSpace(v) || bool.Parse(v),
|
|
||||||
[typeof(char)] = v => v.Single(),
|
[typeof(char)] = v => v.Single(),
|
||||||
[typeof(sbyte)] = v => sbyte.Parse(v, FormatProvider),
|
[typeof(sbyte)] = v => sbyte.Parse(v, FormatProvider),
|
||||||
[typeof(byte)] = v => byte.Parse(v, FormatProvider),
|
[typeof(byte)] = v => byte.Parse(v, FormatProvider),
|
||||||
|
|||||||
@@ -138,7 +138,16 @@ namespace CliFx.Domain
|
|||||||
{
|
{
|
||||||
var argument = commandLineArguments[index];
|
var argument = commandLineArguments[index];
|
||||||
|
|
||||||
if (argument.StartsWith('-'))
|
var isOptionArgument =
|
||||||
|
argument.StartsWith("--", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
argument.Length > 2 &&
|
||||||
|
char.IsLetter(argument[2]) ||
|
||||||
|
argument.StartsWith('-') &&
|
||||||
|
argument.Length > 1 &&
|
||||||
|
char.IsLetter(argument[1]);
|
||||||
|
|
||||||
|
// Break on the first encountered option
|
||||||
|
if (isOptionArgument)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
result.Add(new CommandParameterInput(argument));
|
result.Add(new CommandParameterInput(argument));
|
||||||
@@ -161,7 +170,9 @@ namespace CliFx.Domain
|
|||||||
var argument = commandLineArguments[index];
|
var argument = commandLineArguments[index];
|
||||||
|
|
||||||
// Name
|
// Name
|
||||||
if (argument.StartsWith("--", StringComparison.Ordinal))
|
if (argument.StartsWith("--", StringComparison.Ordinal) &&
|
||||||
|
argument.Length > 2 &&
|
||||||
|
char.IsLetter(argument[2]))
|
||||||
{
|
{
|
||||||
// Flush previous
|
// Flush previous
|
||||||
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
if (!string.IsNullOrWhiteSpace(currentOptionAlias))
|
||||||
@@ -171,7 +182,9 @@ namespace CliFx.Domain
|
|||||||
currentOptionValues = new List<string>();
|
currentOptionValues = new List<string>();
|
||||||
}
|
}
|
||||||
// Short name
|
// Short name
|
||||||
else if (argument.StartsWith('-'))
|
else if (argument.StartsWith('-') &&
|
||||||
|
argument.Length > 1 &&
|
||||||
|
char.IsLetter(argument[1]))
|
||||||
{
|
{
|
||||||
foreach (var alias in argument.Substring(1))
|
foreach (var alias in argument.Substring(1))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ namespace CliFx.Domain
|
|||||||
char? shortName,
|
char? shortName,
|
||||||
string? environmentVariableName,
|
string? environmentVariableName,
|
||||||
bool isRequired,
|
bool isRequired,
|
||||||
string? description)
|
string? description,
|
||||||
: base(property, description)
|
Type? converterType,
|
||||||
|
Type[] validatorTypes)
|
||||||
|
: base(property, description, converterType, validatorTypes)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
ShortName = shortName;
|
ShortName = shortName;
|
||||||
@@ -97,17 +99,35 @@ namespace CliFx.Domain
|
|||||||
attribute.ShortName,
|
attribute.ShortName,
|
||||||
attribute.EnvironmentVariableName,
|
attribute.EnvironmentVariableName,
|
||||||
attribute.IsRequired,
|
attribute.IsRequired,
|
||||||
attribute.Description
|
attribute.Description,
|
||||||
|
attribute.Converter,
|
||||||
|
attribute.Validators
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal partial class CommandOptionSchema
|
internal partial class CommandOptionSchema
|
||||||
{
|
{
|
||||||
public static CommandOptionSchema HelpOption { get; } =
|
public static CommandOptionSchema HelpOption { get; } = new CommandOptionSchema(
|
||||||
new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text.");
|
null,
|
||||||
|
"help",
|
||||||
|
'h',
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"Shows help text.",
|
||||||
|
null,
|
||||||
|
Array.Empty<Type>()
|
||||||
|
);
|
||||||
|
|
||||||
public static CommandOptionSchema VersionOption { get; } =
|
public static CommandOptionSchema VersionOption { get; } = new CommandOptionSchema(
|
||||||
new CommandOptionSchema(null, "version", null, null, false, "Shows version information.");
|
null,
|
||||||
|
"version",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"Shows version information.",
|
||||||
|
null,
|
||||||
|
Array.Empty<Type>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using CliFx.Attributes;
|
using CliFx.Attributes;
|
||||||
@@ -11,8 +12,14 @@ namespace CliFx.Domain
|
|||||||
|
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
public CommandParameterSchema(PropertyInfo? property, int order, string name, string? description)
|
public CommandParameterSchema(
|
||||||
: base(property, description)
|
PropertyInfo? property,
|
||||||
|
int order,
|
||||||
|
string name,
|
||||||
|
string? description,
|
||||||
|
Type? converterType,
|
||||||
|
Type[] validatorTypes)
|
||||||
|
: base(property, description, converterType, validatorTypes)
|
||||||
{
|
{
|
||||||
Order = order;
|
Order = order;
|
||||||
Name = name;
|
Name = name;
|
||||||
@@ -50,7 +57,9 @@ namespace CliFx.Domain
|
|||||||
property,
|
property,
|
||||||
attribute.Order,
|
attribute.Order,
|
||||||
name,
|
name,
|
||||||
attribute.Description
|
attribute.Description,
|
||||||
|
attribute.Converter,
|
||||||
|
attribute.Validators
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,57 @@ namespace CliFx.Domain
|
|||||||
WriteLine();
|
WriteLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteCommandUsage(CommandSchema command, IReadOnlyList<CommandSchema> childCommands)
|
private void WriteCommandUsageLineItem(CommandSchema command, bool showChildCommandsPlaceholder)
|
||||||
|
{
|
||||||
|
// Command name
|
||||||
|
if (!string.IsNullOrWhiteSpace(command.Name))
|
||||||
|
{
|
||||||
|
Write(ConsoleColor.Cyan, command.Name);
|
||||||
|
Write(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child command placeholder
|
||||||
|
if (showChildCommandsPlaceholder)
|
||||||
|
{
|
||||||
|
Write(ConsoleColor.Cyan, "[command]");
|
||||||
|
Write(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
foreach (var parameter in command.Parameters)
|
||||||
|
{
|
||||||
|
Write(parameter.IsScalar
|
||||||
|
? $"<{parameter.Name}>"
|
||||||
|
: $"<{parameter.Name}...>"
|
||||||
|
);
|
||||||
|
Write(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Required options
|
||||||
|
foreach (var option in command.Options.Where(o => o.IsRequired))
|
||||||
|
{
|
||||||
|
Write(ConsoleColor.White, !string.IsNullOrWhiteSpace(option.Name)
|
||||||
|
? $"--{option.Name}"
|
||||||
|
: $"-{option.ShortName}"
|
||||||
|
);
|
||||||
|
Write(' ');
|
||||||
|
|
||||||
|
Write(option.IsScalar
|
||||||
|
? "<value>"
|
||||||
|
: "<values...>"
|
||||||
|
);
|
||||||
|
Write(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options placeholder
|
||||||
|
Write(ConsoleColor.White, "[options]");
|
||||||
|
|
||||||
|
WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteCommandUsage(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandSchema> childCommands)
|
||||||
{
|
{
|
||||||
if (!IsEmpty)
|
if (!IsEmpty)
|
||||||
WriteVerticalMargin();
|
WriteVerticalMargin();
|
||||||
@@ -115,52 +165,23 @@ namespace CliFx.Domain
|
|||||||
// Exe name
|
// Exe name
|
||||||
WriteHorizontalMargin();
|
WriteHorizontalMargin();
|
||||||
Write(_metadata.ExecutableName);
|
Write(_metadata.ExecutableName);
|
||||||
|
Write(' ');
|
||||||
|
|
||||||
// Command name
|
// Current command usage
|
||||||
if (!string.IsNullOrWhiteSpace(command.Name))
|
WriteCommandUsageLineItem(command, childCommands.Any());
|
||||||
{
|
|
||||||
Write(' ');
|
|
||||||
Write(ConsoleColor.Cyan, command.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Child command placeholder
|
// Sub commands usage
|
||||||
if (childCommands.Any())
|
if (childCommands.Any())
|
||||||
{
|
{
|
||||||
Write(' ');
|
WriteVerticalMargin();
|
||||||
Write(ConsoleColor.Cyan, "[command]");
|
|
||||||
|
foreach (var childCommand in childCommands)
|
||||||
|
{
|
||||||
|
WriteHorizontalMargin();
|
||||||
|
Write("... ");
|
||||||
|
WriteCommandUsageLineItem(childCommand, false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parameters
|
|
||||||
foreach (var parameter in command.Parameters)
|
|
||||||
{
|
|
||||||
Write(' ');
|
|
||||||
Write(parameter.IsScalar
|
|
||||||
? $"<{parameter.Name}>"
|
|
||||||
: $"<{parameter.Name}...>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Required options
|
|
||||||
foreach (var option in command.Options.Where(o => o.IsRequired))
|
|
||||||
{
|
|
||||||
Write(' ');
|
|
||||||
Write(ConsoleColor.White, !string.IsNullOrWhiteSpace(option.Name)
|
|
||||||
? $"--{option.Name}"
|
|
||||||
: $"-{option.ShortName}"
|
|
||||||
);
|
|
||||||
|
|
||||||
Write(' ');
|
|
||||||
Write(option.IsScalar
|
|
||||||
? "<value>"
|
|
||||||
: "<values...>"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options placeholder
|
|
||||||
Write(' ');
|
|
||||||
Write(ConsoleColor.White, "[options]");
|
|
||||||
|
|
||||||
WriteLine();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WriteCommandParameters(CommandSchema command)
|
private void WriteCommandParameters(CommandSchema command)
|
||||||
@@ -264,7 +285,7 @@ namespace CliFx.Domain
|
|||||||
if (!option.IsRequired)
|
if (!option.IsRequired)
|
||||||
{
|
{
|
||||||
var defaultValue = argumentDefaultValues.GetValueOrDefault(option);
|
var defaultValue = argumentDefaultValues.GetValueOrDefault(option);
|
||||||
var defaultValueFormatted = FormatDefaultValue(defaultValue);
|
var defaultValueFormatted = TryFormatDefaultValue(defaultValue);
|
||||||
if (defaultValueFormatted != null)
|
if (defaultValueFormatted != null)
|
||||||
{
|
{
|
||||||
Write($"Default: {defaultValueFormatted}.");
|
Write($"Default: {defaultValueFormatted}.");
|
||||||
@@ -334,7 +355,9 @@ namespace CliFx.Domain
|
|||||||
CommandSchema command,
|
CommandSchema command,
|
||||||
IReadOnlyDictionary<CommandArgumentSchema, object?> defaultValues)
|
IReadOnlyDictionary<CommandArgumentSchema, object?> defaultValues)
|
||||||
{
|
{
|
||||||
var childCommands = root.GetChildCommands(command.Name);
|
var commandName = command.Name;
|
||||||
|
var childCommands = root.GetChildCommands(commandName);
|
||||||
|
var descendantCommands = root.GetDescendantCommands(commandName);
|
||||||
|
|
||||||
_console.ResetColor();
|
_console.ResetColor();
|
||||||
|
|
||||||
@@ -342,7 +365,7 @@ namespace CliFx.Domain
|
|||||||
WriteApplicationInfo();
|
WriteApplicationInfo();
|
||||||
|
|
||||||
WriteCommandDescription(command);
|
WriteCommandDescription(command);
|
||||||
WriteCommandUsage(command, childCommands);
|
WriteCommandUsage(command, descendantCommands);
|
||||||
WriteCommandParameters(command);
|
WriteCommandParameters(command);
|
||||||
WriteCommandOptions(command, defaultValues);
|
WriteCommandOptions(command, defaultValues);
|
||||||
WriteCommandChildren(command, childCommands);
|
WriteCommandChildren(command, childCommands);
|
||||||
@@ -354,7 +377,7 @@ namespace CliFx.Domain
|
|||||||
private static string FormatValidValues(IReadOnlyList<string> values) =>
|
private static string FormatValidValues(IReadOnlyList<string> values) =>
|
||||||
values.Select(v => v.Quote()).JoinToString(", ");
|
values.Select(v => v.Quote()).JoinToString(", ");
|
||||||
|
|
||||||
private static string? FormatDefaultValue(object? defaultValue)
|
private static string? TryFormatDefaultValue(object? defaultValue)
|
||||||
{
|
{
|
||||||
if (defaultValue == null)
|
if (defaultValue == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -362,7 +385,7 @@ namespace CliFx.Domain
|
|||||||
// Enumerable
|
// Enumerable
|
||||||
if (!(defaultValue is string) && defaultValue is IEnumerable defaultValues)
|
if (!(defaultValue is string) && defaultValue is IEnumerable defaultValues)
|
||||||
{
|
{
|
||||||
var elementType = defaultValues.GetType().GetEnumerableUnderlyingType() ?? typeof(object);
|
var elementType = defaultValues.GetType().TryGetEnumerableUnderlyingType() ?? typeof(object);
|
||||||
|
|
||||||
// If the ToString() method is not overriden, the default value can't be formatted nicely
|
// If the ToString() method is not overriden, the default value can't be formatted nicely
|
||||||
if (!elementType.IsToStringOverriden())
|
if (!elementType.IsToStringOverriden())
|
||||||
|
|||||||
@@ -114,6 +114,30 @@ namespace CliFx.Domain
|
|||||||
nonLastNonScalarParameter
|
nonLastNonScalarParameter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var invalidConverterParameters = command.Parameters
|
||||||
|
.Where(p => p.ConverterType != null && !p.ConverterType.Implements(typeof(IArgumentValueConverter)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (invalidConverterParameters.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.ParametersWithInvalidConverters(
|
||||||
|
command,
|
||||||
|
invalidConverterParameters
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidValidatorParameters = command.Parameters
|
||||||
|
.Where(p => !p.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (invalidValidatorParameters.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.ParametersWithInvalidValidators(
|
||||||
|
command,
|
||||||
|
invalidValidatorParameters
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateOptions(CommandSchema command)
|
private static void ValidateOptions(CommandSchema command)
|
||||||
@@ -184,6 +208,54 @@ namespace CliFx.Domain
|
|||||||
duplicateEnvironmentVariableNameGroup.ToArray()
|
duplicateEnvironmentVariableNameGroup.ToArray()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var invalidConverterOptions = command.Options
|
||||||
|
.Where(o => o.ConverterType != null && !o.ConverterType.Implements(typeof(IArgumentValueConverter)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (invalidConverterOptions.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.OptionsWithInvalidConverters(
|
||||||
|
command,
|
||||||
|
invalidConverterOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var invalidValidatorOptions = command.Options
|
||||||
|
.Where(o => !o.ValidatorTypes.All(x => x.Implements(typeof(IArgumentValueValidator))))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (invalidValidatorOptions.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.OptionsWithInvalidValidators(
|
||||||
|
command,
|
||||||
|
invalidValidatorOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonLetterFirstCharacterInNameOptions = command.Options
|
||||||
|
.Where(o => !string.IsNullOrWhiteSpace(o.Name) && !char.IsLetter(o.Name[0]))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (nonLetterFirstCharacterInNameOptions.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.OptionsWithNonLetterCharacterName(
|
||||||
|
command,
|
||||||
|
nonLetterFirstCharacterInNameOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonLetterShortNameOptions = command.Options
|
||||||
|
.Where(o => o.ShortName != null && !char.IsLetter(o.ShortName.Value))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (nonLetterShortNameOptions.Any())
|
||||||
|
{
|
||||||
|
throw CliFxException.OptionsWithNonLetterCharacterShortName(
|
||||||
|
command,
|
||||||
|
nonLetterShortNameOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
|
private static void ValidateCommands(IReadOnlyList<CommandSchema> commands)
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ namespace CliFx.Exceptions
|
|||||||
{
|
{
|
||||||
internal static CliFxException DefaultActivatorFailed(Type type, Exception? innerException = null)
|
internal static CliFxException DefaultActivatorFailed(Type type, Exception? innerException = null)
|
||||||
{
|
{
|
||||||
var configureActivatorMethodName = $"{nameof(CliApplicationBuilder)}.{nameof(CliApplicationBuilder.UseTypeActivator)}(...)";
|
var configureActivatorMethodName =
|
||||||
|
$"{nameof(CliApplicationBuilder)}.{nameof(CliApplicationBuilder.UseTypeActivator)}(...)";
|
||||||
|
|
||||||
var message = $@"
|
var message = $@"
|
||||||
Failed to create an instance of type '{type.FullName}'.
|
Failed to create an instance of type '{type.FullName}'.
|
||||||
@@ -172,6 +173,32 @@ If it's not feasible to fit into these constraints, consider using options inste
|
|||||||
return new CliFxException(message.Trim());
|
return new CliFxException(message.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static CliFxException ParametersWithInvalidConverters(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameter(s) with invalid converters:
|
||||||
|
{invalidParameters.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Specified converter must implement {typeof(ArgumentValueConverter<>).FullName}.";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CliFxException ParametersWithInvalidValidators(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandParameterSchema> invalidParameters)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameter(s) with invalid value validators:
|
||||||
|
{invalidParameters.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Specified validators must inherit from {typeof(ArgumentValueValidator<>).FullName}.";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
internal static CliFxException OptionsWithNoName(
|
internal static CliFxException OptionsWithNoName(
|
||||||
CommandSchema command,
|
CommandSchema command,
|
||||||
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||||
@@ -243,6 +270,58 @@ Environment variable names are not case-sensitive.";
|
|||||||
|
|
||||||
return new CliFxException(message.Trim());
|
return new CliFxException(message.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static CliFxException OptionsWithInvalidConverters(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} option(s) with invalid converters:
|
||||||
|
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Specified converter must implement {typeof(ArgumentValueConverter<>).FullName}.";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CliFxException OptionsWithInvalidValidators(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} option(s) with invalid validators:
|
||||||
|
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Specified validators must inherit from {typeof(ArgumentValueValidator<>).FullName}.";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CliFxException OptionsWithNonLetterCharacterName(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains one or more options whose names don't start with a letter character:
|
||||||
|
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Option names must start with a letter character (i.e. not a digit and not a special character).";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CliFxException OptionsWithNonLetterCharacterShortName(
|
||||||
|
CommandSchema command,
|
||||||
|
IReadOnlyList<CommandOptionSchema> invalidOptions)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Command '{command.Type.FullName}' is invalid because it contains one or more options whose short names are not letter characters:
|
||||||
|
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
|
Option short names must be letter characters (i.e. not digits and not special characters).";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// End-user-facing exceptions
|
// End-user-facing exceptions
|
||||||
@@ -372,7 +451,8 @@ Missing values for one or more required options:
|
|||||||
return new CliFxException(message.Trim());
|
return new CliFxException(message.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static CliFxException UnrecognizedParametersProvided(IReadOnlyList<CommandParameterInput> parameterInputs)
|
internal static CliFxException UnrecognizedParametersProvided(
|
||||||
|
IReadOnlyList<CommandParameterInput> parameterInputs)
|
||||||
{
|
{
|
||||||
var message = $@"
|
var message = $@"
|
||||||
Unrecognized parameters provided:
|
Unrecognized parameters provided:
|
||||||
@@ -389,5 +469,36 @@ Unrecognized options provided:
|
|||||||
|
|
||||||
return new CliFxException(message.Trim());
|
return new CliFxException(message.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static CliFxException ValidationFailed(
|
||||||
|
CommandParameterSchema parameter,
|
||||||
|
IReadOnlyList<ValidationResult> failedResults)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Value provided for parameter {parameter.GetUserFacingDisplayString()}:
|
||||||
|
{failedResults.Select(r => r.ErrorMessage).JoinToString(Environment.NewLine)}";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CliFxException ValidationFailed(
|
||||||
|
CommandOptionSchema option,
|
||||||
|
IReadOnlyList<ValidationResult> failedResults)
|
||||||
|
{
|
||||||
|
var message = $@"
|
||||||
|
Value provided for option {option.GetUserFacingDisplayString()}:
|
||||||
|
{failedResults.Select(r => r.ErrorMessage).JoinToString(Environment.NewLine)}";
|
||||||
|
|
||||||
|
return new CliFxException(message.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CliFxException ValidationFailed(
|
||||||
|
CommandArgumentSchema argument,
|
||||||
|
IReadOnlyList<ValidationResult> failedResults) => argument switch
|
||||||
|
{
|
||||||
|
CommandParameterSchema parameter => ValidationFailed(parameter, failedResults),
|
||||||
|
CommandOptionSchema option => ValidationFailed(option, failedResults),
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(argument))
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using CliFx.Internal;
|
||||||
|
|
||||||
namespace CliFx
|
namespace CliFx
|
||||||
{
|
{
|
||||||
@@ -85,7 +86,10 @@ namespace CliFx
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets console foreground color, executes specified action, and sets the color back to the original value.
|
/// Sets console foreground color, executes specified action, and sets the color back to the original value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void WithForegroundColor(this IConsole console, ConsoleColor foregroundColor, Action action)
|
public static void WithForegroundColor(
|
||||||
|
this IConsole console,
|
||||||
|
ConsoleColor foregroundColor,
|
||||||
|
Action action)
|
||||||
{
|
{
|
||||||
var lastColor = console.ForegroundColor;
|
var lastColor = console.ForegroundColor;
|
||||||
console.ForegroundColor = foregroundColor;
|
console.ForegroundColor = foregroundColor;
|
||||||
@@ -98,7 +102,10 @@ namespace CliFx
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets console background color, executes specified action, and sets the color back to the original value.
|
/// Sets console background color, executes specified action, and sets the color back to the original value.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void WithBackgroundColor(this IConsole console, ConsoleColor backgroundColor, Action action)
|
public static void WithBackgroundColor(
|
||||||
|
this IConsole console,
|
||||||
|
ConsoleColor backgroundColor,
|
||||||
|
Action action)
|
||||||
{
|
{
|
||||||
var lastColor = console.BackgroundColor;
|
var lastColor = console.BackgroundColor;
|
||||||
console.BackgroundColor = backgroundColor;
|
console.BackgroundColor = backgroundColor;
|
||||||
@@ -111,7 +118,133 @@ namespace CliFx
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets console foreground and background colors, executes specified action, and sets the colors back to the original values.
|
/// Sets console foreground and background colors, executes specified action, and sets the colors back to the original values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void WithColors(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor, Action action) =>
|
public static void WithColors(
|
||||||
|
this IConsole console,
|
||||||
|
ConsoleColor foregroundColor,
|
||||||
|
ConsoleColor backgroundColor,
|
||||||
|
Action action) =>
|
||||||
console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
|
console.WithForegroundColor(foregroundColor, () => console.WithBackgroundColor(backgroundColor, action));
|
||||||
|
|
||||||
|
private static void WriteException(
|
||||||
|
this IConsole console,
|
||||||
|
Exception exception,
|
||||||
|
int indentLevel)
|
||||||
|
{
|
||||||
|
var exceptionType = exception.GetType();
|
||||||
|
|
||||||
|
var indentationShared = new string(' ', 4 * indentLevel);
|
||||||
|
var indentationLocal = new string(' ', 2);
|
||||||
|
|
||||||
|
// Fully qualified exception type
|
||||||
|
console.Error.Write(indentationShared);
|
||||||
|
console.WithForegroundColor(ConsoleColor.DarkGray, () =>
|
||||||
|
console.Error.Write(exceptionType.Namespace + ".")
|
||||||
|
);
|
||||||
|
console.WithForegroundColor(ConsoleColor.White, () =>
|
||||||
|
console.Error.Write(exceptionType.Name)
|
||||||
|
);
|
||||||
|
console.Error.Write(": ");
|
||||||
|
|
||||||
|
// Exception message
|
||||||
|
console.WithForegroundColor(ConsoleColor.Red, () => console.Error.WriteLine(exception.Message));
|
||||||
|
|
||||||
|
// Recurse into inner exceptions
|
||||||
|
if (exception.InnerException != null)
|
||||||
|
{
|
||||||
|
console.WriteException(exception.InnerException, indentLevel + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse and pretty-print the stack trace
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var stackFrame in StackFrame.ParseMany(exception.StackTrace))
|
||||||
|
{
|
||||||
|
console.Error.Write(indentationShared + indentationLocal);
|
||||||
|
console.Error.Write("at ");
|
||||||
|
|
||||||
|
// "CliFx.Demo.Commands.BookAddCommand."
|
||||||
|
console.WithForegroundColor(ConsoleColor.DarkGray, () =>
|
||||||
|
console.Error.Write(stackFrame.ParentType + ".")
|
||||||
|
);
|
||||||
|
|
||||||
|
// "ExecuteAsync"
|
||||||
|
console.WithForegroundColor(ConsoleColor.Yellow, () =>
|
||||||
|
console.Error.Write(stackFrame.MethodName)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.Error.Write("(");
|
||||||
|
|
||||||
|
for (var i = 0; i < stackFrame.Parameters.Count; i++)
|
||||||
|
{
|
||||||
|
var parameter = stackFrame.Parameters[i];
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
console.Error.Write(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// "IConsole"
|
||||||
|
console.WithForegroundColor(ConsoleColor.Blue, () =>
|
||||||
|
console.Error.Write(parameter.Type)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(parameter.Name))
|
||||||
|
{
|
||||||
|
console.Error.Write(" ");
|
||||||
|
|
||||||
|
// "console"
|
||||||
|
console.WithForegroundColor(ConsoleColor.White, () =>
|
||||||
|
console.Error.Write(parameter.Name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.Error.Write(") ");
|
||||||
|
|
||||||
|
// Location
|
||||||
|
if (!string.IsNullOrWhiteSpace(stackFrame.FilePath))
|
||||||
|
{
|
||||||
|
console.Error.Write("in");
|
||||||
|
console.Error.Write("\n" + indentationShared + indentationLocal + indentationLocal);
|
||||||
|
|
||||||
|
// "E:\Projects\Softdev\CliFx\CliFx.Demo\Commands\"
|
||||||
|
var stackFrameDirectoryPath = Path.GetDirectoryName(stackFrame.FilePath);
|
||||||
|
console.WithForegroundColor(ConsoleColor.DarkGray, () =>
|
||||||
|
console.Error.Write(stackFrameDirectoryPath + Path.DirectorySeparatorChar)
|
||||||
|
);
|
||||||
|
|
||||||
|
// "BookAddCommand.cs"
|
||||||
|
var stackFrameFileName = Path.GetFileName(stackFrame.FilePath);
|
||||||
|
console.WithForegroundColor(ConsoleColor.Yellow, () =>
|
||||||
|
console.Error.Write(stackFrameFileName)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(stackFrame.LineNumber))
|
||||||
|
{
|
||||||
|
console.Error.Write(":");
|
||||||
|
|
||||||
|
// "35"
|
||||||
|
console.WithForegroundColor(ConsoleColor.Blue, () =>
|
||||||
|
console.Error.Write(stackFrame.LineNumber)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.Error.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If any point of parsing has failed - print the stack trace without any formatting
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
console.Error.WriteLine(exception.StackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Should this be public?
|
||||||
|
internal static void WriteException(
|
||||||
|
this IConsole console,
|
||||||
|
Exception exception) =>
|
||||||
|
console.WriteException(exception, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,11 @@ namespace CliFx.Internal.Extensions
|
|||||||
{
|
{
|
||||||
internal static class StringExtensions
|
internal static class StringExtensions
|
||||||
{
|
{
|
||||||
|
public static string? NullIfWhiteSpace(this string str) =>
|
||||||
|
!string.IsNullOrWhiteSpace(str)
|
||||||
|
? str
|
||||||
|
: null;
|
||||||
|
|
||||||
public static string Repeat(this char c, int count) => new string(c, count);
|
public static string Repeat(this char c, int count) => new string(c, count);
|
||||||
|
|
||||||
public static string AsString(this char c) => c.Repeat(1);
|
public static string AsString(this char c) => c.Repeat(1);
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ namespace CliFx.Internal.Extensions
|
|||||||
{
|
{
|
||||||
internal static class TypeExtensions
|
internal static class TypeExtensions
|
||||||
{
|
{
|
||||||
|
public static object CreateInstance(this Type type) => Activator.CreateInstance(type);
|
||||||
|
|
||||||
|
public static T CreateInstance<T>(this Type type) => (T) type.CreateInstance();
|
||||||
|
|
||||||
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
|
public static bool Implements(this Type type, Type interfaceType) => type.GetInterfaces().Contains(interfaceType);
|
||||||
|
|
||||||
public static Type? GetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
|
public static Type? TryGetNullableUnderlyingType(this Type type) => Nullable.GetUnderlyingType(type);
|
||||||
|
|
||||||
public static Type? GetEnumerableUnderlyingType(this Type type)
|
public static Type? TryGetEnumerableUnderlyingType(this Type type)
|
||||||
{
|
{
|
||||||
if (type.IsPrimitive)
|
if (type.IsPrimitive)
|
||||||
return null;
|
return null;
|
||||||
@@ -25,17 +29,20 @@ namespace CliFx.Internal.Extensions
|
|||||||
|
|
||||||
return type
|
return type
|
||||||
.GetInterfaces()
|
.GetInterfaces()
|
||||||
.Select(GetEnumerableUnderlyingType)
|
.Select(TryGetEnumerableUnderlyingType)
|
||||||
.Where(t => t != null)
|
.Where(t => t != null)
|
||||||
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types
|
.OrderByDescending(t => t != typeof(object)) // prioritize more specific types
|
||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MethodInfo GetToStringMethod(this Type type) => type.GetMethod(nameof(ToString), Type.EmptyTypes);
|
public static MethodInfo GetToStringMethod(this Type type) =>
|
||||||
|
// ToString() with no params always exists
|
||||||
|
type.GetMethod(nameof(ToString), Type.EmptyTypes)!;
|
||||||
|
|
||||||
public static bool IsToStringOverriden(this Type type) => type.GetToStringMethod() != typeof(object).GetToStringMethod();
|
public static bool IsToStringOverriden(this Type type) =>
|
||||||
|
type.GetToStringMethod() != typeof(object).GetToStringMethod();
|
||||||
|
|
||||||
public static MethodInfo GetStaticParseMethod(this Type type, bool withFormatProvider = false)
|
public static MethodInfo? TryGetStaticParseMethod(this Type type, bool withFormatProvider = false)
|
||||||
{
|
{
|
||||||
var argumentTypes = withFormatProvider
|
var argumentTypes = withFormatProvider
|
||||||
? new[] {typeof(string), typeof(IFormatProvider)}
|
? new[] {typeof(string), typeof(IFormatProvider)}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ namespace System
|
|||||||
|
|
||||||
public static bool EndsWith(this string str, char c) =>
|
public static bool EndsWith(this string str, char c) =>
|
||||||
str.Length > 0 && str[str.Length - 1] == c;
|
str.Length > 0 && str[str.Length - 1] == c;
|
||||||
|
|
||||||
|
public static string[] Split(this string str, char separator, StringSplitOptions splitOptions) =>
|
||||||
|
str.Split(new[] {separator}, splitOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ namespace System.Collections.Generic
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
|
public static TValue GetValueOrDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dic, TKey key) =>
|
||||||
dic.TryGetValue(key, out var result) ? result! : default!;
|
dic.TryGetValue(key!, out var result) ? result! : default!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
CliFx/Internal/StackFrame.cs
Normal file
121
CliFx/Internal/StackFrame.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using CliFx.Internal.Extensions;
|
||||||
|
|
||||||
|
namespace CliFx.Internal
|
||||||
|
{
|
||||||
|
internal class StackFrameParameter
|
||||||
|
{
|
||||||
|
public string Type { get; }
|
||||||
|
|
||||||
|
public string? Name { get; }
|
||||||
|
|
||||||
|
public StackFrameParameter(string type, string? name)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal partial class StackFrame
|
||||||
|
{
|
||||||
|
public string ParentType { get; }
|
||||||
|
|
||||||
|
public string MethodName { get; }
|
||||||
|
|
||||||
|
public IReadOnlyList<StackFrameParameter> Parameters { get; }
|
||||||
|
|
||||||
|
public string? FilePath { get; }
|
||||||
|
|
||||||
|
public string? LineNumber { get; }
|
||||||
|
|
||||||
|
public StackFrame(
|
||||||
|
string parentType,
|
||||||
|
string methodName,
|
||||||
|
IReadOnlyList<StackFrameParameter> parameters,
|
||||||
|
string? filePath,
|
||||||
|
string? lineNumber)
|
||||||
|
{
|
||||||
|
ParentType = parentType;
|
||||||
|
MethodName = methodName;
|
||||||
|
Parameters = parameters;
|
||||||
|
FilePath = filePath;
|
||||||
|
LineNumber = lineNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal partial class StackFrame
|
||||||
|
{
|
||||||
|
private const string Space = @"[\x20\t]";
|
||||||
|
private const string NotSpace = @"[^\x20\t]";
|
||||||
|
|
||||||
|
// Taken from https://github.com/atifaziz/StackTraceParser
|
||||||
|
private static readonly Regex Pattern = new Regex(@"
|
||||||
|
^
|
||||||
|
" + Space + @"*
|
||||||
|
\w+ " + Space + @"+
|
||||||
|
(?<frame>
|
||||||
|
(?<type> " + NotSpace + @"+ ) \.
|
||||||
|
(?<method> " + NotSpace + @"+? ) " + Space + @"*
|
||||||
|
(?<params> \( ( " + Space + @"* \)
|
||||||
|
| (?<pt> .+?) " + Space + @"+ (?<pn> .+?)
|
||||||
|
(, " + Space + @"* (?<pt> .+?) " + Space + @"+ (?<pn> .+?) )* \) ) )
|
||||||
|
( " + Space + @"+
|
||||||
|
( # Microsoft .NET stack traces
|
||||||
|
\w+ " + Space + @"+
|
||||||
|
(?<file> ( [a-z] \: # Windows rooted path starting with a drive letter
|
||||||
|
| / ) # *nix rooted path starting with a forward-slash
|
||||||
|
.+? )
|
||||||
|
\: \w+ " + Space + @"+
|
||||||
|
(?<line> [0-9]+ ) \p{P}?
|
||||||
|
| # Mono stack traces
|
||||||
|
\[0x[0-9a-f]+\] " + Space + @"+ \w+ " + Space + @"+
|
||||||
|
<(?<file> [^>]+ )>
|
||||||
|
:(?<line> [0-9]+ )
|
||||||
|
)
|
||||||
|
)?
|
||||||
|
)
|
||||||
|
\s*
|
||||||
|
$",
|
||||||
|
RegexOptions.IgnoreCase |
|
||||||
|
RegexOptions.Multiline |
|
||||||
|
RegexOptions.ExplicitCapture |
|
||||||
|
RegexOptions.CultureInvariant |
|
||||||
|
RegexOptions.IgnorePatternWhitespace,
|
||||||
|
TimeSpan.FromSeconds(5)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static IEnumerable<StackFrame> ParseMany(string stackTrace)
|
||||||
|
{
|
||||||
|
var matches = Pattern.Matches(stackTrace).Cast<Match>().ToArray();
|
||||||
|
|
||||||
|
// Ensure success (all lines should be parsed)
|
||||||
|
var isSuccess =
|
||||||
|
matches.Length ==
|
||||||
|
stackTrace.Split('\n', StringSplitOptions.RemoveEmptyEntries).Length;
|
||||||
|
|
||||||
|
if (!isSuccess)
|
||||||
|
{
|
||||||
|
throw new FormatException("Could not parse stack trace.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return from m in matches
|
||||||
|
select m.Groups
|
||||||
|
into groups
|
||||||
|
let pt = groups["pt"].Captures
|
||||||
|
let pn = groups["pn"].Captures
|
||||||
|
select new StackFrame(
|
||||||
|
groups["type"].Value,
|
||||||
|
groups["method"].Value,
|
||||||
|
(
|
||||||
|
from i in Enumerable.Range(0, pt.Count)
|
||||||
|
select new StackFrameParameter(pt[i].Value, pn[i].Value.NullIfWhiteSpace())
|
||||||
|
).ToArray(),
|
||||||
|
groups["file"].Value.NullIfWhiteSpace(),
|
||||||
|
groups["line"].Value.NullIfWhiteSpace()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
187
Readme.md
187
Readme.md
@@ -6,9 +6,9 @@
|
|||||||
[](https://nuget.org/packages/CliFx)
|
[](https://nuget.org/packages/CliFx)
|
||||||
[](https://tyrrrz.me/donate)
|
[](https://tyrrrz.me/donate)
|
||||||
|
|
||||||
CliFx is a simple to use, yet powerful framework for building command line applications. Its primary goal is to completely take over the user input layer, letting you forget about the infrastructure and instead focus on writing your application. This framework uses a declarative class-first approach for defining commands, avoiding excessive boilerplate code and complex configurations.
|
**Project status: active**.
|
||||||
|
|
||||||
An important property of CliFx, when compared to some other libraries, is that it's not just a parser -- it's a complete application framework. The main goal of the library is to provide a consistent and enjoyable development experience when building command line applications. At its core, CliFx is highly opinionated, giving preference to convention over configuration, strictness over extensibility, consistency over ambiguity, and so on.
|
CliFx is a simple to use, yet powerful framework for building command line applications. Its primary goal is to completely take over the user input layer, letting you forget about the infrastructure and instead focus on writing your application. This framework uses a declarative class-first approach for defining commands, avoiding excessive boilerplate code and complex configurations.
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ An important property of CliFx, when compared to some other libraries, is that i
|
|||||||
- Provides comprehensive and colorful auto-generated help text
|
- Provides comprehensive and colorful auto-generated help text
|
||||||
- Highly testable and easy to debug
|
- Highly testable and easy to debug
|
||||||
- Comes with built-in analyzers to help catch common mistakes
|
- Comes with built-in analyzers to help catch common mistakes
|
||||||
- Targets .NET Standard 2.0+
|
- Works with .NET Standard 2.0+, .NET Core 2.0+, .NET Framework 4.6.1+
|
||||||
- No external dependencies
|
- No external dependencies
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
@@ -46,16 +46,13 @@ An important property of CliFx, when compared to some other libraries, is that i
|
|||||||
- [Dependency injection](#dependency-injection)
|
- [Dependency injection](#dependency-injection)
|
||||||
- [Testing](#testing)
|
- [Testing](#testing)
|
||||||
- [Debug and preview mode](#debug-and-preview-mode)
|
- [Debug and preview mode](#debug-and-preview-mode)
|
||||||
- [Reporting progress](#reporting-progress)
|
|
||||||
- [Environment variables](#environment-variables)
|
- [Environment variables](#environment-variables)
|
||||||
|
|
||||||
### Quick start
|
### Quick start
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
To turn your application into a command line interface you need to change your program's `Main` method so that it delegates execution to `CliApplication`.
|
To turn your application into a command line interface you need to change your program's `Main` method so that it delegates execution to `CliApplication`. This is how to do it:
|
||||||
|
|
||||||
The following code will create and run a default `CliApplication` that will resolve commands defined in the calling assembly. Using fluent interface provided by `CliApplicationBuilder` you can easily configure different aspects of your application.
|
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
public static class Program
|
public static class Program
|
||||||
@@ -68,7 +65,9 @@ public static class Program
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
In order to add functionality to your application, you need to define at least one command. Commands are essentially entry points through which the user can interact with your application. You can think of them as something similar to controllers in ASP.NET Core.
|
The above code will create and run a `CliApplication` that will resolve commands defined in the calling assembly. Using fluent interface provided by `CliApplicationBuilder` you can also easily configure other aspects of your application.
|
||||||
|
|
||||||
|
In order to add functionality, however, you need to define at least one command. Commands are essentially entry points through which the user can interact with your application. You can think of them as something similar to controllers in ASP.NET Core.
|
||||||
|
|
||||||
To define a command, just create a new class that implements the `ICommand` interface and annotate it with `[Command]` attribute:
|
To define a command, just create a new class that implements the `ICommand` interface and annotate it with `[Command]` attribute:
|
||||||
|
|
||||||
@@ -86,11 +85,11 @@ public class HelloWorldCommand : ICommand
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To implement `ICommand`, the class needs to define the `ExecuteAsync` method. This is the method that gets called when the user executes the command.
|
To implement `ICommand`, the class needs to define an `ExecuteAsync()` method. This is the method that gets called by CliFx when the user runs the application.
|
||||||
|
|
||||||
To facilitate both asynchronous and synchronous execution, this method returns a `ValueTask`. Since the simple command above executes synchronously, we can just put `return default` at the end. In an asynchronous command, however, we would use the `async`/`await` keywords instead.
|
To facilitate both asynchronous and synchronous execution, this method returns a `ValueTask`. Since the simple command above executes synchronously, we can just put `return default` at the end. In an asynchronous command, however, we would use the `async`/`await` keywords instead.
|
||||||
|
|
||||||
As a parameter, this method takes an instance of `IConsole`, an abstraction around the system console. You should use this abstraction in places where you would normally use `System.Console`, in order to make your command testable.
|
As a parameter, this method takes an instance of `IConsole`, an abstraction around the system console. You should use this abstraction in places where you would normally interact with `System.Console`, in order to make your command testable.
|
||||||
|
|
||||||
With this basic setup, the user can execute your application and get a greeting in return:
|
With this basic setup, the user can execute your application and get a greeting in return:
|
||||||
|
|
||||||
@@ -125,7 +124,7 @@ v1.0
|
|||||||
|
|
||||||
Commands can be configured to take input from command line arguments. To do that, we need to add properties to the command and annotate them with special attributes.
|
Commands can be configured to take input from command line arguments. To do that, we need to add properties to the command and annotate them with special attributes.
|
||||||
|
|
||||||
In CliFx, there are two types of argument bindings: **parameters** and **options**. Parameters are positional arguments that are identified by the order they appear in, while options are identified by their names.
|
In CliFx, there are two types of argument bindings: **parameters** and **options**. Parameters are positional arguments that are identified by the order they appear in, while options are arguments identified by their names.
|
||||||
|
|
||||||
Here's an example command that calculates a logarithm of a value, which uses both a parameter and an option:
|
Here's an example command that calculates a logarithm of a value, which uses both a parameter and an option:
|
||||||
|
|
||||||
@@ -181,7 +180,7 @@ As we can see, in order to execute this command, at a minimum, the user has to s
|
|||||||
4
|
4
|
||||||
```
|
```
|
||||||
|
|
||||||
They can also set the `base` option to override the default logarithm base of 10:
|
They can also set the non-required `base` option to override the default logarithm base of 10:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
> myapp.exe 729 -b 3
|
> myapp.exe 729 -b 3
|
||||||
@@ -195,7 +194,7 @@ They can also set the `base` option to override the default logarithm base of 10
|
|||||||
3.199426017362198
|
3.199426017362198
|
||||||
```
|
```
|
||||||
|
|
||||||
On the other hand, if the user fails to provide the required parameter, they will get an error:
|
On the other hand, if the user fails to provide the parameter, they will get an error, as parameters are always required:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
> myapp.exe -b 10
|
> myapp.exe -b 10
|
||||||
@@ -203,7 +202,7 @@ On the other hand, if the user fails to provide the required parameter, they wil
|
|||||||
Missing value for parameter <value>.
|
Missing value for parameter <value>.
|
||||||
```
|
```
|
||||||
|
|
||||||
Differences between parameters and options:
|
Overall, the difference between parameters and options is as follows:
|
||||||
|
|
||||||
- Parameters are identified by their relative order. Options are identified by two dashes followed by their name, or a single dash followed by their short name (single character).
|
- Parameters are identified by their relative order. Options are identified by two dashes followed by their name, or a single dash followed by their short name (single character).
|
||||||
- Parameters can't be optional. Options are usually optional (as evident by the name), but can be configured to be required as well.
|
- Parameters can't be optional. Options are usually optional (as evident by the name), but can be configured to be required as well.
|
||||||
@@ -231,7 +230,7 @@ More specifically, the following examples are all valid:
|
|||||||
|
|
||||||
Argument parsing in CliFx aims to be as deterministic as possible, ideally yielding the same result no matter the context. The only context-sensitive part in the parser is the command name resolution which needs to know what commands are available in order to discern between arguments that correspond to the command name and arguments which are parameters.
|
Argument parsing in CliFx aims to be as deterministic as possible, ideally yielding the same result no matter the context. The only context-sensitive part in the parser is the command name resolution which needs to know what commands are available in order to discern between arguments that correspond to the command name and arguments which are parameters.
|
||||||
|
|
||||||
Options are always parsed the same way, disregarding the arity of the actual property it binds to. This means that `myapp -i file1.txt file2.txt` will _always_ be parsed as an option with multiple values, even if the underlying bound property is not enumerable. For the same reason, unseparated arguments such as `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to `"file"`.
|
An option is always parsed the same way, regardless of the arity of the actual property it's bound to. This means that `myapp -i file1.txt file2.txt` will _always_ be parsed as an option set to multiple values, even if the underlying property is not enumerable. For the same reason, unseparated arguments such as `myapp -ofile` will be treated as five distinct options `'o'`, `'f'`, `'i'`, `'l'`, `'e'`, instead of `'o'` being set to `"file"`.
|
||||||
|
|
||||||
Because of these rules, order of arguments is semantically important and it always goes like this:
|
Because of these rules, order of arguments is semantically important and it always goes like this:
|
||||||
|
|
||||||
@@ -252,6 +251,7 @@ Parameters and options can have different underlying types:
|
|||||||
- String-initializable types
|
- String-initializable types
|
||||||
- Types with a constructor that accepts a single `string` parameter (`FileInfo`, `DirectoryInfo`, etc.)
|
- Types with a constructor that accepts a single `string` parameter (`FileInfo`, `DirectoryInfo`, etc.)
|
||||||
- Types with a static method `Parse` that accepts a single `string` parameter (and optionally `IFormatProvider`)
|
- Types with a static method `Parse` that accepts a single `string` parameter (and optionally `IFormatProvider`)
|
||||||
|
- Any other type if a custom converter is specified
|
||||||
- Nullable versions of all above types (`decimal?`, `TimeSpan?`, etc.)
|
- Nullable versions of all above types (`decimal?`, `TimeSpan?`, etc.)
|
||||||
- Collections of all above types
|
- Collections of all above types
|
||||||
- Array types (`T[]`)
|
- Array types (`T[]`)
|
||||||
@@ -260,7 +260,58 @@ Parameters and options can have different underlying types:
|
|||||||
|
|
||||||
When defining a parameter of an enumerable type, keep in mind that it has to be the only such parameter and it must be the last in order. Options, on the other hand, don't have this limitation.
|
When defining a parameter of an enumerable type, keep in mind that it has to be the only such parameter and it must be the last in order. Options, on the other hand, don't have this limitation.
|
||||||
|
|
||||||
Example command with an array-backed parameter:
|
- Example command with a custom converter:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
// Maps 2D vectors from AxB notation
|
||||||
|
public class VectorConverter : ArgumentValueConverter<Vector2>
|
||||||
|
{
|
||||||
|
public override Vector2 ConvertFrom(string value)
|
||||||
|
{
|
||||||
|
var components = value.Split('x', 'X', ';');
|
||||||
|
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
|
||||||
|
var y = int.Parse(components[1], CultureInfo.InvariantCulture);
|
||||||
|
return new Vector2(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Command]
|
||||||
|
public class SurfaceCalculatorCommand : ICommand
|
||||||
|
{
|
||||||
|
// Custom converter is used to map raw argument values
|
||||||
|
[CommandParameter(0, Converter = typeof(VectorConverter))]
|
||||||
|
public Vector2 PointA { get; set; }
|
||||||
|
|
||||||
|
[CommandParameter(1, Converter = typeof(VectorConverter))]
|
||||||
|
public Vector2 PointB { get; set; }
|
||||||
|
|
||||||
|
[CommandParameter(2, Converter = typeof(VectorConverter))]
|
||||||
|
public Vector2 PointC { get; set; }
|
||||||
|
|
||||||
|
public ValueTask ExecuteAsync(IConsole console)
|
||||||
|
{
|
||||||
|
var a = (PointB - PointA).Length();
|
||||||
|
var b = (PointC - PointB).Length();
|
||||||
|
var c = (PointA - PointC).Length();
|
||||||
|
|
||||||
|
// Heron's formula
|
||||||
|
var p = (a + b + c) / 2;
|
||||||
|
var surface = Math.Sqrt(p * (p - a) * (p - b) * (p - c));
|
||||||
|
|
||||||
|
console.Output.WriteLine($"Triangle surface area: {surface}");
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> myapp.exe 0x0 0x18 24x0
|
||||||
|
|
||||||
|
Triangle surface area: 216
|
||||||
|
```
|
||||||
|
|
||||||
|
- Example command with an array-backed parameter:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command]
|
[Command]
|
||||||
@@ -350,9 +401,7 @@ public class SubCommand : ICommand
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The user can access other commands by specifying the name before any other arguments, e.g. `myapp.exe cmd1 arg1 -p 42`.
|
There is no limit to the number of commands or the level of their nesting. Once configured, the user can execute a specific command by typing its name before any other arguments, e.g. `myapp.exe cmd1 arg1 -p 42`.
|
||||||
|
|
||||||
In a multi-command application you may also choose to not have a default command, in which case executing your application without any arguments will just show the help text.
|
|
||||||
|
|
||||||
Requesting help on the application above will show:
|
Requesting help on the application above will show:
|
||||||
|
|
||||||
@@ -392,15 +441,15 @@ Commands
|
|||||||
You can run `myapp.exe cmd1 [command] --help` to show help on a specific command.
|
You can run `myapp.exe cmd1 [command] --help` to show help on a specific command.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
In a multi-command application you may also choose to not have a default command and only use named commands. If that's the case, running an application without parameters will simply print help text.
|
||||||
|
|
||||||
### Reporting errors
|
### Reporting errors
|
||||||
|
|
||||||
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered a higher-level concern and thus handled by `CliApplication`, not by individual commands.
|
You may have noticed that commands in CliFx don't return exit codes. This is by design as exit codes are considered an infrastructural concern and thus handled by `CliApplication`, not by individual commands.
|
||||||
|
|
||||||
Commands can report execution failure simply by throwing exceptions just like any other C# code. When an exception is thrown, `CliApplication` will catch it, print the error, and return `1` as the exit code to the calling process.
|
Commands can instead report execution failure by throwing an instance of `CommandException`. Using this exception, you can specify the message printed to stderr and the returned exit code.
|
||||||
|
|
||||||
If you want to communicate a specific error through exit code, you can instead throw an instance of `CommandException` which takes an exit code as a parameter. When a command throws an exception of type `CommandException`, it is assumed that this was a result of a handled error and, as such, only the exception message will be printed to the error stream. If a command throws an exception of any other type, the full stack trace will be printed as well.
|
Here is an example:
|
||||||
|
|
||||||
> Note: Unix systems rely on 8-bit unsigned integers for exit codes, so it's strongly recommended to use values between `1` and `255` to avoid potential overflow issues.
|
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command]
|
[Command]
|
||||||
@@ -452,37 +501,49 @@ public class ExampleCommand : ICommand
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Note: Unix systems rely on 8-bit unsigned integers for exit codes, so it's strongly recommended to use values between `1` and `255` when specifying exit code, in order to avoid potential overflow issues.
|
||||||
|
|
||||||
### Graceful cancellation
|
### Graceful cancellation
|
||||||
|
|
||||||
It is possible to gracefully cancel execution of a command and preform any necessary cleanup. By default an app gets forcefully killed when it receives an interrupt signal (Ctrl+C or Ctrl+Break), but you can override this behavior.
|
The user may abort execution by sending an interrupt signal (Ctrl+C or Ctrl+Break). If your command has critical disposable resources, you can intercept this signal to perform cleanup before exiting.
|
||||||
|
|
||||||
In order to make a command cancellation-aware, you need to call `console.GetCancellationToken()`. This method returns a `CancellationToken` that will trigger when the user issues an interrupt signal. Note that any code that comes before the first call to `GetCancellationToken()` will not be cancellation-aware and as such will terminate instantly. Any subsequent calls to this method will return the same token.
|
In order to make a command cancellation-aware, all you need to do is call `console.GetCancellationToken()`. This method returns a `CancellationToken` that will trigger when the user issues an interrupt signal.
|
||||||
|
|
||||||
|
Note that any operation which precedes `console.GetCancellationToken()` will not be cancellation-aware and as such will not delay the process termination. Calling this method multiple times is fine, as it will always return the same token.
|
||||||
|
|
||||||
|
Here's an example of a command that supports cancellation:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command("cancel")]
|
[Command("cancel")]
|
||||||
public class CancellableCommand : ICommand
|
public class CancellableCommand : ICommand
|
||||||
{
|
{
|
||||||
|
private async ValueTask DoSomethingAsync(CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromMinutes(10), cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
public async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
console.Output.WriteLine("Printed");
|
// Make the command cancellation-aware
|
||||||
|
var cancellation = console.GetCancellationToken();
|
||||||
|
|
||||||
// Long-running cancellable operation that throws when canceled
|
// Execute some long-running cancellable operation
|
||||||
await Task.Delay(Timeout.InfiniteTimeSpan, console.GetCancellationToken());
|
await DoSomethingAsync(cancellation);
|
||||||
|
|
||||||
console.Output.WriteLine("Never printed");
|
console.Output.WriteLine("Done.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep in mind that a command may delay cancellation only once. If the user decides to press Ctrl+C again after the first time, the execution will be forcefully terminated.
|
Keep in mind that a command may delay cancellation only once. If the user decides to send an interrupt signal for the second time, the execution will be terminated immediately.
|
||||||
|
|
||||||
### Dependency injection
|
### Dependency injection
|
||||||
|
|
||||||
CliFx uses an implementation of `ITypeActivator` to initialize commands and by default it only works with types that have parameterless constructors.
|
CliFx uses an implementation of `ITypeActivator` to initialize commands and by default it only works with types that have parameter-less constructors. This is sufficient for majority of scenarios.
|
||||||
|
|
||||||
In real-life scenarios, however, your commands will most likely have dependencies that need to be injected. CliFx doesn't come with its own dependency container but it makes it really easy to integrate any container of your choice.
|
However, in some cases you may also want to initialize commands dynamically with the help of a dependency injection container. CliFx makes it really easy to integrate with any DI framework of your choice.
|
||||||
|
|
||||||
For example, here is how you would configure your application to use [`Microsoft.Extensions.DependencyInjection`](https://nuget.org/packages/Microsoft.Extensions.DependencyInjection) (aka the built-in dependency container in ASP.NET Core).
|
For example, here is how you would configure your application to use [`Microsoft.Extensions.DependencyInjection`](https://nuget.org/packages/Microsoft.Extensions.DependencyInjection) (aka the built-in dependency container in ASP.NET Core):
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
public static class Program
|
public static class Program
|
||||||
@@ -510,9 +571,9 @@ public static class Program
|
|||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
CliFx provides a convenient way to write functional tests for your applications, thanks to the `IConsole` interface.
|
CliFx provides a convenient way to write functional tests for your applications, thanks to the `IConsole` interface. While a command running in production uses `SystemConsole` for console interactions, you can rely on `VirtualConsole` in your tests to validate these interactions in a simulated environment.
|
||||||
|
|
||||||
Instead of interacting with the real console, you can use an instance of `VirtualConsole` to replace the application's stdin, stdout and stderr with your own streams. Using optional parameters you can also choose to substitute only some of the streams, in which case the remaining streams are replaced with no-op stubs:
|
When you initialize an instance of `VirtualConsole`, you can supply your own streams which will be used as the application's stdin, stdout, and stderr. You don't have to supply all of them, however, and any remaining streams will be substituted with a no-op stub.
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
var console = new VirtualConsole(
|
var console = new VirtualConsole(
|
||||||
@@ -529,10 +590,11 @@ var (console, stdOut, stdErr) = VirtualConsole.CreateBuffered();
|
|||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
|
// Get the text that was written so far
|
||||||
var stdOutData = stdOut.GetString();
|
var stdOutData = stdOut.GetString();
|
||||||
```
|
```
|
||||||
|
|
||||||
To illustrate how to use all this, let's look at an example. Assume you want to test a simple command such as this one:
|
To illustrate how to use this, let's look at an example. Assume you want to test a simple command such as this one:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command]
|
[Command]
|
||||||
@@ -558,7 +620,7 @@ public class ConcatCommand : ICommand
|
|||||||
By substituting `IConsole` you can write your test cases like so:
|
By substituting `IConsole` you can write your test cases like so:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
// Integration test at command level
|
// Integration test at the command level
|
||||||
[Test]
|
[Test]
|
||||||
public async Task ConcatCommand_executes_successfully()
|
public async Task ConcatCommand_executes_successfully()
|
||||||
{
|
{
|
||||||
@@ -579,10 +641,10 @@ public async Task ConcatCommand_executes_successfully()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Similarly, you can also test the command end-to-end like so:
|
Similarly, you can also test the entire execution end-to-end like so:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
// End-to-end test at application level
|
// End-to-end test at the application level
|
||||||
[Test]
|
[Test]
|
||||||
public async Task ConcatCommand_executes_successfully()
|
public async Task ConcatCommand_executes_successfully()
|
||||||
{
|
{
|
||||||
@@ -605,9 +667,9 @@ public async Task ConcatCommand_executes_successfully()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
As a general recommendation, it's nearly always more preferable to test at the application level. While you can validate your command's execution adequately simply by testing its `ExecuteAsync` method, testing end-to-end helps you to also catch bugs related to configuration, such as incorrect option names, parameter order, environment variable names, etc.
|
As a general recommendation, it's always more preferable to test at the application level. While you can validate your command's execution adequately simply by testing its `ExecuteAsync()` method, testing end-to-end also helps you catch bugs related to configuration, such as incorrect option names, parameter order, environment variable names, etc.
|
||||||
|
|
||||||
Additionally, it's important to remember that commands in CliFx are not constrained to text and can also produce binary data. In such cases, you can still use the above setup but use `GetBytes` instead of `GetString`:
|
Additionally, it's important to remember that commands in CliFx are not constrained to text and can produce binary data. In such cases, you can still use the above setup but call `GetBytes()` instead of `GetString()`:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
// Act
|
// Act
|
||||||
@@ -615,13 +677,13 @@ await app.RunAsync(args, envVars);
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.That(stdOut.GetBytes(), Is.EqualTo(new byte[] {1, 2, 3, 4, 5}));
|
Assert.That(stdOut.GetBytes(), Is.EqualTo(new byte[] {1, 2, 3, 4, 5}));
|
||||||
```
|
```
|
||||||
|
|
||||||
In some scenarios the binary data may be too large to load in-memory. In situations like this, it's recommended to use `VirtualConsole` directly with custom streams.
|
In some scenarios the binary data may be too large to load in-memory. If that's the case, it's recommended to use `VirtualConsole` directly with custom streams.
|
||||||
|
|
||||||
### Debug and preview mode
|
### Debug and preview mode
|
||||||
|
|
||||||
When troubleshooting issues, you may find it useful to run your app in debug or preview mode. To do it, simply pass the corresponding directive to your app along with other command line arguments.
|
When troubleshooting issues, you may find it useful to run your app in debug or preview mode. To do it, simply pass the corresponding directive to your app along with any other command line arguments.
|
||||||
|
|
||||||
If your application is ran in debug mode (using the `[debug]` directive), it will wait for debugger to be attached before proceeding. This is useful for debugging apps that were ran outside of the IDE.
|
If your application is ran in debug mode (using the `[debug]` directive), it will wait for debugger to be attached before proceeding. This is useful for debugging apps that were ran outside of the IDE.
|
||||||
|
|
||||||
@@ -650,24 +712,11 @@ var app = new CliApplicationBuilder()
|
|||||||
.Build();
|
.Build();
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reporting progress
|
|
||||||
|
|
||||||
CliFx comes with a simple utility for reporting progress to the console, `ProgressTicker`, which renders progress in-place on every tick.
|
|
||||||
|
|
||||||
It implements a well-known `IProgress<double>` interface so you can pass it to methods that are aware of this abstraction.
|
|
||||||
|
|
||||||
To avoid polluting output when it's not bound to a console, `ProgressTicker` will simply no-op if stdout is redirected.
|
|
||||||
|
|
||||||
```c#
|
|
||||||
var progressTicker = console.CreateProgressTicker();
|
|
||||||
|
|
||||||
for (var i = 0.0; i <= 1; i += 0.01)
|
|
||||||
progressTicker.Report(i);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment variables
|
### Environment variables
|
||||||
|
|
||||||
An option can be configured to use the value of an environment variable as a fallback. If an option was not specified by the user, the value will be extracted from that environment variable instead. This also works on options which are marked as required.
|
An option can be configured to use the value of an environment variable as a fallback. If the value for such an option is not directly specified in the arguments, it will be extracted from that environment variable instead.
|
||||||
|
|
||||||
|
Here's an example of a required option that can be either provided directly or extracted from the environment:
|
||||||
|
|
||||||
```c#
|
```c#
|
||||||
[Command]
|
[Command]
|
||||||
@@ -708,15 +757,15 @@ Frequency=3124994 Hz, Resolution=320.0006 ns, Timer=TSC
|
|||||||
DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
|
DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
|
||||||
```
|
```
|
||||||
|
|
||||||
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank |
|
||||||
|------------------------------------- |------------:|----------:|-----------:|------:|--------:|-----:|
|
| ------------------------------------ | ----------: | --------: | ---------: | ----: | ------: | ---: |
|
||||||
| CommandLineParser | 24.79 us | 0.166 us | 0.155 us | 0.49 | 0.00 | 1 |
|
| CommandLineParser | 24.79 us | 0.166 us | 0.155 us | 0.49 | 0.00 | 1 |
|
||||||
| CliFx | 50.27 us | 0.248 us | 0.232 us | 1.00 | 0.00 | 2 |
|
| CliFx | 50.27 us | 0.248 us | 0.232 us | 1.00 | 0.00 | 2 |
|
||||||
| Clipr | 160.22 us | 0.817 us | 0.764 us | 3.19 | 0.02 | 3 |
|
| Clipr | 160.22 us | 0.817 us | 0.764 us | 3.19 | 0.02 | 3 |
|
||||||
| McMaster.Extensions.CommandLineUtils | 166.45 us | 1.111 us | 1.039 us | 3.31 | 0.03 | 4 |
|
| McMaster.Extensions.CommandLineUtils | 166.45 us | 1.111 us | 1.039 us | 3.31 | 0.03 | 4 |
|
||||||
| System.CommandLine | 170.27 us | 0.599 us | 0.560 us | 3.39 | 0.02 | 5 |
|
| System.CommandLine | 170.27 us | 0.599 us | 0.560 us | 3.39 | 0.02 | 5 |
|
||||||
| PowerArgs | 306.12 us | 1.495 us | 1.398 us | 6.09 | 0.03 | 6 |
|
| PowerArgs | 306.12 us | 1.495 us | 1.398 us | 6.09 | 0.03 | 6 |
|
||||||
| Cocona | 1,856.07 us | 48.727 us | 141.367 us | 37.88 | 2.60 | 7 |
|
| Cocona | 1,856.07 us | 48.727 us | 141.367 us | 37.88 | 2.60 | 7 |
|
||||||
|
|
||||||
## Etymology
|
## Etymology
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user