├── .clang-format ├── .github └── workflows │ └── cmake.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── dep └── CMakeLists.txt ├── examples ├── CMakeLists.txt ├── custom_type_conversion.cpp ├── decode_ini_file.cpp ├── encode_ini_file.cpp ├── load_ini_file.cpp └── save_ini_file.cpp ├── include └── inicpp.h └── test ├── CMakeLists.txt ├── main.cpp └── test_inifile.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: DontAlign 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlines: Right 9 | AlignOperands: true 10 | AlignTrailingComments: false 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: None 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterDefinitionReturnType: None 18 | AlwaysBreakAfterReturnType: None 19 | AlwaysBreakBeforeMultilineStrings: false 20 | AlwaysBreakTemplateDeclarations: true 21 | BinPackArguments: false 22 | BinPackParameters: false 23 | BreakBeforeBraces: Custom 24 | BraceWrapping: 25 | AfterClass: true 26 | AfterControlStatement: true 27 | AfterEnum: true 28 | AfterFunction: true 29 | AfterNamespace: true 30 | AfterObjCDeclaration: true 31 | AfterStruct: true 32 | AfterUnion: true 33 | #AfterExternBlock: true 34 | BeforeCatch: true 35 | BeforeElse: true 36 | IndentBraces: false 37 | SplitEmptyFunction: false 38 | SplitEmptyRecord: false 39 | SplitEmptyNamespace: false 40 | BreakBeforeBinaryOperators: None 41 | BreakBeforeInheritanceComma: false 42 | BreakBeforeTernaryOperators: false 43 | BreakConstructorInitializersBeforeComma: false 44 | BreakConstructorInitializers: BeforeColon 45 | BreakAfterJavaFieldAnnotations: false 46 | BreakStringLiterals: true 47 | ColumnLimit: 120 48 | CommentPragmas: '^ IWYU pragma:' 49 | CompactNamespaces: false 50 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 51 | ConstructorInitializerIndentWidth: 4 52 | ContinuationIndentWidth: 4 53 | Cpp11BracedListStyle: true 54 | DerivePointerAlignment: false 55 | DisableFormat: false 56 | ExperimentalAutoDetectBinPacking: false 57 | FixNamespaceComments: false 58 | ForEachMacros: 59 | - foreach 60 | - Q_FOREACH 61 | - BOOST_FOREACH 62 | IncludeCategories: 63 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 64 | Priority: 2 65 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 66 | Priority: 3 67 | - Regex: '.*' 68 | Priority: 1 69 | IncludeIsMainRegex: '(Test)?$' 70 | IndentCaseLabels: false 71 | IndentWidth: 4 72 | IndentWrappedFunctionNames: false 73 | JavaScriptQuotes: Leave 74 | JavaScriptWrapImports: true 75 | KeepEmptyLinesAtTheStartOfBlocks: false 76 | MacroBlockBegin: '' 77 | MacroBlockEnd: '' 78 | MaxEmptyLinesToKeep: 1 79 | NamespaceIndentation: All 80 | ObjCBlockIndentWidth: 2 81 | ObjCSpaceAfterProperty: false 82 | ObjCSpaceBeforeProtocolList: true 83 | PenaltyBreakAssignment: 2 84 | PenaltyBreakBeforeFirstCallParameter: 19 85 | PenaltyBreakComment: 300 86 | PenaltyBreakFirstLessLess: 120 87 | PenaltyBreakString: 1000 88 | PenaltyExcessCharacter: 1000000 89 | PenaltyReturnTypeOnItsOwnLine: 60 90 | PointerAlignment: Right 91 | ReflowComments: true 92 | SortIncludes: true 93 | SortUsingDeclarations: true 94 | SpaceAfterCStyleCast: true 95 | SpaceAfterTemplateKeyword: false 96 | SpaceBeforeAssignmentOperators: true 97 | SpaceBeforeParens: Never 98 | SpaceInEmptyParentheses: false 99 | SpacesBeforeTrailingComments: 1 100 | SpacesInAngles: false 101 | SpacesInContainerLiterals: false 102 | SpacesInCStyleCastParentheses: false 103 | SpacesInParentheses: false 104 | SpacesInSquareBrackets: false 105 | Standard: Cpp11 106 | TabWidth: 4 107 | UseTab: Never 108 | -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | format: 11 | name: Check Code Format 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Check Format 16 | run: 'find test/ examples/ include/ -iname "*.h" -o -iname "*.cpp" -print0 | xargs -0 clang-format --dry-run --Werror' 17 | coverage: 18 | name: Generate Code Coverage 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | - name: Install LCOV 25 | run: | 26 | sudo apt update 27 | sudo apt install -y lcov 28 | - name: Configure 29 | run: cmake -S . -B ${{runner.workspace}}/build -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -DGENERATE_COVERAGE=ON -DINICPP_CXX_STANDARD=17 30 | - name: Build 31 | run: cmake --build ${{runner.workspace}}/build --config Debug 32 | - name: Test 33 | run: ctest -C Debug --output-on-failure --test-dir ${{runner.workspace}}/build 34 | - name: Generate Coverage 35 | run: | 36 | lcov --capture --directory ${{runner.workspace}}/build --include '*/inicpp.h' --output-file ${{runner.workspace}}/lcov.info 37 | lcov --list ${{runner.workspace}}/lcov.info 38 | - name: Codecov 39 | uses: codecov/codecov-action@v4 40 | env: 41 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 42 | with: 43 | file: ${{runner.workspace}}/lcov.info 44 | verbose: true 45 | fail_ci_if_error: true 46 | build: 47 | name: "${{matrix.config.name}} C++${{matrix.cxx}}" 48 | runs-on: ${{matrix.config.os}} 49 | strategy: 50 | matrix: 51 | config: 52 | - os: ubuntu-latest 53 | name: Ubuntu Debug 54 | build_type: Debug 55 | - os: ubuntu-latest 56 | name: Ubuntu Release 57 | build_type: Release 58 | - os: windows-latest 59 | name: Windows Debug 60 | build_type: Debug 61 | - os: windows-latest 62 | name: Windows Release 63 | build_type: Release 64 | - os: macos-latest 65 | name: Mac OS Debug 66 | build_type: Debug 67 | - os: macos-latest 68 | name: Mac OS Release 69 | build_type: Release 70 | cxx: 71 | - 11 72 | - 17 73 | steps: 74 | - uses: actions/checkout@v4 75 | with: 76 | submodules: recursive 77 | - name: Configure 78 | run: cmake -S . -B ${{runner.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.config.build_type}} -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON -DINICPP_CXX_STANDARD=${{matrix.cxx}} 79 | - name: Build 80 | run: cmake --build ${{runner.workspace}}/build --config ${{matrix.config.build_type}} 81 | - name: Test 82 | run: ctest -C ${{matrix.config.build_type}} --output-on-failure --test-dir ${{runner.workspace}}/build 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lcov.info 2 | build/ 3 | Testing/ 4 | .project 5 | .cproject 6 | .settings 7 | .vscode 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dep/Catch2"] 2 | path = dep/Catch2 3 | url = https://github.com/catchorg/Catch2.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists.txt 2 | # 3 | # Author: Fabian Meyer 4 | # Created On: 26 Dec 2015 5 | # License: MIT 6 | 7 | cmake_minimum_required(VERSION 3.15) 8 | 9 | project(inifile-cpp) 10 | 11 | include(CTest) 12 | 13 | set(INICPP_CXX_STANDARD "11" CACHE STRING "C++ standard to use when building tests & examples.") 14 | option(GENERATE_COVERAGE "Enable generating code coverage" OFF) 15 | option(BUILD_TESTS "Enable building unit tests" OFF) 16 | option(BUILD_EXAMPLES "Enable building example applications" OFF) 17 | 18 | set(CMAKE_CXX_STANDARD ${INICPP_CXX_STANDARD}) 19 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 20 | 21 | if(CMAKE_COMPILER_IS_GNUCXX) 22 | add_compile_options(-Wall -Wextra) 23 | endif(CMAKE_COMPILER_IS_GNUCXX) 24 | 25 | if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") 26 | add_compile_options(/WX /wd4530) 27 | endif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") 28 | 29 | 30 | add_subdirectory(dep) 31 | 32 | add_library(inicpp INTERFACE) 33 | target_include_directories(inicpp INTERFACE 34 | $ 35 | $) 36 | add_library(inicpp::inicpp ALIAS inicpp) 37 | install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/inicpp.h TYPE INCLUDE) 38 | 39 | if(GENERATE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") 40 | # Add required flags (GCC & LLVM/Clang) 41 | target_compile_options(inicpp INTERFACE -O0 -g --coverage) 42 | target_link_options(inicpp INTERFACE --coverage) 43 | endif(GENERATE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") 44 | 45 | if(${BUILD_TESTS}) 46 | add_subdirectory(test) 47 | endif(${BUILD_TESTS}) 48 | 49 | if(${BUILD_EXAMPLES}) 50 | add_subdirectory(examples) 51 | endif(${BUILD_EXAMPLES}) 52 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Fabian Meyer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # inifile-cpp 2 | ![License](https://img.shields.io/packagist/l/doctrine/orm.svg) 3 | [![CMake](https://github.com/Rookfighter/inifile-cpp/workflows/CMake/badge.svg)](https://github.com/Rookfighter/inifile-cpp/actions/workflows/cmake.yml) 4 | [![codecov](https://codecov.io/gh/Rookfighter/inifile-cpp/graph/badge.svg?token=39FL0C8NRK)](https://codecov.io/gh/Rookfighter/inifile-cpp) 5 | 6 | ```inifile-cpp``` is a simple and easy to use single header-only ini file en- and decoder for C++. 7 | 8 | ## Install 9 | 10 | Install the headers using the CMake build system: 11 | 12 | ```sh 13 | cd 14 | mkdir build 15 | cd build 16 | cmake .. 17 | make install 18 | ``` 19 | 20 | or simply copy the header file into your project and include it directly. 21 | 22 | ## Usage 23 | 24 | For examples on how to use and extend ```inifile-cpp``` for your custom needs, please have a look at the ```examples/``` directory. 25 | 26 | ```inifile-cpp``` allows loading data from any ```std::istream``` and requires a 27 | single function call or use the overloaded constructor. 28 | 29 | ```cpp 30 | #include 31 | 32 | int main() 33 | { 34 | // create istream object "is" ... 35 | 36 | // use function 37 | ini::IniFile myFirstIni; 38 | myFirstIni.decode(is); 39 | 40 | // or use the constructor 41 | ini::IniFile mySecondIni(is); 42 | } 43 | ``` 44 | 45 | You can directly load ini-data from files by using the ```load()``` function. It requires a file path 46 | and automatically parses its contents: 47 | 48 | ```cpp 49 | #include 50 | 51 | int main() 52 | { 53 | // load an ini file 54 | ini::IniFile myIni; 55 | myIni.load("some/ini/path"); 56 | } 57 | ``` 58 | 59 | You can enable decoding of multi-line values using the ```setMultiLineValues(true)``` function. If you do this, field values may be continued on the next line, after indentation. Each line will be separated by the `\n` character in the final value, and the indentation will be removed. 60 | 61 | ```cpp 62 | #include 63 | 64 | int main() 65 | { 66 | // load an ini file 67 | ini::IniFile myIni; 68 | myIni.setMultiLineValues(true); 69 | myIni.load("some/ini/path"); 70 | } 71 | ``` 72 | 73 | When duplicate fields are decoded the previous value is simply overwritten by default. You can disallow duplicate fields from being overwritten by using the ```allowOverwriteDuplicateFields(false)``` function. If you do this, an exception will be thrown if a duplicate field is found inside a section. 74 | 75 | ```cpp 76 | #include 77 | 78 | int main() 79 | { 80 | // load an ini file 81 | ini::IniFile myIni; 82 | myIni.allowOverwriteDuplicateFields(false); 83 | // throws an exception if the ini file has duplicate fields 84 | myIni.load("some/ini/path"); 85 | } 86 | ``` 87 | 88 | Sections and fields can be accessed using the index operator ```[]```. 89 | The values can be converted to various native types: 90 | 91 | ```cpp 92 | bool myBool = myIni["Foo"]["myBool"].as(); 93 | char myChar = myIni["Foo"]["myChar"].as(); 94 | unsigned char myUChar = myIni["Foo"]["myUChar"].as(); 95 | int myInt = myIni["Foo"]["myInt"].as(); 96 | unsigned int myUInt = myIni["Foo"]["myUInt"].as(); 97 | long myLong = myIni["Foo"]["myLong"].as(); 98 | unsigned long myULong = myIni["Foo"]["myULong"].as(); 99 | float myFloat = myIni["Foo"]["myFloat"].as(); 100 | double myDouble = myIni["Foo"]["myDouble"].as(); 101 | std::string myStr = myIni["Foo"]["myStr"].as(); 102 | const char *myStr2 = myIni["Foo"]["myStr"].as(); 103 | ``` 104 | 105 | Natively supported types are: 106 | 107 | * ```bool``` 108 | * ```char``` 109 | * ```unsigned char``` 110 | * ```short``` 111 | * ```unsigned short``` 112 | * ```int``` 113 | * ```unsigned int``` 114 | * ```long``` 115 | * ```unsigned long``` 116 | * ```float``` 117 | * ```double``` 118 | * ```std::string``` 119 | * ```const char *``` 120 | * ```std::string_view``` 121 | 122 | Custom type conversions can be added by implementing specialized template of the ```ini::Convert``` functor (see examples). 123 | 124 | Values can be assigned to ini fileds just by using the assignment operator. 125 | The content of the inifile can then be written to any ```std::ostream``` object. 126 | 127 | ```cpp 128 | #include 129 | 130 | int main() 131 | { 132 | // create ostream object "os" ... 133 | 134 | ini::IniFile myIni; 135 | 136 | myIni["Foo"]["myInt"] = 1; 137 | myIni["Foo"]["myStr"] = "Hello world"; 138 | myIni["Foo"]["myBool"] = true; 139 | myIni["Bar"]["myDouble"] = 1.2; 140 | 141 | myIni.encode(os); 142 | } 143 | ``` 144 | 145 | You can directly save ini-data to files by using the ```save()``` function. It requires a file path 146 | and automatically stores the ini file contents: 147 | 148 | ```cpp 149 | #include 150 | 151 | int main() 152 | { 153 | ini::IniFile myIni; 154 | 155 | myIni["Foo"]["myInt"] = 1; 156 | myIni["Foo"]["myStr"] = "Hello world"; 157 | myIni["Foo"]["myBool"] = true; 158 | myIni["Bar"]["myDouble"] = 1.2; 159 | 160 | myIni.save("some/ini/path"); 161 | } 162 | ``` 163 | 164 | You can define custom type conversions for inifile-cpp which will be automatically used by the assignment operator and the ```as()``` method of ini fields, e.g. you can add support for ```std::vector``` (see also examples): 165 | 166 | ```cpp 167 | // the conversion functor must live in the "ini" namespace 168 | namespace ini 169 | { 170 | /** Conversion functor to parse std::vectors from an ini field- 171 | * The generic template can be passed down to the vector. */ 172 | template 173 | struct Convert> 174 | { 175 | /** Decodes a std::vector from a string. */ 176 | void decode(const std::string &value, std::vector &result) 177 | { 178 | result.clear(); 179 | 180 | // variable to store the decoded value of each element 181 | T decoded; 182 | // maintain a start and end pos within the string 183 | size_t startPos = 0; 184 | size_t endPos = 0; 185 | size_t cnt; 186 | 187 | while(endPos != std::string::npos) 188 | { 189 | if(endPos != 0) 190 | startPos = endPos + 1; 191 | // search for the next comma as separator 192 | endPos = value.find(',', startPos); 193 | 194 | // if no comma was found use the rest of the string 195 | // as input 196 | if(endPos == std::string::npos) 197 | cnt = value.size() - startPos; 198 | else 199 | cnt = endPos - startPos; 200 | 201 | std::string tmp = value.substr(startPos, cnt); 202 | // use the conversion functor for the type contained in 203 | // the vector, so the vector can use any type that 204 | // is compatible with inifile-cpp 205 | Convert conv; 206 | conv.decode(tmp, decoded); 207 | result.push_back(decoded); 208 | 209 | } 210 | } 211 | 212 | /** Encodes a std::vector to a string. */ 213 | void encode(const std::vector &value, std::string &result) 214 | { 215 | // variable to store the encoded element value 216 | std::string encoded; 217 | // string stream to build the result stream 218 | std::stringstream ss; 219 | for(size_t i = 0; i < value.size(); ++i) 220 | { 221 | // use the conversion functor for the type contained in 222 | // the vector, so the vector can use any type that 223 | // is compatible with inifile-cp 224 | Convert conv; 225 | conv.encode(value[i], encoded); 226 | ss << encoded; 227 | 228 | // if this is not the last element add a comma as separator 229 | if(i != value.size() - 1) 230 | ss << ','; 231 | } 232 | // store the created string in the result 233 | result = ss.str(); 234 | } 235 | }; 236 | } 237 | ``` 238 | 239 | ## Contributing 240 | 241 | If you want to contribute new features or bug fixes, simply file a pull request. 242 | Make sure all CI checks pass, otherwise PRs will not be merged. 243 | 244 | ## License 245 | 246 | `inifile-cpp` is licensed under the [MIT license](https://github.com/Rookfighter/inifile-cpp/blob/main/LICENSE.txt) 247 | -------------------------------------------------------------------------------- /dep/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists.txt 2 | # 3 | # Author: Fabian Meyer 4 | # Created On: 26 Dec 2015 5 | # License: MIT 6 | 7 | set(CATCH2_ROOT "${CMAKE_CURRENT_LIST_DIR}/Catch2" CACHE INTERNAL "") 8 | set(CATCH2_INCLUDE_DIR "${CATCH2_ROOT}/single_include" CACHE INTERNAL "") 9 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists.txt 2 | # 3 | # Author: Fabian Meyer 4 | # Created On: 14 Nov 2020 5 | 6 | add_executable(custom_type_conversion "custom_type_conversion.cpp") 7 | target_link_libraries(custom_type_conversion inicpp::inicpp) 8 | 9 | add_executable(decode_ini_file "decode_ini_file.cpp") 10 | target_link_libraries(decode_ini_file inicpp::inicpp) 11 | 12 | add_executable(encode_ini_file "encode_ini_file.cpp") 13 | target_link_libraries(encode_ini_file inicpp::inicpp) 14 | 15 | add_executable(load_ini_file "load_ini_file.cpp") 16 | target_link_libraries(load_ini_file inicpp::inicpp) 17 | 18 | add_executable(save_ini_file "save_ini_file.cpp") 19 | target_link_libraries(save_ini_file inicpp::inicpp) 20 | -------------------------------------------------------------------------------- /examples/custom_type_conversion.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* decode_ini_file.cpp 3 | * 4 | * Author: Fabian Meyer 5 | * Created On: 14 Nov 2020 6 | */ 7 | 8 | #include 9 | #include 10 | 11 | // the conversion functor must live in the "ini" namespace 12 | namespace ini 13 | { 14 | /** Conversion functor to parse std::vectors from an ini field- 15 | * The generic template can be passed down to the vector. */ 16 | template 17 | struct Convert> 18 | { 19 | /** Decodes a std::vector from a string. */ 20 | void decode(const std::string &value, std::vector &result) 21 | { 22 | result.clear(); 23 | 24 | // variable to store the decoded value of each element 25 | T decoded; 26 | // maintain a start and end pos within the string 27 | size_t startPos = 0; 28 | size_t endPos = 0; 29 | size_t cnt; 30 | 31 | while(endPos != std::string::npos) 32 | { 33 | if(endPos != 0) 34 | startPos = endPos + 1; 35 | // search for the next comma as separator 36 | endPos = value.find(',', startPos); 37 | 38 | // if no comma was found use the rest of the string 39 | // as input 40 | if(endPos == std::string::npos) 41 | cnt = value.size() - startPos; 42 | else 43 | cnt = endPos - startPos; 44 | 45 | std::string tmp = value.substr(startPos, cnt); 46 | // use the conversion functor for the type contained in 47 | // the vector, so the vector can use any type that 48 | // is compatible with inifile-cpp 49 | Convert conv; 50 | conv.decode(tmp, decoded); 51 | result.push_back(decoded); 52 | } 53 | } 54 | 55 | /** Encodes a std::vector to a string. */ 56 | void encode(const std::vector &value, std::string &result) 57 | { 58 | // variable to store the encoded element value 59 | std::string encoded; 60 | // string stream to build the result stream 61 | std::stringstream ss; 62 | for(size_t i = 0; i < value.size(); ++i) 63 | { 64 | // use the conversion functor for the type contained in 65 | // the vector, so the vector can use any type that 66 | // is compatible with inifile-cp 67 | Convert conv; 68 | conv.encode(value[i], encoded); 69 | ss << encoded; 70 | 71 | // if this is not the last element add a comma as separator 72 | if(i != value.size() - 1) 73 | ss << ','; 74 | } 75 | // store the created string in the result 76 | result = ss.str(); 77 | } 78 | }; 79 | } 80 | 81 | int main() 82 | { 83 | // create some ini content that we can parse 84 | std::string content = "[Foo]\nintList=1,2,3,4,5,6,7,8\ndoubleList=3.4,1.2,2.2,4.7"; 85 | 86 | // decode the ini contents 87 | ini::IniFile inputIni; 88 | inputIni.decode(content); 89 | 90 | // print the results 91 | std::cout << "Parsed ini file" << std::endl; 92 | std::cout << "===============" << std::endl; 93 | 94 | // parse the int list 95 | std::vector intList = inputIni["Foo"]["intList"].as>(); 96 | std::cout << "int list:" << std::endl; 97 | for(size_t i = 0; i < intList.size(); ++i) 98 | std::cout << " " << intList[i] << std::endl; 99 | 100 | // parse the double list 101 | std::vector doubleList = inputIni["Foo"]["doubleList"].as>(); 102 | std::cout << "double list:" << std::endl; 103 | for(size_t i = 0; i < doubleList.size(); ++i) 104 | std::cout << " " << doubleList[i] << std::endl; 105 | 106 | std::cout << std::endl; 107 | 108 | // create another ini file for encoding 109 | ini::IniFile outputIni; 110 | outputIni["Bar"]["floatList"] = std::vector{1.0f, 9.3f, 3.256f}; 111 | outputIni["Bar"]["boolList"] = std::vector{true, false, false, true}; 112 | 113 | std::cout << "Encoded ini file" << std::endl; 114 | std::cout << "================" << std::endl; 115 | std::cout << outputIni.encode() << std::endl; 116 | 117 | return 0; 118 | } -------------------------------------------------------------------------------- /examples/decode_ini_file.cpp: -------------------------------------------------------------------------------- 1 | /* decode_ini_file.cpp 2 | * 3 | * Author: Fabian Meyer 4 | * Created On: 14 Nov 2020 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | int main() 11 | { 12 | // create some ini content 13 | std::string content = "[Foo]\nhello=world\nnum=123\n[Test]\nstatus=pass\n[Nothing]"; 14 | ini::IniFile inif; 15 | inif.decode(content); 16 | 17 | // show the parsed contents of the ini file 18 | std::cout << "Parsed ini contents" << std::endl; 19 | std::cout << "Has " << inif.size() << " sections" << std::endl; 20 | for(const auto §ionPair : inif) 21 | { 22 | const std::string §ionName = sectionPair.first; 23 | const ini::IniSection §ion = sectionPair.second; 24 | std::cout << "Section '" << sectionName << "' has " << section.size() << " fields" << std::endl; 25 | 26 | for(const auto &fieldPair : sectionPair.second) 27 | { 28 | const std::string &fieldName = fieldPair.first; 29 | const ini::IniField &field = fieldPair.second; 30 | std::cout << " Field '" << fieldName << "' Value '" << field.as() << "'" << std::endl; 31 | } 32 | } 33 | 34 | return 0; 35 | } -------------------------------------------------------------------------------- /examples/encode_ini_file.cpp: -------------------------------------------------------------------------------- 1 | /* encode_ini_file.cpp 2 | * 3 | * Author: Fabian Meyer 4 | * Created On: 14 Nov 2020 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | int main() 11 | { 12 | ini::IniFile inif; 13 | 14 | inif["Foo"]["hello"] = "world"; 15 | inif["Foo"]["float"] = 1.02f; 16 | inif["Foo"]["int"] = 123; 17 | inif["Another"]["char"] = 'q'; 18 | inif["Another"]["bool"] = true; 19 | 20 | std::string content = inif.encode(); 21 | 22 | std::cout << "Encoded ini file" << std::endl; 23 | std::cout << content << std::endl; 24 | 25 | return 0; 26 | } -------------------------------------------------------------------------------- /examples/load_ini_file.cpp: -------------------------------------------------------------------------------- 1 | /* load_ini_file.cpp 2 | * 3 | * Author: Fabian Meyer 4 | * Created On: 14 Nov 2020 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | int main(int argc, char **argv) 11 | { 12 | if(argc != 2) 13 | { 14 | std::cerr << "usage: load_ini_file [FILE_PATh]" << std::endl; 15 | return 1; 16 | } 17 | 18 | std::string path = argv[1]; 19 | 20 | // load the file 21 | ini::IniFile inif; 22 | inif.load(path); 23 | 24 | // show the parsed contents of the ini file 25 | std::cout << "Parsed ini contents" << std::endl; 26 | std::cout << "Has " << inif.size() << " sections" << std::endl; 27 | for(const auto §ionPair : inif) 28 | { 29 | const std::string §ionName = sectionPair.first; 30 | const ini::IniSection §ion = sectionPair.second; 31 | std::cout << "Section '" << sectionName << "' has " << section.size() << " fields" << std::endl; 32 | 33 | for(const auto &fieldPair : sectionPair.second) 34 | { 35 | const std::string &fieldName = fieldPair.first; 36 | const ini::IniField &field = fieldPair.second; 37 | std::cout << " Field '" << fieldName << "' Value '" << field.as() << "'" << std::endl; 38 | } 39 | } 40 | 41 | return 0; 42 | } -------------------------------------------------------------------------------- /examples/save_ini_file.cpp: -------------------------------------------------------------------------------- 1 | /* save_ini_file.cpp 2 | * 3 | * Author: Fabian Meyer 4 | * Created On: 14 Nov 2020 5 | */ 6 | 7 | #include 8 | #include 9 | 10 | int main(int argc, char **argv) 11 | { 12 | if(argc != 2) 13 | { 14 | std::cerr << "usage: save_ini_file [FILE_PATh]" << std::endl; 15 | return 1; 16 | } 17 | 18 | std::string path = argv[1]; 19 | 20 | ini::IniFile inif; 21 | 22 | inif["Foo"]["hello"] = "world"; 23 | inif["Foo"]["float"] = 1.02f; 24 | inif["Foo"]["int"] = 123; 25 | inif["Another"]["char"] = 'q'; 26 | inif["Another"]["bool"] = true; 27 | 28 | inif.save(path); 29 | 30 | std::cout << "Saved ini file." << std::endl; 31 | 32 | return 0; 33 | } -------------------------------------------------------------------------------- /include/inicpp.h: -------------------------------------------------------------------------------- 1 | /* 2 | * inicpp.h 3 | * 4 | * Created on: 26 Dec 2015 5 | * Author: Fabian Meyer 6 | * License: MIT 7 | */ 8 | 9 | #ifndef INICPP_H_ 10 | #define INICPP_H_ 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #ifdef __cpp_lib_string_view // This one is defined in if we have std::string_view 23 | # include 24 | #endif 25 | 26 | namespace ini 27 | { 28 | /************************************************ 29 | * Helper Functions 30 | ************************************************/ 31 | 32 | /** Returns a string of whitespace characters. */ 33 | constexpr const char *whitespaces() 34 | { 35 | return " \t\n\r\f\v"; 36 | } 37 | 38 | /** Returns a string of indentation characters. */ 39 | constexpr const char *indents() 40 | { 41 | return " \t"; 42 | } 43 | 44 | /** Trims a string in place. 45 | * @param str string to be trimmed in place */ 46 | inline void trim(std::string &str) 47 | { 48 | // first erasing from end should be slighty more efficient 49 | // because erasing from start potentially moves all chars 50 | // multiple indices towards the front. 51 | 52 | auto lastpos = str.find_last_not_of(whitespaces()); 53 | if(lastpos == std::string::npos) 54 | { 55 | str.clear(); 56 | return; 57 | } 58 | 59 | str.erase(lastpos + 1); 60 | str.erase(0, str.find_first_not_of(whitespaces())); 61 | } 62 | 63 | /************************************************ 64 | * Conversion Functors 65 | ************************************************/ 66 | 67 | inline bool strToLong(const std::string &value, long &result) 68 | { 69 | char *endptr; 70 | // check if decimal 71 | result = std::strtol(value.c_str(), &endptr, 10); 72 | if(*endptr == '\0') 73 | return true; 74 | // check if octal 75 | result = std::strtol(value.c_str(), &endptr, 8); 76 | if(*endptr == '\0') 77 | return true; 78 | // check if hex 79 | result = std::strtol(value.c_str(), &endptr, 16); 80 | if(*endptr == '\0') 81 | return true; 82 | 83 | return false; 84 | } 85 | 86 | inline bool strToULong(const std::string &value, unsigned long &result) 87 | { 88 | char *endptr; 89 | // check if decimal 90 | result = std::strtoul(value.c_str(), &endptr, 10); 91 | if(*endptr == '\0') 92 | return true; 93 | // check if octal 94 | result = std::strtoul(value.c_str(), &endptr, 8); 95 | if(*endptr == '\0') 96 | return true; 97 | // check if hex 98 | result = std::strtoul(value.c_str(), &endptr, 16); 99 | if(*endptr == '\0') 100 | return true; 101 | 102 | return false; 103 | } 104 | 105 | template 106 | struct Convert 107 | {}; 108 | 109 | template<> 110 | struct Convert 111 | { 112 | void decode(const std::string &value, bool &result) 113 | { 114 | std::string str(value); 115 | std::transform(str.begin(), str.end(), str.begin(), [](const char c){ 116 | return static_cast(::toupper(c)); 117 | }); 118 | 119 | if(str == "TRUE") 120 | result = true; 121 | else if(str == "FALSE") 122 | result = false; 123 | else 124 | throw std::invalid_argument("field is not a bool"); 125 | } 126 | 127 | void encode(const bool value, std::string &result) 128 | { 129 | result = value ? "true" : "false"; 130 | } 131 | }; 132 | 133 | template<> 134 | struct Convert 135 | { 136 | void decode(const std::string &value, char &result) 137 | { 138 | assert(value.size() > 0); 139 | result = value[0]; 140 | } 141 | 142 | void encode(const char value, std::string &result) 143 | { 144 | result = value; 145 | } 146 | }; 147 | 148 | template<> 149 | struct Convert 150 | { 151 | void decode(const std::string &value, unsigned char &result) 152 | { 153 | assert(value.size() > 0); 154 | result = value[0]; 155 | } 156 | 157 | void encode(const unsigned char value, std::string &result) 158 | { 159 | result = value; 160 | } 161 | }; 162 | 163 | template<> 164 | struct Convert 165 | { 166 | void decode(const std::string &value, short &result) 167 | { 168 | long tmp; 169 | if(!strToLong(value, tmp)) 170 | throw std::invalid_argument("field is not a short"); 171 | result = static_cast(tmp); 172 | } 173 | 174 | void encode(const short value, std::string &result) 175 | { 176 | std::stringstream ss; 177 | ss << value; 178 | result = ss.str(); 179 | } 180 | }; 181 | 182 | template<> 183 | struct Convert 184 | { 185 | void decode(const std::string &value, unsigned short &result) 186 | { 187 | unsigned long tmp; 188 | if(!strToULong(value, tmp)) 189 | throw std::invalid_argument("field is not an unsigned short"); 190 | result = static_cast(tmp); 191 | } 192 | 193 | void encode(const unsigned short value, std::string &result) 194 | { 195 | std::stringstream ss; 196 | ss << value; 197 | result = ss.str(); 198 | } 199 | }; 200 | 201 | template<> 202 | struct Convert 203 | { 204 | void decode(const std::string &value, int &result) 205 | { 206 | long tmp; 207 | if(!strToLong(value, tmp)) 208 | throw std::invalid_argument("field is not an int"); 209 | result = static_cast(tmp); 210 | } 211 | 212 | void encode(const int value, std::string &result) 213 | { 214 | std::stringstream ss; 215 | ss << value; 216 | result = ss.str(); 217 | } 218 | }; 219 | 220 | template<> 221 | struct Convert 222 | { 223 | void decode(const std::string &value, unsigned int &result) 224 | { 225 | unsigned long tmp; 226 | if(!strToULong(value, tmp)) 227 | throw std::invalid_argument("field is not an unsigned int"); 228 | result = static_cast(tmp); 229 | } 230 | 231 | void encode(const unsigned int value, std::string &result) 232 | { 233 | std::stringstream ss; 234 | ss << value; 235 | result = ss.str(); 236 | } 237 | }; 238 | 239 | template<> 240 | struct Convert 241 | { 242 | void decode(const std::string &value, long &result) 243 | { 244 | if(!strToLong(value, result)) 245 | throw std::invalid_argument("field is not a long"); 246 | } 247 | 248 | void encode(const long value, std::string &result) 249 | { 250 | std::stringstream ss; 251 | ss << value; 252 | result = ss.str(); 253 | } 254 | }; 255 | 256 | template<> 257 | struct Convert 258 | { 259 | void decode(const std::string &value, unsigned long &result) 260 | { 261 | if(!strToULong(value, result)) 262 | throw std::invalid_argument("field is not an unsigned long"); 263 | } 264 | 265 | void encode(const unsigned long value, std::string &result) 266 | { 267 | std::stringstream ss; 268 | ss << value; 269 | result = ss.str(); 270 | } 271 | }; 272 | 273 | template<> 274 | struct Convert 275 | { 276 | void decode(const std::string &value, double &result) 277 | { 278 | result = std::stod(value); 279 | } 280 | 281 | void encode(const double value, std::string &result) 282 | { 283 | std::stringstream ss; 284 | ss << value; 285 | result = ss.str(); 286 | } 287 | }; 288 | 289 | template<> 290 | struct Convert 291 | { 292 | void decode(const std::string &value, float &result) 293 | { 294 | result = std::stof(value); 295 | } 296 | 297 | void encode(const float value, std::string &result) 298 | { 299 | std::stringstream ss; 300 | ss << value; 301 | result = ss.str(); 302 | } 303 | }; 304 | 305 | template<> 306 | struct Convert 307 | { 308 | void decode(const std::string &value, std::string &result) 309 | { 310 | result = value; 311 | } 312 | 313 | void encode(const std::string &value, std::string &result) 314 | { 315 | result = value; 316 | } 317 | }; 318 | 319 | #ifdef __cpp_lib_string_view 320 | template<> 321 | struct Convert 322 | { 323 | void decode(const std::string &value, std::string_view &result) 324 | { 325 | result = value; 326 | } 327 | 328 | void encode(const std::string_view value, std::string &result) 329 | { 330 | result = value; 331 | } 332 | }; 333 | #endif 334 | 335 | template<> 336 | struct Convert 337 | { 338 | void encode(const char* const &value, std::string &result) 339 | { 340 | result = value; 341 | } 342 | 343 | void decode(const std::string &value, const char* &result) 344 | { 345 | result = value.c_str(); 346 | } 347 | }; 348 | 349 | template<> 350 | struct Convert 351 | { 352 | void encode(const char* const &value, std::string &result) 353 | { 354 | result = value; 355 | } 356 | }; 357 | 358 | template 359 | struct Convert 360 | { 361 | void encode(const char *value, std::string &result) 362 | { 363 | result = value; 364 | } 365 | }; 366 | 367 | class IniField 368 | { 369 | private: 370 | std::string value_; 371 | 372 | public: 373 | IniField() : value_() 374 | {} 375 | 376 | IniField(const std::string &value) : value_(value) 377 | {} 378 | IniField(const IniField &field) : value_(field.value_) 379 | {} 380 | 381 | ~IniField() 382 | {} 383 | 384 | template 385 | T as() const 386 | { 387 | Convert conv; 388 | T result; 389 | conv.decode(value_, result); 390 | return result; 391 | } 392 | 393 | template 394 | IniField &operator=(const T &value) 395 | { 396 | Convert conv; 397 | conv.encode(value, value_); 398 | return *this; 399 | } 400 | 401 | IniField &operator=(const IniField &field) 402 | { 403 | value_ = field.value_; 404 | return *this; 405 | } 406 | }; 407 | 408 | struct StringInsensitiveLess 409 | { 410 | bool operator()(std::string lhs, std::string rhs) const 411 | { 412 | std::transform(lhs.begin(), lhs.end(), lhs.begin(), [](const char c){ 413 | return static_cast(::tolower(c)); 414 | }); 415 | std::transform(rhs.begin(), rhs.end(), rhs.begin(), [](const char c){ 416 | return static_cast(::tolower(c)); 417 | }); 418 | return lhs < rhs; 419 | } 420 | }; 421 | 422 | template 423 | class IniSectionBase : public std::map 424 | { 425 | public: 426 | IniSectionBase() 427 | {} 428 | ~IniSectionBase() 429 | {} 430 | }; 431 | 432 | using IniSection = IniSectionBase>; 433 | using IniSectionCaseInsensitive = IniSectionBase; 434 | 435 | template 436 | class IniFileBase : public std::map, Comparator> 437 | { 438 | private: 439 | char fieldSep_ = '='; 440 | char esc_ = '\\'; 441 | std::vector commentPrefixes_ = { "#" , ";" }; 442 | bool multiLineValues_ = false; 443 | bool overwriteDuplicateFields_ = true; 444 | 445 | void eraseComment(const std::string &commentPrefix, 446 | std::string &str, 447 | std::string::size_type startpos = 0) 448 | { 449 | size_t prefixpos = str.find(commentPrefix, startpos); 450 | if(std::string::npos == prefixpos) 451 | return; 452 | // Found a comment prefix, is it escaped? 453 | if(0 != prefixpos && str[prefixpos - 1] == esc_) 454 | { 455 | // The comment prefix is escaped, so just delete the escape char 456 | // and keep erasing after the comment prefix 457 | str.erase(prefixpos - 1, 1); 458 | eraseComment( 459 | commentPrefix, str, prefixpos - 1 + commentPrefix.size()); 460 | } 461 | else 462 | { 463 | str.erase(prefixpos); 464 | } 465 | } 466 | 467 | void eraseComments(std::string &str) 468 | { 469 | for(const std::string &commentPrefix : commentPrefixes_) 470 | eraseComment(commentPrefix, str); 471 | } 472 | 473 | /** Tries to find a suitable comment prefix for the string data at the given 474 | * position. Returns commentPrefixes_.end() if not match was found. */ 475 | std::vector::const_iterator findCommentPrefix(const std::string &str, 476 | const std::size_t startpos) const 477 | { 478 | // if startpos is invalid simply return "not found" 479 | if(startpos >= str.size()) 480 | return commentPrefixes_.end(); 481 | 482 | for(size_t i = 0; i < commentPrefixes_.size(); ++i) 483 | { 484 | const std::string &prefix = commentPrefixes_[i]; 485 | // if this comment prefix is longer than the string view itself 486 | // then skip 487 | if(prefix.size() + startpos > str.size()) 488 | continue; 489 | 490 | bool match = true; 491 | for(size_t j = 0; j < prefix.size() && match; ++j) 492 | match = str[startpos + j] == prefix[j]; 493 | 494 | if(match) 495 | return commentPrefixes_.begin() + i; 496 | } 497 | 498 | return commentPrefixes_.end(); 499 | } 500 | 501 | void writeEscaped(std::ostream &os, const std::string &str) const 502 | { 503 | for(size_t i = 0; i < str.length(); ++i) 504 | { 505 | auto prefixpos = findCommentPrefix(str, i); 506 | // if no suitable prefix was found at this position 507 | // then simply write the current character 508 | if(prefixpos != commentPrefixes_.end()) 509 | { 510 | const std::string &prefix = *prefixpos; 511 | os.put(esc_); 512 | os.write(prefix.c_str(), prefix.size()); 513 | i += prefix.size() - 1; 514 | } 515 | else if (multiLineValues_ && str[i] == '\n') 516 | os.write("\n\t", 2); 517 | else 518 | os.put(str[i]); 519 | } 520 | } 521 | 522 | public: 523 | IniFileBase() = default; 524 | 525 | IniFileBase(const char fieldSep, const char comment) 526 | : fieldSep_(fieldSep), commentPrefixes_(1, std::string(1, comment)) 527 | {} 528 | 529 | IniFileBase(const std::string &filename) 530 | { 531 | load(filename); 532 | } 533 | 534 | IniFileBase(std::istream &is) 535 | { 536 | decode(is); 537 | } 538 | 539 | IniFileBase(const char fieldSep, 540 | const std::vector &commentPrefixes) 541 | : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) 542 | {} 543 | 544 | IniFileBase(const std::string &filename, 545 | const char fieldSep, 546 | const std::vector &commentPrefixes) 547 | : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) 548 | { 549 | load(filename); 550 | } 551 | 552 | IniFileBase(std::istream &is, 553 | const char fieldSep, 554 | const std::vector &commentPrefixes) 555 | : fieldSep_(fieldSep), commentPrefixes_(commentPrefixes) 556 | { 557 | decode(is); 558 | } 559 | 560 | ~IniFileBase() 561 | {} 562 | 563 | /** Sets the separator charactor for fields in the INI file. 564 | * @param sep separator character to be used. */ 565 | void setFieldSep(const char sep) 566 | { 567 | fieldSep_ = sep; 568 | } 569 | 570 | /** Sets the character that should be interpreted as the start of comments. 571 | * Default is '#'. 572 | * Note: If the inifile contains the comment character as data it must be prefixed with 573 | * the configured escape character. 574 | * @param comment comment character to be used. */ 575 | void setCommentChar(const char comment) 576 | { 577 | commentPrefixes_ = {std::string(1, comment)}; 578 | } 579 | 580 | /** Sets the list of strings that should be interpreted as the start of comments. 581 | * Default is [ "#" ]. 582 | * Note: If the inifile contains any comment string as data it must be prefixed with 583 | * the configured escape character. 584 | * @param commentPrefixes vector of comment prefix strings to be used. */ 585 | void setCommentPrefixes(const std::vector &commentPrefixes) 586 | { 587 | commentPrefixes_ = commentPrefixes; 588 | } 589 | 590 | /** Sets the character that should be used to escape comment prefixes. 591 | * Default is '\'. 592 | * @param esc escape character to be used. */ 593 | void setEscapeChar(const char esc) 594 | { 595 | esc_ = esc; 596 | } 597 | 598 | /** Sets whether or not to parse multi-line field values. 599 | * Default is false. 600 | * @param enable enable or disable? */ 601 | void setMultiLineValues(bool enable) 602 | { 603 | multiLineValues_ = enable; 604 | } 605 | 606 | /** Sets whether or not overwriting duplicate fields is allowed. 607 | * If overwriting duplicate fields is not allowed, 608 | * an exception is thrown when a duplicate field is found inside a section. 609 | * Default is true. 610 | * @param allowed Is overwriting duplicate fields allowed or not? */ 611 | void allowOverwriteDuplicateFields(bool allowed) 612 | { 613 | overwriteDuplicateFields_ = allowed; 614 | } 615 | 616 | /** Tries to decode a ini file from the given input stream. 617 | * @param is input stream from which data should be read. */ 618 | void decode(std::istream &is) 619 | { 620 | this->clear(); 621 | int lineNo = 0; 622 | IniSectionBase *currentSection = nullptr; 623 | std::string mutliLineValueFieldName = ""; 624 | std::string line; 625 | // iterate file line by line 626 | while(!is.eof() && !is.fail()) 627 | { 628 | std::getline(is, line, '\n'); 629 | eraseComments(line); 630 | bool hasIndent = line.find_first_not_of(indents()) != 0; 631 | trim(line); 632 | ++lineNo; 633 | 634 | // skip if line is empty 635 | if(line.size() == 0) 636 | continue; 637 | 638 | if(line[0] == '[') 639 | { 640 | // line is a section 641 | // check if the section is also closed on same line 642 | std::size_t pos = line.find("]"); 643 | if(pos == std::string::npos) 644 | { 645 | std::stringstream ss; 646 | ss << "l." << lineNo 647 | << ": ini parsing failed, section not closed"; 648 | throw std::logic_error(ss.str()); 649 | } 650 | // check if the section name is empty 651 | if(pos == 1) 652 | { 653 | std::stringstream ss; 654 | ss << "l." << lineNo 655 | << ": ini parsing failed, section is empty"; 656 | throw std::logic_error(ss.str()); 657 | } 658 | 659 | // retrieve section name 660 | std::string secName = line.substr(1, pos - 1); 661 | currentSection = &((*this)[secName]); 662 | 663 | // clear multiline value field name 664 | // a new section means there is no value to continue 665 | mutliLineValueFieldName = ""; 666 | } 667 | else 668 | { 669 | // line is a field definition 670 | // check if section was already opened 671 | if(currentSection == nullptr) 672 | { 673 | std::stringstream ss; 674 | ss << "l." << lineNo 675 | << ": ini parsing failed, field has no section" 676 | " or ini file in use by another application"; 677 | throw std::logic_error(ss.str()); 678 | } 679 | 680 | // find key value separator 681 | std::size_t pos = line.find(fieldSep_); 682 | if (multiLineValues_ && hasIndent && mutliLineValueFieldName != "") 683 | { 684 | // extend a multi-line value 685 | IniField previous_value = (*currentSection)[mutliLineValueFieldName]; 686 | std::string value = previous_value.as() + "\n" + line; 687 | (*currentSection)[mutliLineValueFieldName] = value; 688 | } 689 | else if(pos == std::string::npos) 690 | { 691 | std::stringstream ss; 692 | ss << "l." << lineNo 693 | << ": ini parsing failed, no '" 694 | << fieldSep_ 695 | << "' found"; 696 | if (multiLineValues_) 697 | ss << ", and not a multi-line value continuation"; 698 | throw std::logic_error(ss.str()); 699 | } 700 | else 701 | { 702 | // retrieve field name and value 703 | std::string name = line.substr(0, pos); 704 | trim(name); 705 | if (!overwriteDuplicateFields_ && currentSection->count(name) != 0) 706 | { 707 | std::stringstream ss; 708 | ss << "l." << lineNo 709 | << ": ini parsing failed, duplicate field found"; 710 | throw std::logic_error(ss.str()); 711 | } 712 | std::string value = line.substr(pos + 1, std::string::npos); 713 | trim(value); 714 | (*currentSection)[name] = value; 715 | // store last field name for potential multi-line values 716 | mutliLineValueFieldName = name; 717 | } 718 | } 719 | } 720 | } 721 | 722 | /** Tries to decode a ini file from the given input string. 723 | * @param content string to be decoded. */ 724 | void decode(const std::string &content) 725 | { 726 | std::istringstream ss(content); 727 | decode(ss); 728 | } 729 | 730 | /** Tries to load and decode a ini file from the file at the given path. 731 | * @param fileName path to the file that should be loaded. */ 732 | void load(const std::string &fileName) 733 | { 734 | std::ifstream is(fileName.c_str()); 735 | decode(is); 736 | } 737 | 738 | /** Encodes this inifile object and writes the output to the given stream. 739 | * @param os target stream. */ 740 | void encode(std::ostream &os) const 741 | { 742 | // iterate through all sections in this file 743 | for(const auto &filePair : *this) 744 | { 745 | os.put('['); 746 | writeEscaped(os, filePair.first); 747 | os.put(']'); 748 | os.put('\n'); 749 | 750 | // iterate through all fields in the section 751 | for(const auto &secPair : filePair.second) 752 | { 753 | writeEscaped(os, secPair.first); 754 | os.put(fieldSep_); 755 | writeEscaped(os, secPair.second.template as()); 756 | os.put('\n'); 757 | } 758 | 759 | // Add a newline after each section 760 | os.put('\n'); 761 | } 762 | } 763 | 764 | /** Encodes this inifile object as string and returns the result. 765 | * @return encoded infile string. */ 766 | std::string encode() const 767 | { 768 | std::ostringstream ss; 769 | encode(ss); 770 | return ss.str(); 771 | } 772 | 773 | /** Saves this inifile object to the file at the given path. 774 | * @param fileName path to the file where the data should be stored. */ 775 | void save(const std::string &fileName) const 776 | { 777 | std::ofstream os(fileName.c_str()); 778 | encode(os); 779 | } 780 | }; 781 | 782 | using IniFile = IniFileBase>; 783 | using IniSection = IniSectionBase>; 784 | using IniFileCaseInsensitive = IniFileBase; 785 | using IniSectionCaseInsensitive = IniSectionBase; 786 | } 787 | 788 | #endif 789 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists.txt 2 | # 3 | # Author: Fabian Meyer 4 | # Created On: 12 Jul 2019 5 | 6 | include_directories( 7 | ${CATCH2_INCLUDE_DIR} 8 | ) 9 | 10 | add_executable(unit_tests 11 | "main.cpp" 12 | "test_inifile.cpp" 13 | ) 14 | target_link_libraries(unit_tests inicpp::inicpp) 15 | 16 | add_test(NAME unit_tests COMMAND unit_tests) 17 | -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * main.cpp 3 | * 4 | * Created on: 26 Dec 2015 5 | * Author: Fabian Meyer 6 | * License: MIT 7 | */ 8 | 9 | #define CATCH_CONFIG_MAIN 10 | #include 11 | -------------------------------------------------------------------------------- /test/test_inifile.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * test_inifile.cpp 3 | * 4 | * Created on: 26 Dec 2015 5 | * Author: Fabian Meyer 6 | * License: MIT 7 | */ 8 | 9 | #include "inicpp.h" 10 | #include 11 | #include 12 | #include 13 | 14 | TEST_CASE("parse ini file", "IniFile") 15 | { 16 | std::istringstream ss(("[Foo]\nbar=hello world\n[Test]")); 17 | ini::IniFile inif(ss); 18 | 19 | REQUIRE(inif.size() == 2); 20 | REQUIRE(inif["Foo"]["bar"].as() == "hello world"); 21 | REQUIRE(inif["Test"].size() == 0); 22 | } 23 | 24 | TEST_CASE("parse empty file", "IniFile") 25 | { 26 | std::istringstream ss(""); 27 | ini::IniFile inif(ss); 28 | 29 | REQUIRE(inif.size() == 0); 30 | } 31 | 32 | TEST_CASE("parse comment only file", "IniFile") 33 | { 34 | std::istringstream ss("# this is a comment"); 35 | ini::IniFile inif(ss); 36 | 37 | REQUIRE(inif.size() == 0); 38 | } 39 | 40 | TEST_CASE("parse empty section", "IniFile") 41 | { 42 | std::istringstream ss("[Foo]"); 43 | ini::IniFile inif(ss); 44 | 45 | REQUIRE(inif.size() == 1); 46 | REQUIRE(inif["Foo"].size() == 0); 47 | } 48 | 49 | TEST_CASE("parse empty field", "IniFile") 50 | { 51 | std::istringstream ss("[Foo]\nbar="); 52 | ini::IniFile inif(ss); 53 | 54 | REQUIRE(inif.size() == 1); 55 | REQUIRE(inif["Foo"].size() == 1); 56 | REQUIRE(inif["Foo"]["bar"].as() == ""); 57 | } 58 | 59 | TEST_CASE("parse section with duplicate field", "IniFile") 60 | { 61 | std::istringstream ss("[Foo]\nbar=hello\nbar=world"); 62 | ini::IniFile inif(ss); 63 | 64 | REQUIRE(inif.size() == 1); 65 | REQUIRE(inif["Foo"].size() == 1); 66 | REQUIRE(inif["Foo"]["bar"].as() == "world"); 67 | } 68 | 69 | TEST_CASE("parse section with duplicate field and overwriteDuplicateFields_ set to true", "IniFile") 70 | { 71 | ini::IniFile inif; 72 | inif.allowOverwriteDuplicateFields(true); 73 | inif.decode("[Foo]\nbar=hello\nbar=world"); 74 | 75 | REQUIRE(inif.size() == 1); 76 | REQUIRE(inif["Foo"].size() == 1); 77 | REQUIRE(inif["Foo"]["bar"].as() == "world"); 78 | } 79 | 80 | TEST_CASE("parse field as bool", "IniFile") 81 | { 82 | std::istringstream ss("[Foo]\nbar1=true\nbar2=false\nbar3=tRuE"); 83 | ini::IniFile inif(ss); 84 | 85 | REQUIRE(inif.size() == 1); 86 | REQUIRE(inif["Foo"].size() == 3); 87 | REQUIRE(inif["Foo"]["bar1"].as()); 88 | REQUIRE_FALSE(inif["Foo"]["bar2"].as()); 89 | REQUIRE(inif["Foo"]["bar3"].as()); 90 | } 91 | 92 | TEST_CASE("parse field as char", "IniFile") 93 | { 94 | std::istringstream ss("[Foo]\nbar1=c\nbar2=q"); 95 | ini::IniFile inif(ss); 96 | 97 | REQUIRE(inif.size() == 1); 98 | REQUIRE(inif["Foo"].size() == 2); 99 | REQUIRE(inif["Foo"]["bar1"].as() == 'c'); 100 | REQUIRE(inif["Foo"]["bar2"].as() == 'q'); 101 | } 102 | 103 | TEST_CASE("parse field as unsigned char", "IniFile") 104 | { 105 | std::istringstream ss("[Foo]\nbar1=c\nbar2=q"); 106 | ini::IniFile inif(ss); 107 | 108 | REQUIRE(inif.size() == 1); 109 | REQUIRE(inif["Foo"].size() == 2); 110 | REQUIRE(inif["Foo"]["bar1"].as() == 'c'); 111 | REQUIRE(inif["Foo"]["bar2"].as() == 'q'); 112 | } 113 | 114 | TEST_CASE("parse field as short", "IniFile") 115 | { 116 | std::istringstream ss("[Foo]\nbar1=1\nbar2=-2"); 117 | ini::IniFile inif(ss); 118 | 119 | REQUIRE(inif.size() == 1); 120 | REQUIRE(inif["Foo"].size() == 2); 121 | REQUIRE(inif["Foo"]["bar1"].as() == 1); 122 | REQUIRE(inif["Foo"]["bar2"].as() == -2); 123 | } 124 | 125 | TEST_CASE("parse field as unsigned short", "IniFile") 126 | { 127 | std::istringstream ss("[Foo]\nbar1=1\nbar2=13"); 128 | ini::IniFile inif(ss); 129 | 130 | REQUIRE(inif.size() == 1); 131 | REQUIRE(inif["Foo"].size() == 2); 132 | REQUIRE(inif["Foo"]["bar1"].as() == 1); 133 | REQUIRE(inif["Foo"]["bar2"].as() == 13); 134 | } 135 | 136 | TEST_CASE("parse field as int", "IniFile") 137 | { 138 | std::istringstream ss("[Foo]\nbar1=1\nbar2=-2"); 139 | ini::IniFile inif(ss); 140 | 141 | REQUIRE(inif.size() == 1); 142 | REQUIRE(inif["Foo"].size() == 2); 143 | REQUIRE(inif["Foo"]["bar1"].as() == 1); 144 | REQUIRE(inif["Foo"]["bar2"].as() == -2); 145 | } 146 | 147 | TEST_CASE("parse field as unsigned int", "IniFile") 148 | { 149 | std::istringstream ss("[Foo]\nbar1=1\nbar2=13"); 150 | ini::IniFile inif(ss); 151 | 152 | REQUIRE(inif.size() == 1); 153 | REQUIRE(inif["Foo"].size() == 2); 154 | REQUIRE(inif["Foo"]["bar1"].as() == 1); 155 | REQUIRE(inif["Foo"]["bar2"].as() == 13); 156 | } 157 | 158 | TEST_CASE("parse field as long", "IniFile") 159 | { 160 | std::istringstream ss("[Foo]\nbar1=1\nbar2=-2"); 161 | ini::IniFile inif(ss); 162 | 163 | REQUIRE(inif.size() == 1); 164 | REQUIRE(inif["Foo"].size() == 2); 165 | REQUIRE(inif["Foo"]["bar1"].as() == 1); 166 | REQUIRE(inif["Foo"]["bar2"].as() == -2); 167 | } 168 | 169 | TEST_CASE("parse field as unsigned long", "IniFile") 170 | { 171 | std::istringstream ss("[Foo]\nbar1=1\nbar2=13"); 172 | ini::IniFile inif(ss); 173 | 174 | REQUIRE(inif.size() == 1); 175 | REQUIRE(inif["Foo"].size() == 2); 176 | REQUIRE(inif["Foo"]["bar1"].as() == 1); 177 | REQUIRE(inif["Foo"]["bar2"].as() == 13); 178 | } 179 | 180 | TEST_CASE("parse field as double", "IniFile") 181 | { 182 | std::istringstream ss("[Foo]\nbar1=1.2\nbar2=1\nbar3=-2.4"); 183 | ini::IniFile inif(ss); 184 | 185 | REQUIRE(inif.size() == 1); 186 | REQUIRE(inif["Foo"].size() == 3); 187 | REQUIRE(inif["Foo"]["bar1"].as() == Approx(1.2).margin(1e-3)); 188 | REQUIRE(inif["Foo"]["bar2"].as() == Approx(1.0).margin(1e-3)); 189 | REQUIRE(inif["Foo"]["bar3"].as() == Approx(-2.4).margin(1e-3)); 190 | } 191 | 192 | TEST_CASE("parse field as float", "IniFile") 193 | { 194 | std::istringstream ss("[Foo]\nbar1=1.2\nbar2=1\nbar3=-2.4"); 195 | ini::IniFile inif(ss); 196 | 197 | REQUIRE(inif.size() == 1); 198 | REQUIRE(inif["Foo"].size() == 3); 199 | REQUIRE(inif["Foo"]["bar1"].as() == Approx(1.2f).margin(1e-3f)); 200 | REQUIRE(inif["Foo"]["bar2"].as() == Approx(1.0f).margin(1e-3f)); 201 | REQUIRE(inif["Foo"]["bar3"].as() == Approx(-2.4f).margin(1e-3f)); 202 | } 203 | 204 | TEST_CASE("parse field as std::string", "IniFile") 205 | { 206 | std::istringstream ss("[Foo]\nbar1=hello\nbar2=world"); 207 | ini::IniFile inif(ss); 208 | 209 | REQUIRE(inif.size() == 1); 210 | REQUIRE(inif["Foo"].size() == 2); 211 | REQUIRE(inif["Foo"]["bar1"].as() == "hello"); 212 | REQUIRE(inif["Foo"]["bar2"].as() == "world"); 213 | } 214 | 215 | TEST_CASE("parse field as const char*", "IniFile") 216 | { 217 | std::istringstream ss("[Foo]\nbar1=hello\nbar2=world"); 218 | ini::IniFile inif(ss); 219 | 220 | REQUIRE(inif.size() == 1); 221 | REQUIRE(inif["Foo"].size() == 2); 222 | REQUIRE(std::strcmp(inif["Foo"]["bar1"].as(), "hello") == 0); 223 | REQUIRE(std::strcmp(inif["Foo"]["bar2"].as(), "world") == 0); 224 | } 225 | 226 | #ifdef __cpp_lib_string_view 227 | TEST_CASE("parse field as std::string_view", "IniFile") 228 | { 229 | std::istringstream ss("[Foo]\nbar1=hello\nbar2=world"); 230 | ini::IniFile inif(ss); 231 | 232 | REQUIRE(inif.size() == 1); 233 | REQUIRE(inif["Foo"].size() == 2); 234 | REQUIRE(inif["Foo"]["bar1"].as() == "hello"); 235 | REQUIRE(inif["Foo"]["bar2"].as() == "world"); 236 | } 237 | #endif 238 | 239 | TEST_CASE("parse field with custom field sep", "IniFile") 240 | { 241 | std::istringstream ss("[Foo]\nbar1:true\nbar2:false\nbar3:tRuE"); 242 | ini::IniFile inif; 243 | 244 | inif.setFieldSep(':'); 245 | inif.decode(ss); 246 | 247 | REQUIRE(inif.size() == 1); 248 | REQUIRE(inif["Foo"].size() == 3); 249 | REQUIRE(inif["Foo"]["bar1"].as()); 250 | REQUIRE_FALSE(inif["Foo"]["bar2"].as()); 251 | REQUIRE(inif["Foo"]["bar3"].as()); 252 | } 253 | 254 | TEST_CASE("parse with comment", "IniFile") 255 | { 256 | std::istringstream ss("[Foo]\n# this is a test\nbar=bla"); 257 | ini::IniFile inif(ss); 258 | 259 | REQUIRE(inif.size() == 1); 260 | REQUIRE(inif["Foo"].size() == 1); 261 | REQUIRE(inif["Foo"]["bar"].as() == "bla"); 262 | } 263 | 264 | TEST_CASE("parse with custom comment char prefix", "IniFile") 265 | { 266 | std::istringstream ss("[Foo]\n$ this is a test\nbar=bla"); 267 | ini::IniFile inif; 268 | 269 | inif.setFieldSep('='); 270 | inif.setCommentChar('$'); 271 | inif.decode(ss); 272 | 273 | REQUIRE(inif.size() == 1); 274 | REQUIRE(inif["Foo"].size() == 1); 275 | REQUIRE(inif["Foo"]["bar"].as() == "bla"); 276 | } 277 | 278 | TEST_CASE("parse with multi char comment prefix", "IniFile") 279 | { 280 | std::istringstream ss("[Foo]\nREM this is a test\nbar=bla"); 281 | ini::IniFile inif(ss, '=', {"REM"}); 282 | 283 | REQUIRE(inif.size() == 1); 284 | REQUIRE(inif["Foo"].size() == 1); 285 | REQUIRE(inif["Foo"]["bar"].as() == "bla"); 286 | } 287 | 288 | TEST_CASE("parse with multiple multi char comment prefixes", "IniFile") 289 | { 290 | std::istringstream ss("[Foo]\n" 291 | "REM this is a comment\n" 292 | "#Also a comment\n" 293 | "//Even this is a comment\n" 294 | "bar=bla"); 295 | ini::IniFile inif(ss, '=', {"REM", "#", "//"}); 296 | 297 | REQUIRE(inif.size() == 1); 298 | REQUIRE(inif["Foo"].size() == 1); 299 | REQUIRE(inif["Foo"]["bar"].as() == "bla"); 300 | } 301 | 302 | TEST_CASE("comment prefixes can be set after construction", "IniFile") 303 | { 304 | std::istringstream ss("[Foo]\n" 305 | "REM this is a comment\n" 306 | "#Also a comment\n" 307 | "//Even this is a comment\n" 308 | "bar=bla"); 309 | ini::IniFile inif; 310 | inif.setCommentPrefixes({"REM", "#", "//"}); 311 | inif.decode(ss); 312 | 313 | REQUIRE(inif.size() == 1); 314 | REQUIRE(inif["Foo"].size() == 1); 315 | REQUIRE(inif["Foo"]["bar"].as() == "bla"); 316 | } 317 | 318 | TEST_CASE("comments are allowed after escaped comments", "IniFile") 319 | { 320 | std::istringstream ss("[Foo]\n" 321 | "hello=world \\## this is a comment\n" 322 | "more=of this \\# \\#\n"); 323 | ini::IniFile inif(ss); 324 | 325 | REQUIRE(inif["Foo"]["hello"].as() == "world #"); 326 | REQUIRE(inif["Foo"]["more"].as() == "of this # #"); 327 | } 328 | 329 | TEST_CASE("escape char right before a comment prefix escapes all the comment prefix", "IniFile") 330 | { 331 | std::istringstream ss("[Foo]\n" 332 | "weird1=note \\### this is not a comment\n" 333 | "weird2=but \\#### this is a comment"); 334 | ini::IniFile inif(ss, '=', {"##"}); 335 | 336 | REQUIRE(inif["Foo"]["weird1"].as() == "note ### this is not a comment"); 337 | REQUIRE(inif["Foo"]["weird2"].as() == "but ##"); 338 | } 339 | 340 | TEST_CASE("escape comment when writing", "IniFile") 341 | { 342 | ini::IniFile inif('=', {"#"}); 343 | 344 | inif["Fo#o"] = ini::IniSection(); 345 | inif["Fo#o"]["he#llo"] = "world"; 346 | inif["Fo#o"]["world"] = "he#llo"; 347 | 348 | std::string str = inif.encode(); 349 | 350 | REQUIRE(str == "[Fo\\#o]\n" 351 | "he\\#llo=world\n" 352 | "world=he\\#llo\n\n"); 353 | } 354 | 355 | TEST_CASE("decode what we encoded", "IniFile") 356 | { 357 | std::string content = "[Fo\\#o]\n" 358 | "he\\REMllo=worl\\REMd\n" 359 | "world=he\\//llo\n\n"; 360 | 361 | ini::IniFile inif('=', {"#", "REM", "//"}); 362 | 363 | // decode the string 364 | inif.decode(content); 365 | 366 | REQUIRE(inif.size() == 1); 367 | REQUIRE(inif.find("Fo#o") != inif.end()); 368 | REQUIRE(inif["Fo#o"].size() == 2); 369 | REQUIRE(inif["Fo#o"].find("heREMllo") != inif["Fo#o"].end()); 370 | REQUIRE(inif["Fo#o"].find("world") != inif["Fo#o"].end()); 371 | REQUIRE(inif["Fo#o"]["heREMllo"].as() == "worlREMd"); 372 | REQUIRE(inif["Fo#o"]["world"].as() == "he//llo"); 373 | 374 | auto actual = inif.encode(); 375 | 376 | REQUIRE(content == actual); 377 | 378 | inif.decode(actual); 379 | 380 | REQUIRE(inif.size() == 1); 381 | REQUIRE(inif.find("Fo#o") != inif.end()); 382 | REQUIRE(inif["Fo#o"].size() == 2); 383 | REQUIRE(inif["Fo#o"].find("heREMllo") != inif["Fo#o"].end()); 384 | REQUIRE(inif["Fo#o"].find("world") != inif["Fo#o"].end()); 385 | REQUIRE(inif["Fo#o"]["heREMllo"].as() == "worlREMd"); 386 | REQUIRE(inif["Fo#o"]["world"].as() == "he//llo"); 387 | 388 | auto actual2 = inif.encode(); 389 | 390 | REQUIRE(content == actual2); 391 | } 392 | 393 | TEST_CASE("save with bool fields", "IniFile") 394 | { 395 | ini::IniFile inif; 396 | inif["Foo"]["bar1"] = true; 397 | inif["Foo"]["bar2"] = false; 398 | 399 | std::string result = inif.encode(); 400 | REQUIRE(result == "[Foo]\nbar1=true\nbar2=false\n\n"); 401 | } 402 | 403 | TEST_CASE("save with char fields", "IniFile") 404 | { 405 | ini::IniFile inif; 406 | inif["Foo"]["bar1"] = static_cast('c'); 407 | inif["Foo"]["bar2"] = static_cast('q'); 408 | 409 | std::string result = inif.encode(); 410 | REQUIRE(result == "[Foo]\nbar1=c\nbar2=q\n\n"); 411 | } 412 | 413 | TEST_CASE("save with unsigned char fields", "IniFile") 414 | { 415 | ini::IniFile inif; 416 | inif["Foo"]["bar1"] = static_cast('c'); 417 | inif["Foo"]["bar2"] = static_cast('q'); 418 | 419 | std::string result = inif.encode(); 420 | REQUIRE(result == "[Foo]\nbar1=c\nbar2=q\n\n"); 421 | } 422 | 423 | TEST_CASE("save with short fields", "IniFile") 424 | { 425 | ini::IniFile inif; 426 | inif["Foo"]["bar1"] = static_cast(1); 427 | inif["Foo"]["bar2"] = static_cast(-2); 428 | 429 | std::string result = inif.encode(); 430 | REQUIRE(result == "[Foo]\nbar1=1\nbar2=-2\n\n"); 431 | } 432 | 433 | TEST_CASE("save with unsigned short fields", "IniFile") 434 | { 435 | ini::IniFile inif; 436 | inif["Foo"]["bar1"] = static_cast(1); 437 | inif["Foo"]["bar2"] = static_cast(13); 438 | 439 | std::string result = inif.encode(); 440 | REQUIRE(result == "[Foo]\nbar1=1\nbar2=13\n\n"); 441 | } 442 | 443 | TEST_CASE("save with int fields", "IniFile") 444 | { 445 | ini::IniFile inif; 446 | inif["Foo"]["bar1"] = static_cast(1); 447 | inif["Foo"]["bar2"] = static_cast(-2); 448 | 449 | std::string result = inif.encode(); 450 | REQUIRE(result == "[Foo]\nbar1=1\nbar2=-2\n\n"); 451 | } 452 | 453 | TEST_CASE("save with unsigned int fields", "IniFile") 454 | { 455 | ini::IniFile inif; 456 | inif["Foo"]["bar1"] = static_cast(1); 457 | inif["Foo"]["bar2"] = static_cast(13); 458 | 459 | std::string result = inif.encode(); 460 | REQUIRE(result == "[Foo]\nbar1=1\nbar2=13\n\n"); 461 | } 462 | 463 | TEST_CASE("save with long fields", "IniFile") 464 | { 465 | ini::IniFile inif; 466 | inif["Foo"]["bar1"] = static_cast(1); 467 | inif["Foo"]["bar2"] = static_cast(-2); 468 | 469 | std::string result = inif.encode(); 470 | REQUIRE(result == "[Foo]\nbar1=1\nbar2=-2\n\n"); 471 | } 472 | 473 | TEST_CASE("save with unsigned long fields", "IniFile") 474 | { 475 | ini::IniFile inif; 476 | inif["Foo"]["bar1"] = static_cast(1); 477 | inif["Foo"]["bar2"] = static_cast(13); 478 | 479 | std::string result = inif.encode(); 480 | REQUIRE(result == "[Foo]\nbar1=1\nbar2=13\n\n"); 481 | } 482 | 483 | TEST_CASE("save with double fields", "IniFile") 484 | { 485 | ini::IniFile inif; 486 | inif["Foo"]["bar1"] = static_cast(1.2); 487 | inif["Foo"]["bar2"] = static_cast(-2.4); 488 | 489 | std::string result = inif.encode(); 490 | REQUIRE(result == "[Foo]\nbar1=1.2\nbar2=-2.4\n\n"); 491 | } 492 | 493 | TEST_CASE("save with float fields", "IniFile") 494 | { 495 | ini::IniFile inif; 496 | inif["Foo"]["bar1"] = static_cast(1.2f); 497 | inif["Foo"]["bar2"] = static_cast(-2.4f); 498 | 499 | std::string result = inif.encode(); 500 | REQUIRE(result == "[Foo]\nbar1=1.2\nbar2=-2.4\n\n"); 501 | } 502 | 503 | TEST_CASE("save with std::string fields", "IniFile") 504 | { 505 | ini::IniFile inif; 506 | inif["Foo"]["bar1"] = static_cast("hello"); 507 | inif["Foo"]["bar2"] = static_cast("world"); 508 | 509 | std::string result = inif.encode(); 510 | REQUIRE(result == "[Foo]\nbar1=hello\nbar2=world\n\n"); 511 | } 512 | 513 | TEST_CASE("save with const char* fields", "IniFile") 514 | { 515 | ini::IniFile inif; 516 | inif["Foo"]["bar1"] = static_cast("hello"); 517 | inif["Foo"]["bar2"] = static_cast("world"); 518 | 519 | std::string result = inif.encode(); 520 | REQUIRE(result == "[Foo]\nbar1=hello\nbar2=world\n\n"); 521 | } 522 | 523 | TEST_CASE("save with char* fields", "IniFile") 524 | { 525 | ini::IniFile inif; 526 | char bar1[6] = "hello"; 527 | char bar2[6] = "world"; 528 | inif["Foo"]["bar1"] = static_cast(bar1); 529 | inif["Foo"]["bar2"] = static_cast(bar2); 530 | 531 | std::string result = inif.encode(); 532 | REQUIRE(result == "[Foo]\nbar1=hello\nbar2=world\n\n"); 533 | } 534 | 535 | TEST_CASE("save with string literal fields", "IniFile") 536 | { 537 | ini::IniFile inif; 538 | inif["Foo"]["bar1"] = "hello"; 539 | inif["Foo"]["bar2"] = "world"; 540 | 541 | std::string result = inif.encode(); 542 | REQUIRE(result == "[Foo]\nbar1=hello\nbar2=world\n\n"); 543 | } 544 | 545 | #ifdef __cpp_lib_string_view 546 | TEST_CASE("save with std::string_view fields", "IniFile") 547 | { 548 | ini::IniFile inif; 549 | inif["Foo"]["bar1"] = std::string_view("hello"); 550 | inif["Foo"]["bar2"] = std::string_view("world"); 551 | 552 | std::string result = inif.encode(); 553 | REQUIRE(result == "[Foo]\nbar1=hello\nbar2=world\n\n"); 554 | } 555 | #endif 556 | 557 | TEST_CASE("save with custom field sep", "IniFile") 558 | { 559 | ini::IniFile inif(':', '#'); 560 | inif["Foo"]["bar1"] = true; 561 | inif["Foo"]["bar2"] = false; 562 | 563 | std::string result = inif.encode(); 564 | REQUIRE(result == "[Foo]\nbar1:true\nbar2:false\n\n"); 565 | } 566 | 567 | TEST_CASE("inline comments in sections are discarded", "IniFile") 568 | { 569 | std::istringstream ss("[Foo] # This is an inline comment\nbar=Hello world!"); 570 | ini::IniFile inif(ss); 571 | 572 | REQUIRE(inif.find("Foo") != inif.end()); 573 | } 574 | 575 | TEST_CASE("inline comments in fields are discarded", "IniFile") 576 | { 577 | std::istringstream ss("[Foo]\n" 578 | "bar=Hello #world!"); 579 | ini::IniFile inif(ss); 580 | 581 | REQUIRE(inif["Foo"]["bar"].as() == "Hello"); 582 | } 583 | 584 | TEST_CASE("inline comments can be escaped", "IniFile") 585 | { 586 | std::istringstream ss("[Foo]\n" 587 | "bar=Hello \\#world!"); 588 | ini::IniFile inif(ss); 589 | 590 | REQUIRE(inif["Foo"]["bar"].as() == "Hello #world!"); 591 | } 592 | 593 | TEST_CASE("escape characters are kept if not before a comment prefix", "IniFile") 594 | { 595 | std::istringstream ss("[Foo]\n" 596 | "bar=Hello \\world!"); 597 | ini::IniFile inif(ss); 598 | 599 | REQUIRE(inif["Foo"]["bar"].as() == "Hello \\world!"); 600 | } 601 | 602 | TEST_CASE("multi-line values are read correctly with space indents", "IniFile") 603 | { 604 | std::istringstream ss("[Foo]\n" 605 | "bar=Hello\n" 606 | " world!"); 607 | ini::IniFile inif; 608 | inif.setMultiLineValues(true); 609 | inif.decode(ss); 610 | 611 | REQUIRE(inif["Foo"]["bar"].as() == "Hello\nworld!"); 612 | } 613 | 614 | TEST_CASE("multi-line values are read correctly with tab indents", "IniFile") 615 | { 616 | std::istringstream ss("[Foo]\n" 617 | "bar=Hello\n" 618 | "\tworld!"); 619 | ini::IniFile inif; 620 | inif.setMultiLineValues(true); 621 | inif.decode(ss); 622 | 623 | REQUIRE(inif["Foo"]["bar"].as() == "Hello\nworld!"); 624 | } 625 | 626 | TEST_CASE("multi-line values discard end-of-line comments", "IniFile") 627 | { 628 | std::istringstream ss("[Foo]\n" 629 | "bar=Hello ; everyone\n" 630 | " world! ; comment"); 631 | ini::IniFile inif; 632 | inif.setMultiLineValues(true); 633 | inif.decode(ss); 634 | 635 | REQUIRE(inif["Foo"]["bar"].as() == "Hello\nworld!"); 636 | } 637 | 638 | TEST_CASE("multi-line values discard interspersed comment lines", "IniFile") 639 | { 640 | std::istringstream ss("[Foo]\n" 641 | "bar=Hello\n" 642 | "; everyone\n" 643 | " world!"); 644 | ini::IniFile inif; 645 | inif.setMultiLineValues(true); 646 | inif.decode(ss); 647 | 648 | REQUIRE(inif["Foo"]["bar"].as() == "Hello\nworld!"); 649 | } 650 | 651 | TEST_CASE("multi-line values should not be parsed when disabled", "IniFile") 652 | { 653 | std::istringstream ss("[Foo]\n" 654 | " bar=Hello\n" 655 | " baz=world!"); 656 | ini::IniFile inif; 657 | inif.setMultiLineValues(false); 658 | inif.decode(ss); 659 | 660 | REQUIRE(inif["Foo"]["bar"].as() == "Hello"); 661 | REQUIRE(inif["Foo"]["baz"].as() == "world!"); 662 | } 663 | 664 | TEST_CASE("multi-line values should be parsed when enabled, even when the continuation contains =", "IniFile") 665 | { 666 | std::istringstream ss("[Foo]\n" 667 | " bar=Hello\n" 668 | " baz=world!"); 669 | ini::IniFile inif; 670 | inif.setMultiLineValues(true); 671 | inif.decode(ss); 672 | 673 | REQUIRE(inif["Foo"]["bar"].as() == "Hello\nbaz=world!"); 674 | REQUIRE(inif["Foo"]["baz"].as() == ""); 675 | } 676 | 677 | TEST_CASE("when multi-line values are enabled, write newlines as multi-line value continuations", "IniFile") 678 | { 679 | ini::IniFile inif; 680 | inif.setMultiLineValues(true); 681 | 682 | inif["Foo"] = ini::IniSection(); 683 | inif["Foo"]["bar"] = "Hello\nworld!"; 684 | 685 | std::string str = inif.encode(); 686 | 687 | REQUIRE(str == "[Foo]\n" 688 | "bar=Hello\n" 689 | "\tworld!\n\n"); 690 | } 691 | 692 | TEST_CASE("stringInsensitiveLess operator() returns true if and only if first parameter is less than the second " 693 | "ignoring sensitivity", 694 | "StringInsensitiveLessFunctor") 695 | { 696 | ini::StringInsensitiveLess cc; 697 | 698 | REQUIRE(cc("a", "b")); 699 | REQUIRE(cc("a", "B")); 700 | REQUIRE(cc("aaa", "aaB")); 701 | } 702 | 703 | TEST_CASE( 704 | "stringInsensitiveLess operator() returns false when words differs only in case", "StringInsensitiveLessFunctor") 705 | { 706 | ini::StringInsensitiveLess cc; 707 | 708 | REQUIRE(cc("AA", "aa") == false); 709 | } 710 | 711 | TEST_CASE("stringInsensitiveLess operator() has a case insensitive strict weak ordering policy", 712 | "StringInsensitiveLessFunctor") 713 | { 714 | ini::StringInsensitiveLess cc; 715 | 716 | REQUIRE(cc("B", "a") == false); 717 | } 718 | 719 | TEST_CASE("default inifile parser is case sensitive", "IniFile") 720 | { 721 | std::istringstream ss("[FOO]\nbar=bla"); 722 | ini::IniFile inif(ss); 723 | 724 | REQUIRE(inif.find("foo") == inif.end()); 725 | REQUIRE(inif["FOO"].find("BAR") == inif["FOO"].end()); 726 | } 727 | 728 | TEST_CASE("case insensitive inifile ignores case of section", "IniFile") 729 | { 730 | std::istringstream ss("[FOO]\nbar=bla"); 731 | ini::IniFileCaseInsensitive inif(ss); 732 | 733 | REQUIRE(inif.find("foo") != inif.end()); 734 | REQUIRE(inif.find("FOO") != inif.end()); 735 | } 736 | 737 | TEST_CASE("case insensitive inifile ignores case of field", "IniFile") 738 | { 739 | std::istringstream ss("[FOO]\nbar=bla"); 740 | ini::IniFileCaseInsensitive inif(ss); 741 | 742 | REQUIRE(inif["FOO"].find("BAR") != inif["FOO"].end()); 743 | } 744 | 745 | TEST_CASE(".as<>() works with IniFileCaseInsensitive", "IniFile") 746 | { 747 | std::istringstream ss("[FOO]\nbar=bla"); 748 | ini::IniFileCaseInsensitive inif(ss); 749 | 750 | REQUIRE(inif["FOO"]["bar"].as() == "bla"); 751 | } 752 | 753 | TEST_CASE("trim() works with empty strings", "TrimFunction") 754 | { 755 | std::string example1 = ""; 756 | std::string example2 = " \t\n "; 757 | 758 | ini::trim(example1); 759 | ini::trim(example2); 760 | 761 | REQUIRE(example1.size() == 0); 762 | REQUIRE(example2.size() == 0); 763 | } 764 | 765 | TEST_CASE("trim() works with already trimmed strings", "TrimFunction") 766 | { 767 | std::string example1 = "example_text"; 768 | std::string example2 = "example \t\n text"; 769 | 770 | ini::trim(example1); 771 | ini::trim(example2); 772 | 773 | REQUIRE(example1 == "example_text"); 774 | REQUIRE(example2 == "example \t\n text"); 775 | } 776 | 777 | TEST_CASE("trim() works with untrimmed strings", "TrimFunction") 778 | { 779 | std::string example1 = "example text "; 780 | std::string example2 = " example text"; 781 | std::string example3 = " example text "; 782 | std::string example4 = " \t\n example \t\n text \t\n "; 783 | 784 | ini::trim(example1); 785 | ini::trim(example2); 786 | ini::trim(example3); 787 | ini::trim(example4); 788 | 789 | REQUIRE(example1 == "example text"); 790 | REQUIRE(example2 == "example text"); 791 | REQUIRE(example3 == "example text"); 792 | REQUIRE(example4 == "example \t\n text"); 793 | } 794 | 795 | /*************************************************** 796 | * Failing Tests 797 | ***************************************************/ 798 | 799 | TEST_CASE("fail to load unclosed section", "IniFile") 800 | { 801 | ini::IniFile inif; 802 | REQUIRE_THROWS(inif.decode("[Foo\nbar=bla")); 803 | } 804 | 805 | TEST_CASE("fail to load field without equal", "IniFile") 806 | { 807 | ini::IniFile inif; 808 | REQUIRE_THROWS(inif.decode("[Foo]\nbar")); 809 | } 810 | 811 | TEST_CASE("fail to parse a multi-line field without indentation (when enabled)", "IniFile") 812 | { 813 | ini::IniFile inif; 814 | inif.setMultiLineValues(true); 815 | REQUIRE_THROWS(inif.decode("[Foo]\nbar=Hello\nworld!")); 816 | } 817 | 818 | TEST_CASE("fail to parse a multi-line field without indentation (when disabled)", "IniFile") 819 | { 820 | ini::IniFile inif; 821 | inif.setMultiLineValues(false); 822 | REQUIRE_THROWS(inif.decode("[Foo]\nbar=Hello\nworld!")); 823 | } 824 | 825 | TEST_CASE("fail to continue multi-line field without start (when enabled)", "IniFile") 826 | { 827 | ini::IniFile inif; 828 | inif.setMultiLineValues(true); 829 | REQUIRE_THROWS(inif.decode("[Foo]\n world!\nbar=Hello")); 830 | } 831 | 832 | TEST_CASE("fail to continue multi-line field without start (when disabled)", "IniFile") 833 | { 834 | ini::IniFile inif; 835 | inif.setMultiLineValues(false); 836 | REQUIRE_THROWS(inif.decode("[Foo]\n world!\nbar=Hello")); 837 | } 838 | 839 | TEST_CASE("fail to parse as bool", "IniFile") 840 | { 841 | std::istringstream ss("[Foo]\nbar=bla"); 842 | ini::IniFile inif(ss); 843 | 844 | REQUIRE(inif.size() == 1); 845 | REQUIRE(inif["Foo"].size() == 1); 846 | REQUIRE_THROWS(inif["Foo"]["bar"].as()); 847 | } 848 | 849 | TEST_CASE("fail to parse as int", "IniFile") 850 | { 851 | std::istringstream ss("[Foo]\nbar=bla"); 852 | ini::IniFile inif(ss); 853 | 854 | REQUIRE(inif.size() == 1); 855 | REQUIRE(inif["Foo"].size() == 1); 856 | REQUIRE_THROWS(inif["Foo"]["bar"].as()); 857 | } 858 | 859 | TEST_CASE("fail to parse as double", "IniFile") 860 | { 861 | std::istringstream ss("[Foo]\nbar=bla"); 862 | ini::IniFile inif(ss); 863 | 864 | REQUIRE(inif.size() == 1); 865 | REQUIRE(inif["Foo"].size() == 1); 866 | REQUIRE_THROWS(inif["Foo"]["bar"].as()); 867 | } 868 | 869 | TEST_CASE("fail to parse field without section", "IniFile") 870 | { 871 | ini::IniFile inif; 872 | REQUIRE_THROWS(inif.decode("bar=bla")); 873 | } 874 | 875 | TEST_CASE("spaces are not taken into account in field names", "IniFile") 876 | { 877 | std::istringstream ss(("[Foo]\n \t bar \t =hello world")); 878 | ini::IniFile inif(ss); 879 | 880 | REQUIRE(inif["Foo"].find("bar") != inif["Foo"].end()); 881 | REQUIRE(inif["Foo"]["bar"].as() == "hello world"); 882 | } 883 | 884 | TEST_CASE("spaces are not taken into account in field values", "IniFile") 885 | { 886 | std::istringstream ss(("[Foo]\nbar= \t hello world \t ")); 887 | ini::IniFile inif(ss); 888 | 889 | REQUIRE(inif["Foo"]["bar"].as() == "hello world"); 890 | } 891 | 892 | TEST_CASE("spaces are not taken into account in sections", "IniFile") 893 | { 894 | std::istringstream ss(" \t [Foo] \t \nbar=bla"); 895 | ini::IniFile inif(ss); 896 | 897 | REQUIRE(inif.find("Foo") != inif.end()); 898 | } 899 | 900 | TEST_CASE("parse section with duplicate field and overwriteDuplicateFields_ set to false", "IniFile") 901 | { 902 | ini::IniFile inif; 903 | inif.allowOverwriteDuplicateFields(false); 904 | REQUIRE_THROWS(inif.decode("[Foo]\nbar=hello\nbar=world")); 905 | } 906 | --------------------------------------------------------------------------------