mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90a01e729b | ||
|
|
ac01c2aecb | ||
|
|
4acffe925c | ||
|
|
18f53eeeef | ||
|
|
03d6942540 | ||
|
|
9be811a89a | ||
|
|
f9f5a4696b | ||
|
|
d6da687170 | ||
|
|
eba66d0878 | ||
|
|
8c682766bd | ||
|
|
39d626c8d8 | ||
|
|
a338ac8ce2 | ||
|
|
11637127cb | ||
|
|
4e12aefafb | ||
|
|
144d3592fb | ||
|
|
6f82c2f0f9 | ||
|
|
b8c60717d5 | ||
|
|
fec6850c39 | ||
|
|
6a378ad946 | ||
|
|
11579f11b1 |
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.3.3
|
uses: actions/checkout@v2.3.3
|
||||||
|
|
||||||
- name: Install .NET Core
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@v1.7.2
|
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.3.3
|
uses: actions/checkout@v2.3.3
|
||||||
|
|
||||||
- name: Install .NET Core
|
- name: Install .NET
|
||||||
uses: actions/setup-dotnet@v1.7.2
|
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
|
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
### 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)
|
### 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 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))
|
||||||
|
|||||||
@@ -2,17 +2,16 @@
|
|||||||
<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.1" />
|
<PackageReference Include="Gu.Roslyn.Asserts" Version="3.3.1" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.1" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.4.0" />
|
||||||
|
|||||||
@@ -153,9 +153,9 @@ public class MyCommand : ICommand
|
|||||||
|
|
||||||
// language=cs
|
// language=cs
|
||||||
@"
|
@"
|
||||||
public class MyConverter : IArgumentValueConverter
|
public class MyConverter : ArgumentValueConverter<string>
|
||||||
{
|
{
|
||||||
public object ConvertFrom(string value) => value;
|
public string ConvertFrom(string value) => value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command]
|
[Command]
|
||||||
@@ -164,6 +164,30 @@ public class MyCommand : ICommand
|
|||||||
[CommandParameter(0, Converter = typeof(MyConverter))]
|
[CommandParameter(0, Converter = typeof(MyConverter))]
|
||||||
public string Param { get; set; }
|
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;
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
@@ -281,9 +305,9 @@ public class MyCommand : ICommand
|
|||||||
|
|
||||||
// language=cs
|
// language=cs
|
||||||
@"
|
@"
|
||||||
public class MyConverter : IArgumentValueConverter
|
public class MyConverter : ArgumentValueConverter<string>
|
||||||
{
|
{
|
||||||
public object ConvertFrom(string value) => value;
|
public string ConvertFrom(string value) => value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Command]
|
[Command]
|
||||||
@@ -292,6 +316,30 @@ public class MyCommand : ICommand
|
|||||||
[CommandOption('o', Converter = typeof(MyConverter))]
|
[CommandOption('o', Converter = typeof(MyConverter))]
|
||||||
public string Option { get; set; }
|
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;
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
@@ -438,6 +486,30 @@ public class MyCommand : ICommand
|
|||||||
[CommandParameter(0, Converter = typeof(MyConverter))]
|
[CommandParameter(0, Converter = typeof(MyConverter))]
|
||||||
public string Param { get; set; }
|
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;
|
||||||
}"
|
}"
|
||||||
)
|
)
|
||||||
@@ -566,6 +638,68 @@ public class MyCommand : ICommand
|
|||||||
[CommandOption('o', Converter = typeof(MyConverter))]
|
[CommandOption('o', Converter = typeof(MyConverter))]
|
||||||
public string Option { get; set; }
|
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
|
||||||
{
|
{
|
||||||
@@ -18,12 +19,16 @@ namespace CliFx.Analyzers
|
|||||||
DiagnosticDescriptors.CliFx0023,
|
DiagnosticDescriptors.CliFx0023,
|
||||||
DiagnosticDescriptors.CliFx0024,
|
DiagnosticDescriptors.CliFx0024,
|
||||||
DiagnosticDescriptors.CliFx0025,
|
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.CliFx0046,
|
||||||
|
DiagnosticDescriptors.CliFx0047,
|
||||||
|
DiagnosticDescriptors.CliFx0048,
|
||||||
|
DiagnosticDescriptors.CliFx0049
|
||||||
);
|
);
|
||||||
|
|
||||||
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
|
private static bool IsScalarType(ITypeSymbol typeSymbol) =>
|
||||||
@@ -57,14 +62,24 @@ namespace CliFx.Analyzers
|
|||||||
.NamedArguments
|
.NamedArguments
|
||||||
.Where(a => a.Key == "Converter")
|
.Where(a => a.Key == "Converter")
|
||||||
.Select(a => a.Value.Value)
|
.Select(a => a.Value.Value)
|
||||||
.FirstOrDefault() as ITypeSymbol;
|
.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
|
Converter = converter,
|
||||||
|
Validators = validators
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -140,6 +155,18 @@ namespace CliFx.Analyzers
|
|||||||
DiagnosticDescriptors.CliFx0025, parameter.Property.Locations.First()
|
DiagnosticDescriptors.CliFx0025, parameter.Property.Locations.First()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalid validators
|
||||||
|
var invalidValidatorsParameters = parameters
|
||||||
|
.Where(p => !p.Validators.All(v => v.AllInterfaces.Any(KnownSymbols.IsArgumentValueValidatorInterface)))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (var parameter in invalidValidatorsParameters)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(Diagnostic.Create(
|
||||||
|
DiagnosticDescriptors.CliFx0026, parameter.Property.Locations.First()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CheckCommandOptionProperties(
|
private static void CheckCommandOptionProperties(
|
||||||
@@ -175,7 +202,16 @@ namespace CliFx.Analyzers
|
|||||||
.NamedArguments
|
.NamedArguments
|
||||||
.Where(a => a.Key == "Converter")
|
.Where(a => a.Key == "Converter")
|
||||||
.Select(a => a.Value.Value)
|
.Select(a => a.Value.Value)
|
||||||
.FirstOrDefault() as ITypeSymbol;
|
.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
|
||||||
{
|
{
|
||||||
@@ -183,7 +219,8 @@ namespace CliFx.Analyzers
|
|||||||
Name = name,
|
Name = name,
|
||||||
ShortName = shortName,
|
ShortName = shortName,
|
||||||
EnvironmentVariableName = envVarName,
|
EnvironmentVariableName = envVarName,
|
||||||
Converter = converter
|
Converter = converter,
|
||||||
|
Validators = validators
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
@@ -270,6 +307,42 @@ namespace CliFx.Analyzers
|
|||||||
DiagnosticDescriptors.CliFx0046, option.Property.Locations.First()
|
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)
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ namespace CliFx.Analyzers
|
|||||||
"Usage", DiagnosticSeverity.Error, true
|
"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",
|
||||||
@@ -95,6 +102,27 @@ namespace CliFx.Analyzers
|
|||||||
"Usage", DiagnosticSeverity.Error, true
|
"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",
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ namespace CliFx.Analyzers
|
|||||||
public static bool IsArgumentValueConverterInterface(ISymbol symbol) =>
|
public static bool IsArgumentValueConverterInterface(ISymbol symbol) =>
|
||||||
symbol.DisplayNameMatches("CliFx.IArgumentValueConverter");
|
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,7 +3,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net5.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<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.8" />
|
<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>
|
||||||
|
|||||||
@@ -252,6 +252,26 @@ namespace CliFx.Tests
|
|||||||
_output.WriteLine(stdErr.GetString());
|
_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()
|
||||||
{
|
{
|
||||||
@@ -411,5 +431,65 @@ namespace CliFx.Tests
|
|||||||
|
|
||||||
_output.WriteLine(stdErr.GetString());
|
_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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,9 +14,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliWrap" Version="3.2.0" />
|
<PackageReference Include="CliWrap" Version="3.2.2" />
|
||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.1" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.1.2" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
<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" />
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,9 +165,9 @@ namespace CliFx.Tests.Commands
|
|||||||
public CustomConvertible(int value) => Value = value;
|
public CustomConvertible(int value) => Value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CustomConvertibleConverter : IArgumentValueConverter
|
public class CustomConvertibleConverter : ArgumentValueConverter<CustomConvertible>
|
||||||
{
|
{
|
||||||
public object ConvertFrom(string value) =>
|
public override CustomConvertible ConvertFrom(string value) =>
|
||||||
new CustomConvertible(int.Parse(value, CultureInfo.InvariantCulture));
|
new CustomConvertible(int.Parse(value, CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.5</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>
|
||||||
|
|||||||
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,22 +27,11 @@ 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>
|
||||||
public string? EnvironmentVariableName { get; set; }
|
public string? EnvironmentVariableName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Type of converter to use when mapping the argument value.
|
|
||||||
/// Converter must implement <see cref="IArgumentValueConverter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public Type? Converter { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
|
/// Initializes an instance of <see cref="CommandOptionAttribute"/>.
|
||||||
/// </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,17 +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>
|
|
||||||
/// Type of converter to use when mapping the argument value.
|
|
||||||
/// Converter must implement <see cref="IArgumentValueConverter"/>.
|
|
||||||
/// </summary>
|
|
||||||
public Type? Converter { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
|
/// Initializes an instance of <see cref="CommandParameterAttribute"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="all" />
|
||||||
<PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
|
<PackageReference Include="Nullable" Version="1.3.0" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -19,11 +19,18 @@ namespace CliFx.Domain
|
|||||||
|
|
||||||
public Type? ConverterType { get; }
|
public Type? ConverterType { get; }
|
||||||
|
|
||||||
protected CommandArgumentSchema(PropertyInfo? property, string? description, Type? converterType)
|
public Type[] ValidatorTypes { get; }
|
||||||
|
|
||||||
|
protected CommandArgumentSchema(
|
||||||
|
PropertyInfo? property,
|
||||||
|
string? description,
|
||||||
|
Type? converterType,
|
||||||
|
Type[] validatorTypes)
|
||||||
{
|
{
|
||||||
Property = property;
|
Property = property;
|
||||||
Description = description;
|
Description = description;
|
||||||
ConverterType = converterType;
|
ConverterType = converterType;
|
||||||
|
ValidatorTypes = validatorTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type? TryGetEnumerableArgumentUnderlyingType() =>
|
private Type? TryGetEnumerableArgumentUnderlyingType() =>
|
||||||
@@ -132,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);
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ namespace CliFx.Domain
|
|||||||
string? environmentVariableName,
|
string? environmentVariableName,
|
||||||
bool isRequired,
|
bool isRequired,
|
||||||
string? description,
|
string? description,
|
||||||
Type? converterType)
|
Type? converterType,
|
||||||
: base(property, description, converterType)
|
Type[] validatorTypes)
|
||||||
|
: base(property, description, converterType, validatorTypes)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
ShortName = shortName;
|
ShortName = shortName;
|
||||||
@@ -99,17 +100,34 @@ namespace CliFx.Domain
|
|||||||
attribute.EnvironmentVariableName,
|
attribute.EnvironmentVariableName,
|
||||||
attribute.IsRequired,
|
attribute.IsRequired,
|
||||||
attribute.Description,
|
attribute.Description,
|
||||||
attribute.Converter
|
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);
|
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);
|
null,
|
||||||
|
"version",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"Shows version information.",
|
||||||
|
null,
|
||||||
|
Array.Empty<Type>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,8 +17,9 @@ namespace CliFx.Domain
|
|||||||
int order,
|
int order,
|
||||||
string name,
|
string name,
|
||||||
string? description,
|
string? description,
|
||||||
Type? converterType)
|
Type? converterType,
|
||||||
: base(property, description, converterType)
|
Type[] validatorTypes)
|
||||||
|
: base(property, description, converterType, validatorTypes)
|
||||||
{
|
{
|
||||||
Order = order;
|
Order = order;
|
||||||
Name = name;
|
Name = name;
|
||||||
@@ -57,7 +58,8 @@ namespace CliFx.Domain
|
|||||||
attribute.Order,
|
attribute.Order,
|
||||||
name,
|
name,
|
||||||
attribute.Description,
|
attribute.Description,
|
||||||
attribute.Converter
|
attribute.Converter,
|
||||||
|
attribute.Validators
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,18 @@ namespace CliFx.Domain
|
|||||||
invalidConverterParameters
|
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)
|
||||||
@@ -208,6 +220,42 @@ namespace CliFx.Domain
|
|||||||
invalidConverterOptions
|
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}'.
|
||||||
@@ -180,7 +181,20 @@ If it's not feasible to fit into these constraints, consider using options inste
|
|||||||
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameter(s) with invalid converters:
|
Command '{command.Type.FullName}' is invalid because it contains {invalidParameters.Count} parameter(s) with invalid converters:
|
||||||
{invalidParameters.JoinToString(Environment.NewLine)}
|
{invalidParameters.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
Specified converter must implement {typeof(IArgumentValueConverter).FullName}.";
|
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());
|
return new CliFxException(message.Trim());
|
||||||
}
|
}
|
||||||
@@ -265,7 +279,46 @@ Environment variable names are not case-sensitive.";
|
|||||||
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} option(s) with invalid converters:
|
Command '{command.Type.FullName}' is invalid because it contains {invalidOptions.Count} option(s) with invalid converters:
|
||||||
{invalidOptions.JoinToString(Environment.NewLine)}
|
{invalidOptions.JoinToString(Environment.NewLine)}
|
||||||
|
|
||||||
Specified converter must implement {typeof(IArgumentValueConverter).FullName}.";
|
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());
|
return new CliFxException(message.Trim());
|
||||||
}
|
}
|
||||||
@@ -398,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:
|
||||||
@@ -415,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,13 +0,0 @@
|
|||||||
namespace CliFx
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Implements custom conversion logic that maps an argument value to a domain type.
|
|
||||||
/// </summary>
|
|
||||||
public interface IArgumentValueConverter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converts an input value to object of required type.
|
|
||||||
/// </summary>
|
|
||||||
public object ConvertFrom(string value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
[](https://nuget.org/packages/CliFx)
|
[](https://nuget.org/packages/CliFx)
|
||||||
[](https://tyrrrz.me/donate)
|
[](https://tyrrrz.me/donate)
|
||||||
|
|
||||||
|
**Project status: active**.
|
||||||
|
|
||||||
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.
|
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
|
||||||
@@ -262,9 +264,9 @@ When defining a parameter of an enumerable type, keep in mind that it has to be
|
|||||||
|
|
||||||
```c#
|
```c#
|
||||||
// Maps 2D vectors from AxB notation
|
// Maps 2D vectors from AxB notation
|
||||||
public class VectorConverter : IArgumentValueConverter
|
public class VectorConverter : ArgumentValueConverter<Vector2>
|
||||||
{
|
{
|
||||||
public object ConvertFrom(string value)
|
public override Vector2 ConvertFrom(string value)
|
||||||
{
|
{
|
||||||
var components = value.Split('x', 'X', ';');
|
var components = value.Split('x', 'X', ';');
|
||||||
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
|
var x = int.Parse(components[0], CultureInfo.InvariantCulture);
|
||||||
|
|||||||
Reference in New Issue
Block a user