mirror of
https://github.com/SabreTools/SabreTools.CommandLine.git
synced 2026-11-15 16:40:42 +00:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7831da365c | ||
|
|
c5ec963fd1 | ||
|
|
4efc3becf5 | ||
|
|
88c2c25a71 | ||
|
|
71ef11ac49 | ||
|
|
2753759b33 | ||
|
|
09f08a9031 | ||
|
|
8695e519c6 | ||
|
|
754aa6bc32 | ||
|
|
1da60f1091 | ||
|
|
5fd4b8b0a5 | ||
|
|
2bca1ff744 | ||
|
|
8be595bf56 | ||
|
|
47d067df1c | ||
|
|
90bfcc8fe2 | ||
|
|
be081c701d | ||
|
|
66b0e27d4e | ||
|
|
573fe2e160 | ||
|
|
6ad9fa4521 | ||
|
|
a261f428f2 | ||
|
|
5011331164 | ||
|
|
e257c30892 | ||
|
|
029cfe6dc6 | ||
|
|
f38b8734b5 | ||
|
|
df54b92031 | ||
|
|
39acb90dbc | ||
|
|
43083d35e1 | ||
|
|
fddbfa963d | ||
|
|
94ac68e5a0 | ||
|
|
0c3373e9f0 | ||
|
|
f122bf6b6d | ||
|
|
81b300bae6 | ||
|
|
a0576df3ee | ||
|
|
e26a222755 | ||
|
|
608cdae1d8 | ||
|
|
0bb996153a | ||
|
|
1ecce85a13 | ||
|
|
644527dbfa | ||
|
|
1bd158eba0 |
167
.editorconfig
Normal file
167
.editorconfig
Normal file
@@ -0,0 +1,167 @@
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
|
||||
# Indentation and spacing
|
||||
charset = utf-8
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# New line preferences
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
max_line_length = unset
|
||||
|
||||
# using directive preferences
|
||||
csharp_using_directive_placement = outside_namespace
|
||||
dotnet_diagnostic.IDE0005.severity = error
|
||||
|
||||
# Code-block preferences
|
||||
csharp_style_namespace_declarations = block_scoped
|
||||
csharp_style_prefer_method_group_conversion = true
|
||||
csharp_style_prefer_top_level_statements = false
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true
|
||||
csharp_style_inlined_variable_declaration = true
|
||||
csharp_style_unused_value_assignment_preference = discard_variable
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable
|
||||
dotnet_diagnostic.IDE0001.severity = warning
|
||||
dotnet_diagnostic.IDE0002.severity = warning
|
||||
dotnet_diagnostic.IDE0004.severity = warning
|
||||
dotnet_diagnostic.IDE0010.severity = error
|
||||
dotnet_diagnostic.IDE0051.severity = warning
|
||||
dotnet_diagnostic.IDE0052.severity = warning
|
||||
dotnet_diagnostic.IDE0072.severity = warning
|
||||
dotnet_diagnostic.IDE0080.severity = warning
|
||||
dotnet_diagnostic.IDE0100.severity = error
|
||||
dotnet_diagnostic.IDE0110.severity = error
|
||||
dotnet_diagnostic.IDE0120.severity = warning
|
||||
dotnet_diagnostic.IDE0121.severity = warning
|
||||
dotnet_diagnostic.IDE0240.severity = error
|
||||
dotnet_diagnostic.IDE0241.severity = error
|
||||
dotnet_style_coalesce_expression = true
|
||||
dotnet_style_namespace_match_folder = false
|
||||
dotnet_style_null_propagation = true
|
||||
dotnet_style_prefer_auto_properties = true
|
||||
dotnet_style_prefer_collection_expression = when_types_loosely_match
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
|
||||
dotnet_style_prefer_compound_assignment = true
|
||||
csharp_style_prefer_simple_property_accessors = true
|
||||
dotnet_style_prefer_simplified_interpolation = true
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true
|
||||
csharp_style_prefer_unbound_generic_type_in_nameof = true
|
||||
|
||||
# Field preferences
|
||||
dotnet_diagnostic.IDE0044.severity = warning
|
||||
dotnet_style_readonly_field = true
|
||||
|
||||
# Language keyword vs. framework types preferences
|
||||
dotnet_diagnostic.IDE0049.severity = error
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true
|
||||
dotnet_style_predefined_type_for_member_access = true
|
||||
|
||||
# Modifier preferences
|
||||
csharp_prefer_static_local_function = true
|
||||
csharp_style_prefer_readonly_struct = true
|
||||
dotnet_diagnostic.IDE0036.severity = warning
|
||||
dotnet_diagnostic.IDE0040.severity = error
|
||||
dotnet_diagnostic.IDE0380.severity = error
|
||||
dotnet_style_require_accessibility_modifiers = always
|
||||
|
||||
# New-line preferences
|
||||
dotnet_diagnostic.IDE2000.severity = warning
|
||||
dotnet_diagnostic.IDE2002.severity = warning
|
||||
dotnet_diagnostic.IDE2003.severity = warning
|
||||
dotnet_diagnostic.IDE2004.severity = warning
|
||||
dotnet_diagnostic.IDE2005.severity = warning
|
||||
dotnet_diagnostic.IDE2006.severity = warning
|
||||
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false
|
||||
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false
|
||||
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false
|
||||
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false
|
||||
dotnet_style_allow_multiple_blank_lines_experimental = false
|
||||
dotnet_style_allow_statement_immediately_after_block_experimental = false
|
||||
|
||||
# Null-checking preferences
|
||||
csharp_style_conditional_delegate_call = true
|
||||
|
||||
# Parameter preferences
|
||||
dotnet_code_quality_unused_parameters = all
|
||||
dotnet_diagnostic.IDE0280.severity = error
|
||||
|
||||
# Parentheses preferences
|
||||
dotnet_diagnostic.IDE0047.severity = warning
|
||||
dotnet_diagnostic.IDE0048.severity = warning
|
||||
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_other_operators = always_for_clarity
|
||||
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
|
||||
|
||||
# Pattern-matching preferences
|
||||
dotnet_diagnostic.IDE0019.severity = warning
|
||||
dotnet_diagnostic.IDE0020.severity = warning
|
||||
dotnet_diagnostic.IDE0038.severity = warning
|
||||
dotnet_diagnostic.IDE0066.severity = none
|
||||
dotnet_diagnostic.IDE0083.severity = warning
|
||||
dotnet_diagnostic.IDE0260.severity = warning
|
||||
csharp_style_pattern_matching_over_as_with_null_check = true
|
||||
csharp_style_pattern_matching_over_is_with_cast_check = true
|
||||
csharp_style_prefer_not_pattern = true
|
||||
csharp_style_prefer_pattern_matching = true
|
||||
|
||||
# this. and Me. preferences
|
||||
dotnet_style_qualification_for_event = false
|
||||
dotnet_style_qualification_for_field = false
|
||||
dotnet_style_qualification_for_method = false
|
||||
dotnet_style_qualification_for_property = false
|
||||
|
||||
# var preferences
|
||||
csharp_style_var_for_built_in_types = false
|
||||
csharp_style_var_when_type_is_apparent = true
|
||||
|
||||
# .NET formatting options
|
||||
dotnet_separate_import_directive_groups = false
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# C# formatting options
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = false
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_new_line_before_catch = true
|
||||
csharp_new_line_before_else = true
|
||||
csharp_new_line_before_finally = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_before_members_in_object_initializers = true
|
||||
csharp_new_line_before_open_brace = all
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
csharp_preserve_single_line_blocks = true
|
||||
csharp_preserve_single_line_statements = true
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = false
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
75
.github/workflows/build_and_test.yml
vendored
75
.github/workflows/build_and_test.yml
vendored
@@ -1,40 +1,49 @@
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
|
||||
- name: Run publish script
|
||||
run: ./publish-nix.sh
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Upload to rolling
|
||||
uses: ncipollo/release-action@v1.14.0
|
||||
with:
|
||||
allowUpdates: True
|
||||
artifacts: "*.nupkg,*.snupkg"
|
||||
body: 'Last built commit: ${{ github.sha }}'
|
||||
name: 'Rolling Release'
|
||||
prerelease: True
|
||||
replacesArtifacts: True
|
||||
tag: "rolling"
|
||||
updateOnlyUnreleased: True
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: |
|
||||
8.0.x
|
||||
9.0.x
|
||||
10.0.x
|
||||
11.0.x
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
|
||||
- name: Run publish script
|
||||
run: ./publish-nix.sh
|
||||
|
||||
- name: Update rolling tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git tag -f rolling
|
||||
git push origin :refs/tags/rolling || true
|
||||
git push origin rolling --force
|
||||
|
||||
- name: Upload to rolling
|
||||
uses: ncipollo/release-action@v1.20.0
|
||||
with:
|
||||
allowUpdates: True
|
||||
artifacts: "*.nupkg,*.snupkg"
|
||||
body: "Last built commit: ${{ github.sha }}"
|
||||
name: "Rolling Release"
|
||||
prerelease: True
|
||||
replacesArtifacts: True
|
||||
tag: "rolling"
|
||||
updateOnlyUnreleased: True
|
||||
|
||||
35
.github/workflows/check_pr.yml
vendored
35
.github/workflows/check_pr.yml
vendored
@@ -3,21 +3,22 @@ name: Build PR
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: |
|
||||
6.0.x
|
||||
8.0.x
|
||||
9.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: |
|
||||
8.0.x
|
||||
9.0.x
|
||||
10.0.x
|
||||
11.0.x
|
||||
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test
|
||||
|
||||
329
.gitignore
vendored
329
.gitignore
vendored
@@ -1,22 +1,18 @@
|
||||
*.swp
|
||||
*.*~
|
||||
project.lock.json
|
||||
.DS_Store
|
||||
*.pyc
|
||||
nupkg/
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
|
||||
# Rider
|
||||
.idea
|
||||
## 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
|
||||
*.lscache
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
@@ -24,15 +20,312 @@ nupkg/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
msbuild.log
|
||||
msbuild.err
|
||||
msbuild.wrn
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# 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/
|
||||
**/Properties/launchSettings.json
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
|
||||
# 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
|
||||
.cr/
|
||||
|
||||
# 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/
|
||||
|
||||
24
.vscode/tasks.json
vendored
Executable file
24
.vscode/tasks.json
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"command": "dotnet",
|
||||
"type": "shell",
|
||||
"args": [
|
||||
"build",
|
||||
// Ask dotnet build to generate full paths for file names.
|
||||
"/property:GenerateFullPaths=true",
|
||||
// Do not generate summary otherwise it leads to duplicate errors in Problems panel
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2016-2025 Matt Nadareski
|
||||
Copyright (c) 2016-2026 Matt Nadareski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ Three reference feature implementations are included in the `SabreTools.CommandL
|
||||
| --- | --- |
|
||||
| `Help` | Outputs either a generic help text or one specific to a feature name included as the second argument |
|
||||
| `HelpExtended` | Outputs either a generic help text with extended descriptions or one specific to a feature name included as the second argument |
|
||||
| `Manpage` | Outputs a roff-formatted manpage based on the current command set |
|
||||
| `Version` | Outputs the version number of the program, derived from the informational version |
|
||||
|
||||
## Releases
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using SabreTools.CommandLine.Inputs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.CommandLine.Inputs;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.CommandLine.Test
|
||||
@@ -11,9 +14,16 @@ namespace SabreTools.CommandLine.Test
|
||||
var input1 = new FlagInput("input1", "--input1", "input1");
|
||||
var input2 = new FlagInput("input2", "--input2", "input2");
|
||||
|
||||
var feature1 = new MockFeature("feature1", "feature1", "feature1");
|
||||
var inputA = new FlagInput("inputA", "--inputA", "inputA");
|
||||
var inputB = new FlagInput("inputB", "--inputB", "inputB");
|
||||
feature1.Add(inputA);
|
||||
feature1.Add(inputB);
|
||||
|
||||
var featureSet = new CommandSet();
|
||||
featureSet.Add(input1);
|
||||
featureSet.Add(input2);
|
||||
featureSet.AddFrom(feature1);
|
||||
|
||||
var actualInput1 = featureSet["input1"];
|
||||
Assert.NotNull(actualInput1);
|
||||
@@ -25,6 +35,14 @@ namespace SabreTools.CommandLine.Test
|
||||
|
||||
var actualInput3 = featureSet["input3"];
|
||||
Assert.Null(actualInput3);
|
||||
|
||||
var actualInputA = featureSet["inputA"];
|
||||
Assert.NotNull(actualInputA);
|
||||
Assert.Equal("inputA", actualInputA.Name);
|
||||
|
||||
var actualinputB = featureSet["inputB"];
|
||||
Assert.NotNull(actualinputB);
|
||||
Assert.Equal("inputB", actualinputB.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -90,5 +108,926 @@ namespace SabreTools.CommandLine.Test
|
||||
bool actualTop3 = featureSet.IsTopLevel("input3");
|
||||
Assert.False(actualTop3);
|
||||
}
|
||||
|
||||
#region GetBoolean
|
||||
|
||||
[Fact]
|
||||
public void GetBoolean_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new BooleanInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
bool actual = commandSet.GetBoolean("c");
|
||||
Assert.False(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBoolean_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetBoolean("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBoolean_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new BooleanInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "true"], ref index);
|
||||
|
||||
bool actual = commandSet.GetBoolean("b");
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBoolean_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new BooleanInput("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "true"], ref index);
|
||||
|
||||
bool actual = commandSet.GetBoolean("c");
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetFeature
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_InvalidKey_Null()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockFeature("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Feature? actual = commandSet.GetFeature("c");
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetFeature("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockFeature("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b"], ref index);
|
||||
|
||||
Feature? actual = commandSet.GetFeature("b");
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("b", actual.Name);
|
||||
Assert.True(actual.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new MockFeature("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c"], ref index);
|
||||
|
||||
Feature? actual = commandSet.GetFeature("c");
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("c", actual.Name);
|
||||
Assert.True(actual.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetInt8
|
||||
|
||||
[Fact]
|
||||
public void GetInt8_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int8Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
sbyte actual = commandSet.GetInt8("c");
|
||||
Assert.Equal(sbyte.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt8_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetInt8("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt8_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int8Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
sbyte actual = commandSet.GetInt8("b");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt8_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new Int8Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
sbyte actual = commandSet.GetInt8("c");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetInt16
|
||||
|
||||
[Fact]
|
||||
public void GetInt16_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int16Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
short actual = commandSet.GetInt16("c");
|
||||
Assert.Equal(short.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt16_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetInt16("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt16_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int16Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
short actual = commandSet.GetInt16("b");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt16_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new Int16Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
short actual = commandSet.GetInt16("c");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetInt32
|
||||
|
||||
[Fact]
|
||||
public void GetInt32_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int32Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int actual = commandSet.GetInt32("c");
|
||||
Assert.Equal(int.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt32_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetInt32("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt32_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int32Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
int actual = commandSet.GetInt32("b");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt32_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new Int32Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
int actual = commandSet.GetInt32("c");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetInt64
|
||||
|
||||
[Fact]
|
||||
public void GetInt64_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int64Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
long actual = commandSet.GetInt64("c");
|
||||
Assert.Equal(long.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt64_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetInt64("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt64_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new Int64Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
long actual = commandSet.GetInt64("b");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetInt64_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new Int64Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
long actual = commandSet.GetInt64("c");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetString
|
||||
|
||||
[Fact]
|
||||
public void GetString_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new StringInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
string? actual = commandSet.GetString("c");
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetString_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetString("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetString_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new StringInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "value"], ref index);
|
||||
|
||||
string? actual = commandSet.GetString("b");
|
||||
Assert.Equal("value", actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetString_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new StringInput("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "value"], ref index);
|
||||
|
||||
string? actual = commandSet.GetString("c");
|
||||
Assert.Equal("value", actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetStringList
|
||||
|
||||
[Fact]
|
||||
public void GetStringList_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new StringListInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
List<string> actual = commandSet.GetStringList("c");
|
||||
Assert.Empty(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStringList_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetStringList("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStringList_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new StringListInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "value"], ref index);
|
||||
|
||||
List<string> actual = commandSet.GetStringList("b");
|
||||
string value = Assert.Single(actual);
|
||||
Assert.Equal("value", value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetStringList_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new StringListInput("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "value"], ref index);
|
||||
|
||||
List<string> actual = commandSet.GetStringList("c");
|
||||
string value = Assert.Single(actual);
|
||||
Assert.Equal("value", value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetUInt8
|
||||
|
||||
[Fact]
|
||||
public void GetUInt8_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt8Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
byte actual = commandSet.GetUInt8("c");
|
||||
Assert.Equal(byte.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt8_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetUInt8("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt8_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt8Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
byte actual = commandSet.GetUInt8("b");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt8_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new UInt8Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
byte actual = commandSet.GetUInt8("c");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetUInt16
|
||||
|
||||
[Fact]
|
||||
public void GetUInt16_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt16Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
ushort actual = commandSet.GetUInt16("c");
|
||||
Assert.Equal(ushort.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt16_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetUInt16("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt16_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt16Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
ushort actual = commandSet.GetUInt16("b");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt16_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new UInt16Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
ushort actual = commandSet.GetUInt16("c");
|
||||
Assert.Equal(5, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetUInt32
|
||||
|
||||
[Fact]
|
||||
public void GetUInt32_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt32Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
uint actual = commandSet.GetUInt32("c");
|
||||
Assert.Equal(uint.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt32_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetUInt32("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt32_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt32Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
uint actual = commandSet.GetUInt32("b");
|
||||
Assert.Equal(5u, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt32_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new UInt32Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
uint actual = commandSet.GetUInt32("c");
|
||||
Assert.Equal(5u, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetUInt64
|
||||
|
||||
[Fact]
|
||||
public void GetUInt64_InvalidKey_DefaultValue()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt64Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
ulong actual = commandSet.GetUInt64("c");
|
||||
Assert.Equal(ulong.MinValue, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt64_Exists_WrongType_Throws()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = commandSet.GetUInt64("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt64_Exists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new UInt64Input("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b", "5"], ref index);
|
||||
|
||||
ulong actual = commandSet.GetUInt64("b");
|
||||
Assert.Equal(5u, actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetUInt64_NestedExists_Returns()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
commandSet.Add(child);
|
||||
var subChild = new UInt64Input("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c", "5"], ref index);
|
||||
|
||||
ulong actual = commandSet.GetUInt64("c");
|
||||
Assert.Equal(5u, actual);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ProcessArgs
|
||||
|
||||
[Fact]
|
||||
public void ProcessArgs_EmptyArgs_Success()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
|
||||
string[] args = [];
|
||||
|
||||
bool actual = commandSet.ProcessArgs(args);
|
||||
Assert.True(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessArgs_ValidArgs_Success()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
Feature feature = new MockFeature("a", "a", "a");
|
||||
feature.Add(new FlagInput("b", "b", "b"));
|
||||
feature.Add(new FlagInput("c", "c", "c"));
|
||||
commandSet.Add(feature);
|
||||
|
||||
string[] args = ["a", "b", "c"];
|
||||
|
||||
bool actual = commandSet.ProcessArgs(args);
|
||||
Assert.True(actual);
|
||||
Assert.Empty(feature.Inputs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessArgs_InvalidArg_AddedAsGeneric()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
Feature feature = new MockFeature("a", "a", "a");
|
||||
feature.Add(new FlagInput("b", "b", "b"));
|
||||
feature.Add(new FlagInput("d", "d", "d"));
|
||||
commandSet.Add(feature);
|
||||
|
||||
string[] args = ["a", "b", "c"];
|
||||
|
||||
bool actual = commandSet.ProcessArgs(args);
|
||||
Assert.True(actual);
|
||||
string input = Assert.Single(feature.Inputs);
|
||||
Assert.Equal("c", input);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessArgs_NestedArgs_Success()
|
||||
{
|
||||
var commandSet = new CommandSet();
|
||||
Feature feature = new MockFeature("a", "a", "a");
|
||||
var sub = new FlagInput("b", "b", "b");
|
||||
sub.Add(new FlagInput("c", "c", "c"));
|
||||
feature.Add(sub);
|
||||
commandSet.Add(feature);
|
||||
|
||||
string[] args = ["a", "b", "c"];
|
||||
|
||||
bool actual = commandSet.ProcessArgs(args);
|
||||
Assert.True(actual);
|
||||
Assert.Empty(feature.Inputs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Manpage Output
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_Structure_ContainsRequiredSections()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample")
|
||||
{
|
||||
Version = "sample 1.0",
|
||||
Description = "do sample things",
|
||||
};
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".TH \"SAMPLE\" \"1\"", page);
|
||||
Assert.Contains(".SH NAME", page);
|
||||
Assert.Contains("sample \\- do sample things", page);
|
||||
Assert.Contains(".SH SYNOPSIS", page);
|
||||
Assert.Contains(".SH DESCRIPTION", page);
|
||||
Assert.Contains(".SH OPTIONS", page);
|
||||
Assert.Contains(".TP", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_EscapesHyphensInFlags()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".B \\-\\-convert", page);
|
||||
Assert.DoesNotContain(".B --convert", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_Verbose_TogglesDetailedText()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string terse = set.OutputManpage(info, includeVerbose: false);
|
||||
string verbose = set.OutputManpage(info, includeVerbose: true);
|
||||
|
||||
Assert.DoesNotContain("Built\\-in to most of the programs", terse);
|
||||
Assert.Contains("Built\\-in to most of the programs", verbose);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_NestsChildren()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".RS", page);
|
||||
Assert.Contains(".RE", page);
|
||||
Assert.Contains(".B \\-\\-output", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_HonorsCustomOptionsHeading()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { OptionsHeading = "COMMANDS" };
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
Assert.Contains(".SH COMMANDS", page);
|
||||
Assert.DoesNotContain(".SH OPTIONS", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_LeavesBlankDateForStamping()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { Version = "sample 1.0" };
|
||||
|
||||
string page = set.OutputManpage(info);
|
||||
|
||||
// Section, then an empty date field, then the version
|
||||
Assert.Contains(".TH \"SAMPLE\" \"1\" \"\" \"sample 1.0\"", page);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_LinesWithinWidth()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { Description = "do sample things" };
|
||||
|
||||
string page = set.OutputManpage(info, includeVerbose: true);
|
||||
|
||||
foreach (string line in page.Split('\n'))
|
||||
{
|
||||
Assert.True(line.Length <= 78, $"Line exceeds 78 columns: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputManpage_FileOverload_WritesSameContent()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample");
|
||||
|
||||
string expected = set.OutputManpage(info);
|
||||
string path = Path.Combine(Path.GetTempPath(), $"sabretools-manpage-{Guid.NewGuid():N}.1");
|
||||
try
|
||||
{
|
||||
set.OutputManpage(path, info);
|
||||
string actual = File.ReadAllText(path);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Build a representative command set with nested inputs and detail text
|
||||
/// </summary>
|
||||
private static CommandSet BuildSampleSet()
|
||||
{
|
||||
var set = new CommandSet("Sample header line");
|
||||
|
||||
var help = new MockFeature("Help",
|
||||
["?", "h", "help"],
|
||||
"Show this help",
|
||||
"Built-in to most of the programs is a basic help text.");
|
||||
set.Add(help);
|
||||
|
||||
var convert = new MockFeature("Convert",
|
||||
"--convert",
|
||||
"Convert input files",
|
||||
"Converts the provided input files into the desired output format.");
|
||||
convert.Add(new StringInput("output", "--output", "Set the output path"));
|
||||
convert.Add(new FlagInput("force", "--force", "Overwrite existing files"));
|
||||
set.Add(convert);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock Feature implementation for testing
|
||||
/// </summary>
|
||||
private class MockFeature : Feature
|
||||
{
|
||||
public MockFeature(string name, string flag, string description, string? detailed = null)
|
||||
: base(name, flag, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
public MockFeature(string name, string[] flags, string description, string? detailed = null)
|
||||
: base(name, flags, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Execute() => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool VerifyInputs() => true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock UserInput implementation for testing
|
||||
/// </summary>
|
||||
private class MockUserInput : UserInput<object?>
|
||||
{
|
||||
public MockUserInput(string name, string flag, string description, string? detailed = null)
|
||||
: base(name, flag, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
public MockUserInput(string name, string[] flags, string description, string? detailed = null)
|
||||
: base(name, flags, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ProcessInput(string[] args, ref int index) => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override string FormatFlags() => string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
83
SabreTools.CommandLine.Test/Features/ManpageTests.cs
Normal file
83
SabreTools.CommandLine.Test/Features/ManpageTests.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SabreTools.CommandLine.Features;
|
||||
using SabreTools.CommandLine.Inputs;
|
||||
using Xunit;
|
||||
|
||||
namespace SabreTools.CommandLine.Test.Features
|
||||
{
|
||||
public class ManpageTests
|
||||
{
|
||||
[Fact]
|
||||
public void ManpageFeature_WritesPageToStandardOutput()
|
||||
{
|
||||
var set = BuildSampleSet();
|
||||
var info = new ManpageInfo("sample") { Version = "sample 1.0" };
|
||||
set.Add(new Manpage(info));
|
||||
|
||||
var original = Console.Out;
|
||||
var writer = new StringWriter();
|
||||
try
|
||||
{
|
||||
Console.SetOut(writer);
|
||||
bool result = set.ProcessArgs(["man"]);
|
||||
Assert.True(result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(original);
|
||||
}
|
||||
|
||||
string actual = writer.ToString();
|
||||
Assert.Contains(".TH \"SAMPLE\" \"1\"", actual);
|
||||
Assert.Contains(".SH NAME", actual);
|
||||
Assert.Contains(".B \\-\\-convert", actual);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a representative command set with nested inputs and detail text
|
||||
/// </summary>
|
||||
private static CommandSet BuildSampleSet()
|
||||
{
|
||||
var set = new CommandSet("Sample header line");
|
||||
|
||||
var help = new MockFeature("Help",
|
||||
["?", "h", "help"],
|
||||
"Show this help",
|
||||
"Built-in to most of the programs is a basic help text.");
|
||||
set.Add(help);
|
||||
|
||||
var convert = new MockFeature("Convert",
|
||||
"--convert",
|
||||
"Convert input files",
|
||||
"Converts the provided input files into the desired output format.");
|
||||
convert.Add(new StringInput("output", "--output", "Set the output path"));
|
||||
convert.Add(new FlagInput("force", "--force", "Overwrite existing files"));
|
||||
set.Add(convert);
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock Feature implementation for testing
|
||||
/// </summary>
|
||||
private class MockFeature : Feature
|
||||
{
|
||||
public MockFeature(string name, string flag, string description, string? detailed = null)
|
||||
: base(name, flag, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
public MockFeature(string name, string[] flags, string description, string? detailed = null)
|
||||
: base(name, flags, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Execute() => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool VerifyInputs() => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +114,65 @@ namespace SabreTools.CommandLine.Test.Inputs
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetFeature
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_InvalidKey_Null()
|
||||
{
|
||||
UserInput userInput = new MockUserInput("a", "a", "a");
|
||||
var child = new MockFeature("b", "b", "b");
|
||||
userInput.Add(child);
|
||||
|
||||
Feature? actual = userInput.GetFeature("c");
|
||||
Assert.Null(actual);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_Exists_WrongType_Throws()
|
||||
{
|
||||
UserInput userInput = new MockUserInput("a", "a", "a");
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
userInput.Add(child);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => _ = userInput.GetFeature("b"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_Exists_Returns()
|
||||
{
|
||||
UserInput userInput = new MockUserInput("a", "a", "a");
|
||||
var child = new MockFeature("b", "b", "b");
|
||||
userInput.Add(child);
|
||||
|
||||
int index = 0;
|
||||
child.ProcessInput(["b"], ref index);
|
||||
|
||||
Feature? actual = userInput.GetFeature("b");
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("b", actual.Name);
|
||||
Assert.True(actual.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetFeature_NestedExists_Returns()
|
||||
{
|
||||
UserInput userInput = new MockUserInput("a", "a", "a");
|
||||
var child = new MockUserInput("b", "b", "b");
|
||||
userInput.Add(child);
|
||||
var subChild = new MockFeature("c", "c", "c");
|
||||
child.Add(subChild);
|
||||
|
||||
int index = 0;
|
||||
subChild.ProcessInput(["c"], ref index);
|
||||
|
||||
Feature? actual = userInput.GetFeature("c");
|
||||
Assert.NotNull(actual);
|
||||
Assert.Equal("c", actual.Name);
|
||||
Assert.True(actual.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetInt8
|
||||
|
||||
[Fact]
|
||||
@@ -666,6 +725,28 @@ namespace SabreTools.CommandLine.Test.Inputs
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Mock Feature implementation for testing
|
||||
/// </summary>
|
||||
private class MockFeature : Feature
|
||||
{
|
||||
public MockFeature(string name, string flag, string description, string? detailed = null)
|
||||
: base(name, flag, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
public MockFeature(string name, string[] flags, string description, string? detailed = null)
|
||||
: base(name, flags, description, detailed)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Execute() => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool VerifyInputs() => true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mock UserInput implementation for testing
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0;net11.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -12,13 +12,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<PackageReference Include="coverlet.collector" Version="10.1.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using SabreTools.CommandLine.Features;
|
||||
using SabreTools.CommandLine.Inputs;
|
||||
using SabreTools.CommandLine.Tools;
|
||||
|
||||
namespace SabreTools.CommandLine
|
||||
{
|
||||
@@ -39,6 +42,41 @@ namespace SabreTools.CommandLine
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Custom help text to print instead of the automatically
|
||||
/// generated generic help
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This only replaces the automatically generated help
|
||||
/// for <see cref="OutputGenericHelp"/>. It does not impact
|
||||
/// either <see cref="OutputAllHelp"/> or <see cref="OutputFeatureHelp"/>.
|
||||
/// </remarks>
|
||||
public string? CustomHelp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Feature that represents the default functionality
|
||||
/// for a command set
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is recommended to use this in applications that
|
||||
/// do not need multiple distinct functional modes.
|
||||
///
|
||||
/// When printing help text, the flags and description
|
||||
/// of this feature will be omitted, instead printing
|
||||
/// the children of the feature directly at the
|
||||
/// top level.
|
||||
///
|
||||
/// If the default feature is included as a normal
|
||||
/// top-level input for the command set, then the flags
|
||||
/// will be printed twice - once under the feature
|
||||
/// itself and once at the top level.
|
||||
/// </remarks>
|
||||
public Feature? DefaultFeature { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
@@ -46,6 +84,15 @@ namespace SabreTools.CommandLine
|
||||
/// </summary>
|
||||
public CommandSet() { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CommandSet with a printable header
|
||||
/// </summary>
|
||||
/// <param name="header">Custom commandline header to be printed when outputting help</param>
|
||||
public CommandSet(string header)
|
||||
{
|
||||
_header.Add(header);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CommandSet with a printable header
|
||||
/// </summary>
|
||||
@@ -55,6 +102,17 @@ namespace SabreTools.CommandLine
|
||||
_header.AddRange(header);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CommandSet with a printable header
|
||||
/// </summary>
|
||||
/// <param name="header">Custom commandline header to be printed when outputting help</param>
|
||||
/// <param name="footer">Custom commandline footer to be printed when outputting help</param>
|
||||
public CommandSet(string header, string footer)
|
||||
{
|
||||
_header.Add(header);
|
||||
_footer.Add(footer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CommandSet with a printable header
|
||||
/// </summary>
|
||||
@@ -99,6 +157,565 @@ namespace SabreTools.CommandLine
|
||||
public void Add(UserInput input)
|
||||
=> _inputs.Add(input.Name, input);
|
||||
|
||||
/// <summary>
|
||||
/// Add all children from an input to the set
|
||||
/// </summary>
|
||||
/// <param name="input">UserInput object to retrieve children from</param>
|
||||
/// <remarks>
|
||||
/// This should only be used in situations where an input is defined
|
||||
/// but not used within the context of the command set directly.
|
||||
///
|
||||
/// This is helpful for when there are applications with default functionality
|
||||
/// that need to be able to expose both defined features as well as
|
||||
/// the default functionality in help text.
|
||||
///
|
||||
/// If there is any overlap between existing names and the names from
|
||||
/// any of the children, this operation will overwrite them.
|
||||
/// </reamrks>
|
||||
public void AddFrom(UserInput input)
|
||||
{
|
||||
foreach (var kvp in input.Children)
|
||||
{
|
||||
_inputs.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Children
|
||||
|
||||
/// <summary>
|
||||
/// Get a boolean value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public bool GetBoolean(string key, bool defaultValue = false)
|
||||
{
|
||||
if (TryGetBoolean(key, out bool value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a Feature value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <returns>The value if found, null otherwise</returns>
|
||||
public Feature? GetFeature(string key)
|
||||
{
|
||||
if (TryGetFeature(key, out Feature? value))
|
||||
return value;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int8 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public sbyte GetInt8(string key, sbyte defaultValue = sbyte.MinValue)
|
||||
{
|
||||
if (TryGetInt8(key, out sbyte value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int16 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public short GetInt16(string key, short defaultValue = short.MinValue)
|
||||
{
|
||||
if (TryGetInt16(key, out short value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int32 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public int GetInt32(string key, int defaultValue = int.MinValue)
|
||||
{
|
||||
if (TryGetInt32(key, out int value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int64 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public long GetInt64(string key, long defaultValue = long.MinValue)
|
||||
{
|
||||
if (TryGetInt64(key, out long value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a string value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public string? GetString(string key, string? defaultValue = null)
|
||||
{
|
||||
if (TryGetString(key, out string? value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a string list value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public List<string> GetStringList(string key)
|
||||
{
|
||||
if (TryGetStringList(key, out var value))
|
||||
return value;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt8 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public byte GetUInt8(string key, byte defaultValue = byte.MinValue)
|
||||
{
|
||||
if (TryGetUInt8(key, out byte value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt16 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public ushort GetUInt16(string key, ushort defaultValue = ushort.MinValue)
|
||||
{
|
||||
if (TryGetUInt16(key, out ushort value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt32 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public uint GetUInt32(string key, uint defaultValue = uint.MinValue)
|
||||
{
|
||||
if (TryGetUInt32(key, out uint value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt64 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>The value if found, the default value otherwise</returns>
|
||||
public ulong GetUInt64(string key, ulong defaultValue = ulong.MinValue)
|
||||
{
|
||||
if (TryGetUInt64(key, out ulong value, defaultValue))
|
||||
return value;
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a boolean value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetBoolean(string key, out bool value, bool defaultValue = false)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is BooleanInput b)
|
||||
{
|
||||
value = b.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
else if (input is FlagInput f)
|
||||
{
|
||||
value = f.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Input is not a bool");
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetBoolean(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a Feature value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetFeature(string key, out Feature? value)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Feature i)
|
||||
throw new ArgumentException("Input is not a Feature");
|
||||
|
||||
value = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetFeature(key, out value))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int8 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetInt8(string key, out sbyte value, sbyte defaultValue = sbyte.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int8Input i)
|
||||
throw new ArgumentException("Input is not an sbyte");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetInt8(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int16 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetInt16(string key, out short value, short defaultValue = short.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int16Input i)
|
||||
throw new ArgumentException("Input is not a short");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetInt16(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int32 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetInt32(string key, out int value, int defaultValue = int.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int32Input i)
|
||||
throw new ArgumentException("Input is not an int");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetInt32(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int64 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetInt64(string key, out long value, long defaultValue = long.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int64Input l)
|
||||
throw new ArgumentException("Input is not a long");
|
||||
|
||||
value = l.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetInt64(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a string value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetString(string key, out string? value, string? defaultValue = null)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not StringInput s)
|
||||
throw new ArgumentException("Input is not a string");
|
||||
|
||||
value = s.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetString(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a string list value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetStringList(string key, out List<string> value)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not StringListInput l)
|
||||
throw new ArgumentException("Input is not a list");
|
||||
|
||||
value = l.Value ?? [];
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetStringList(key, out value))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt8 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetUInt8(string key, out byte value, byte defaultValue = byte.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt8Input i)
|
||||
throw new ArgumentException("Input is not an byte");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetUInt8(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt16 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetUInt16(string key, out ushort value, ushort defaultValue = ushort.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt16Input i)
|
||||
throw new ArgumentException("Input is not a ushort");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetUInt16(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt32 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetUInt32(string key, out uint value, uint defaultValue = uint.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt32Input i)
|
||||
throw new ArgumentException("Input is not an uint");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetUInt32(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a UInt64 value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <param name="defaultValue">Optional default value if not found</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetUInt64(string key, out ulong value, ulong defaultValue = ulong.MinValue)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (_inputs.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt64Input l)
|
||||
throw new ArgumentException("Input is not a ulong");
|
||||
|
||||
value = l.Value ?? defaultValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in _inputs.Values)
|
||||
{
|
||||
if (child.TryGetUInt64(key, out value, defaultValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = defaultValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Inputs
|
||||
@@ -163,7 +780,8 @@ namespace SabreTools.CommandLine
|
||||
/// <summary>
|
||||
/// Output top-level features only
|
||||
/// </summary>
|
||||
public void OutputGenericHelp()
|
||||
/// <param name="detailed">True if the detailed descriptions should be formatted and output, false otherwise</param>
|
||||
public void OutputGenericHelp(bool detailed = false)
|
||||
{
|
||||
// Start building the output list
|
||||
List<string> output = [];
|
||||
@@ -172,13 +790,34 @@ namespace SabreTools.CommandLine
|
||||
if (_header.Count > 0)
|
||||
output.AddRange(_header);
|
||||
|
||||
// Now append all available top-level flags
|
||||
output.Add("Available options:");
|
||||
foreach (var input in _inputs.Values)
|
||||
// If custom help text is defined
|
||||
if (CustomHelp is not null)
|
||||
{
|
||||
var outputs = input.Format(pre: 2, midpoint: 30);
|
||||
if (outputs != null)
|
||||
output.AddRange(outputs);
|
||||
string customHelp = CustomHelp.Replace("\r\n", "\n");
|
||||
string[] customLines = customHelp.Split('\n');
|
||||
output.AddRange(customLines);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Append all available top-level flags
|
||||
output.Add("Available options:");
|
||||
foreach (var input in _inputs.Values)
|
||||
{
|
||||
var outputs = input.Format(pre: 2, midpoint: 30, detailed);
|
||||
if (outputs is not null)
|
||||
output.AddRange(outputs);
|
||||
}
|
||||
|
||||
// If there is a default feature
|
||||
if (DefaultFeature is not null)
|
||||
{
|
||||
foreach (var input in DefaultFeature.Children)
|
||||
{
|
||||
var outputs = input.Value.Format(pre: 2, midpoint: 30, detailed);
|
||||
if (outputs is not null)
|
||||
output.AddRange(outputs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append the footer, if needed
|
||||
@@ -192,7 +831,8 @@ namespace SabreTools.CommandLine
|
||||
/// <summary>
|
||||
/// Output all features recursively
|
||||
/// </summary>
|
||||
public void OutputAllHelp()
|
||||
/// <param name="detailed">True if the detailed descriptions should be formatted and output, false otherwise</param>
|
||||
public void OutputAllHelp(bool detailed = false)
|
||||
{
|
||||
// Start building the output list
|
||||
List<string> output = [];
|
||||
@@ -201,15 +841,26 @@ namespace SabreTools.CommandLine
|
||||
if (_header.Count > 0)
|
||||
output.AddRange(_header);
|
||||
|
||||
// Now append all available flags recursively
|
||||
// Append all available flags recursively
|
||||
output.Add("Available options:");
|
||||
foreach (var input in _inputs.Values)
|
||||
{
|
||||
var outputs = input.FormatRecursive(pre: 2, midpoint: 30, detailed: true);
|
||||
if (outputs != null)
|
||||
var outputs = input.FormatRecursive(pre: 2, midpoint: 30, detailed);
|
||||
if (outputs is not null)
|
||||
output.AddRange(outputs);
|
||||
}
|
||||
|
||||
// If there is a default feature
|
||||
if (DefaultFeature is not null)
|
||||
{
|
||||
foreach (var input in DefaultFeature.Children)
|
||||
{
|
||||
var outputs = input.Value.Format(pre: 2, midpoint: 30, detailed);
|
||||
if (outputs is not null)
|
||||
output.AddRange(outputs);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the footer, if needed
|
||||
if (_footer.Count > 0)
|
||||
output.AddRange(_footer);
|
||||
@@ -313,5 +964,190 @@ namespace SabreTools.CommandLine
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Manpage Output
|
||||
|
||||
/// <summary>
|
||||
/// Generate a man page from the current set of inputs
|
||||
/// </summary>
|
||||
/// <param name="info">Document-level metadata for the man page</param>
|
||||
/// <param name="includeVerbose">True if detailed descriptions should be included, false otherwise</param>
|
||||
/// <returns>The complete man page as a roff-formatted string</returns>
|
||||
/// <remarks>
|
||||
/// The output uses only the portable man(7) macro set so that it
|
||||
/// renders without warnings under both groff and mandoc. The page
|
||||
/// content is derived from the same model used to format help text,
|
||||
/// so it stays in sync with the output of the help features.
|
||||
/// </remarks>
|
||||
public string OutputManpage(ManpageInfo info, bool includeVerbose = false)
|
||||
{
|
||||
// Start building the output list
|
||||
List<string> output = [];
|
||||
|
||||
// Leading comment and title line
|
||||
output.Add(".\\\" Generated from the command-line model. Do not edit by hand.");
|
||||
output.Add(BuildTitleHeader(info));
|
||||
|
||||
// Name section
|
||||
output.Add(".SH NAME");
|
||||
string nameLine = Roff.Escape(info.Name);
|
||||
if (!string.IsNullOrEmpty(info.Description))
|
||||
nameLine += $" \\- {Roff.Escape(info.Description)}";
|
||||
|
||||
output.Add(nameLine);
|
||||
|
||||
// Synopsis section
|
||||
output.Add(".SH SYNOPSIS");
|
||||
output.Add(item: $".B {Roff.Escape(info.Name)}");
|
||||
output.Add("[options]");
|
||||
|
||||
// Description section, derived from the header lines
|
||||
if (_header.Count > 0)
|
||||
{
|
||||
output.Add(".SH DESCRIPTION");
|
||||
foreach (string line in _header)
|
||||
{
|
||||
output.AddRange(Roff.FormatText(line));
|
||||
}
|
||||
}
|
||||
|
||||
// Options section, derived from all available inputs
|
||||
output.Add($".SH {info.OptionsHeading}");
|
||||
foreach (var input in _inputs.Values)
|
||||
{
|
||||
output.AddRange(input.FormatManpage(includeVerbose));
|
||||
}
|
||||
|
||||
// If there is a default feature, include its children directly
|
||||
if (DefaultFeature is not null)
|
||||
{
|
||||
foreach (var input in DefaultFeature.Children.Values)
|
||||
{
|
||||
output.AddRange(input.FormatManpage(includeVerbose));
|
||||
}
|
||||
}
|
||||
|
||||
// Notes section, derived from the footer lines
|
||||
if (_footer.Count > 0)
|
||||
{
|
||||
output.Add(".SH NOTES");
|
||||
foreach (string line in _footer)
|
||||
{
|
||||
output.AddRange(Roff.FormatText(line));
|
||||
}
|
||||
}
|
||||
|
||||
// Join the lines with a trailing newline for a well-formed file
|
||||
return string.Join("\n", [.. output]) + "\n";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a man page from the current set of inputs and write it to a file
|
||||
/// </summary>
|
||||
/// <param name="path">Path to write the generated man page to</param>
|
||||
/// <param name="info">Document-level metadata for the man page</param>
|
||||
/// <param name="includeVerbose">True if detailed descriptions should be included, false otherwise</param>
|
||||
/// <remarks>The file is written as UTF-8 without a byte order mark</remarks>
|
||||
public void OutputManpage(string path, ManpageInfo info, bool includeVerbose = false)
|
||||
{
|
||||
string content = OutputManpage(info, includeVerbose);
|
||||
File.WriteAllText(path, content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the roff <c>.TH</c> title line from the page metadata
|
||||
/// </summary>
|
||||
/// <param name="info">Document-level metadata for the man page</param>
|
||||
/// <returns>The formatted title line</returns>
|
||||
private static string BuildTitleHeader(ManpageInfo info)
|
||||
{
|
||||
return ".TH "
|
||||
+ Quote(info.Name.ToUpperInvariant())
|
||||
+ " " + Quote(info.Section)
|
||||
+ " " + Quote(info.Date)
|
||||
+ " " + Quote(info.Version)
|
||||
+ " " + Quote(info.Title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap an escaped value in double quotes for a <c>.TH</c> field
|
||||
/// </summary>
|
||||
/// <param name="value">Value to quote, if any</param>
|
||||
/// <returns>The escaped, quoted value</returns>
|
||||
private static string Quote(string? value)
|
||||
=> $"\"{Roff.EscapeField(value)}\"";
|
||||
|
||||
#endregion
|
||||
|
||||
#region Processing
|
||||
|
||||
/// <summary>
|
||||
/// Process args list with default handling
|
||||
/// </summary>
|
||||
/// <param name="args">Set of arguments to process</param>
|
||||
/// <returns>True if all arguments were processed correctly, false otherwise</returns>
|
||||
/// <remarks>
|
||||
/// This default processing implementation assumes a few key points:
|
||||
/// - Top-level items are all <see cref="Feature"/>
|
||||
/// - There is only top-level item allowed at a time
|
||||
/// - The first argument is always the <see cref="Feature"/> flag
|
||||
/// </remarks>
|
||||
public bool ProcessArgs(string[] args)
|
||||
{
|
||||
// If there's no arguments, show help
|
||||
if (args.Length == 0)
|
||||
{
|
||||
OutputAllHelp();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get the first argument as a feature flag
|
||||
string featureName = args[0];
|
||||
|
||||
// Get the associated feature
|
||||
var topLevel = GetTopLevel(featureName);
|
||||
if (topLevel is null || topLevel is not Feature feature)
|
||||
{
|
||||
Console.WriteLine($"'{featureName}' is not valid feature flag");
|
||||
OutputFeatureHelp(featureName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle default help functionality
|
||||
if (topLevel is Help helpFeature)
|
||||
{
|
||||
helpFeature.ProcessArgs(args, 0, this);
|
||||
return true;
|
||||
}
|
||||
else if (topLevel is HelpExtended helpExtFeature)
|
||||
{
|
||||
helpExtFeature.ProcessArgs(args, 0, this);
|
||||
return true;
|
||||
}
|
||||
else if (topLevel is Manpage manFeature)
|
||||
{
|
||||
manFeature.ProcessArgs(args, 0, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Now verify that all other flags are valid
|
||||
if (!feature.ProcessArgs(args, 1))
|
||||
{
|
||||
OutputFeatureHelp(topLevel.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If inputs are required
|
||||
if (feature.RequiresInputs && !feature.VerifyInputs())
|
||||
{
|
||||
OutputFeatureHelp(topLevel.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now execute the current feature
|
||||
return feature.Execute();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
72
SabreTools.CommandLine/Features/Manpage.cs
Normal file
72
SabreTools.CommandLine/Features/Manpage.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
|
||||
namespace SabreTools.CommandLine.Features
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference feature that emits a man page for the enclosing command set
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This mirrors the built-in <see cref="Help"/> feature: it must be given
|
||||
/// the enclosing <see cref="CommandSet"/> so that it can render the page
|
||||
/// from the same model used to format help text. The roff output is
|
||||
/// written to standard output so that it can be redirected to a file at
|
||||
/// packaging time, for example <c>tool man > tool.1</c>.
|
||||
/// </remarks>
|
||||
public class Manpage : Feature
|
||||
{
|
||||
public const string DisplayName = "Manpage";
|
||||
|
||||
private static readonly string[] _defaultFlags = ["man"];
|
||||
|
||||
private const string _description = "Generate a man page";
|
||||
|
||||
private const string _detailedDescription = "Write a roff-formatted man page for this program to standard output.";
|
||||
|
||||
/// <summary>
|
||||
/// Document-level metadata for the generated man page
|
||||
/// </summary>
|
||||
private readonly ManpageInfo _info;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if detailed descriptions should be included
|
||||
/// </summary>
|
||||
private readonly bool _includeVerbose;
|
||||
|
||||
public Manpage(ManpageInfo info, bool includeVerbose = true)
|
||||
: base(DisplayName, _defaultFlags, _description, _detailedDescription)
|
||||
{
|
||||
_info = info;
|
||||
_includeVerbose = includeVerbose;
|
||||
RequiresInputs = false;
|
||||
}
|
||||
|
||||
public Manpage(ManpageInfo info, string[] flags, bool includeVerbose = true)
|
||||
: base(DisplayName, flags, _description, _detailedDescription)
|
||||
{
|
||||
_info = info;
|
||||
_includeVerbose = includeVerbose;
|
||||
RequiresInputs = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool ProcessArgs(string[] args, int index)
|
||||
=> ProcessArgs(args, index, null);
|
||||
|
||||
/// <inheritdoc cref="ProcessArgs(string[], int)"/>
|
||||
/// <param name="parentSet">Reference to the enclosing parent set</param>
|
||||
public bool ProcessArgs(string[] args, int index, CommandSet? parentSet)
|
||||
{
|
||||
// The page can only be rendered with the enclosing set
|
||||
if (parentSet is not null)
|
||||
Console.Write(parentSet.OutputManpage(_info, _includeVerbose));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool VerifyInputs() => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Execute() => true;
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ namespace SabreTools.CommandLine.Features
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.GetEntryAssembly();
|
||||
if (assembly == null)
|
||||
if (assembly is null)
|
||||
return null;
|
||||
|
||||
var assemblyVersion = Attribute.GetCustomAttribute(assembly, typeof(AssemblyInformationalVersionAttribute)) as AssemblyInformationalVersionAttribute;
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="bool"/>
|
||||
/// Represents a user input bounded to the range of <see cref="bool"/>
|
||||
/// </summary>
|
||||
public class BooleanInput : UserInput<bool?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="short"/>
|
||||
/// Represents a user input bounded to the range of <see cref="short"/>
|
||||
/// </summary>
|
||||
public class Int16Input : UserInput<short?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="int"/>
|
||||
/// Represents a user input bounded to the range of <see cref="int"/>
|
||||
/// </summary>
|
||||
public class Int32Input : UserInput<int?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="long"/>
|
||||
/// Represents a user input bounded to the range of <see cref="long"/>
|
||||
/// </summary>
|
||||
public class Int64Input : UserInput<long?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="sbyte"/>
|
||||
/// Represents a user input bounded to the range of <see cref="sbyte"/>
|
||||
/// </summary>
|
||||
public class Int8Input : UserInput<sbyte?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="ushort"/>
|
||||
/// Represents a user input bounded to the range of <see cref="ushort"/>
|
||||
/// </summary>
|
||||
public class UInt16Input : UserInput<ushort?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="uint"/>
|
||||
/// Represents a user input bounded to the range of <see cref="uint"/>
|
||||
/// </summary>
|
||||
public class UInt32Input : UserInput<uint?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="ulong"/>
|
||||
/// Represents a user input bounded to the range of <see cref="ulong"/>
|
||||
/// </summary>
|
||||
public class UInt64Input : UserInput<ulong?>
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a user input bounded to the range of <see cref="byte"/>
|
||||
/// Represents a user input bounded to the range of <see cref="byte"/>
|
||||
/// </summary>
|
||||
public class UInt8Input : UserInput<byte?>
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using SabreTools.CommandLine.Tools;
|
||||
|
||||
namespace SabreTools.CommandLine.Inputs
|
||||
{
|
||||
@@ -41,7 +42,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
/// <summary>
|
||||
/// Set of children associated with this input
|
||||
/// </summary>
|
||||
protected readonly Dictionary<string, UserInput> Children = [];
|
||||
protected internal readonly Dictionary<string, UserInput> Children = [];
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -86,7 +87,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
/// </summary>
|
||||
public UserInput? this[UserInput subfeature]
|
||||
{
|
||||
get
|
||||
get
|
||||
{
|
||||
if (!Children.TryGetValue(subfeature.Name, out var input))
|
||||
return null;
|
||||
@@ -135,6 +136,19 @@ namespace SabreTools.CommandLine.Inputs
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a Feature value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <returns>The value if found, null otherwise</returns>
|
||||
public Feature? GetFeature(string key)
|
||||
{
|
||||
if (TryGetFeature(key, out Feature? value))
|
||||
return value;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int8 value from a named input
|
||||
/// </summary>
|
||||
@@ -298,7 +312,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Feature is not a bool");
|
||||
throw new ArgumentException("Input is not a bool");
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
@@ -312,6 +326,35 @@ namespace SabreTools.CommandLine.Inputs
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a Feature value from a named input
|
||||
/// </summary>
|
||||
/// <param name="key">Input name to retrieve, if possible</param>
|
||||
/// <param name="value">Value that was found, default value otherwise</param>
|
||||
/// <returns>True if the value was found, false otherwise</returns>
|
||||
public bool TryGetFeature(string key, out Feature? value)
|
||||
{
|
||||
// Try to check immediate children
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Feature i)
|
||||
throw new ArgumentException("Input is not a Feature");
|
||||
|
||||
value = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check all children recursively
|
||||
foreach (var child in Children.Values)
|
||||
{
|
||||
if (child.TryGetFeature(key, out value))
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an Int8 value from a named input
|
||||
/// </summary>
|
||||
@@ -325,7 +368,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int8Input i)
|
||||
throw new ArgumentException("Feature is not an sbyte");
|
||||
throw new ArgumentException("Input is not an sbyte");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -355,7 +398,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int16Input i)
|
||||
throw new ArgumentException("Feature is not a short");
|
||||
throw new ArgumentException("Input is not a short");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -385,7 +428,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int32Input i)
|
||||
throw new ArgumentException("Feature is not an int");
|
||||
throw new ArgumentException("Input is not an int");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -415,7 +458,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not Int64Input l)
|
||||
throw new ArgumentException("Feature is not a long");
|
||||
throw new ArgumentException("Input is not a long");
|
||||
|
||||
value = l.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -445,7 +488,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not StringInput s)
|
||||
throw new ArgumentException("Feature is not a string");
|
||||
throw new ArgumentException("Input is not a string");
|
||||
|
||||
value = s.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -474,7 +517,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not StringListInput l)
|
||||
throw new ArgumentException("Feature is not a list");
|
||||
throw new ArgumentException("Input is not a list");
|
||||
|
||||
value = l.Value ?? [];
|
||||
return true;
|
||||
@@ -504,7 +547,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt8Input i)
|
||||
throw new ArgumentException("Feature is not an byte");
|
||||
throw new ArgumentException("Input is not an byte");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -534,7 +577,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt16Input i)
|
||||
throw new ArgumentException("Feature is not a ushort");
|
||||
throw new ArgumentException("Input is not a ushort");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -564,7 +607,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt32Input i)
|
||||
throw new ArgumentException("Feature is not an uint");
|
||||
throw new ArgumentException("Input is not an uint");
|
||||
|
||||
value = i.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -594,7 +637,7 @@ namespace SabreTools.CommandLine.Inputs
|
||||
if (Children.TryGetValue(key, out var input))
|
||||
{
|
||||
if (input is not UInt64Input l)
|
||||
throw new ArgumentException("Feature is not a ulong");
|
||||
throw new ArgumentException("Input is not a ulong");
|
||||
|
||||
value = l.Value ?? defaultValue;
|
||||
return true;
|
||||
@@ -681,13 +724,45 @@ namespace SabreTools.CommandLine.Inputs
|
||||
protected abstract string FormatFlags();
|
||||
|
||||
/// <summary>
|
||||
/// Create a padding space based on the given length
|
||||
/// Create roff man page entries for this input and all of its children
|
||||
/// </summary>
|
||||
/// <param name="spaces">Number of padding spaces to add</param>
|
||||
/// <returns>String with requested number of blank spaces</returns>
|
||||
private static string CreatePadding(int spaces) => spaces > 0
|
||||
? string.Empty.PadRight(spaces)
|
||||
: string.Empty;
|
||||
/// <param name="includeVerbose">True if the detailed description should be included, false otherwise</param>
|
||||
/// <returns>Roff man page lines for this input and its children</returns>
|
||||
internal List<string> FormatManpage(bool includeVerbose)
|
||||
{
|
||||
List<string> output = [];
|
||||
|
||||
// Use the flags as the tag, falling back to the name when none are formatted
|
||||
string tag = FormatFlags();
|
||||
if (tag.Length == 0)
|
||||
tag = Name;
|
||||
|
||||
// Tagged paragraph: bold flags followed by the description
|
||||
output.Add(".TP");
|
||||
output.Add(".B " + Roff.Escape(tag));
|
||||
output.AddRange(Roff.FormatText(Description));
|
||||
|
||||
// Append the detailed description as an additional paragraph
|
||||
if (includeVerbose && !string.IsNullOrEmpty(DetailedDescription))
|
||||
{
|
||||
output.Add(".sp");
|
||||
output.AddRange(Roff.FormatText(DetailedDescription));
|
||||
}
|
||||
|
||||
// Indent and append all children recursively
|
||||
if (Children.Count > 0)
|
||||
{
|
||||
output.Add(".RS");
|
||||
foreach (var child in Children.Values)
|
||||
{
|
||||
output.AddRange(child.FormatManpage(includeVerbose));
|
||||
}
|
||||
|
||||
output.Add(".RE");
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format the standard help output line
|
||||
@@ -797,6 +872,15 @@ namespace SabreTools.CommandLine.Inputs
|
||||
return outputList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a padding space based on the given length
|
||||
/// </summary>
|
||||
/// <param name="spaces">Number of padding spaces to add</param>
|
||||
/// <returns>String with requested number of blank spaces</returns>
|
||||
private static string CreatePadding(int spaces) => spaces > 0
|
||||
? string.Empty.PadRight(spaces)
|
||||
: string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Create formatted help text including all children
|
||||
/// </summary>
|
||||
|
||||
@@ -27,10 +27,10 @@ namespace SabreTools.CommandLine.Inputs
|
||||
#region Instance Methods
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override abstract bool ProcessInput(string[] args, ref int index);
|
||||
public abstract override bool ProcessInput(string[] args, ref int index);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override abstract string FormatFlags();
|
||||
protected abstract override string FormatFlags();
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
69
SabreTools.CommandLine/ManpageInfo.cs
Normal file
69
SabreTools.CommandLine/ManpageInfo.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
namespace SabreTools.CommandLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Document-level metadata used when generating a man page
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// These values populate the parts of a man page that cannot be
|
||||
/// derived from the command-line model itself, such as the
|
||||
/// <c>.TH</c> title line and the <c>NAME</c> section.
|
||||
/// </remarks>
|
||||
public class ManpageInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Program name as invoked on the command line
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used for the <c>.TH</c> title line, the <c>NAME</c> section,
|
||||
/// and the <c>SYNOPSIS</c> section.
|
||||
/// </remarks>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Manual section the page belongs to
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to section 1 (user commands)</remarks>
|
||||
public string Section { get; set; } = "1";
|
||||
|
||||
/// <summary>
|
||||
/// Date used for the <c>.TH</c> title line
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When left null or empty, the field is emitted blank so that a
|
||||
/// downstream packager can stamp it at build time.
|
||||
/// </remarks>
|
||||
public string? Date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Version string used for the <c>.TH</c> title line
|
||||
/// </summary>
|
||||
public string? Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Manual title used for the <c>.TH</c> title line
|
||||
/// </summary>
|
||||
/// <remarks>Typically a value such as "User Commands"</remarks>
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Short description used for the <c>NAME</c> section
|
||||
/// </summary>
|
||||
/// <remarks>Should be a single printable line</remarks>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Section heading used for the list of inputs
|
||||
/// </summary>
|
||||
/// <remarks>Defaults to "OPTIONS"</remarks>
|
||||
public string OptionsHeading { get; set; } = "OPTIONS";
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="ManpageInfo"/> for the given program name
|
||||
/// </summary>
|
||||
/// <param name="name">Program name as invoked on the command line</param>
|
||||
public ManpageInfo(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Assembly Properties -->
|
||||
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>net20;net35;net40;net452;net462;net472;net48;netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0;net10.0;net11.0;netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<CheckEolTargetFramework>false</CheckEolTargetFramework>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
@@ -11,12 +11,12 @@
|
||||
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Version>1.3.1</Version>
|
||||
<Version>1.6.0</Version>
|
||||
|
||||
<!-- Package Properties -->
|
||||
<Authors>Matt Nadareski</Authors>
|
||||
<Description>Commandline parsing and output library</Description>
|
||||
<Copyright>Copyright (c)2016-2025 Matt Nadareski</Copyright>
|
||||
<Copyright>Copyright (c)2016-2026 Matt Nadareski</Copyright>
|
||||
<PackageProjectUrl>https://github.com/SabreTools/</PackageProjectUrl>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/SabreTools/SabreTools.CommandLine</RepositoryUrl>
|
||||
|
||||
150
SabreTools.CommandLine/Tools/Roff.cs
Normal file
150
SabreTools.CommandLine/Tools/Roff.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace SabreTools.CommandLine.Tools
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for emitting roff-formatted man page text
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Output is restricted to the portable man(7) macro set so that the
|
||||
/// same page renders without warnings under both groff and mandoc.
|
||||
/// </remarks>
|
||||
internal static class Roff
|
||||
{
|
||||
/// <summary>
|
||||
/// Maximum input line length, in characters, before wrapping
|
||||
/// </summary>
|
||||
private const int LineWidth = 78;
|
||||
|
||||
/// <summary>
|
||||
/// Escape a value for safe inclusion in roff output
|
||||
/// </summary>
|
||||
/// <param name="value">Value to escape, if any</param>
|
||||
/// <returns>The escaped value, or an empty string if null or empty</returns>
|
||||
public static string Escape(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
// Backslashes are escaped first so that escapes introduced by
|
||||
// later replacements are not themselves re-escaped
|
||||
string escaped = value!.Replace("\\", "\\e");
|
||||
|
||||
// Render hyphens as literal ASCII minus signs instead of letting
|
||||
// groff translate them to Unicode hyphens in UTF-8 output
|
||||
escaped = escaped.Replace("-", "\\-");
|
||||
|
||||
return escaped;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escape a value for inclusion in a quoted <c>.TH</c> title field
|
||||
/// </summary>
|
||||
/// <param name="value">Value to escape, if any</param>
|
||||
/// <returns>The escaped value, or an empty string if null or empty</returns>
|
||||
/// <remarks>
|
||||
/// Unlike <see cref="Escape"/>, hyphens are left intact so that the
|
||||
/// date field remains parseable by mandoc and version strings render
|
||||
/// verbatim. Embedded quotes are escaped so they cannot terminate the
|
||||
/// surrounding field.
|
||||
/// </remarks>
|
||||
public static string EscapeField(string? value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
return value!.Replace("\\", "\\e")
|
||||
.Replace("\"", "\\(dq");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a block of text into wrapped, escaped roff output lines
|
||||
/// </summary>
|
||||
/// <param name="text">Text block to format, which may contain newlines</param>
|
||||
/// <returns>Roff-safe output lines for the text block</returns>
|
||||
public static List<string> FormatText(string? text)
|
||||
{
|
||||
List<string> output = [];
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return output;
|
||||
|
||||
// Normalize line endings and split into paragraphs on blank lines
|
||||
string normalized = text!.Replace("\r\n", "\n").Replace("\r", "\n");
|
||||
string[] paragraphs = normalized.Split(["\n\n"], StringSplitOptions.None);
|
||||
|
||||
for (int i = 0; i < paragraphs.Length; i++)
|
||||
{
|
||||
// Collapse single newlines within a paragraph into spaces
|
||||
string paragraph = paragraphs[i].Replace("\n", " ").Trim();
|
||||
if (paragraph.Length == 0)
|
||||
continue;
|
||||
|
||||
// Separate consecutive paragraphs with a blank line
|
||||
if (output.Count > 0)
|
||||
output.Add(".sp");
|
||||
|
||||
List<string> wrapped = WrapParagraph(paragraph);
|
||||
output.AddRange(wrapped);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrap a single paragraph into escaped roff output lines
|
||||
/// </summary>
|
||||
/// <param name="paragraph">Single-line paragraph text to wrap</param>
|
||||
/// <returns>Escaped output lines no longer than the configured width</returns>
|
||||
private static List<string> WrapParagraph(string paragraph)
|
||||
{
|
||||
List<string> lines = [];
|
||||
string[] words = paragraph.Split(' ');
|
||||
|
||||
var sb = new StringBuilder();
|
||||
for (int i = 0; i < words.Length; i++)
|
||||
{
|
||||
string word = Escape(words[i]);
|
||||
if (word.Length == 0)
|
||||
continue;
|
||||
|
||||
if (sb.Length == 0)
|
||||
{
|
||||
sb.Append(word);
|
||||
}
|
||||
else if (sb.Length + 1 + word.Length <= LineWidth)
|
||||
{
|
||||
sb.Append(' ');
|
||||
sb.Append(word);
|
||||
}
|
||||
else
|
||||
{
|
||||
lines.Add(Protect(sb.ToString()));
|
||||
sb = new StringBuilder();
|
||||
sb.Append(word);
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
lines.Add(Protect(sb.ToString()));
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protect a text line from being parsed as a roff control line
|
||||
/// </summary>
|
||||
/// <param name="line">Line to protect</param>
|
||||
/// <returns>The line, prefixed with a zero-width escape when needed</returns>
|
||||
private static string Protect(string line)
|
||||
{
|
||||
// A line beginning with a control character would be treated as a
|
||||
// request; a leading zero-width escape forces it to be text
|
||||
if (line.Length > 0 && (line[0] == '.' || line[0] == '\''))
|
||||
return $"\\&{line}";
|
||||
|
||||
return line;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#! /bin/bash
|
||||
|
||||
# This batch file assumes the following:
|
||||
# - .NET 9.0 (or newer) SDK is installed and in PATH
|
||||
# - .NET 11.0 (or newer) SDK is installed and in PATH
|
||||
#
|
||||
# If any of these are not satisfied, the operation may fail
|
||||
# in an unpredictable way and result in an incomplete output.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# This batch file assumes the following:
|
||||
# - .NET 9.0 (or newer) SDK is installed and in PATH
|
||||
# - .NET 11.0 (or newer) SDK is installed and in PATH
|
||||
#
|
||||
# If any of these are not satisfied, the operation may fail
|
||||
# in an unpredictable way and result in an incomplete output.
|
||||
|
||||
Reference in New Issue
Block a user