poafloc

poafloc - Parser Of Arguments For Lines Of Commands
git clone git://git.dimitrijedobrota.com/poafloc.git
Log | Files | Refs | README | LICENSE

commit 42b158159fc1188afc6b1afecf30ac0184fb0c52
parent fce2a3185e54b0777e79090200f3dc6824a48d08
Author: Dimitrije Dobrota <mail@dimitrijedobrota.com>
Date:   Mon, 17 Jun 2024 15:33:55 +0200

Rewrite CMake project for better integration

Diffstat:
M.clang-format | 252++++++++++++++++++++++++++++++++++---------------------------------------------
A.clang-tidy | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
A.codespellrc | 6++++++
M.gitignore | 15+++++++++++++--
ABUILDING.md | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCMakeLists.txt | 153+++++++++++++++++++++++++++++++++++++------------------------------------------
ACMakePresets.json | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ACODE_OF_CONDUCT.md | 5+++++
ACONTRIBUTING.md | 15+++++++++++++++
AHACKING.md | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MREADME.md | 45+++++++++++++++------------------------------
Acmake/coverage.cmake | 33+++++++++++++++++++++++++++++++++
Acmake/dev-mode.cmake | 16++++++++++++++++
Acmake/folders.cmake | 21+++++++++++++++++++++
Acmake/install-config.cmake | 1+
Acmake/install-rules.cmake | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmake/lint-targets.cmake | 34++++++++++++++++++++++++++++++++++
Acmake/lint.cmake | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmake/prelude.cmake | 10++++++++++
Acmake/project-is-top-level.cmake | 6++++++
Acmake/spell-targets.cmake | 22++++++++++++++++++++++
Acmake/spell.cmake | 29+++++++++++++++++++++++++++++
Acmake/variables.cmake | 41+++++++++++++++++++++++++++++++++++++++++
Ddemo/CMakeLists.txt | 17-----------------
Ddemo/main.c | 84-------------------------------------------------------------------------------
Ddemo/main.cpp | 87-------------------------------------------------------------------------------
Aexample/CMakeLists.txt | 26++++++++++++++++++++++++++
Aexample/example_c.c | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexample/example_cpp.cpp | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dinclude/poafloc.h | 121-------------------------------------------------------------------------------
Dinclude/poafloc.hpp | 111-------------------------------------------------------------------------------
Ainclude/poafloc/poafloc.h | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/poafloc/poafloc.hpp | 134+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DpoaflocConfig.cmake.in | 6------
Asource/c_bindings.cpp | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asource/help.cpp | 222+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/poafloc.cpp | 379+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/trie.cpp | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/CMakeLists.txt | 14--------------
Dsrc/c_bindings.cpp | 29-----------------------------
Dsrc/help.cpp | 189-------------------------------------------------------------------------------
Dsrc/poafloc.cpp | 287-------------------------------------------------------------------------------
Dsrc/trie.cpp | 53-----------------------------------------------------
Atest/CMakeLists.txt | 25+++++++++++++++++++++++++
Atest/source/poafloc_test.cpp | 8++++++++
45 files changed, 2427 insertions(+), 1257 deletions(-)

diff --git a/.clang-format b/.clang-format @@ -1,216 +1,178 @@ --- -Language: Cpp -# BasedOnStyle: Microsoft +Language: Cpp +# BasedOnStyle: Chromium AccessModifierOffset: -2 AlignAfterOpenBracket: Align -AlignArrayOfStructures: None -AlignConsecutiveAssignments: - Enabled: false - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: true -AlignConsecutiveBitFields: - Enabled: false - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: false -AlignConsecutiveDeclarations: - Enabled: false - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: false -AlignConsecutiveMacros: - Enabled: false - AcrossEmptyLines: false - AcrossComments: false - AlignCompound: false - PadOperators: false +AlignConsecutiveMacros: false +AlignConsecutiveAssignments: true +AlignConsecutiveBitFields: false +AlignConsecutiveDeclarations: false AlignEscapedNewlines: Right -AlignOperands: Align -AlignTrailingComments: true +AlignOperands: DontAlign +AlignTrailingComments: false AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: false AllowAllParametersOfDeclarationOnNextLine: true AllowShortEnumsOnASingleLine: false -AllowShortBlocksOnASingleLine: Always -AllowShortCaseLabelsOnASingleLine: true -AllowShortFunctionsOnASingleLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: Inline AllowShortLambdasOnASingleLine: All AllowShortIfStatementsOnASingleLine: AllIfsAndElse -AllowShortLoopsOnASingleLine: false +AllowShortLoopsOnASingleLine: true AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None -AlwaysBreakBeforeMultilineStrings: false -AlwaysBreakTemplateDeclarations: MultiLine -AttributeMacros: - - __capability -BinPackArguments: true -BinPackParameters: true +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: false +BinPackParameters: false BraceWrapping: - AfterCaseLabel: false - AfterClass: false - AfterControlStatement: false - AfterEnum: false - AfterFunction: false - AfterNamespace: false + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: Always + AfterEnum: true + AfterFunction: true + AfterNamespace: false AfterObjCDeclaration: false - AfterStruct: false - AfterUnion: false - AfterExternBlock: false - BeforeCatch: false - BeforeElse: false - BeforeLambdaBody: false - BeforeWhile: false - IndentBraces: false + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: true + BeforeLambdaBody: true + BeforeWhile: false + IndentBraces: false SplitEmptyFunction: true SplitEmptyRecord: true SplitEmptyNamespace: true -BreakBeforeBinaryOperators: None -BreakBeforeConceptDeclarations: Always +BreakBeforeBinaryOperators: NonAssignment BreakBeforeBraces: Custom -BreakBeforeInheritanceComma: false -BreakInheritanceList: BeforeColon +# BreakBeforeInheritanceComma: true +BreakInheritanceList: BeforeComma BreakBeforeTernaryOperators: true -BreakConstructorInitializersBeforeComma: false -BreakConstructorInitializers: BeforeColon -BreakAfterJavaFieldAnnotations: false +BreakConstructorInitializersBeforeComma: true +BreakConstructorInitializers: BeforeComma +BreakAfterJavaFieldAnnotations: true BreakStringLiterals: true -ColumnLimit: 79 -CommentPragmas: '^ IWYU pragma:' -QualifierAlignment: Leave +ColumnLimit: 79 +CommentPragmas: '^ IWYU pragma:' CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: true -DeriveLineEnding: true +DeriveLineEnding: false DerivePointerAlignment: false -DisableFormat: false -EmptyLineAfterAccessModifier: Never -EmptyLineBeforeAccessModifier: LogicalBlock +DisableFormat: false ExperimentalAutoDetectBinPacking: false -PackConstructorInitializers: BinPack -BasedOnStyle: '' -ConstructorInitializerAllOnOneLineOrOnePerLine: false -AllowAllConstructorInitializersOnNextLine: true FixNamespaceComments: true ForEachMacros: - foreach - Q_FOREACH - BOOST_FOREACH -IfMacros: - - KJ_IF_MAYBE -IncludeBlocks: Preserve +IncludeBlocks: Regroup IncludeCategories: - - Regex: '^"(llvm|llvm-c|clang|clang-c)/' - Priority: 2 - SortPriority: 0 - CaseSensitive: false - - Regex: '^(<|"(gtest|gmock|isl|json)/)' - Priority: 3 - SortPriority: 0 - CaseSensitive: false - - Regex: '.*' - Priority: 1 - SortPriority: 0 - CaseSensitive: false -IncludeIsMainRegex: '(Test)?$' + # Standard library headers come before anything else + - Regex: '^<[a-z_]+>' + Priority: -1 + - Regex: '^<.+\.h(pp)?>' + Priority: 1 + - Regex: '^<.*' + Priority: 2 + - Regex: '.*' + Priority: 3 +IncludeIsMainRegex: '' IncludeIsMainSourceRegex: '' -IndentAccessModifiers: false -IndentCaseLabels: false +IndentCaseLabels: true IndentCaseBlocks: false IndentGotoLabels: true -IndentPPDirectives: None -IndentExternBlock: AfterExternBlock -IndentRequiresClause: true -IndentWidth: 4 +IndentPPDirectives: AfterHash +IndentExternBlock: NoIndent +IndentWidth: 2 IndentWrappedFunctionNames: false -InsertBraces: false -InsertTrailingCommas: None -JavaScriptQuotes: Leave +InsertTrailingCommas: Wrapped +JavaScriptQuotes: Double JavaScriptWrapImports: true -KeepEmptyLinesAtTheStartOfBlocks: true -LambdaBodyIndentation: Signature +KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: '' -MacroBlockEnd: '' +MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None -ObjCBinPackProtocolList: Auto +ObjCBinPackProtocolList: Never ObjCBlockIndentWidth: 2 ObjCBreakBeforeNestedBlockParam: true ObjCSpaceAfterProperty: false ObjCSpaceBeforeProtocolList: true PenaltyBreakAssignment: 2 -PenaltyBreakBeforeFirstCallParameter: 19 +PenaltyBreakBeforeFirstCallParameter: 1 PenaltyBreakComment: 300 PenaltyBreakFirstLessLess: 120 -PenaltyBreakOpenParenthesis: 0 PenaltyBreakString: 1000 PenaltyBreakTemplateDeclaration: 10 PenaltyExcessCharacter: 1000000 -PenaltyReturnTypeOnItsOwnLine: 1000 -PenaltyIndentedWhitespace: 0 -PointerAlignment: Right -PPIndentWidth: -1 -ReferenceAlignment: Pointer -ReflowComments: true -RemoveBracesLLVM: false -RequiresClausePosition: OwnLine -SeparateDefinitionBlocks: Leave -ShortNamespaceLines: 1 -SortIncludes: CaseSensitive -SortJavaStaticImport: Before +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + - ParseTestProto + - ParsePartialTestProto + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: true SortUsingDeclarations: true SpaceAfterCStyleCast: false SpaceAfterLogicalNot: false -SpaceAfterTemplateKeyword: true +SpaceAfterTemplateKeyword: false SpaceBeforeAssignmentOperators: true -SpaceBeforeCaseColon: false -SpaceBeforeCpp11BracedList: false +SpaceBeforeCpp11BracedList: true SpaceBeforeCtorInitializerColon: true SpaceBeforeInheritanceColon: true -SpaceBeforeParens: ControlStatements -SpaceBeforeParensOptions: - AfterControlStatements: true - AfterForeachMacros: true - AfterFunctionDefinitionName: false - AfterFunctionDeclarationName: false - AfterIfMacros: true - AfterOverloadedOperator: false - AfterRequiresInClause: false - AfterRequiresInExpression: false - BeforeNonEmptyParentheses: false -SpaceAroundPointerQualifiers: Default +SpaceBeforeParens: ControlStatementsExceptForEachMacros SpaceBeforeRangeBasedForLoopColon: true SpaceInEmptyBlock: false SpaceInEmptyParentheses: false -SpacesBeforeTrailingComments: 1 -SpacesInAngles: Never +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false SpacesInConditionalStatement: false -SpacesInContainerLiterals: true +SpacesInContainerLiterals: false SpacesInCStyleCastParentheses: false -SpacesInLineCommentPrefix: - Minimum: 1 - Maximum: -1 SpacesInParentheses: false SpacesInSquareBrackets: false SpaceBeforeSquareBrackets: false -BitFieldColonSpacing: Both -Standard: Latest -StatementAttributeLikeMacros: - - Q_EMIT +Standard: Auto StatementMacros: - Q_UNUSED - QT_REQUIRE_VERSION -TabWidth: 4 -UseCRLF: false -UseTab: Never +TabWidth: 8 +UseCRLF: false +UseTab: Never WhitespaceSensitiveMacros: - STRINGIZE - PP_STRINGIZE - BOOST_PP_STRINGIZE - - NS_SWIFT_NAME - - CF_SWIFT_NAME ... - diff --git a/.clang-tidy b/.clang-tidy @@ -0,0 +1,172 @@ +--- +# Enable ALL the things! Except not really +# misc-non-private-member-variables-in-classes: the options don't do anything +# modernize-use-nodiscard: too aggressive, attribute is situationally useful +Checks: "*,\ + -altera-*,\ + -fuchsia-*,\ + -llvmlibc-*,\ + -*-avoid-goto,\ + -*-braces-around-statements,\ + -*-c-arrays,\ + -*-vararg,\ + -*-array*decay,\ + -bugprone-argument-comment,\ + -bugprone-easily-swappable-parameters,\ + -cert-dcl50-cpp,\ + -concurrency-mt-unsafe,\ + -cppcoreguidelines-avoid-magic-numbers,\ + -fuchsia-multiple-inheritance,\ + -hicpp-signed-bitwise,\ + -llvm-header-guard,\ + -llvm-include-order,\ + -misc-no-recursion,\ + -misc-non-private-member-variables-in-classes,\ + -modernize-use-nodiscard,\ + -modernize-use-trailing-return-type,\ + -readability-function-cognitive-complexity,\ + -readability-magic-numbers +" +WarningsAsErrors: '' +CheckOptions: + - key: 'bugprone-argument-comment.StrictMode' + value: 'true' +# Prefer using enum classes with 2 values for parameters instead of bools + - key: 'bugprone-argument-comment.CommentBoolLiterals' + value: 'true' + - key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts' + value: 'true' + - key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression' + value: 'true' + - key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison' + value: 'true' + - key: 'readability-simplify-boolean-expr.ChainedConditionalReturn' + value: 'true' + - key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment' + value: 'true' + - key: 'readability-uniqueptr-delete-release.PreferResetCall' + value: 'true' + - key: 'cppcoreguidelines-init-variables.MathHeader' + value: '<cmath>' + - key: 'cppcoreguidelines-narrowing-conversions.PedanticMode' + value: 'true' + - key: 'readability-else-after-return.WarnOnUnfixable' + value: 'true' + - key: 'readability-else-after-return.WarnOnConditionVariables' + value: 'true' + - key: 'readability-inconsistent-declaration-parameter-name.Strict' + value: 'true' + - key: 'readability-qualified-auto.AddConstToQualified' + value: 'true' + - key: 'readability-redundant-access-specifiers.CheckFirstDeclaration' + value: 'true' +# These seem to be the most common identifier styles + - key: 'readability-identifier-naming.AbstractClassCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ClassMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstantPointerParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprFunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ConstexprVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.EnumCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.EnumConstantCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.FunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalConstantPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalFunctionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.GlobalVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.InlineNamespaceCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalConstantPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalPointerCase' + value: 'lower_case' + - key: 'readability-identifier-naming.LocalVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.MacroDefinitionCase' + value: 'UPPER_CASE' + - key: 'readability-identifier-naming.MemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.MethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.NamespaceCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ParameterPackCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PointerParameterCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PrivateMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PrivateMemberPrefix' + value: 'm_' + - key: 'readability-identifier-naming.PrivateMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ProtectedMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ProtectedMemberPrefix' + value: 'm_' + - key: 'readability-identifier-naming.ProtectedMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PublicMemberCase' + value: 'lower_case' + - key: 'readability-identifier-naming.PublicMethodCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ScopedEnumConstantCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.StaticConstantCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StaticVariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.StructCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.TemplateTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.TypeAliasCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TypedefCase' + value: 'lower_case' + - key: 'readability-identifier-naming.TypeTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.TypeTemplateParameterIgnoredRegexp' + value: 'expr-type' + - key: 'readability-identifier-naming.UnionCase' + value: 'lower_case' + - key: 'readability-identifier-naming.ValueTemplateParameterCase' + value: 'CamelCase' + - key: 'readability-identifier-naming.VariableCase' + value: 'lower_case' + - key: 'readability-identifier-naming.VirtualMethodCase' + value: 'lower_case' +... diff --git a/.codespellrc b/.codespellrc @@ -0,0 +1,6 @@ +[codespell] +builtin = clear,rare,en-GB_to_en-US,names,informal,code +check-filenames = +check-hidden = +skip = */.git,*/build,*/prefix +quiet-level = 2 diff --git a/.gitignore b/.gitignore @@ -1,2 +1,13 @@ -build -.cache +**/.DS_Store +.idea/ +.vs/ +.vscode/ +build/ +cmake-build-*/ +prefix/ +.clangd +CMakeLists.txt.user +CMakeUserPresets.json +compile_commands.json +env.bat +env.ps1 diff --git a/BUILDING.md b/BUILDING.md @@ -0,0 +1,89 @@ +# Building with CMake + +## Build + +This project doesn't require any special command-line flags to build to keep +things simple. + +Here are the steps for building in release mode with a single-configuration +generator, like the Unix Makefiles one: + +```sh +cmake -S . -B build -D CMAKE_BUILD_TYPE=Release +cmake --build build +``` + +Here are the steps for building in release mode with a multi-configuration +generator, like the Visual Studio ones: + +```sh +cmake -S . -B build +cmake --build build --config Release +``` + +### Building with MSVC + +Note that MSVC by default is not standards compliant and you need to pass some +flags to make it behave properly. See the `flags-msvc` preset in the +[CMakePresets.json](CMakePresets.json) file for the flags and with what +variable to provide them to CMake during configuration. + +### Building on Apple Silicon + +CMake supports building on Apple Silicon properly since 3.20.1. Make sure you +have the [latest version][1] installed. + +## Install + +This project doesn't require any special command-line flags to install to keep +things simple. As a prerequisite, the project has to be built with the above +commands already. + +The below commands require at least CMake 3.15 to run, because that is the +version in which [Install a Project][2] was added. + +Here is the command for installing the release mode artifacts with a +single-configuration generator, like the Unix Makefiles one: + +```sh +cmake --install build +``` + +Here is the command for installing the release mode artifacts with a +multi-configuration generator, like the Visual Studio ones: + +```sh +cmake --install build --config Release +``` + +### CMake package + +This project exports a CMake package to be used with the [`find_package`][3] +command of CMake: + +* Package name: `poafloc` +* Target name: `poafloc::poafloc` + +Example usage: + +```cmake +find_package(poafloc REQUIRED) +# Declare the imported target as a build requirement using PRIVATE, where +# project_target is a target created in the consuming project +target_link_libraries( + project_target PRIVATE + poafloc::poafloc +) +``` + +### Note to packagers + +The `CMAKE_INSTALL_INCLUDEDIR` is set to a path other than just `include` if +the project is configured as a top level project to avoid indirectly including +other libraries when installed to a common prefix. Please review the +[install-rules.cmake](cmake/install-rules.cmake) file for the full set of +install rules. + +[1]: https://cmake.org/download/ +[2]: https://cmake.org/cmake/help/latest/manual/cmake.1.html#install-a-project +[3]: https://cmake.org/cmake/help/latest/command/find_package.html diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -1,100 +1,89 @@ -cmake_minimum_required(VERSION 3.25.2) -set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +cmake_minimum_required(VERSION 3.14) + +include(cmake/prelude.cmake) project( poafloc - VERSION 1.0.0 + VERSION 1.0.1 DESCRIPTION "Parser Of Arguments For Lines Of Commands" - LANGUAGES CXX C + HOMEPAGE_URL "https://git.dimitrijedobrota.com/poafloc.git" + LANGUAGES C CXX ) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -set(CMAKE_C_STANDARD 17) -set(CMAKE_C_STANDARD_REQUIRED ON) -set(CMAKE_C_EXTENSIONS OFF) - -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) -endif() - -include(GNUInstallDirs) +include(cmake/project-is-top-level.cmake) +include(cmake/variables.cmake) -set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) -set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) +# ---- Declare library ---- -set(INSTALL_LIBDIR ${CMAKE_INSTALL_LIBDIR} CACHE PATH "Installation directory for libraries") -set(INSTALL_BINDIR ${CMAKE_INSTALL_BINDIR} CACHE PATH "Installation directory for executables") -set(INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} CACHE PATH "Installation directory for header files") -if(WIN32 AND NOT CYGWIN) - set(DEF_INSTALL_CMAKEDIR CMake) -else() - set(DEF_INSTALL_CMAKEDIR share/cmake/${PROJECT_NAME}) -endif() -set(INSTALL_CMAKEDIR ${DEF_INSTALL_CMAKEDIR} CACHE PATH "Installation directory for CMake files") - -message(STATUS "Build type set to ${CMAKE_BUILD_TYPE}") -message(STATUS "Project will be installed to ${CMAKE_INSTALL_PREFIX}") -foreach(p LIB BIN INCLUDE CMAKE) - file(TO_NATIVE_PATH ${CMAKE_INSTALL_PREFIX}/${INSTALL_${p}DIR} _path) - message(STATUS "installing ${p} components to ${_path}") - unset(_path) -endforeach() - -add_subdirectory(src) -add_subdirectory(demo) - -install( - TARGETS - poafloc - EXPORT - poaflocTargets - ARCHIVE - DESTINATION ${INSTALL_LIBDIR} - COMPONENT lib - RUNTIME - DESTINATION ${INSTALL_BINDIR} - COMPONENT bin - LIBRARY - DESTINATION ${INSTALL_LIBDIR} - COMPONENT lib - PUBLIC_HEADER - DESTINATION ${INSTALL_INCLUDEDIR}/poafloc - COMPONENT dev +add_library( + poafloc_poafloc + source/poafloc.cpp + source/c_bindings.cpp + source/help.cpp + source/trie.cpp ) +add_library(poafloc::poafloc ALIAS poafloc_poafloc) -install( - EXPORT - poaflocTargets - NAMESPACE - "poafloc::" - DESTINATION - ${INSTALL_CMAKEDIR} - COMPONENT - dev +include(GenerateExportHeader) +generate_export_header( + poafloc_poafloc + BASE_NAME poafloc + EXPORT_FILE_NAME export/poafloc/poafloc_export.hpp + CUSTOM_CONTENT_FROM_VARIABLE pragma_suppress_c4251 ) -include(CMakePackageConfigHelpers) +if(NOT BUILD_SHARED_LIBS) + target_compile_definitions(poafloc_poafloc PUBLIC POAFLOC_STATIC_DEFINE) +endif() -write_basic_package_version_file( - ${CMAKE_CURRENT_BINARY_DIR}/poaflocConfigVersion.cmake - VERSION ${PROJECT_VERSION} - COMPATIBILITY SameMajorVersion +set_target_properties( + poafloc_poafloc PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES + VERSION "${PROJECT_VERSION}" + SOVERSION "${PROJECT_VERSION_MAJOR}" + EXPORT_NAME poafloc + OUTPUT_NAME poafloc ) -configure_package_config_file( - ${PROJECT_SOURCE_DIR}/poaflocConfig.cmake.in - ${CMAKE_CURRENT_BINARY_DIR}/poaflocConfig.cmake - INSTALL_DESTINATION ${INSTALL_CMAKEDIR} +target_include_directories( + poafloc_poafloc ${warning_guard} + PUBLIC + "\$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>" ) -install( - FILES - ${CMAKE_CURRENT_BINARY_DIR}/poaflocConfig.cmake - ${CMAKE_CURRENT_BINARY_DIR}/poaflocConfigVersion.cmake - DESTINATION - ${INSTALL_CMAKEDIR} +target_include_directories( + poafloc_poafloc SYSTEM + PUBLIC + "\$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/export>" ) + +target_compile_features(poafloc_poafloc PUBLIC cxx_std_20) + +# ---- Install rules ---- + +if(NOT CMAKE_SKIP_INSTALL_RULES) + include(cmake/install-rules.cmake) +endif() + +# ---- Examples ---- + +if(PROJECT_IS_TOP_LEVEL) + option(BUILD_EXAMPLES "Build examples tree." "${poafloc_DEVELOPER_MODE}") + if(BUILD_EXAMPLES) + add_subdirectory(example) + endif() +endif() + +# ---- Developer mode ---- + +if(NOT poafloc_DEVELOPER_MODE) + return() +elseif(NOT PROJECT_IS_TOP_LEVEL) + message( + AUTHOR_WARNING + "Developer mode is intended for developers of poafloc" + ) +endif() + +include(cmake/dev-mode.cmake) diff --git a/CMakePresets.json b/CMakePresets.json @@ -0,0 +1,160 @@ +{ + "version": 2, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "cmake-pedantic", + "hidden": true, + "warnings": { + "dev": true, + "deprecated": true, + "uninitialized": true, + "unusedCli": true, + "systemVars": false + }, + "errors": { + "dev": true, + "deprecated": true + } + }, + { + "name": "dev-mode", + "hidden": true, + "inherits": "cmake-pedantic", + "cacheVariables": { + "poafloc_DEVELOPER_MODE": "ON" + } + }, + { + "name": "cppcheck", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_CPPCHECK": "cppcheck;--inline-suppr" + } + }, + { + "name": "clang-tidy", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=^${sourceDir}/" + } + }, + { + "name": "ci-std", + "description": "This preset makes sure the project actually builds with at least the specified standard", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_EXTENSIONS": "OFF", + "CMAKE_CXX_STANDARD": "20", + "CMAKE_CXX_STANDARD_REQUIRED": "ON" + } + }, + { + "name": "flags-gcc-clang", + "description": "These flags are supported by both GCC and Clang", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS=1 -fstack-protector-strong -fcf-protection=full -fstack-clash-protection -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast", + "CMAKE_EXE_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed,-z,noexecstack,-z,relro,-z,now,-z,nodlopen", + "CMAKE_SHARED_LINKER_FLAGS": "-Wl,--allow-shlib-undefined,--as-needed,-z,noexecstack,-z,relro,-z,now,-z,nodlopen" + } + }, + { + "name": "flags-appleclang", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-fstack-protector-strong -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wcast-qual -Wformat=2 -Wundef -Werror=float-equal -Wshadow -Wcast-align -Wunused -Wnull-dereference -Wdouble-promotion -Wimplicit-fallthrough -Wextra-semi -Woverloaded-virtual -Wnon-virtual-dtor -Wold-style-cast" + } + }, + { + "name": "flags-msvc", + "description": "Note that all the flags after /W4 are required for MSVC to conform to the language standard", + "hidden": true, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/sdl /guard:cf /utf-8 /diagnostics:caret /w14165 /w44242 /w44254 /w44263 /w34265 /w34287 /w44296 /w44365 /w44388 /w44464 /w14545 /w14546 /w14547 /w14549 /w14555 /w34619 /w34640 /w24826 /w14905 /w14906 /w14928 /w45038 /W4 /permissive- /volatile:iso /Zc:inline /Zc:preprocessor /Zc:enumTypes /Zc:lambda /Zc:__cplusplus /Zc:externConstexpr /Zc:throwingNew /EHsc", + "CMAKE_EXE_LINKER_FLAGS": "/machine:x64 /guard:cf", + "CMAKE_SHARED_LINKER_FLAGS": "/machine:x64 /guard:cf" + } + }, + { + "name": "ci-linux", + "inherits": ["flags-gcc-clang", "ci-std"], + "generator": "Unix Makefiles", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "ci-darwin", + "inherits": ["flags-appleclang", "ci-std"], + "generator": "Xcode", + "hidden": true + }, + { + "name": "ci-win64", + "inherits": ["flags-msvc", "ci-std"], + "generator": "Visual Studio 17 2022", + "architecture": "x64", + "hidden": true + }, + { + "name": "coverage-linux", + "binaryDir": "${sourceDir}/build/coverage", + "inherits": "ci-linux", + "hidden": true, + "cacheVariables": { + "ENABLE_COVERAGE": "ON", + "CMAKE_BUILD_TYPE": "Coverage", + "CMAKE_CXX_FLAGS_COVERAGE": "-Og -g --coverage -fkeep-inline-functions -fkeep-static-functions", + "CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage", + "CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage" + } + }, + { + "name": "ci-coverage", + "inherits": ["coverage-linux", "dev-mode"], + "cacheVariables": { + "COVERAGE_HTML_COMMAND": "" + } + }, + { + "name": "ci-sanitize", + "binaryDir": "${sourceDir}/build/sanitize", + "inherits": ["ci-linux", "dev-mode"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Sanitize", + "CMAKE_CXX_FLAGS_SANITIZE": "-U_FORTIFY_SOURCE -O2 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common" + } + }, + { + "name": "ci-build", + "binaryDir": "${sourceDir}/build", + "hidden": true + }, + { + "name": "ci-multi-config", + "description": "Speed up multi-config generators by generating only one configuration instead of the defaults", + "hidden": true, + "cacheVariables": { + "CMAKE_CONFIGURATION_TYPES": "Release" + } + }, + { + "name": "ci-macos", + "inherits": ["ci-build", "ci-darwin", "dev-mode", "ci-multi-config"] + }, + { + "name": "ci-ubuntu", + "inherits": ["ci-build", "ci-linux", "clang-tidy", "cppcheck", "dev-mode"] + }, + { + "name": "ci-windows", + "inherits": ["ci-build", "ci-win64", "dev-mode", "ci-multi-config"] + } + ] +} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md @@ -0,0 +1,5 @@ +# Code of Conduct + +* You will be judged by your contributions first, and your sense of humor + second. +* Nobody owes you anything. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing + +## Code of Conduct + +Please see the [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) document. + + +## Getting started + +Helpful notes for developers can be found in the [`HACKING.md`](HACKING.md) +document. + +In addition to he above, if you use the presets file as instructed, then you +should NOT check it into source control, just as the CMake documentation +suggests. diff --git a/HACKING.md b/HACKING.md @@ -0,0 +1,174 @@ +# Hacking + +Here is some wisdom to help you build and test this project as a developer and +potential contributor. + +If you plan to contribute, please read the [CONTRIBUTING](CONTRIBUTING.md) +guide. + +## Developer mode + +Build system targets that are only useful for developers of this project are +hidden if the `poafloc_DEVELOPER_MODE` option is disabled. Enabling this +option makes tests and other developer targets and options available. Not +enabling this option means that you are a consumer of this project and thus you +have no need for these targets and options. + +Developer mode is always set to on in CI workflows. + +### Presets + +This project makes use of [presets][1] to simplify the process of configuring +the project. As a developer, you are recommended to always have the [latest +CMake version][2] installed to make use of the latest Quality-of-Life +additions. + +You have a few options to pass `poafloc_DEVELOPER_MODE` to the configure +command, but this project prefers to use presets. + +As a developer, you should create a `CMakeUserPresets.json` file at the root of +the project: + +```json +{ + "version": 2, + "cmakeMinimumRequired": { + "major": 3, + "minor": 14, + "patch": 0 + }, + "configurePresets": [ + { + "name": "dev", + "binaryDir": "${sourceDir}/build/dev", + "inherits": ["dev-mode", "ci-<os>"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "configuration": "Debug" + } + ], + "testPresets": [ + { + "name": "dev", + "configurePreset": "dev", + "configuration": "Debug", + "output": { + "outputOnFailure": true + } + } + ] +} +``` + +You should replace `<os>` in your newly created presets file with the name of +the operating system you have, which may be `win64`, `linux` or `darwin`. You +can see what these correspond to in the +[`CMakePresets.json`](CMakePresets.json) file. + +`CMakeUserPresets.json` is also the perfect place in which you can put all +sorts of things that you would otherwise want to pass to the configure command +in the terminal. + +> **Note** +> Some editors are pretty greedy with how they open projects with presets. +> Some just randomly pick a preset and start configuring without your consent, +> which can be confusing. Make sure that your editor configures when you +> actually want it to, for example in CLion you have to make sure only the +> `dev-dev preset` has `Enable profile` ticked in +> `File > Settings... > Build, Execution, Deployment > CMake` and in Visual +> Studio you have to set the option `Never run configure step automatically` +> in `Tools > Options > CMake` **prior to opening the project**, after which +> you can manually configure using `Project > Configure Cache`. + +### Configure, build and test + +If you followed the above instructions, then you can configure, build and test +the project respectively with the following commands from the project root on +any operating system with any build system: + +```sh +cmake --preset=dev +cmake --build --preset=dev +ctest --preset=dev +``` + +If you are using a compatible editor (e.g. VSCode) or IDE (e.g. CLion, VS), you +will also be able to select the above created user presets for automatic +integration. + +Please note that both the build and test commands accept a `-j` flag to specify +the number of jobs to use, which should ideally be specified to the number of +threads your CPU has. You may also want to add that to your preset using the +`jobs` property, see the [presets documentation][1] for more details. + +### Developer mode targets + +These are targets you may invoke using the build command from above, with an +additional `-t <target>` flag: + +#### `coverage` + +Available if `ENABLE_COVERAGE` is enabled. This target processes the output of +the previously run tests when built with coverage configuration. The commands +this target runs can be found in the `COVERAGE_TRACE_COMMAND` and +`COVERAGE_HTML_COMMAND` cache variables. The trace command produces an info +file by default, which can be submitted to services with CI integration. The +HTML command uses the trace command's output to generate an HTML document to +`<binary-dir>/coverage_html` by default. + +#### `docs` + +Available if `BUILD_MCSS_DOCS` is enabled. Builds to documentation using +Doxygen and m.css. The output will go to `<binary-dir>/docs` by default +(customizable using `DOXYGEN_OUTPUT_DIRECTORY`). + +#### `format-check` and `format-fix` + +These targets run the clang-format tool on the codebase to check errors and to +fix them respectively. Customization available using the `FORMAT_PATTERNS` and +`FORMAT_COMMAND` cache variables. + +#### `run-examples` + +Runs all the examples created by the `add_example` command. + +#### `spell-check` and `spell-fix` + +These targets run the codespell tool on the codebase to check errors and to fix +them respectively. Customization available using the `SPELL_COMMAND` cache +variable. + +## Running tests on Windows with `BUILD_SHARED_LIBS=ON` + +If you are building a shared library on Windows, you must add the path to the +DLL file to `PATH` when you want to run tests. One way you could do that is by +using PowerShell and writing a script for it, e.g. `env.ps1` at the project +root: + +```powershell +$oldPrompt = (Get-Command prompt).ScriptBlock + +function prompt() { "(Debug) $(& $oldPrompt)" } + +$VsInstallPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -Property InstallationPath +$Env:Path += ";$VsInstallPath\Common7\IDE;$Pwd\build\dev\Debug" +``` + +Then you can source this script by running `. env.ps1`. This particular +example will only work for Debug builds. + +### Passing `PATH` to editors + +Make sure you launch your editor of choice from the console with the above +script sourced. Look for `(Debug)` in the prompt to confirm, then run e.g. +`code .` for VScode or `devenv .` for Visual Studio. + +[1]: https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html +[2]: https://cmake.org/download/ diff --git a/README.md b/README.md @@ -2,7 +2,6 @@ Command-line argument parser for C and C++ written in C++20 - ## Description This project is heavily inspired by GNU argp and the initial goal was to have @@ -21,43 +20,23 @@ both languages simultaneously. There were a lot of caveats and edge cases to be understood first, and later implemented. -## Getting Started - -### Dependencies - -- Linux - * CMake 3.25.2 or latter - * Compiler with C++20 support (tested: clang 16.0.5, gcc 13.2.0) - -- Windows - * Not tested - +## Dependencies -### Installing +* CMake 3.25.2 or latter +* Compiler with C++20 support (tested: clang 16.0.5, gcc 13.2.0) -* Clone the repo -* Make a build folder and cd into it -* Run `cmake <path to cloned repo>` to set up the build scripts [Release mode] -* Run `make` to build the project -* Run `cmake --build . --target install` to install the project +## Building and installing -### Using the library +See the [BUILDING](BUILDING.md) document. -- Cmake: - * Add `find_package(poafloc 1 CONFIG REQUIRED)` to the top level CMakeLists.txt - * Add `poafloc` to `target_link_libraries` of the desired target -- Other: - * Link against `poafloc` with `-lpoafloc` +## Usage +> Please reference example folder for relevant usage example. -### Usage -> Please reference demo folder for relevant usage example. - - -### Help +## Help Refer to [GNU argp documentation](https://www.gnu.org/software/libc/manual/html_node/Argp.html) @@ -68,9 +47,15 @@ Refer to [GNU argp documentation](https://www.gnu.org/software/libc/manual/html_ * Initial Release +## Contributing + +See the [CONTRIBUTING](CONTRIBUTING.md) document. + + ## License -This project is licensed under the MIT License - see the LICENSE.md file for details +This project is licensed under the MIT License - +see the [LICENSE](LICENSE.md) document for details ## Acknowledgments diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake @@ -0,0 +1,33 @@ +# ---- Variables ---- + +# We use variables separate from what CTest uses, because those have +# customization issues +set( + COVERAGE_TRACE_COMMAND + lcov -c -q + -o "${PROJECT_BINARY_DIR}/coverage.info" + -d "${PROJECT_BINARY_DIR}" + --include "${PROJECT_SOURCE_DIR}/*" + CACHE STRING + "; separated command to generate a trace for the 'coverage' target" +) + +set( + COVERAGE_HTML_COMMAND + genhtml --legend -f -q + "${PROJECT_BINARY_DIR}/coverage.info" + -p "${PROJECT_SOURCE_DIR}" + -o "${PROJECT_BINARY_DIR}/coverage_html" + CACHE STRING + "; separated command to generate an HTML report for the 'coverage' target" +) + +# ---- Coverage target ---- + +add_custom_target( + coverage + COMMAND ${COVERAGE_TRACE_COMMAND} + COMMAND ${COVERAGE_HTML_COMMAND} + COMMENT "Generating coverage report" + VERBATIM +) diff --git a/cmake/dev-mode.cmake b/cmake/dev-mode.cmake @@ -0,0 +1,16 @@ +include(cmake/folders.cmake) + +include(CTest) +if(BUILD_TESTING) + add_subdirectory(test) +endif() + +option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF) +if(ENABLE_COVERAGE) + include(cmake/coverage.cmake) +endif() + +include(cmake/lint-targets.cmake) +include(cmake/spell-targets.cmake) + +add_folders(Project) diff --git a/cmake/folders.cmake b/cmake/folders.cmake @@ -0,0 +1,21 @@ +set_property(GLOBAL PROPERTY USE_FOLDERS YES) + +# Call this function at the end of a directory scope to assign a folder to +# targets created in that directory. Utility targets will be assigned to the +# UtilityTargets folder, otherwise to the ${name}Targets folder. If a target +# already has a folder assigned, then that target will be skipped. +function(add_folders name) + get_property(targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) + foreach(target IN LISTS targets) + get_property(folder TARGET "${target}" PROPERTY FOLDER) + if(DEFINED folder) + continue() + endif() + set(folder Utility) + get_property(type TARGET "${target}" PROPERTY TYPE) + if(NOT type STREQUAL "UTILITY") + set(folder "${name}") + endif() + set_property(TARGET "${target}" PROPERTY FOLDER "${folder}Targets") + endforeach() +endfunction() diff --git a/cmake/install-config.cmake b/cmake/install-config.cmake @@ -0,0 +1 @@ +include("${CMAKE_CURRENT_LIST_DIR}/poaflocTargets.cmake") diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake @@ -0,0 +1,72 @@ +if(PROJECT_IS_TOP_LEVEL) + set( + CMAKE_INSTALL_INCLUDEDIR "include/poafloc-${PROJECT_VERSION}" + CACHE STRING "" + ) + set_property(CACHE CMAKE_INSTALL_INCLUDEDIR PROPERTY TYPE PATH) +endif() + +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +# find_package(<package>) call for consumers to find this project +set(package poafloc) + +install( + DIRECTORY + include/ + "${PROJECT_BINARY_DIR}/export/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + COMPONENT poafloc_Development +) + +install( + TARGETS poafloc_poafloc + EXPORT poaflocTargets + RUNTIME # + COMPONENT poafloc_Runtime + LIBRARY # + COMPONENT poafloc_Runtime + NAMELINK_COMPONENT poafloc_Development + ARCHIVE # + COMPONENT poafloc_Development + INCLUDES # + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" +) + +write_basic_package_version_file( + "${package}ConfigVersion.cmake" + COMPATIBILITY SameMajorVersion +) + +# Allow package maintainers to freely override the path for the configs +set( + poafloc_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${package}" + CACHE STRING "CMake package config location relative to the install prefix" +) +set_property(CACHE poafloc_INSTALL_CMAKEDIR PROPERTY TYPE PATH) +mark_as_advanced(poafloc_INSTALL_CMAKEDIR) + +install( + FILES cmake/install-config.cmake + DESTINATION "${poafloc_INSTALL_CMAKEDIR}" + RENAME "${package}Config.cmake" + COMPONENT poafloc_Development +) + +install( + FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake" + DESTINATION "${poafloc_INSTALL_CMAKEDIR}" + COMPONENT poafloc_Development +) + +install( + EXPORT poaflocTargets + NAMESPACE poafloc:: + DESTINATION "${poafloc_INSTALL_CMAKEDIR}" + COMPONENT poafloc_Development +) + +if(PROJECT_IS_TOP_LEVEL) + include(CPack) +endif() diff --git a/cmake/lint-targets.cmake b/cmake/lint-targets.cmake @@ -0,0 +1,34 @@ +set( + FORMAT_PATTERNS + source/*.cpp source/*.hpp + include/*.hpp + test/*.cpp test/*.hpp + example/*.cpp example/*.hpp + CACHE STRING + "; separated patterns relative to the project source dir to format" +) + +set(FORMAT_COMMAND clang-format CACHE STRING "Formatter to use") + +add_custom_target( + format-check + COMMAND "${CMAKE_COMMAND}" + -D "FORMAT_COMMAND=${FORMAT_COMMAND}" + -D "PATTERNS=${FORMAT_PATTERNS}" + -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Linting the code" + VERBATIM +) + +add_custom_target( + format-fix + COMMAND "${CMAKE_COMMAND}" + -D "FORMAT_COMMAND=${FORMAT_COMMAND}" + -D "PATTERNS=${FORMAT_PATTERNS}" + -D FIX=YES + -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Fixing the code" + VERBATIM +) diff --git a/cmake/lint.cmake b/cmake/lint.cmake @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.14) + +macro(default name) + if(NOT DEFINED "${name}") + set("${name}" "${ARGN}") + endif() +endmacro() + +default(FORMAT_COMMAND clang-format) +default( + PATTERNS + source/*.cpp source/*.hpp + include/*.hpp + test/*.cpp test/*.hpp + example/*.cpp example/*.hpp +) +default(FIX NO) + +set(flag --output-replacements-xml) +set(args OUTPUT_VARIABLE output) +if(FIX) + set(flag -i) + set(args "") +endif() + +file(GLOB_RECURSE files ${PATTERNS}) +set(badly_formatted "") +set(output "") +string(LENGTH "${CMAKE_SOURCE_DIR}/" path_prefix_length) + +foreach(file IN LISTS files) + execute_process( + COMMAND "${FORMAT_COMMAND}" --style=file "${flag}" "${file}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE result + ${args} + ) + if(NOT result EQUAL "0") + message(FATAL_ERROR "'${file}': formatter returned with ${result}") + endif() + if(NOT FIX AND output MATCHES "\n<replacement offset") + string(SUBSTRING "${file}" "${path_prefix_length}" -1 relative_file) + list(APPEND badly_formatted "${relative_file}") + endif() + set(output "") +endforeach() + +if(NOT badly_formatted STREQUAL "") + list(JOIN badly_formatted "\n" bad_list) + message("The following files are badly formatted:\n\n${bad_list}\n") + message(FATAL_ERROR "Run again with FIX=YES to fix these files.") +endif() diff --git a/cmake/prelude.cmake b/cmake/prelude.cmake @@ -0,0 +1,10 @@ +# ---- In-source guard ---- + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR) + message( + FATAL_ERROR + "In-source builds are not supported. " + "Please read the BUILDING document before trying to build this project. " + "You may need to delete 'CMakeCache.txt' and 'CMakeFiles/' first." + ) +endif() diff --git a/cmake/project-is-top-level.cmake b/cmake/project-is-top-level.cmake @@ -0,0 +1,6 @@ +# This variable is set by project() in CMake 3.21+ +string( + COMPARE EQUAL + "${CMAKE_SOURCE_DIR}" "${PROJECT_SOURCE_DIR}" + PROJECT_IS_TOP_LEVEL +) diff --git a/cmake/spell-targets.cmake b/cmake/spell-targets.cmake @@ -0,0 +1,22 @@ +set(SPELL_COMMAND codespell CACHE STRING "Spell checker to use") + +add_custom_target( + spell-check + COMMAND "${CMAKE_COMMAND}" + -D "SPELL_COMMAND=${SPELL_COMMAND}" + -P "${PROJECT_SOURCE_DIR}/cmake/spell.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Checking spelling" + VERBATIM +) + +add_custom_target( + spell-fix + COMMAND "${CMAKE_COMMAND}" + -D "SPELL_COMMAND=${SPELL_COMMAND}" + -D FIX=YES + -P "${PROJECT_SOURCE_DIR}/cmake/spell.cmake" + WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}" + COMMENT "Fixing spelling errors" + VERBATIM +) diff --git a/cmake/spell.cmake b/cmake/spell.cmake @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.14) + +macro(default name) + if(NOT DEFINED "${name}") + set("${name}" "${ARGN}") + endif() +endmacro() + +default(SPELL_COMMAND codespell) +default(FIX NO) + +set(flag "") +if(FIX) + set(flag -w) +endif() + +execute_process( + COMMAND "${SPELL_COMMAND}" ${flag} + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + RESULT_VARIABLE result +) + +if(result EQUAL "65") + message(FATAL_ERROR "Run again with FIX=YES to fix these errors.") +elseif(result EQUAL "64") + message(FATAL_ERROR "Spell checker printed the usage info. Bad arguments?") +elseif(NOT result EQUAL "0") + message(FATAL_ERROR "Spell checker returned with ${result}") +endif() diff --git a/cmake/variables.cmake b/cmake/variables.cmake @@ -0,0 +1,41 @@ +# ---- Developer mode ---- + +# Developer mode enables targets and code paths in the CMake scripts that are +# only relevant for the developer(s) of poafloc +# Targets necessary to build the project must be provided unconditionally, so +# consumers can trivially build and package the project +if(PROJECT_IS_TOP_LEVEL) + option(poafloc_DEVELOPER_MODE "Enable developer mode" OFF) + option(BUILD_SHARED_LIBS "Build shared libs." OFF) +endif() + +# ---- Suppress C4251 on Windows ---- + +# Please see include/poafloc/poafloc.hpp for more details +set(pragma_suppress_c4251 " +/* This needs to suppress only for MSVC */ +#if defined(_MSC_VER) && !defined(__ICL) +# define POAFLOC_SUPPRESS_C4251 _Pragma(\"warning(suppress:4251)\") +#else +# define POAFLOC_SUPPRESS_C4251 +#endif +") + +# ---- Warning guard ---- + +# target_include_directories with the SYSTEM modifier will request the compiler +# to omit warnings from the provided paths, if the compiler supports that +# This is to provide a user experience similar to find_package when +# add_subdirectory or FetchContent is used to consume this project +set(warning_guard "") +if(NOT PROJECT_IS_TOP_LEVEL) + option( + poafloc_INCLUDES_WITH_SYSTEM + "Use SYSTEM modifier for poafloc's includes, disabling warnings" + ON + ) + mark_as_advanced(poafloc_INCLUDES_WITH_SYSTEM) + if(poafloc_INCLUDES_WITH_SYSTEM) + set(warning_guard SYSTEM) + endif() +endif() diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt @@ -1,17 +0,0 @@ -add_executable(demo main.cpp) -target_link_libraries(demo PRIVATE poafloc) -target_include_directories(demo PRIVATE ../include) -set_target_properties(demo PROPERTIES LANGUAGE CXX) -set_target_properties(demo PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} -) - -add_executable(cdemo main.c) -target_link_libraries(cdemo PRIVATE poafloc) -target_include_directories(cdemo PRIVATE ../include) -set_target_properties(cdemo PROPERTIES LANGUAGE C) -set_target_properties(cdemo PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} -) diff --git a/demo/main.c b/demo/main.c @@ -1,84 +0,0 @@ -#include "poafloc.h" - -#include <stdio.h> - -void error(const char *message) { fprintf(stderr, "%s\n", message); } - -typedef struct { - const char *output_file; - const char *input_file; - - int debug; - int hex; - int relocatable; -} arguments_t; - -int parse_opt(int key, const char *arg, poafloc_parser_t *parser) { - arguments_t *arguments = (arguments_t *)poafloc_parser_input(parser); - - switch (key) { - case 777: arguments->debug = 1; break; - case 'h': - if (arguments->relocatable) error("cannot mix -hex and -relocatable"); - arguments->hex = 1; - break; - case 'r': - if (arguments->hex) error("cannot mix -hex and -relocatable"); - arguments->relocatable = 1; - break; - case 'o': arguments->output_file = arg ? arg : "stdout"; break; - case 'i': arguments->input_file = arg; break; - // case Parser::Key::ARG: arguments->args.push_back(arg); break; - case POAFLOC_KEY_ERROR: fprintf(stderr, "handled error\n"); - case POAFLOC_KEY_INIT: - arguments->input_file = "stdin"; - arguments->output_file = "stdout"; - } - - return 0; -} - -// clang-format off -static const poafloc_option_t options[] = { - { 0, 'R', 0, 0, "random 0-group option"}, - { 0, 0, 0, 0, "Program mode", 1}, - {"relocatable", 'r', 0, 0, "Output in relocatable format"}, - { "hex", 'h', 0, 0, "Output in hex format"}, - {"hexadecimal", 0, 0, POAFLOC_OPTION_ALIAS | POAFLOC_OPTION_HIDDEN}, - { 0, 0, 0, 0, "For developers", 4}, - { "debug", 777, 0, 0, "Enable debugging mode"}, - { 0, 0, 0, 0, "Input/output", 3}, - { "output", 'o', "file", POAFLOC_OPTION_ARG_OPTIONAL, "Output file, default stdout"}, - { 0, 'i', "file", 0, "Input file"}, - { 0, 0, 0, 0, "Informational Options", -1}, - {0}, -}; - -static const poafloc_arg_t argp = { - options, parse_opt, "doc string\nother usage", - "First half of the message\vsecond half of the message" -}; -// clang-format on - -int main(int argc, char *argv[]) { - arguments_t arguments = {0}; - - if (poafloc_parse(&argp, argc, argv, 0, &arguments)) { - error("There was an error while parsing arguments"); - return 1; - } - - printf("Command line options:\n"); - printf("\t input: %s\n", arguments.input_file); - printf("\t output: %s\n", arguments.output_file); - printf("\t hex: %d\n", arguments.hex); - printf("\t debug: %d\n", arguments.debug); - printf("\t relocatable: %d\n", arguments.relocatable); - - // std::cout << "\t args: "; - // for (const auto &arg : arguments.args) - // std::cout << arg << " "; - // std::cout << std::endl; - - return 0; -} diff --git a/demo/main.cpp b/demo/main.cpp @@ -1,87 +0,0 @@ -#include "poafloc.hpp" - -#include <cstdint> -#include <iostream> -#include <vector> - -using namespace poafloc; - -void error(const std::string &message) { std::cerr << message << std::endl; } -struct arguments_t { - const char *output_file = "stdout"; - const char *input_file = "stdin"; - - bool debug = 0; - bool hex = 0; - bool relocatable = 0; - - std::vector<const char *> args; -}; - -int parse_opt(int key, const char *arg, Parser *parser) { - auto arguments = (arguments_t *)parser->input(); - - switch (key) { - case 777: arguments->debug = true; break; - case 'h': - if (arguments->relocatable) error("cannot mix -hex and -relocatable"); - arguments->hex = true; - break; - case 'r': - if (arguments->hex) error("cannot mix -hex and -relocatable"); - arguments->relocatable = true; - break; - case 'o': arguments->output_file = arg ? arg : "stdout"; break; - case 'i': arguments->input_file = arg; break; - case Key::ARG: arguments->args.push_back(arg); break; - case Key::ERROR: help(parser, stderr, STD_ERR); - } - - return 0; -} - -// clang-format off -static const option_t options[] = { - { 0, 'R', 0, 0, "random 0-group option"}, - { 0, 0, 0, 0, "Program mode", 1}, - {"relocatable", 'r', 0, 0, "Output in relocatable format"}, - { "hex", 'h', 0, 0, "Output in hex format"}, - {"hexadecimal", 0, 0, ALIAS | HIDDEN}, - { 0, 0, 0, 0, "For developers", 4}, - { "debug", 777, 0, 0, "Enable debugging mode"}, - { 0, 0, 0, 0, "Input/output", 3}, - { "output", 'o', "file", ARG_OPTIONAL, "Output file, default stdout"}, - { 0, 'i', "file", 0, "Input file"}, - { 0, 0, 0, 0, "Informational Options", -1}, - {0}, -}; - -static const arg_t argp = { - options, parse_opt, "doc string\nother usage", - "First half of the message\vsecond half of the message" -}; -// clang-format on - -int main(int argc, char *argv[]) { - arguments_t arguments; - - if (parse(&argp, argc, argv, 0, &arguments)) { - error("There was an error while parsing arguments"); - return 1; - } - - std::cout << "Command line options: " << std::endl; - - std::cout << "\t input: " << arguments.input_file << std::endl; - std::cout << "\t output: " << arguments.output_file << std::endl; - std::cout << "\t hex: " << arguments.hex << std::endl; - std::cout << "\t debug: " << arguments.debug << std::endl; - std::cout << "\t relocatable: " << arguments.relocatable << std::endl; - - std::cout << "\t args: "; - for (const auto &arg : arguments.args) - std::cout << arg << " "; - std::cout << std::endl; - - return 0; -} diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.14) + +project(poaflocExamples CXX) + +include(../cmake/project-is-top-level.cmake) +include(../cmake/folders.cmake) + +if(PROJECT_IS_TOP_LEVEL) + find_package(poafloc REQUIRED) +endif() + +add_custom_target(run-examples) + +function(add_example NAME EXT) + add_executable("${NAME}" "${NAME}.${EXT}") + target_link_libraries("${NAME}" PRIVATE poafloc::poafloc) + target_compile_features("${NAME}" PRIVATE cxx_std_20) + add_custom_target("run_${NAME}" COMMAND "${NAME}" VERBATIM) + add_dependencies("run_${NAME}" "${NAME}") + add_dependencies(run-examples "run_${NAME}") +endfunction() + +add_example(example_c c) +add_example(example_cpp cpp) + +add_folders(Example) diff --git a/example/example_c.c b/example/example_c.c @@ -0,0 +1,101 @@ +#include <poafloc/poafloc.h> +#include <stdio.h> + +void error(const char* message) +{ + (void)fprintf(stderr, "%s\n", message); +} + +typedef struct +{ + const char* output_file; + const char* input_file; + + int debug; + int hex; + int relocatable; +} arguments_t; + +int parse_opt(int key, const char* arg, poafloc_parser_t* parser) +{ + arguments_t* arguments = (arguments_t*)poafloc_parser_input(parser); + + switch (key) + { + case 777: + arguments->debug = 1; + break; + case 'h': + if (arguments->relocatable) error("cannot mix -hex and -relocatable"); + arguments->hex = 1; + break; + case 'r': + if (arguments->hex) error("cannot mix -hex and -relocatable"); + arguments->relocatable = 1; + break; + case 'o': + arguments->output_file = arg ? arg : "stdout"; + break; + case 'i': + arguments->input_file = arg; + break; + // case Parser::Key::ARG: arguments->args.push_back(arg); break; + case POAFLOC_KEY_ERROR: + (void)fprintf(stderr, "handled error\n"); + break; + case POAFLOC_KEY_INIT: + arguments->input_file = "stdin"; + arguments->output_file = "stdout"; + default: + break; + } + + return 0; +} + +// clang-format off +static const poafloc_option_t options[] = { + { 0, 'R', 0, 0, "random 0-group option"}, + { 0, 0, 0, 0, "Program mode", 1}, + {"relocatable", 'r', 0, 0, "Output in relocatable format"}, + { "hex", 'h', 0, 0, "Output in hex format"}, + {"hexadecimal", 0, 0, POAFLOC_OPTION_ALIAS | POAFLOC_OPTION_HIDDEN}, + { 0, 0, 0, 0, "For developers", 4}, + { "debug", 777, 0, 0, "Enable debugging mode"}, + { 0, 0, 0, 0, "Input/output", 3}, + { "output", 'o', "file", POAFLOC_OPTION_ARG_OPTIONAL, "Output file, default stdout"}, + { 0, 'i', "file", 0, "Input file"}, + { 0, 0, 0, 0, "Informational Options", -1}, + {0}, +}; + +static const poafloc_arg_t argp = { + options, parse_opt, "doc string\nother usage", + "First half of the message\vsecond half of the message" +}; +// clang-format on + +int main(int argc, char* argv[]) +{ + arguments_t arguments = {0}; + + if (poafloc_parse(&argp, argc, argv, 0, &arguments)) + { + error("There was an error while parsing arguments"); + return 1; + } + + printf("Command line options:\n"); + printf("\t input: %s\n", arguments.input_file); + printf("\t output: %s\n", arguments.output_file); + printf("\t hex: %d\n", arguments.hex); + printf("\t debug: %d\n", arguments.debug); + printf("\t relocatable: %d\n", arguments.relocatable); + + // std::cout << "\t args: "; + // for (const auto &arg : arguments.args) + // std::cout << arg << " "; + // std::cout << std::endl; + + return 0; +} diff --git a/example/example_cpp.cpp b/example/example_cpp.cpp @@ -0,0 +1,107 @@ +#include <cstdint> +#include <iostream> +#include <vector> + +#include <poafloc/poafloc.hpp> + +using namespace poafloc; // NOLINT + +void error(const std::string& message) +{ + std::cerr << message << std::endl; +} + +struct arguments_t +{ + const char* output_file = "stdout"; + const char* input_file = "stdin"; + + bool debug = false; + bool hex = false; + bool relocatable = false; + + std::vector<const char*> args; +}; + +int parse_opt(int key, const char* arg, Parser* parser) +{ + auto* arguments = static_cast<arguments_t*>(parser->input()); + + switch (key) + { + case 777: + arguments->debug = true; + break; + case 'h': + if (arguments->relocatable) error("cannot mix -hex and -relocatable"); + arguments->hex = true; + break; + case 'r': + if (arguments->hex) error("cannot mix -hex and -relocatable"); + arguments->relocatable = true; + break; + case 'o': + arguments->output_file = arg != nullptr ? arg : "stdout"; + break; + case 'i': + arguments->input_file = arg; + break; + case Key::ARG: + arguments->args.push_back(arg); + break; + case Key::ERROR: + help(parser, stderr, STD_ERR); + break; + default: + break; + } + + return 0; +} + +// clang-format off +static const option_t options[] = { + { nullptr, 'R', nullptr, 0, "random 0-group option"}, + { nullptr, 0, nullptr, 0, "Program mode", 1}, + {"relocatable", 'r', nullptr, 0, "Output in relocatable format"}, + { "hex", 'h', nullptr, 0, "Output in hex format"}, + {"hexadecimal", 0, nullptr, ALIAS | HIDDEN}, + { nullptr, 0, nullptr, 0, "For developers", 4}, + { "debug", 777, nullptr, 0, "Enable debugging mode"}, + { nullptr, 0, nullptr, 0, "Input/output", 3}, + { "output", 'o', "file", ARG_OPTIONAL, "Output file, default stdout"}, + { nullptr, 'i', "file", 0, "Input file"}, + { nullptr, 0, nullptr, 0, "Informational Options", -1}, + {}, +}; + +static const arg_t argp = { + options, parse_opt, "doc string\nother usage", + "First half of the message\vsecond half of the message" +}; +// clang-format on + +int main(int argc, char* argv[]) +{ + arguments_t arguments; + + if (parse(&argp, argc, argv, 0, &arguments) != 0) + { + error("There was an error while parsing arguments"); + return 1; + } + + std::cout << "Command line options: " << std::endl; + + std::cout << "\t input: " << arguments.input_file << std::endl; + std::cout << "\t output: " << arguments.output_file << std::endl; + std::cout << "\t hex: " << arguments.hex << std::endl; + std::cout << "\t debug: " << arguments.debug << std::endl; + std::cout << "\t relocatable: " << arguments.relocatable << std::endl; + + std::cout << "\t args: "; + for (const auto& arg : arguments.args) std::cout << arg << " "; + std::cout << std::endl; + + return 0; +} diff --git a/include/poafloc.h b/include/poafloc.h @@ -1,121 +0,0 @@ -#ifndef POAFLOC_POAFLOC_H -#define POAFLOC_POAFLOC_H - -#ifdef __cplusplus - -#include <cstdio> - -#define MANGLE_ENUM(enumn, name) name -#define ENUM_OPTION Option -#define ENUM_KEY Key -#define ENUM_HELP Help -#define ENUM_PARSE Parse - -extern "C" { -namespace poafloc { - -struct Parser; -typedef Parser poafloc_parser_t; - -#else - -#include <stdio.h> - -#define MANGLE_ENUM(enumn, name) POAFLOC_##enumn##_##name -#define ENUM_OPTION poafloc_option_e -#define ENUM_KEY poafloc_key_e -#define ENUM_HELP poafloc_help_e -#define ENUM_PARSE poafloc_parse_e - -struct __Parser; -typedef struct __Parser poafloc_parser_t; - -#endif - -typedef struct { - char const *name; - int key; - char const *arg; - int flags; - char const *message; - int group; -} poafloc_option_t; - -typedef int (*poafloc_parse_f)(int key, const char *arg, poafloc_parser_t *parser); - -typedef struct { - poafloc_option_t const *options; - poafloc_parse_f parse; - char const *doc; - char const *message; -} poafloc_arg_t; - -enum ENUM_OPTION { - MANGLE_ENUM(OPTION, ARG_OPTIONAL) = 0x1, - MANGLE_ENUM(OPTION, HIDDEN) = 0x2, - MANGLE_ENUM(OPTION, ALIAS) = 0x4, -}; - -enum ENUM_KEY { - MANGLE_ENUM(KEY, ARG) = 0, - MANGLE_ENUM(KEY, END) = 0x1000001, - MANGLE_ENUM(KEY, NO_ARGS) = 0x1000002, - MANGLE_ENUM(KEY, INIT) = 0x1000003, - MANGLE_ENUM(KEY, SUCCESS) = 0x1000004, - MANGLE_ENUM(KEY, ERROR) = 0x1000005, -}; - -enum ENUM_HELP { - - MANGLE_ENUM(HELP, SHORT_USAGE) = 0x1, - MANGLE_ENUM(HELP, USAGE) = 0x2, - MANGLE_ENUM(HELP, SEE) = 0x4, - MANGLE_ENUM(HELP, LONG) = 0x8, - - MANGLE_ENUM(HELP, EXIT_ERR) = 0x10, - MANGLE_ENUM(HELP, EXIT_OK) = 0x20, - - MANGLE_ENUM(HELP, STD_ERR) = MANGLE_ENUM(HELP, SEE) | - MANGLE_ENUM(HELP, EXIT_ERR), - - MANGLE_ENUM(HELP, STD_HELP) = MANGLE_ENUM(HELP, LONG) | - MANGLE_ENUM(HELP, EXIT_OK), - - MANGLE_ENUM(HELP, STD_USAGE) = MANGLE_ENUM(HELP, USAGE) | - MANGLE_ENUM(HELP, EXIT_ERR), -}; - -enum ENUM_PARSE { - MANGLE_ENUM(PARSE, NO_ERRS) = 0x1, - MANGLE_ENUM(PARSE, NO_HELP) = 0x2, - MANGLE_ENUM(PARSE, NO_EXIT) = 0x4, - MANGLE_ENUM(PARSE, SILENT) = 0x8, - MANGLE_ENUM(PARSE, IN_ORDER) = 0x10, -}; - -#if !defined __cplusplus || defined WITH_C_BINDINGS - -void *poafloc_parser_input(poafloc_parser_t *parser); - -void poafloc_usage(poafloc_parser_t *parser); - -void poafloc_help(const poafloc_parser_t *state, FILE *stream, unsigned flags); - -int poafloc_parse(const poafloc_arg_t *argp, int argc, char *argv[], unsigned flags, - void *input); - -void poafloc_failure(const poafloc_parser_t *parser, int status, int errnum, - const char *fmt, ...); - -#endif - -#undef MANGLE_ENUM -#undef ENUM_OPTION -#undef ENUM_KEY - -#ifdef __cplusplus -} // namespace poafloc -} // extern "C" -#endif - -#endif diff --git a/include/poafloc.hpp b/include/poafloc.hpp @@ -1,111 +0,0 @@ -#ifndef POAFLOC_POAFLOC_HPP -#define POAFLOC_POAFLOC_HPP - -#include "poafloc.h" - -#include <cstdarg> -#include <string> -#include <unordered_map> -#include <vector> - -namespace poafloc { - -using option_t = poafloc_option_t; -using arg_t = poafloc_arg_t; - -int parse(const arg_t *argp, int argc, char *argv[], unsigned flags, - void *input) noexcept; - -void usage(const Parser *parser); -void help(const Parser *parser, FILE *stream, unsigned flags); - -void failure(const Parser *parser, int status, int errnum, const char *fmt, - va_list args); - -void failure(const Parser *parser, int status, int errnum, const char *fmt, - ...); - -class Parser { - public: - void *input() const { return m_input; } - const char *name() const { return m_name; } - unsigned flags() const { return m_flags; } - - private: - friend int parse(const arg_t *, int, char **, unsigned, void *) noexcept; - friend void help(const Parser *parser, FILE *stream, unsigned flags); - - Parser(const arg_t *argp, unsigned flags, void *input); - Parser(const Parser &) = delete; - Parser(Parser &&) = delete; - Parser &operator=(const Parser &) = delete; - Parser &operator=(Parser &&) = delete; - ~Parser() noexcept = default; - - int parse(int argc, char *argv[]); - - int handle_unknown(bool shrt, const char *argv); - int handle_missing(bool shrt, const char *argv); - int handle_excess(const char *argv); - - void print_other_usages(FILE *stream) const; - void help(FILE *stream) const; - void usage(FILE *stream) const; - void see(FILE *stream) const; - - static const char *basename(const char *name); - - struct help_entry_t { - help_entry_t(const char *arg, const char *message, int group, - bool opt = false) - : arg(arg), message(message), group(group), opt(opt) {} - - void push(char sh) { opt_short.push_back(sh); } - void push(const char *lg) { opt_long.push_back(lg); } - - bool operator<(const help_entry_t &rhs) const; - - const char *arg; - const char *message; - int group; - bool opt; - - std::vector<const char *> opt_long; - std::vector<char> opt_short; - }; - - class trie_t { - public: - trie_t() = default; - trie_t(const trie_t &) = delete; - trie_t(trie_t &&) = delete; - trie_t &operator=(const trie_t &) = delete; - trie_t &operator=(trie_t &&) = delete; - ~trie_t() noexcept; - - bool insert(const char *option, int key); - int get(const char *option) const; - - private: - static bool is_valid(const char *option); - - trie_t *children[26] = {0}; - int count = 0; - int key = 0; - bool terminal = false; - }; - - const arg_t *argp; - unsigned m_flags; - void *m_input; - - const char *m_name; - - std::unordered_map<int, const option_t *> options; - std::vector<help_entry_t> help_entries; - trie_t trie; -}; - -} // namespace poafloc - -#endif diff --git a/include/poafloc/poafloc.h b/include/poafloc/poafloc.h @@ -0,0 +1,149 @@ +#ifndef POAFLOC_POAFLOC_H +#define POAFLOC_POAFLOC_H + +#ifdef __cplusplus + +# include <cstdio> + +# include "poafloc/poafloc_export.hpp" + +// NOLINTNEXTLINE +# define MANGLE_ENUM(enumn, name) name + +# define ENUM_OPTION Option +# define ENUM_KEY Key +# define ENUM_HELP Help +# define ENUM_PARSE Parse + +# define INIT_0 = 0 +# define INIT_NULLPTR = nullptr + +extern "C" +{ +namespace poafloc { + +#else + +# include <stdio.h> + +# define MANGLE_ENUM(enumn, name) POAFLOC_##enumn##_##name + +# define ENUM_OPTION poafloc_option_e +# define ENUM_KEY poafloc_key_e +# define ENUM_HELP poafloc_help_e +# define ENUM_PARSE poafloc_parse_e + +# define INIT_0 +# define INIT_NULLPTR + +#endif + +struct Parser; +typedef struct Parser poafloc_parser_t; // NOLINT + +typedef struct // NOLINT + POAFLOC_EXPORT +{ + char const* name INIT_NULLPTR; + int key INIT_0; + char const* arg INIT_NULLPTR; + int flags INIT_0; + char const* message INIT_NULLPTR; + int group INIT_0; +} poafloc_option_t; + +// NOLINTNEXTLINE +typedef int (*poafloc_parse_f)(int key, + const char* arg, + poafloc_parser_t* parser); + +// NOLINTNEXTLINE +typedef struct +{ + poafloc_option_t const* options INIT_NULLPTR; + poafloc_parse_f parse INIT_NULLPTR; + char const* doc INIT_NULLPTR; + char const* message INIT_NULLPTR; +} poafloc_arg_t; + +enum ENUM_OPTION +{ + MANGLE_ENUM(OPTION, ARG_OPTIONAL) = 0x1, + MANGLE_ENUM(OPTION, HIDDEN) = 0x2, + MANGLE_ENUM(OPTION, ALIAS) = 0x4, +}; + +enum ENUM_KEY +{ + MANGLE_ENUM(KEY, ARG) = 0, + MANGLE_ENUM(KEY, END) = 0x1000001, + MANGLE_ENUM(KEY, NO_ARGS) = 0x1000002, + MANGLE_ENUM(KEY, INIT) = 0x1000003, + MANGLE_ENUM(KEY, SUCCESS) = 0x1000004, + MANGLE_ENUM(KEY, ERROR) = 0x1000005, +}; + +enum ENUM_HELP +{ + MANGLE_ENUM(HELP, SHORT_USAGE) = 0x1, + MANGLE_ENUM(HELP, USAGE) = 0x2, + MANGLE_ENUM(HELP, SEE) = 0x4, + MANGLE_ENUM(HELP, LONG) = 0x8, + + MANGLE_ENUM(HELP, EXIT_ERR) = 0x10, + MANGLE_ENUM(HELP, EXIT_OK) = 0x20, + + MANGLE_ENUM(HELP, STD_ERR) = MANGLE_ENUM(HELP, SEE) + | MANGLE_ENUM(HELP, EXIT_ERR), + + MANGLE_ENUM(HELP, STD_HELP) = MANGLE_ENUM(HELP, LONG) + | MANGLE_ENUM(HELP, EXIT_OK), + + MANGLE_ENUM(HELP, STD_USAGE) = MANGLE_ENUM(HELP, USAGE) + | MANGLE_ENUM(HELP, EXIT_ERR), +}; + +enum ENUM_PARSE +{ + MANGLE_ENUM(PARSE, NO_ERRS) = 0x1, + MANGLE_ENUM(PARSE, NO_HELP) = 0x2, + MANGLE_ENUM(PARSE, NO_EXIT) = 0x4, + MANGLE_ENUM(PARSE, SILENT) = 0x8, + MANGLE_ENUM(PARSE, IN_ORDER) = 0x10, +}; + +#if !defined __cplusplus || defined WITH_C_BINDINGS + +void poafloc_usage(poafloc_parser_t* parser); + +void poafloc_help(const poafloc_parser_t* state, FILE* stream, unsigned flags); + +int poafloc_parse(const poafloc_arg_t* argp, + int argc, + char* argv[], + unsigned flags, + void* input); + +void* poafloc_parser_input(poafloc_parser_t* parser); + +void poafloc_failure(const poafloc_parser_t* parser, + int status, + int errnum, + const char* fmt, + ...); + +#endif + +#undef MANGLE_ENUM +#undef ENUM_OPTION +#undef ENUM_KEY + +#undef INIT_0 +#undef INIT_NULLPTR + +#ifdef __cplusplus +} // namespace poafloc +} // extern "C" +#endif + +#endif diff --git a/include/poafloc/poafloc.hpp b/include/poafloc/poafloc.hpp @@ -0,0 +1,134 @@ +#ifndef POAFLOC_POAFLOC_HPP +#define POAFLOC_POAFLOC_HPP + +#include <array> +#include <cstdarg> +#include <memory> +#include <string> +#include <unordered_map> +#include <vector> + +#include "poafloc.h" + +namespace poafloc { + +using option_t = poafloc_option_t; +using arg_t = poafloc_arg_t; + +int parse(const arg_t* argp, + int argc, + char* argv[], + unsigned flags, + void* input) noexcept; + +void usage(const Parser* parser); +void help(const Parser* parser, FILE* stream, unsigned flags); + +void failure(const Parser* parser, + int status, + int errnum, + const char* fmt, + va_list args); + +void failure( + const Parser* parser, int status, int errnum, const char* fmt, ...); + +struct Parser +{ + Parser(const Parser&) = delete; + Parser(Parser&&) = delete; + Parser& operator=(const Parser&) = delete; + Parser& operator=(Parser&&) = delete; + + void* input() const { return m_input; } + unsigned flags() const { return m_flags; } + const std::string& name() const { return m_name; } + +private: + friend void help(const Parser* parser, FILE* stream, unsigned flags); + friend int parse(const arg_t* argp, + int argc, + char** argv, + unsigned flags, + void* input) noexcept; + + Parser(const arg_t* argp, unsigned flags, void* input); + ~Parser() noexcept = default; + + int parse(std::size_t argc, char* argv[]); + + int handle_unknown(bool shrt, const char* argv); + int handle_missing(bool shrt, const char* argv); + int handle_excess(const char* argv); + + void print_other_usages(FILE* stream) const; + void help(FILE* stream) const; + void usage(FILE* stream) const; + void see(FILE* stream) const; + + static std::string basename(const std::string& name); + + struct help_entry_t + { + help_entry_t(const char* arg, + const char* message, + int group, + bool opt = false) + : m_arg(arg) + , m_message(message) + , m_group(group) + , m_opt(opt) + { + } + + void push(char shrt) { m_opt_short.push_back(shrt); } + void push(const char* lng) { m_opt_long.push_back(lng); } + + bool operator<(const help_entry_t& rhs) const; + + const char* m_arg; + const char* m_message; + int m_group; + bool m_opt; + + std::vector<const char*> m_opt_long; + std::vector<char> m_opt_short; + }; + + class trie_t + { + public: + trie_t() = default; + ~trie_t() noexcept = default; + + trie_t(const trie_t&) = delete; + trie_t(trie_t&&) = delete; + trie_t& operator=(const trie_t&) = delete; + trie_t& operator=(trie_t&&) = delete; + + bool insert(const std::string& option, int key); + int get(const std::string& option) const; + + private: + static bool is_valid(const std::string& option); + + std::array<std::unique_ptr<trie_t>, 26> m_children; + int m_count = 0; + int m_key = 0; + bool m_terminal = false; + }; + + const arg_t* m_argp; + unsigned m_flags; + void* m_input; + + std::string m_name; + + std::unordered_map<int, const option_t*> m_options; + std::vector<help_entry_t> m_help_entries; + trie_t m_trie; +}; + +} // namespace poafloc + +#endif diff --git a/poaflocConfig.cmake.in b/poaflocConfig.cmake.in @@ -1,6 +0,0 @@ -@PACKAGE_INIT@ -include("${CMAKE_CURRENT_LIST_DIR}/poaflocTargets.cmake") - -check_required_components( - "poafloc" -) diff --git a/source/c_bindings.cpp b/source/c_bindings.cpp @@ -0,0 +1,47 @@ +#include <cstdarg> + +#include "poafloc/poafloc.h" +#include "poafloc/poafloc.hpp" + +extern "C" +{ +namespace poafloc { + +int poafloc_parse(const poafloc_arg_t* argp, + int argc, + char* argv[], + unsigned flags, + void* input) +{ + return parse(argp, argc, argv, flags, input); +} + +void* poafloc_parser_input(poafloc_parser_t* parser) +{ + return parser->input(); +} + +void poafloc_usage(poafloc_parser_t* parser) +{ + return usage(parser); +} + +void poafloc_help(const poafloc_parser_t* parser, FILE* stream, unsigned flags) +{ + help(parser, stream, flags); +} + +void poafloc_failure(const poafloc_parser_t* parser, + int status, + int errnum, + const char* fmt, + ...) +{ + std::va_list args; + va_start(args, fmt); + failure(parser, status, errnum, fmt, args); + va_end(args); +} + +} // namespace poafloc +} diff --git a/source/help.cpp b/source/help.cpp @@ -0,0 +1,222 @@ +#include <cstring> +#include <format> +#include <sstream> + +#include "poafloc/poafloc.hpp" + +namespace poafloc { + +bool Parser::help_entry_t::operator<(const help_entry_t& rhs) const +{ + if (m_group != rhs.m_group) + { + if ((m_group != 0) && (rhs.m_group != 0)) + { + if (m_group < 0 && rhs.m_group < 0) return m_group < rhs.m_group; + if (m_group < 0 || rhs.m_group < 0) return rhs.m_group < 0; + return m_group < rhs.m_group; + } + + return m_group == 0; + } + + const char ch1 = !m_opt_long.empty() ? m_opt_long.front()[0] + : !m_opt_short.empty() ? m_opt_short.front() + : '0'; + + const char ch22 = !rhs.m_opt_long.empty() ? rhs.m_opt_long.front()[0] + : !rhs.m_opt_short.empty() ? rhs.m_opt_short.front() + : '0'; + + if (ch1 != ch22) + { + return ch1 < ch22; + } + + if (!m_opt_long.empty() || !rhs.m_opt_long.empty()) + { + return !m_opt_long.empty(); + } + + return std::strcmp(m_opt_long.front(), rhs.m_opt_long.front()) < 0; +} + +void Parser::print_other_usages(FILE* stream) const +{ + if (m_argp->doc != nullptr) + { + std::istringstream iss(m_argp->doc); + std::string str; + + std::getline(iss, str, '\n'); + std::ignore = std::fprintf(stream, " %s", str.c_str()); + + while (std::getline(iss, str, '\n')) + { + std::ignore = std::fprintf( + stream, "\n or: %s [OPTIONS...] %s", m_name.c_str(), str.c_str()); + } + } +} + +void Parser::help(FILE* stream) const +{ + std::string msg1; + std::string msg2; + + if (m_argp->message != nullptr) + { + std::istringstream iss(m_argp->message); + std::getline(iss, msg1, '\v'); + std::getline(iss, msg2, '\v'); + } + + std::ignore = std::fprintf(stream, "Usage: %s [OPTIONS...]", m_name.c_str()); + print_other_usages(stream); + + if (!msg1.empty()) std::ignore = std::fprintf(stream, "\n%s", msg1.c_str()); + std::ignore = std::fprintf(stream, "\n\n"); + + bool first = true; + for (const auto& entry : m_help_entries) + { + bool prev = false; + + if (entry.m_opt_short.empty() && entry.m_opt_long.empty()) + { + if (!first) std::ignore = std::putc('\n', stream); + if (entry.m_message != nullptr) + std::ignore = std::fprintf(stream, " %s:\n", entry.m_message); + + continue; + } + + first = false; + + std::string message = " "; + for (const char shrt : entry.m_opt_short) + { + if (!prev) prev = true; + else message += ", "; + + message += std::format("-{}", shrt); + + if ((entry.m_arg == nullptr) || !entry.m_opt_long.empty()) continue; + + if (entry.m_opt) message += std::format("[{}]", entry.m_arg); + else message += std::format(" {}", entry.m_arg); + } + + if (!prev) message += " "; + + for (const auto* const lng : entry.m_opt_long) + { + if (!prev) prev = true; + else message += ", "; + + message += std::format("--{}", lng); + + if (entry.m_arg == nullptr) continue; + + if (entry.m_opt) message += std::format("[={}]", entry.m_arg); + else message += std::format("={}", entry.m_arg); + } + + static const int limit = 30; + if (size(message) < limit) + message += std::string(limit - size(message), ' '); + + std::ignore = std::fprintf(stream, "%s", message.c_str()); + + if (entry.m_message != nullptr) + { + std::istringstream iss(entry.m_message); + std::size_t count = 0; + std::string str; + + std::ignore = std::fprintf(stream, " "); + while (iss >> str) + { + count += size(str); + if (count > limit) + { + std::ignore = std::fprintf(stream, "\n%*c", limit + 5, ' '); + count = size(str); + } + std::ignore = std::fprintf(stream, "%s ", str.c_str()); + } + } + std::ignore = std::putc('\n', stream); + } + + if (!msg2.empty()) + std::ignore = std::fprintf(stream, "\n%s\n", msg2.c_str()); +} + +void Parser::usage(FILE* stream) const +{ + static const std::size_t limit = 60; + static std::size_t count = 0; + + const auto print = [&stream](const std::string& message) + { + if (count + size(message) > limit) + count = static_cast<std::size_t>(std::fprintf(stream, "\n ")); + + std::ignore = std::fprintf(stream, "%s", message.c_str()); + count += size(message); + }; + + std::string message = std::format("Usage: {}", m_name); + + message += " [-"; + for (const auto& entry : m_help_entries) + { + if (entry.m_arg != nullptr) continue; + + for (const char shrt : entry.m_opt_short) message += shrt; + } + message += "]"; + + std::ignore = std::fprintf(stream, "%s", message.c_str()); + count = size(message); + + for (const auto& entry : m_help_entries) + { + if (entry.m_arg == nullptr) continue; + for (const char shrt : entry.m_opt_short) + { + if (entry.m_opt) print(std::format(" [-{}[{}]]", shrt, entry.m_arg)); + else print(std::format(" [-{} {}]", shrt, entry.m_arg)); + } + } + + for (const auto& entry : m_help_entries) + { + for (const char* name : entry.m_opt_long) + { + if (entry.m_arg == nullptr) + { + print(std::format(" [--{}]", name)); + continue; + } + + if (entry.m_opt) print(std::format(" [--{}[={}]]", name, entry.m_arg)); + else print(std::format(" [--{}={}]", name, entry.m_arg)); + } + } + + print_other_usages(stream); + std::ignore = std::putc('\n', stream); +} + +void Parser::see(FILE* stream) const +{ + std::ignore = + std::fprintf(stream, + "Try '%s --help' or '%s --usage' for more information\n", + m_name.c_str(), + m_name.c_str()); +} + +} // namespace poafloc diff --git a/source/poafloc.cpp b/source/poafloc.cpp @@ -0,0 +1,379 @@ +#include <algorithm> +#include <cstring> +#include <format> +#include <iostream> +#include <sstream> + +#include "poafloc/poafloc.hpp" + +namespace poafloc { + +int parse(const arg_t* argp, + int argc, + char* argv[], + unsigned flags, + void* input) noexcept +{ + Parser parser(argp, flags, input); + return parser.parse(static_cast<std::size_t>(argc), argv); +} + +void usage(const Parser* parser) +{ + help(parser, stderr, Help::STD_USAGE); +} + +void help(const Parser* parser, FILE* stream, unsigned flags) +{ + if ((parser == nullptr) || (stream == nullptr)) return; + + if ((flags & LONG) != 0U) parser->help(stream); + else if ((flags & USAGE) != 0U) parser->usage(stream); + else if ((flags & SEE) != 0U) parser->see(stream); + + if ((parser->flags() & NO_EXIT) != 0U) return; + + if ((flags & EXIT_ERR) != 0U) exit(2); + if ((flags & EXIT_OK) != 0U) exit(0); +} + +void failure(const Parser* parser, + int status, + int errnum, + const char* fmt, + std::va_list args) +{ + (void)errnum; + + std::cerr << parser->name() << ": "; + std::ignore = std::vfprintf(stderr, fmt, args); // NOLINT + std::cerr << '\n'; + + if (status != 0) exit(status); +} + +void failure( + const Parser* parser, int status, int errnum, const char* fmt, ...) +{ + std::va_list args; + va_start(args, fmt); + failure(parser, status, errnum, fmt, args); + va_end(args); +} + +Parser::Parser(const arg_t* argp, unsigned flags, void* input) + : m_argp(argp) + , m_flags(flags) + , m_input(input) +{ + int group = 0; + int key_last = 0; + bool hidden = false; + + for (int i = 0; true; i++) + { + const auto& opt = argp->options[i]; // NOLINT + + if ((opt.name == nullptr) && (opt.key == 0) && (opt.message == nullptr)) + break; + + if ((opt.name == nullptr) && (opt.key == 0)) + { + group = opt.group != 0U ? opt.group : group + 1; + m_help_entries.emplace_back(nullptr, opt.message, group); + continue; + } + + if (opt.key == 0) + { + // non alias without a key, silently ignoring + if ((opt.flags & ALIAS) == 0) continue; + + // nothing to alias, silently ignoring + if (key_last == 0) continue; + + // option not valid, silently ignoring + if (!m_trie.insert(opt.name, key_last)) continue; + + if (hidden) continue; + if ((opt.flags & Option::HIDDEN) != 0U) continue; + + m_help_entries.back().push(opt.name); + } + else + { + // duplicate key, silently ignoring + if (!m_options.contains(opt.key)) continue; + + if (opt.name != nullptr) m_trie.insert(opt.name, opt.key); + m_options[key_last = opt.key] = &opt; + + bool const arg_opt = (opt.flags & Option::ARG_OPTIONAL) != 0U; + + if ((opt.flags & ALIAS) == 0U) + { + hidden = (opt.flags & Option::HIDDEN) != 0; + if (hidden) continue; + + m_help_entries.emplace_back(opt.arg, opt.message, group, arg_opt); + + if (opt.name != nullptr) m_help_entries.back().push(opt.name); + + if (std::isprint(opt.key) != 0U) + { + m_help_entries.back().push(static_cast<char>(opt.key & 0xFF)); + } + } + else + { + // nothing to alias, silently ignoring + if (key_last == 0) continue; + if (hidden) continue; + if ((opt.flags & Option::HIDDEN) != 0U) continue; + + if (opt.name != nullptr) m_help_entries.back().push(opt.name); + if (std::isprint(opt.key) != 0U) + { + m_help_entries.back().push(static_cast<char>(opt.key & 0xFF)); + } + } + } + } + + if ((m_flags & NO_HELP) == 0U) + { + m_help_entries.emplace_back(nullptr, "Give this help list", -1); + m_help_entries.back().push("help"); + m_help_entries.back().push('?'); + m_help_entries.emplace_back(nullptr, "Give a short usage message", -1); + m_help_entries.back().push("usage"); + } + + std::sort(begin(m_help_entries), end(m_help_entries)); +} + +int Parser::parse(std::size_t argc, char* argv[]) +{ + const std::span args(argv, argv + argc); + + std::vector<const char*> args_free; + std::size_t idx = 0; + int arg_cnt = 0; + int err_code = 0; + + const bool is_help = (m_flags & NO_HELP) == 0U; + const bool is_error = (m_flags & NO_ERRS) == 0U; + + m_name = basename(args[0]); + m_argp->parse(Key::INIT, nullptr, this); + + for (idx = 1; idx < argc; idx++) + { + if (args[idx][0] != '-') + { + if ((m_flags & IN_ORDER) != 0U) m_argp->parse(Key::ARG, args[idx], this); + else args_free.push_back(args[idx]); + + arg_cnt++; + continue; + } + + // stop parsing options, rest are normal arguments + if (std::strcmp(args[idx], "--") == 0) break; + + if (args[idx][1] != '-') + { // short option + const std::string opt = args[idx] + 1; + + // loop over ganged options + for (std::size_t j = 0; opt[j] != 0; j++) + { + const char key = opt[j]; + + if (is_help && key == '?') + { + if (is_error) ::poafloc::help(this, stderr, STD_HELP); + continue; + } + + if (!m_options.contains(key)) + { + err_code = handle_unknown(false, args[idx]); + goto error; + } + + const auto* option = m_options[key]; + bool const is_opt = (option->flags & ARG_OPTIONAL) != 0; + if (option->arg == nullptr) + { + m_argp->parse(key, nullptr, this); + } + if (opt[j + 1] != 0) + { + m_argp->parse(key, opt.substr(j + 1).c_str(), this); + break; + } + if (is_opt) m_argp->parse(key, nullptr, this); + else if (idx + 1 != argc) + { + m_argp->parse(key, args[++idx], this); + break; + } + else + { + err_code = handle_missing(true, args[idx]); + goto error; + } + } + } + else + { // long option + const std::string tmp = args[idx] + 2; + const auto pos = tmp.find_first_of('='); + const auto opt = tmp.substr(0, pos); + const auto arg = tmp.substr(pos + 1); + + if (is_help && opt == "help") + { + if (pos != std::string::npos) + { + err_code = handle_excess(args[idx]); + goto error; + } + + if (!is_error) continue; + ::poafloc::help(this, stderr, STD_HELP); + } + + if (is_help && opt == "usage") + { + if (pos != std::string::npos) + { + err_code = handle_excess(args[idx]); + goto error; + } + + if (!is_error) continue; + ::poafloc::help(this, stderr, STD_USAGE); + } + + const int key = m_trie.get(opt); + if (key == 0) + { + err_code = handle_unknown(false, args[idx]); + goto error; + } + + const auto* option = m_options[key]; + if (pos != std::string::npos && option->arg == nullptr) + { + err_code = handle_excess(args[idx]); + goto error; + } + + const bool is_opt = (option->flags & ARG_OPTIONAL) != 0; + + if (option->arg == nullptr) + { + m_argp->parse(key, nullptr, this); + continue; + } + + if (pos != std::string::npos) + { + m_argp->parse(key, arg.c_str(), this); + continue; + } + + if (is_opt) + { + m_argp->parse(key, nullptr, this); + continue; + } + + if (idx + 1 != argc) + { + m_argp->parse(key, args[++idx], this); + continue; + } + + err_code = handle_missing(false, args[idx]); + goto error; + } + } + + // parse previous arguments if IN_ORDER is not set + for (const auto* const arg : args_free) m_argp->parse(Key::ARG, arg, this); + + // parse rest argv as normal arguments + for (idx = idx + 1; idx < argc; idx++) + { + m_argp->parse(Key::ARG, args[idx], this); + arg_cnt++; + } + + if (arg_cnt == 0) m_argp->parse(Key::NO_ARGS, nullptr, this); + + m_argp->parse(Key::END, nullptr, this); + m_argp->parse(Key::SUCCESS, nullptr, this); + + return 0; + +error: + return err_code; +} + +int Parser::handle_unknown(bool shrt, const char* argv) +{ + if ((m_flags & NO_ERRS) != 0U) + return m_argp->parse(Key::ERROR, nullptr, this); + + static const char* const unknown_fmt[2] = { + "unrem_argpized option '-%s'\n", + "invalid option -- '%s'\n", + }; + + failure(this, 1, 0, unknown_fmt[shrt], argv + 1); // NOLINT + see(stderr); + + if ((m_flags & NO_EXIT) != 0U) return 1; + exit(1); +} + +int Parser::handle_missing(bool shrt, const char* argv) +{ + if ((m_flags & NO_ERRS) != 0U) + return m_argp->parse(Key::ERROR, nullptr, this); + + static const char* const missing_fmt[2] = { + "option '-%s' requires an argument\n", + "option requires an argument -- '%s'\n", + }; + + failure(this, 2, 0, missing_fmt[shrt], argv + 1); // NOLINT + see(stderr); + + if ((m_flags & NO_EXIT) != 0U) return 2; + exit(2); +} + +int Parser::handle_excess(const char* argv) +{ + if ((m_flags & NO_ERRS) != 0U) + { + return m_argp->parse(Key::ERROR, nullptr, this); + } + + failure(this, 3, 0, "option '%s' doesn't allow an argument\n", argv); + see(stderr); + + if ((m_flags & NO_EXIT) != 0U) return 3; + exit(3); +} + +std::string Parser::basename(const std::string& name) +{ + return name.substr(name.find_first_of('/') + 1); +} + +} // namespace poafloc diff --git a/source/trie.cpp b/source/trie.cpp @@ -0,0 +1,65 @@ +#include <algorithm> +#include <cstdint> + +#include "poafloc/poafloc.hpp" + +namespace poafloc { + +bool Parser::trie_t::insert(const std::string& option, int key) +{ + trie_t* crnt = this; + + if (!is_valid(option)) + { + return false; + } + + for (const char chr : option) + { + if (!crnt->m_terminal) crnt->m_key = key; + crnt->m_count++; + + const size_t idx = static_cast<unsigned>(chr) - 'a'; + if (!crnt->m_children.at(idx)) + crnt->m_children.at(idx) = std::make_unique<trie_t>(); + + crnt = crnt->m_children.at(idx).get(); + } + + crnt->m_terminal = true; + crnt->m_key = key; + + return true; +} + +int Parser::trie_t::get(const std::string& option) const +{ + const trie_t* crnt = this; + + if (!is_valid(option)) + { + return 0; + } + + for (const char chr : option) + { + const size_t idx = static_cast<unsigned>(chr) - 'a'; + if (!crnt->m_children.at(idx)) return 0; + + crnt = crnt->m_children.at(idx).get(); + } + + if (!crnt->m_terminal && crnt->m_count > 1) + { + return 0; + } + return crnt->m_key; +} + +bool Parser::trie_t::is_valid(const std::string& option) +{ + return std::all_of( + begin(option), end(option), [](char chr) { return std::islower(chr); }); +} + +} // namespace poafloc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt @@ -1,14 +0,0 @@ -add_library(poafloc STATIC poafloc.cpp help.cpp trie.cpp c_bindings.cpp) -target_include_directories(poafloc PRIVATE ../include) -target_compile_definitions(poafloc PRIVATE WITH_C_BINDINGS) -set_target_properties(poafloc PROPERTIES LINKER_LANGUAGE CXX) - -set_target_properties(poafloc PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} - OUTPUT_NAME "poafloc" - DEBUG_POSTFIX "d" - PUBLIC_HEADER "include/poafloc.hpp;include/poafloc.h" - MACOSX_RPATH ON - WINDOWS_EXPORT_ALL_SYMBOLS ON -) diff --git a/src/c_bindings.cpp b/src/c_bindings.cpp @@ -1,29 +0,0 @@ -#include "poafloc.h" -#include "poafloc.hpp" - -#include <cstdarg> - -namespace poafloc { - -int poafloc_parse(const poafloc_arg_t *argp, int argc, char *argv[], unsigned flags, - void *input) { - return parse(argp, argc, argv, flags, input); -} - -void *poafloc_parser_input(poafloc_parser_t *parser) { return parser->input(); } - -void poafloc_usage(poafloc_parser_t *parser) { return usage(parser); } - -void poafloc_help(const poafloc_parser_t *parser, FILE *stream, unsigned flags) { - help(parser, stream, flags); -} - -void poafloc_failure(const poafloc_parser_t *parser, int status, int errnum, - const char *fmt, ...) { - std::va_list args; - va_start(args, fmt); - failure(parser, status, errnum, fmt, args); - va_end(args); -} - -} // namespace args diff --git a/src/help.cpp b/src/help.cpp @@ -1,189 +0,0 @@ -#include "poafloc.hpp" - -#include <cstring> -#include <format> -#include <sstream> - -namespace poafloc { - -bool Parser::help_entry_t::operator<(const help_entry_t &rhs) const { - if (group != rhs.group) { - if (group && rhs.group) { - if (group < 0 && rhs.group < 0) return group < rhs.group; - if (group < 0 || rhs.group < 0) return rhs.group < 0; - return group < rhs.group; - } - - return !group; - } - - const char l1 = !opt_long.empty() ? opt_long.front()[0] - : !opt_short.empty() ? opt_short.front() - : '0'; - - const char l2 = !rhs.opt_long.empty() ? rhs.opt_long.front()[0] - : !rhs.opt_short.empty() ? rhs.opt_short.front() - : '0'; - - if (l1 != l2) return l1 < l2; - if (!opt_long.empty() || !rhs.opt_long.empty()) return !opt_long.empty(); - return std::strcmp(opt_long.front(), rhs.opt_long.front()) < 0; -} - -void Parser::print_other_usages(FILE *stream) const { - if (argp->doc) { - std::istringstream iss(argp->doc); - std::string s; - - std::getline(iss, s, '\n'); - std::fprintf(stream, " %s", s.c_str()); - - while (std::getline(iss, s, '\n')) { - std::fprintf(stream, "\n or: %s [OPTIONS...] %s", m_name, - s.c_str()); - } - } -} - -void Parser::help(FILE *stream) const { - std::string m1, m2; - if (argp->message) { - std::istringstream iss(argp->message); - std::getline(iss, m1, '\v'); - std::getline(iss, m2, '\v'); - } - - std::fprintf(stream, "Usage: %s [OPTIONS...]", m_name); - print_other_usages(stream); - - if (!m1.empty()) std::fprintf(stream, "\n%s", m1.c_str()); - std::fprintf(stream, "\n\n"); - - bool first = true; - for (const auto &entry : help_entries) { - bool prev = false; - - if (entry.opt_short.empty() && entry.opt_long.empty()) { - if (!first) std::putc('\n', stream); - if (entry.message) std::fprintf(stream, " %s:\n", entry.message); - continue; - } - - first = false; - - std::string message = " "; - for (const char c : entry.opt_short) { - if (!prev) prev = true; - else message += ", "; - - message += std::format("-{}", c); - - if (!entry.arg || !entry.opt_long.empty()) continue; - - if (entry.opt) message += std::format("[{}]", entry.arg); - else message += std::format(" {}", entry.arg); - } - - if (!prev) message += " "; - - for (const auto l : entry.opt_long) { - if (!prev) prev = true; - else message += ", "; - - message += std::format("--{}", l); - - if (!entry.arg) continue; - - if (entry.opt) message += std::format("[={}]", entry.arg); - else message += std::format("={}", entry.arg); - } - - static const int limit = 30; - if (size(message) < limit) { - message += std::string(limit - size(message), ' '); - } - - std::fprintf(stream, "%s", message.c_str()); - - if (entry.message) { - std::istringstream iss(entry.message); - std::size_t count = 0; - std::string s; - - std::fprintf(stream, " "); - while (iss >> s) { - count += size(s); - if (count > limit) { - std::fprintf(stream, "\n%*c", limit + 5, ' '); - count = size(s); - } - std::fprintf(stream, "%s ", s.c_str()); - } - } - std::putc('\n', stream); - } - - if (!m2.empty()) std::fprintf(stream, "\n%s\n", m2.c_str()); -} - -void Parser::usage(FILE *stream) const { - static const std::size_t limit = 60; - static std::size_t count = 0; - - static const auto print = [&stream](const std::string &message) { - if (count + size(message) > limit) { - std::fprintf(stream, "\n "); - count = 6; - } - std::fprintf(stream, "%s", message.c_str()); - count += size(message); - }; - - std::string message = std::format("Usage: {}", m_name); - - message += " [-"; - for (const auto &entry : help_entries) { - if (entry.arg) continue; - for (const char c : entry.opt_short) { - message += c; - } - } - message += "]"; - - std::fprintf(stream, "%s", message.c_str()); - count = size(message); - - for (const auto &entry : help_entries) { - if (!entry.arg) continue; - for (const char c : entry.opt_short) { - if (entry.opt) print(std::format(" [-{}[{}]]", c, entry.arg)); - else print(std::format(" [-{} {}]", c, entry.arg)); - } - } - - for (const auto &entry : help_entries) { - for (const char *name : entry.opt_long) { - if (!entry.arg) { - print(std::format(" [--{}]", name)); - continue; - } - - if (entry.opt) { - print(std::format(" [--{}[={}]]", name, entry.arg)); - } else { - print(std::format(" [--{}={}]", name, entry.arg)); - } - } - } - - print_other_usages(stream); - std::putc('\n', stream); -} - -void Parser::see(FILE *stream) const { - std::fprintf(stream, - "Try '%s --help' or '%s --usage' for more information\n", - m_name, m_name); -} - -} // namespace args diff --git a/src/poafloc.cpp b/src/poafloc.cpp @@ -1,287 +0,0 @@ -#include "poafloc.hpp" - -#include <algorithm> -#include <cstring> -#include <format> -#include <iostream> -#include <sstream> - -namespace poafloc { - -int parse(const arg_t *argp, int argc, char *argv[], unsigned flags, - void *input) noexcept { - Parser parser(argp, flags, input); - return parser.parse(argc, argv); -} - -void usage(const Parser *parser) { help(parser, stderr, Help::STD_USAGE); } -void help(const Parser *parser, FILE *stream, unsigned flags) { - if (!parser || !stream) return; - - if (flags & LONG) parser->help(stream); - else if (flags & USAGE) parser->usage(stream); - else if (flags & SEE) parser->see(stream); - - if (parser->flags() & NO_EXIT) return; - if (flags & EXIT_ERR) exit(2); - if (flags & EXIT_OK) exit(0); -} - -void failure(const Parser *parser, int status, int errnum, const char *fmt, - std::va_list args) { - (void)errnum; - std::fprintf(stderr, "%s: ", parser->name()); - std::vfprintf(stderr, fmt, args); - std::putc('\n', stderr); - if (status) exit(status); -} - -void failure(const Parser *parser, int status, int errnum, const char *fmt, - ...) { - std::va_list args; - va_start(args, fmt); - failure(parser, status, errnum, fmt, args); - va_end(args); -} - -Parser::Parser(const arg_t *argp, unsigned flags, void *input) - : argp(argp), m_flags(flags), m_input(input) { - int group = 0, key_last = 0; - bool hidden = false; - - for (int i = 0; true; i++) { - const auto &opt = argp->options[i]; - if (!opt.name && !opt.key && !opt.message) break; - - if (!opt.name && !opt.key) { - group = opt.group ? opt.group : group + 1; - help_entries.emplace_back(nullptr, opt.message, group); - continue; - } - - if (!opt.key) { - // non alias without a key, silently ignoring - if (!(opt.flags & ALIAS)) continue; - - // nothing to alias, silently ignoring - if (!key_last) continue; - - // option not valid, silently ignoring - if (!trie.insert(opt.name, key_last)) continue; - - if (hidden) continue; - if (opt.flags & Option::HIDDEN) continue; - - help_entries.back().push(opt.name); - } else { - // duplicate key, silently ignoring - if (options.count(opt.key)) continue; - - if (opt.name) trie.insert(opt.name, opt.key); - options[key_last = opt.key] = &opt; - - bool arg_opt = opt.flags & Option::ARG_OPTIONAL; - - if (!(opt.flags & ALIAS)) { - if ((hidden = opt.flags & Option::HIDDEN)) continue; - - help_entries.emplace_back(opt.arg, opt.message, group, - arg_opt); - - if (opt.name) help_entries.back().push(opt.name); - if (std::isprint(opt.key)) help_entries.back().push(opt.key); - } else { - // nothing to alias, silently ignoring - if (!key_last) continue; - - if (hidden) continue; - if (opt.flags & Option::HIDDEN) continue; - - if (opt.name) help_entries.back().push(opt.name); - if (std::isprint(opt.key)) help_entries.back().push(opt.key); - } - } - } - - if (!(m_flags & NO_HELP)) { - help_entries.emplace_back(nullptr, "Give this help list", -1); - help_entries.back().push("help"); - help_entries.back().push('?'); - - help_entries.emplace_back(nullptr, "Give a short usage message", -1); - help_entries.back().push("usage"); - } - - std::sort(begin(help_entries), end(help_entries)); -} - -int Parser::parse(int argc, char *argv[]) { - std::vector<const char *> args; - int arg_cnt = 0, err_code = 0, i; - - const bool is_help = !(m_flags & NO_HELP); - const bool is_error = !(m_flags & NO_ERRS); - - m_name = basename(argv[0]); - - argp->parse(Key::INIT, 0, this); - - for (i = 1; i < argc; i++) { - if (argv[i][0] != '-') { - if (m_flags & IN_ORDER) argp->parse(Key::ARG, argv[i], this); - else args.push_back(argv[i]); - arg_cnt++; - continue; - } - - // stop parsing options, rest are normal arguments - if (!std::strcmp(argv[i], "--")) break; - - if (argv[i][1] != '-') { // short option - const char *opt = argv[i] + 1; - - // loop over ganged options - for (int j = 0; opt[j]; j++) { - const char key = opt[j]; - - if (is_help && key == '?') { - if (is_error) ::poafloc::help(this, stderr, STD_HELP); - continue; - } - - if (!options.count(key)) { - err_code = handle_unknown(1, argv[i]); - goto error; - } - - const auto *option = options[key]; - bool is_opt = option->flags & ARG_OPTIONAL; - if (!option->arg) argp->parse(key, nullptr, this); - if (opt[j + 1] != 0) { - argp->parse(key, opt + j + 1, this); - break; - } else if (is_opt) argp->parse(key, nullptr, this); - else if (i + 1 != argc) { - argp->parse(key, argv[++i], this); - break; - } else { - err_code = handle_missing(1, argv[i]); - goto error; - } - } - } else { // long option - const char *opt = argv[i] + 2; - const auto is_eq = std::strchr(opt, '='); - - std::string opt_s = !is_eq ? opt : std::string(opt, is_eq - opt); - - if (is_help && opt_s == "help") { - if (is_eq) { - err_code = handle_excess(argv[i]); - goto error; - } - if (is_error) ::poafloc::help(this, stderr, STD_HELP); - continue; - } - - if (is_help && opt_s == "usage") { - if (is_eq) { - err_code = handle_excess(argv[i]); - goto error; - } - if (is_error) ::poafloc::help(this, stderr, STD_USAGE); - continue; - } - - const int key = trie.get(opt_s.data()); - if (!key) { - err_code = handle_unknown(0, argv[i]); - goto error; - } - - const auto *option = options[key]; - if (!option->arg && is_eq) { - err_code = handle_excess(argv[i]); - goto error; - } - - bool is_opt = option->flags & ARG_OPTIONAL; - if (!option->arg) argp->parse(key, nullptr, this); - else if (is_eq) argp->parse(key, is_eq + 1, this); - else if (is_opt) argp->parse(key, nullptr, this); - else if (i + 1 != argc) argp->parse(key, argv[++i], this); - else { - err_code = handle_missing(0, argv[i]); - goto error; - } - } - } - - // parse previous arguments if IN_ORDER is not set - for (const auto arg : args) { - argp->parse(Key::ARG, arg, this); - } - - // parse rest argv as normal arguments - for (i = i + 1; i < argc; i++) { - argp->parse(Key::ARG, argv[i], this); - arg_cnt++; - } - - if (!arg_cnt) argp->parse(Key::NO_ARGS, 0, this); - - argp->parse(Key::END, 0, this); - argp->parse(Key::SUCCESS, 0, this); - - return 0; - -error: - return err_code; -} - -int Parser::handle_unknown(bool shrt, const char *argv) { - if (m_flags & NO_ERRS) return argp->parse(Key::ERROR, 0, this); - - static const char *const unknown_fmt[2] = { - "unrecognized option '-%s'\n", - "invalid option -- '%s'\n", - }; - - failure(this, 1, 0, unknown_fmt[shrt], argv + 1); - see(stderr); - - if (m_flags & NO_EXIT) return 1; - exit(1); -} - -int Parser::handle_missing(bool shrt, const char *argv) { - if (m_flags & NO_ERRS) return argp->parse(Key::ERROR, 0, this); - - static const char *const missing_fmt[2] = { - "option '-%s' requires an argument\n", - "option requires an argument -- '%s'\n", - }; - - failure(this, 2, 0, missing_fmt[shrt], argv + 1); - see(stderr); - - if (m_flags & NO_EXIT) return 2; - exit(2); -} - -int Parser::handle_excess(const char *argv) { - if (m_flags & NO_ERRS) return argp->parse(Key::ERROR, 0, this); - - failure(this, 3, 0, "option '%s' doesn't allow an argument\n", argv); - see(stderr); - - if (m_flags & NO_EXIT) return 3; - exit(3); -} - -const char *Parser::basename(const char *name) { - const char *name_sh = std::strrchr(name, '/'); - return name_sh ? name_sh + 1 : name; -} - -} // namespace args diff --git a/src/trie.cpp b/src/trie.cpp @@ -1,53 +0,0 @@ -#include "poafloc.hpp" - -#include <cstdint> - -namespace poafloc { - -Parser::trie_t::~trie_t() noexcept { - for (uint8_t i = 0; i < 26; i++) { - delete children[i]; - } -} - -bool Parser::trie_t::insert(const char *option, int key) { - trie_t *crnt = this; - - if (!is_valid(option)) return false; - for (; *option; option++) { - if (!crnt->terminal) crnt->key = key; - crnt->count++; - - const uint8_t idx = *option - 'a'; - if (!crnt->children[idx]) crnt->children[idx] = new trie_t(); - crnt = crnt->children[idx]; - } - - crnt->terminal = true; - crnt->key = key; - - return true; -} - -int Parser::trie_t::get(const char *option) const { - const trie_t *crnt = this; - - if (!is_valid(option)) return 0; - for (; *option; option++) { - const uint8_t idx = *option - 'a'; - if (!crnt->children[idx]) return 0; - crnt = crnt->children[idx]; - } - - if (!crnt->terminal && crnt->count > 1) return 0; - return crnt->key; -} - -bool Parser::trie_t::is_valid(const char *option) { - for (; *option; option++) { - if (!std::islower(*option)) return false; - } - return true; -} - -} // namespace args diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.14) + +project(poaflocTests LANGUAGES CXX) + +include(../cmake/project-is-top-level.cmake) +include(../cmake/folders.cmake) + +# ---- Dependencies ---- + +if(PROJECT_IS_TOP_LEVEL) + find_package(poafloc REQUIRED) + enable_testing() +endif() + +# ---- Tests ---- + +add_executable(poafloc_test source/poafloc_test.cpp) +target_link_libraries(poafloc_test PRIVATE poafloc::poafloc) +target_compile_features(poafloc_test PRIVATE cxx_std_20) + +add_test(NAME poafloc_test COMMAND poafloc_test) + +# ---- End-of-file commands ---- + +add_folders(Test) diff --git a/test/source/poafloc_test.cpp b/test/source/poafloc_test.cpp @@ -0,0 +1,8 @@ +#include <string> + +#include "poafloc/poafloc.hpp" + +int main() +{ + return 0; +}