├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── CMakeLists.txt ├── README.md ├── catch2 ├── catch_amalgamated.cpp └── catch_amalgamated.hpp ├── include └── tortellini.hh ├── logo.png └── test.cc /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | tab_width = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [{*.json,*.json.example,*.gyp,*.yml,*.yaml}] 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [{*.py,*.asm}] 17 | indent_style = space 18 | 19 | [*.py] 20 | indent_size = 4 21 | 22 | [*.asm] 23 | indent_size = 8 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | 28 | # Ideal settings - some plugins might support these. 29 | [*.js] 30 | quote_type = single 31 | 32 | [{*.c,*.cc,*.h,*.hh,*.cpp,*.hpp,*.m,*.mm,*.mpp,*.js,*.java,*.go,*.rs,*.php,*.ng,*.jsx,*.ts,*.d,*.cs,*.swift}] 33 | curly_bracket_next_line = false 34 | spaces_around_operators = true 35 | spaces_around_brackets = outside 36 | # close enough to 1TB 37 | indent_brace_style = K&R 38 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: qix- 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | 4 | jobs: 5 | buildlinux: 6 | name: Test 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 3 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Install CMake 12 | run: | 13 | sudo apt install cmake 14 | - name: Configure 15 | run: | 16 | mkdir build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON 17 | - name: Build 18 | run: | 19 | cmake --build build 20 | - name: Test 21 | run: | 22 | cd build && ctest -VV 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /.cache/ 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project (tortellini) 2 | cmake_minimum_required (VERSION 3.2) 3 | 4 | add_library (tortellini INTERFACE) 5 | target_include_directories (tortellini INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") 6 | target_sources (tortellini INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include/tortellini.hh") 7 | 8 | if (BUILD_TESTING) 9 | enable_testing () 10 | add_executable (tortellini-tests test.cc catch2/catch_amalgamated.hpp catch2/catch_amalgamated.cpp) 11 | target_link_libraries (tortellini-tests PRIVATE tortellini) 12 | 13 | target_compile_options (tortellini-tests PRIVATE 14 | $,/W4,-Wall -Wextra -Werror -pedantic> 15 | ) 16 | 17 | add_test ( 18 | NAME tortellini-test 19 | COMMAND $ -s 20 | ) 21 | endif () 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Tortellini 3 |

4 | 5 | The stupid - _and I mean really, really stupid_ - INI file reader and writer for C++11 and above. Calorie free (no dependencies)! 6 | 7 | ```c++ 8 | #include 9 | 10 | // (optional) 11 | #include 12 | 13 | int main() { 14 | tortellini::ini ini; 15 | 16 | // (optional) Read INI in from a file. 17 | // Default construction and subsequent assignment is just fine, too. 18 | std::ifstream in("config.ini"); 19 | in >> ini; 20 | 21 | // Retrieval 22 | // 23 | // All value retrievals must be done via the "default" operator 24 | // (the pipe operator). 25 | // 26 | // The right-hand-side type is what is returned. Anything other 27 | // than a string-like type causes a parse (see caveats section below). 28 | // 29 | // All keys are case-INsensitive. This includes section headers. 30 | std::string s = ini["Section"]["key"] | "default string"; 31 | int i = ini["Section"]["key"] | 42; // default int 32 | long l = ini["Section"]["key"] | 42L; // default long 33 | long long ll = ini["Section"]["key"] | 42LL; // default long long 34 | unsigned int u = ini["Section"]["key"] | 42u; // default unsigned int 35 | unsigned long ul = ini["Section"]["key"] | 42UL; // default unsigned long 36 | unsigned long long ull = ini["Section"]["key"] | 42ULL; // default unsigned long long 37 | float f = ini["Section"]["key"] | 42.0f; // default float 38 | double d = ini["Section"]["key"] | 42.0; // default double 39 | long double ld = ini["Section"]["key"] | 42.0L; // default long double 40 | bool b = ini["Section"]["key"] | true; // default bool 41 | 42 | // Assignment 43 | // 44 | // (This example uses the same key, but of course 45 | // in reality this would just be overwriting the 46 | // same key over and over again - probably not 47 | // what you'd really want. I would hope I wouldn't 48 | // have to explain this, but you never know.) 49 | ini["New Section"]["new-key"] = "Noodles are tasty."; 50 | ini["New Section"]["new-key"] = 1234; 51 | ini["New Section"]["new-key"] = 1234u; 52 | ini["New Section"]["new-key"] = 1234L; 53 | ini["New Section"]["new-key"] = 1234UL; 54 | ini["New Section"]["new-key"] = 1234LL; 55 | ini["New Section"]["new-key"] = 1234ULL; 56 | ini["New Section"]["new-key"] = 1234.0f; 57 | ini["New Section"]["new-key"] = 1234.0; 58 | ini["New Section"]["new-key"] = 1234.0L; 59 | ini["New Section"]["new-key"] = true; 60 | 61 | // "Naked" section (top-most key/value pairs, before a section header) 62 | // denoted by empty string in section selector 63 | ini[""]["naked-key"] = "I'll be at the very top, without a section"; 64 | 65 | // You can also iterate over all sections 66 | for (auto &pair : ini) { 67 | std::cout << pair.name << "\n"; // the name of the section 68 | int v = pair.section["some-key"] | 1234; 69 | std::cout << "some-key=" << v << "\n"; 70 | } 71 | 72 | // (optional) Write INI to file. 73 | std::ofstream out("config.ini"); 74 | out << ini; 75 | } 76 | ``` 77 | 78 | # Things you should know. 79 | 80 | This library has very few bells and whistles. It doesn't do anything fancy. 81 | 82 | Here are the guarantees/features: 83 | 84 | - Case-insensitive keys and section headers. Reading from a key/section with different cases will work fine, and writing to an existing section/key will preserve the casing. 85 | - Untouched output (from using `stream << ini`) is guaranteed to be parsable (by using `stream >> ini`). 86 | - Barring I/O issues or exceptions coming from the standard library, Tortellini will not throw or abort (see below section about "invalid data"). 87 | - _Pasta in, pasta out._ Source strings (from a parse) are preserved to the output. If you use `yes` instead of `true`, Tortellini will preserve that (unless the application overwrites the value). 88 | - `yes`, `1` and `true` are all parsable as `bool(true)`. All other values equate to `false`. **NOTE:** This means that `ini[""]["b"] | true` will return `false`, _not_ `true`, if the key exists but is not a valid, parsable truth-ey value. This may be counter-intuitive for some users. 89 | - All values are inherently string, and only parsed when retrieved. 90 | 91 | Here are the caveats: 92 | 93 | - **Do not store or share anything returned from the subscript operators (`[]`).** They return temporary objects that are meant for short-lived interactions. They are NOT lifetime safe and expect the underlying `tortellini::ini` instance to subsist beyond their own lifetimes. 94 | - Invalid keys, values or section names skip the line entirely. This condition is henceforth referred to as "invalid data". 95 | - Integer overflows when parsing are invalid data. 96 | - Mismatched `]` for a `[` line is invalid data. Yes, keys _will_ bleed into the preceding section name. That's user error, not your application's. _Embrace it._ 97 | - Empty keys or empty values (excluding leading/trailing whitespace!) are "invalid data" in that they might be valid but not included in the resulting data and thus _won't_ be re-emitted. 98 | - Empty sections (or sections with 100% invalid data as key/value pairs) are themselves invalid data and are not emitted. 99 | - `[]` is a valid section name. It means the "naked" section. Re-emitting the INI will move those keys to the top regardless of where `[]` is positioned in the input file. 100 | - No comments are supported; `;` is a valid (string) character. 101 | - No caching or memoization; if you retrieve anything but a `std::string`, there _will_ be a parse. I never said Tortellini was hyper-over-optimized. 102 | 103 | # Testing 104 | You can test by running: 105 | 106 | ```bash 107 | mkdir build && cd build 108 | cmake .. -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug 109 | cmake --build . 110 | ctest 111 | ``` 112 | 113 | # License 114 | 115 | You have two choices in license. Pick whichever one you want. 116 | Tortellini is uncucumbered. Go crazy. Eat some pasta. 117 | 118 | ### Unlicense 119 | 120 | ``` 121 | This is free and unencumbered software released into the public domain. 122 | 123 | Anyone is free to copy, modify, publish, use, compile, sell, or 124 | distribute this software, either in source code form or as a compiled 125 | binary, for any purpose, commercial or non-commercial, and by any 126 | means. 127 | 128 | In jurisdictions that recognize copyright laws, the author or authors 129 | of this software dedicate any and all copyright interest in the 130 | software to the public domain. We make this dedication for the benefit 131 | of the public at large and to the detriment of our heirs and 132 | successors. We intend this dedication to be an overt act of 133 | relinquishment in perpetuity of all present and future rights to this 134 | software under copyright law. 135 | 136 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 137 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 138 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 139 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 140 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 141 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 142 | OTHER DEALINGS IN THE SOFTWARE. 143 | 144 | For more information, please refer to 145 | ``` 146 | 147 | ### MIT License 148 | 149 | ``` 150 | Copyright (c) 2020 Josh Junon 151 | 152 | Permission is hereby granted, free of charge, to any person obtaining a copy 153 | of this software and associated documentation files (the "Software"), to deal 154 | in the Software without restriction, including without limitation the rights 155 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 156 | copies of the Software, and to permit persons to whom the Software is 157 | furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in all 160 | copies or substantial portions of the Software. 161 | 162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 168 | SOFTWARE. 169 | ``` 170 | -------------------------------------------------------------------------------- /include/tortellini.hh: -------------------------------------------------------------------------------- 1 | #ifndef TORTELLINI_HH__ 2 | #define TORTELLINI_HH__ 3 | #pragma once 4 | 5 | #ifndef NOMINMAX 6 | # define _TORTELLINI_UNDEFINE_NOMINMAX_PLEASE 7 | # define NOMINMAX 8 | #endif 9 | 10 | /* 11 | ⣀⣀⣀⣀⣀⣀⣀⡀ 12 | ⢀⠄ ⠉⠉⠉⠛⠛⠿⠿⢶⣶⣤⣄⡀ 13 | ⢀⣠⠞⠁ ⠈⠉⠛⠻⠶⣄⡀ 14 | ⢀⣴⠟⠁ ⣀⣠⣤⠴⠖⠒⠈⠉ ⠑⠲⢤⣄⡀ ⠉⠑⠤⡀ 15 | ⣰⡿⠃ ⣠⣴⡾⠟⠋⠁ ⠉⠻⢷⣦⡀ 16 | ⣼⡟ ⢀⣴⡿⠛⠁ ⠉⠻⣦ ⡀ 17 | ⣼⡟ ⣠⡿⠋ ⣀⣤⡀ ⠑⠄ ⢰⡀ 18 | ⢸⡟ ⣰⠟ ⢀⣤⣦⡄ ⠰⣿⣿⣿ ⣇ 19 | ⣿⠁ ⢠⡏ ⠸⣿⣿⡟ ⣤⣤⣀⣠⣾⣦ ⠉⠉⠁ ⢰ ⣿ 20 | ⢀⡏ ⢸ ⡀⠙⠿⣿⣿⠟⠁ ⢠⡏ ⣿⡄ 21 | ⢸⠃ ⠈ ⢸⣷ ⣴⡟ ⣿⠃ 22 | ⠘ ⠘⠡⢤⣤⣀⣀ ⣠⣾⠟ ⢸⣿ 23 | ⠉⠙⠛⠛⠓ ⢀⣴⠾⠋⠁ ⣼⠇ 24 | ⠑⠲⢶⣤⣤⣤⣤⣄⣀⣀⣀⣀ ⡼ ⠐⠊⠉ ⢠⡟ 25 | ⠸⣄ ⠈⠉⠉⠉⠉⠁ ⢸⡇ ⢀⡞ 26 | ⢻⣦ ⢸⡇ ⢀⠄⢀⠎ 27 | ⠹⢷⡀ ⠘⣿⡀ ⢀⣠⣴⠞⠁ 28 | ⠑ ⠘⢧ ⢀⣠⡶⠟⠋⠁ ⠠⣷ 29 | ⠒⠤⣤⣀⣀ ⠑ ⠊⠉ ⣀ ⠈ 30 | ⠉⠛⠻⠿⠶⠶⠤⠤⠄ ⠠⠤⠤⣤⣤⣤⣤⣶⣶⡶⠶⠞⠛⠉ 31 | 32 | TORTELLINI 33 | The really, really dumb INI file format. 34 | 35 | Made for Tide Online. 36 | Cooked up on 6 Mar, 2020. 37 | 38 | ---------------------------------------------------------------------------- 39 | 40 | Released under a dual-license. Take your pick, no whammies. 41 | 42 | UNLICENSE 43 | 44 | This is free and unencumbered software released into the public domain. 45 | 46 | Anyone is free to copy, modify, publish, use, compile, sell, or 47 | distribute this software, either in source code form or as a compiled 48 | binary, for any purpose, commercial or non-commercial, and by any 49 | means. 50 | 51 | In jurisdictions that recognize copyright laws, the author or authors 52 | of this software dedicate any and all copyright interest in the 53 | software to the public domain. We make this dedication for the benefit 54 | of the public at large and to the detriment of our heirs and 55 | successors. We intend this dedication to be an overt act of 56 | relinquishment in perpetuity of all present and future rights to this 57 | software under copyright law. 58 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 60 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 61 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 62 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 63 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 64 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 65 | OTHER DEALINGS IN THE SOFTWARE. 66 | 67 | For more information, please refer to 68 | 69 | MIT LICENSE 70 | 71 | Copyright (c) 2020 Josh Junon 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy 74 | of this software and associated documentation files (the "Software"), to deal 75 | in the Software without restriction, including without limitation the rights 76 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 77 | copies of the Software, and to permit persons to whom the Software is 78 | furnished to do so, subject to the following conditions: 79 | 80 | The above copyright notice and this permission notice shall be included in all 81 | copies or substantial portions of the Software. 82 | 83 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 84 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 85 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 86 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 87 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 88 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 89 | SOFTWARE. 90 | 91 | ---------------------------------------------------------------------------- 92 | */ 93 | 94 | #include 95 | #include 96 | #include 97 | #include 98 | #include 99 | #include 100 | #include 101 | #include 102 | #include 103 | #include 104 | 105 | namespace tortellini { 106 | 107 | class ini final { 108 | friend class value; 109 | 110 | struct case_insensitive { 111 | struct case_insensitive_compare { 112 | bool operator()(const unsigned char &l, const unsigned char &r) const { 113 | // < for map 114 | return std::tolower(l) < std::tolower(r); 115 | } 116 | 117 | inline static bool compare(const unsigned char &l, const unsigned char &r) { 118 | // == for OOB calls 119 | return std::tolower(l) == std::tolower(r); 120 | } 121 | }; 122 | 123 | static inline bool compare(const std::string &l, const std::string &r) noexcept { 124 | // std::equal (like ==) for OOB calls 125 | if (l.size() != r.size()) { 126 | return false; 127 | } 128 | 129 | return std::equal( 130 | l.begin(), l.end(), 131 | r.begin(), 132 | case_insensitive_compare::compare 133 | ); 134 | } 135 | 136 | inline bool operator()(const std::string &l, const std::string &r) const noexcept { 137 | // lex compare (like <) for map 138 | return std::lexicographical_compare( 139 | l.begin(), l.end(), 140 | r.begin(), r.end(), 141 | case_insensitive_compare() 142 | ); 143 | } 144 | }; 145 | 146 | public: 147 | class section; 148 | 149 | class value final { 150 | friend section; 151 | 152 | std::string &_value; 153 | 154 | inline value(std::string &_value) 155 | : _value(_value) 156 | {} 157 | 158 | value(const value &) = delete; 159 | void operator=(const value &) = delete; 160 | 161 | inline value(value &&) = default; 162 | 163 | template 164 | static inline T strparse(const std::string &s, T fallback) noexcept { 165 | if (s.empty()) return fallback; 166 | 167 | try { 168 | size_t idx; 169 | T res = Fn(s, &idx); 170 | return s[idx] ? fallback : res; 171 | } catch (std::out_of_range &) { 172 | return fallback; 173 | } catch (std::invalid_argument &) { 174 | return fallback; 175 | } 176 | } 177 | 178 | template 179 | static inline T strparse(const std::string &s, T fallback) noexcept { 180 | if (s.empty()) return fallback; 181 | 182 | try { 183 | size_t idx; 184 | T res = Fn(s, &idx, 0); 185 | return s[idx] ? fallback : res; 186 | } catch (std::out_of_range &) { 187 | return fallback; 188 | } catch (std::invalid_argument &) { 189 | return fallback; 190 | } 191 | } 192 | 193 | template 194 | typename std::enable_if< 195 | !std::is_convertible::value, 196 | std::string 197 | >::type to_string(T r) const { 198 | if (std::is_same::value) { 199 | return r ? "yes" : "no"; 200 | } else if (std::is_floating_point::value) { 201 | std::ostringstream out; 202 | out << std::setprecision(std::numeric_limits::max_digits10 - 1) << r; 203 | return out.str(); 204 | } else { 205 | return std::to_string(r); 206 | } 207 | } 208 | 209 | template 210 | typename std::enable_if< 211 | std::is_convertible::value, 212 | std::string 213 | >::type to_string(T r) const { 214 | return r; 215 | } 216 | 217 | public: 218 | template 219 | inline value & operator =(const T &v) { 220 | _value = to_string(v); 221 | return *this; 222 | } 223 | 224 | inline bool operator |(bool fallback) const { 225 | return _value.empty() 226 | ? fallback 227 | : ( 228 | case_insensitive::compare(_value, "1") 229 | || case_insensitive::compare(_value, "true") 230 | || case_insensitive::compare(_value, "yes") 231 | ); 232 | } 233 | 234 | inline std::string operator |(std::string fallback) const { 235 | return _value.empty() ? fallback : _value; 236 | } 237 | 238 | inline std::string operator |(const char *fallback) const { 239 | return _value.empty() ? fallback : _value; 240 | } 241 | 242 | inline unsigned long operator |(unsigned long fallback) const { 243 | return strparse(_value, fallback); 244 | } 245 | 246 | inline unsigned long long operator |(unsigned long long fallback) const { 247 | return strparse(_value, fallback); 248 | } 249 | 250 | inline long operator |(long fallback) const { 251 | return strparse(_value, fallback); 252 | } 253 | 254 | inline long long operator |(long long fallback) const { 255 | return strparse(_value, fallback); 256 | } 257 | 258 | inline float operator |(float fallback) const { 259 | return strparse(_value, fallback); 260 | } 261 | 262 | inline double operator |(double fallback) const { 263 | return strparse(_value, fallback); 264 | } 265 | 266 | inline long double operator |(long double fallback) const { 267 | return strparse(_value, fallback); 268 | } 269 | 270 | inline int operator |(int fallback) const { 271 | return strparse(_value, fallback); 272 | } 273 | 274 | inline unsigned int operator |(unsigned int fallback) const { 275 | /* 276 | This is necessary because there is no std::stou. 277 | */ 278 | try { 279 | size_t idx; 280 | unsigned long ul = std::stoul(_value, &idx, 0); 281 | 282 | if ( 283 | sizeof(unsigned int) != sizeof(unsigned long) 284 | && ul > std::numeric_limits::max() 285 | ) { 286 | // out of range 287 | return fallback; 288 | } 289 | 290 | return static_cast(ul); 291 | } catch (std::out_of_range &) { 292 | return fallback; 293 | } catch (std::invalid_argument &) { 294 | return fallback; 295 | } 296 | } 297 | }; 298 | 299 | class section final { 300 | friend class ini; 301 | 302 | std::map &_mapref; 303 | 304 | inline section(std::map &_mapref) 305 | : _mapref(_mapref) 306 | {} 307 | 308 | section(const section &) = delete; 309 | void operator=(const section &) = delete; 310 | 311 | inline section(section &&) = default; 312 | 313 | public: 314 | inline value operator[](std::string key) const { 315 | return value(_mapref[key]); 316 | } 317 | }; 318 | 319 | private: 320 | std::map, case_insensitive> _sections; 321 | 322 | static inline void ltrim(std::string &s) { 323 | s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](int ch) { 324 | return !std::isspace(ch); 325 | })); 326 | } 327 | 328 | static inline void rtrim(std::string &s) { 329 | s.erase(std::find_if(s.rbegin(), s.rend(), [](int ch) { 330 | return !std::isspace(ch); 331 | }).base(), s.end()); 332 | } 333 | 334 | static inline void trim(std::string &s) { 335 | ltrim(s); 336 | rtrim(s); 337 | } 338 | 339 | public: 340 | class iterator { 341 | friend class ini; 342 | using section_iterator = decltype(_sections)::iterator; 343 | section_iterator _itr; 344 | inline iterator(section_iterator itr) 345 | : _itr(itr) 346 | {} 347 | 348 | public: 349 | struct section_pair { 350 | std::string name; 351 | ::tortellini::ini::section section; 352 | }; 353 | 354 | inline ~iterator() = default; 355 | 356 | inline section_pair operator*() noexcept { 357 | return { _itr->first, section(_itr->second) }; 358 | } 359 | 360 | inline iterator & operator++() { 361 | _itr++; 362 | return *this; 363 | } 364 | 365 | inline bool operator==(const iterator &other) const { 366 | return _itr == other._itr; 367 | } 368 | inline bool operator!=(const iterator &other) const { 369 | return _itr != other._itr; 370 | } 371 | }; 372 | 373 | inline ini() = default; 374 | inline ini(const ini &) = default; 375 | inline ini(ini &&) = default; 376 | 377 | inline section operator[](std::string name) noexcept { 378 | return section(_sections[name]); 379 | } 380 | 381 | inline iterator begin() { 382 | return {_sections.begin()}; 383 | } 384 | 385 | inline iterator end() { 386 | return {_sections.end()}; 387 | } 388 | 389 | template 390 | friend inline TStream & operator>>(TStream &stream, tortellini::ini &ini) { 391 | std::string line; 392 | std::string section_name = ""; 393 | bool first_line = true; 394 | 395 | while (std::getline(stream, line)) { 396 | if (first_line) { 397 | first_line = false; 398 | 399 | // trim BOM if it is present. 400 | if (line.length() >= 3 && line.substr(0, 3) == "\xEF\xBB\xBF") { 401 | line = line.substr(3); 402 | } 403 | } 404 | 405 | trim(line); 406 | 407 | if (line.empty()) continue; 408 | 409 | if (line[0] == '[') { 410 | size_t idx = line.find_first_of("]"); 411 | if (idx == std::string::npos) continue; // invalid, drop line 412 | section_name = line.substr(1, idx - 1); 413 | trim(section_name); 414 | continue; 415 | } 416 | 417 | std::string key; 418 | std::string value; 419 | 420 | size_t idx = line.find_first_of("="); 421 | if (idx == std::string::npos) continue; // invalid, drop line 422 | 423 | key = line.substr(0, idx); 424 | trim(key); 425 | 426 | if (key.empty()) continue; 427 | 428 | value = line.substr(idx + 1); 429 | trim(value); 430 | 431 | if (value.empty()) continue; // not really "invalid" but we choose not to keep it 432 | 433 | ini[section_name][key] = value; 434 | } 435 | 436 | return stream; 437 | } 438 | 439 | template 440 | friend inline TStream & operator<<(TStream &stream, const tortellini::ini &ini) { 441 | bool has_sections = false; 442 | 443 | // force emit empty section if it exists 444 | { 445 | const auto &itr = ini._sections.find(""); 446 | if (itr != ini._sections.cend()) { 447 | for (const auto &kv : itr->second) { 448 | if (kv.first.empty() || kv.second.empty()) continue; 449 | stream << kv.first << " = " << kv.second << std::endl; 450 | has_sections = true; 451 | } 452 | } 453 | } 454 | 455 | for (const auto §ion : ini._sections) { 456 | if (section.first.empty()) continue; // already emitted 457 | 458 | bool has_emitted = false; 459 | 460 | for (const auto &kv : section.second) { 461 | if (kv.first.empty() || kv.second.empty()) continue; 462 | 463 | if (!has_emitted) { 464 | if (has_sections) stream << std::endl; 465 | stream << '[' << section.first << ']' << std::endl; 466 | has_emitted = true; 467 | has_sections = true; 468 | } 469 | 470 | stream << kv.first << " = " << kv.second << std::endl; 471 | } 472 | } 473 | 474 | return stream; 475 | } 476 | }; 477 | 478 | } 479 | 480 | #ifdef _TORTELLINI_UNDEFINE_NOMINMAX_PLEASE 481 | # undef NOMINMAX 482 | #endif 483 | #endif 484 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qix-/tortellini/b47a39af410e600ece54ba1d8b990f5699e02407/logo.png -------------------------------------------------------------------------------- /test.cc: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch2/catch_amalgamated.hpp" 3 | #include "../include/tortellini.hh" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | using namespace Catch; 10 | 11 | namespace{ 12 | template 13 | void test_read(const std::string text, typename std::enable_if::value, T>::type expected){ 14 | tortellini::ini ini; 15 | std::stringstream iss; 16 | iss << "v = " << text << "\n"; 17 | iss >> ini; 18 | const auto uut = ini[""]["v"] | T{}; 19 | CHECK(uut == expected); 20 | } 21 | 22 | template 23 | void test_read(const std::string text, typename std::enable_if::value, T>::type expected){ 24 | tortellini::ini ini; 25 | std::stringstream iss; 26 | iss << "v = " << text << "\n"; 27 | iss >> ini; 28 | const auto uut = ini[""]["v"] | T{}; 29 | CHECK(uut == Approx(expected)); 30 | } 31 | 32 | template 33 | void test_default_wrong_type(const typename std::enable_if::value, T>::type expected){ 34 | tortellini::ini ini; 35 | const auto text = "v = hello\n"; 36 | std::istringstream iss{text}; 37 | iss >> ini; 38 | const auto uut = ini[""]["v"] | expected; 39 | CHECK(uut == expected); 40 | } 41 | 42 | template 43 | void test_default_wrong_type(const typename std::enable_if::value, T>::type expected){ 44 | tortellini::ini ini; 45 | const auto text = "v = hello\n"; 46 | std::istringstream iss{text}; 47 | iss >> ini; 48 | const auto uut = ini[""]["v"] | expected; 49 | CHECK(uut == Approx(expected)); 50 | } 51 | 52 | template 53 | void test_oor(const typename std::enable_if::value && !std::is_same::value, T>::type expected){ 54 | tortellini::ini ini; 55 | const auto max = std::to_string(std::numeric_limits::max()); 56 | const auto oor = max + "00"; 57 | const auto text = "v = " + oor + "\n"; 58 | std::istringstream iss{text}; 59 | iss >> ini; 60 | const auto uut = ini[""]["v"] | expected; 61 | CHECK(uut == expected); 62 | } 63 | 64 | template 65 | void test_oor(const typename std::enable_if::value, T>::type expected){ 66 | tortellini::ini ini; 67 | const auto max = std::to_string(std::numeric_limits::max()); 68 | const auto oor = "1" + max; 69 | const auto text = "v = " + oor + "\n"; 70 | std::istringstream iss{text}; 71 | iss >> ini; 72 | const auto uut = ini[""]["v"] | expected; 73 | CHECK(uut == Approx(expected)); 74 | } 75 | 76 | template 77 | void test_oor(const typename std::enable_if::value, T>::type expected){ 78 | tortellini::ini ini; 79 | const auto text = "v = 5\n"; 80 | std::istringstream iss{text}; 81 | iss >> ini; 82 | const auto uut = ini[""]["v"] | expected; 83 | CHECK(!uut); 84 | } 85 | 86 | template 87 | void test_empty(const typename std::enable_if::value, T>::type expected){ 88 | tortellini::ini ini; 89 | const auto uut = ini[""]["v"] | expected; 90 | CHECK(uut == expected); 91 | } 92 | 93 | template 94 | void test_empty(const typename std::enable_if::value, T>::type expected){ 95 | tortellini::ini ini; 96 | const auto uut = ini[""]["v"] | expected; 97 | CHECK(uut == Approx(expected)); 98 | } 99 | 100 | template 101 | void test_create(const typename std::enable_if::value, T>::type value, const std::string alternative_text){ 102 | tortellini::ini ini; 103 | ini[""]["uut"] = value; 104 | std::ostringstream oss; 105 | oss << ini; 106 | 107 | std::stringstream ss; 108 | ss << "uut = " << alternative_text << "\n"; 109 | CHECK(oss.str() == ss.str()); 110 | } 111 | 112 | template 113 | void test_create(const typename std::enable_if::value, T>::type expected){ 114 | tortellini::ini ini; 115 | ini[""]["uut"] = expected; 116 | std::ostringstream oss; 117 | oss << ini; 118 | 119 | std::stringstream ss; 120 | ss << "uut = " << std::setprecision(std::numeric_limits::max_digits10 -1) << expected << "\n"; 121 | CHECK(oss.str() == ss.str()); 122 | } 123 | } 124 | TEST_CASE("Reading an ini"){ 125 | tortellini::ini ini; 126 | 127 | SECTION("Can read in"){ 128 | std::istringstream iss{"hello"}; 129 | iss >> ini; 130 | } 131 | 132 | SECTION("Can read a global section"){ 133 | const auto text = std::string{"naked = rat\n"}; 134 | std::istringstream ss{text}; 135 | ss >> ini; 136 | CHECK(std::string{ini[""]["naked"] | "none"} == "rat"); 137 | } 138 | 139 | SECTION("Can read another section"){ 140 | const auto text = std::string{"[hello]\nnaked = rat\n"}; 141 | std::istringstream ss{text}; 142 | ss >> ini; 143 | CHECK(std::string{ini["hello"]["naked"] | "none"} == "rat"); 144 | } 145 | 146 | SECTION("Can read periods in key names"){ 147 | const auto text = std::string{"foo.0 = 10\nfoo.1 = 20\n"}; 148 | std::istringstream ss{text}; 149 | ss >> ini; 150 | CHECK((ini[""]["foo.0"] | 0) == 10); 151 | CHECK((ini[""]["foo.1"] | 0) == 20); 152 | } 153 | 154 | SECTION("int"){ 155 | test_read("5", 5); 156 | } 157 | 158 | SECTION("string"){ 159 | test_read("hello", "hello"); 160 | } 161 | 162 | SECTION("const char *"){ 163 | test_read("hello", "hello"); 164 | } 165 | 166 | SECTION("char"){ 167 | test_read("4", '4'-0x30); 168 | test_read("3", '3'-0x30); 169 | } 170 | 171 | SECTION("bool"){ 172 | test_read("1", true); 173 | test_read("yes", true); 174 | test_read("true", true); 175 | test_read("0", false); 176 | test_read("no", false); 177 | test_read("false", false); 178 | } 179 | 180 | SECTION("case insensitive header"){ 181 | const auto text = std::string{"[HelLo]\nv = 4\n"}; 182 | std::istringstream ss{text}; 183 | ss >> ini; 184 | CHECK(int{ini["HELLO"]["v"] | 9999} == 4); 185 | } 186 | 187 | SECTION("case insensitive variable name"){ 188 | const auto text = std::string{"[hello]\nv = 4\n"}; 189 | std::istringstream ss{text}; 190 | ss >> ini; 191 | CHECK(int{ini["hello"]["V"] | 9999} == 4); 192 | } 193 | 194 | SECTION("Can handle blank lines"){ 195 | const auto text = std::string{"\n[hello]\n\n\nv = 4"}; 196 | std::istringstream iss{text}; 197 | iss >> ini; 198 | CHECK(int{ini["hello"]["v"] | -99} == 4); 199 | } 200 | 201 | SECTION("unsigned long"){ 202 | test_read("400", 400); 203 | } 204 | 205 | SECTION("long"){ 206 | test_read("400", 400); 207 | test_read("-32", -32); 208 | } 209 | 210 | SECTION("float"){ 211 | test_read("433", 433); 212 | test_read("433.3", 433.3); 213 | test_read("-432.2", -432.2); 214 | test_read("-32", -32); 215 | } 216 | 217 | SECTION("long double"){ 218 | test_read("433", 433); 219 | test_read("433.3", 433.3); 220 | test_read("-433", -433); 221 | test_read("-433.2", -433.2); 222 | } 223 | SECTION("unsigned int"){ 224 | test_read("33", 33); 225 | } 226 | 227 | } 228 | 229 | TEST_CASE("File reading failures"){ 230 | tortellini::ini ini; 231 | SECTION("Discards incomplete header"){ 232 | const auto text = "[hello\nv = 4"; 233 | std::istringstream ss{text}; 234 | ss >> ini; 235 | CHECK(int{ini["hello"]["v"] | -99} == -99); 236 | } 237 | } 238 | 239 | TEST_CASE("Can use default when value is wrong type"){ 240 | 241 | SECTION("unsigned long"){ 242 | test_default_wrong_type(5432); 243 | } 244 | 245 | SECTION("Int"){ 246 | test_default_wrong_type(543); 247 | test_default_wrong_type(-5432); 248 | } 249 | 250 | SECTION("bool"){ 251 | test_default_wrong_type(false); // All wrong types are false 252 | } 253 | 254 | SECTION("double"){ 255 | test_default_wrong_type(4.3); 256 | test_default_wrong_type(-432.02); 257 | } 258 | 259 | SECTION("uint"){ 260 | test_default_wrong_type(432); 261 | } 262 | 263 | SECTION("unsigned long long"){ 264 | test_default_wrong_type(5432); 265 | } 266 | 267 | SECTION("long long"){ 268 | test_default_wrong_type(5432); 269 | test_default_wrong_type(-432); 270 | } 271 | 272 | SECTION("long"){ 273 | test_default_wrong_type(43); 274 | test_default_wrong_type(-432); 275 | } 276 | 277 | SECTION("long double"){ 278 | test_default_wrong_type(543.2); 279 | test_default_wrong_type(-80.9); 280 | } 281 | 282 | SECTION("float"){ 283 | test_default_wrong_type(432); 284 | test_default_wrong_type(-4.3); 285 | } 286 | } 287 | 288 | TEST_CASE("Can use defaults when value out of range"){ 289 | 290 | SECTION("Int"){ 291 | test_oor(44); 292 | test_oor(-42); 293 | } 294 | 295 | SECTION("bool"){ 296 | test_oor(true); 297 | test_oor(false); 298 | } 299 | 300 | SECTION("double"){ 301 | test_oor(66.601); 302 | test_oor(-543); 303 | } 304 | 305 | SECTION("uint"){ 306 | test_oor(44); 307 | } 308 | 309 | SECTION("unsigned long"){ 310 | test_oor(666); 311 | } 312 | 313 | SECTION("unsigned long long"){ 314 | test_oor(333333); 315 | } 316 | 317 | SECTION("long long"){ 318 | test_oor(666); 319 | test_oor(-543234532); 320 | } 321 | 322 | SECTION("long"){ 323 | test_oor(3324); 324 | test_oor(-432); 325 | } 326 | 327 | SECTION("long double"){ 328 | test_oor(432.2); 329 | test_oor(-54345343); 330 | } 331 | 332 | SECTION("float"){ 333 | test_oor(5432.3); 334 | test_oor(-4343.43); 335 | } 336 | } 337 | 338 | 339 | TEST_CASE("Can use defaults when nothing present"){ 340 | 341 | SECTION("const char*"){ 342 | test_empty("Hhello world"); 343 | } 344 | 345 | SECTION("String"){ 346 | test_empty("hello world"); 347 | } 348 | 349 | SECTION("Int"){ 350 | test_empty(44); 351 | test_empty(-432); 352 | } 353 | 354 | SECTION("bool"){ 355 | test_empty(true); 356 | test_empty(false); 357 | } 358 | 359 | SECTION("double"){ 360 | test_empty(654.3); 361 | test_empty(-432.2); 362 | } 363 | 364 | SECTION("uint"){ 365 | test_empty(543); 366 | } 367 | 368 | SECTION("unsigned long"){ 369 | test_empty(543); 370 | } 371 | 372 | SECTION("unsigned long long"){ 373 | test_empty(543); 374 | } 375 | 376 | SECTION("long long"){ 377 | test_empty(543); 378 | test_empty(-4321); 379 | } 380 | 381 | SECTION("long"){ 382 | test_empty(5432); 383 | test_empty(-432); 384 | } 385 | 386 | SECTION("long double"){ 387 | test_empty(543.5432); 388 | test_empty(-543.2); 389 | } 390 | 391 | SECTION("float"){ 392 | test_empty(543.3); 393 | test_empty(-32.2); 394 | } 395 | } 396 | 397 | template 398 | void test_create(const typename std::enable_if::value, T>::type expected){ 399 | tortellini::ini ini; 400 | ini[""]["uut"] = expected; 401 | std::ostringstream oss; 402 | oss << ini; 403 | 404 | std::stringstream ss; 405 | ss << "uut = " << expected << "\n"; 406 | CHECK(oss.str() == ss.str()); 407 | } 408 | 409 | 410 | 411 | TEST_CASE("Creating an ini"){ 412 | tortellini::ini ini; 413 | 414 | SECTION("Can create string"){ 415 | test_create("hello world"); 416 | } 417 | 418 | SECTION("Char*"){ 419 | test_create("hello"); 420 | } 421 | 422 | SECTION("Can create bool true"){ 423 | test_create(true, "yes"); 424 | test_create(false, "no"); 425 | } 426 | 427 | SECTION("Can create int"){ 428 | test_create(10); 429 | test_create(-33); 430 | } 431 | 432 | SECTION("Can create double"){ 433 | test_create(432.2); 434 | test_create(-432.0); 435 | } 436 | 437 | SECTION("Can do float"){ 438 | test_create(5432.2); 439 | test_create(-5432); 440 | } 441 | 442 | SECTION("Can do long double"){ 443 | test_create(5432.0); 444 | test_create(-543.2); 445 | } 446 | 447 | SECTION("Can create unsigned int"){ 448 | test_create(543); 449 | } 450 | 451 | SECTION("Can create long"){ 452 | test_create(5432); 453 | } 454 | 455 | SECTION("Can create ulong"){ 456 | test_create(5432); 457 | } 458 | 459 | SECTION("Can create empty top section"){ 460 | ini[""]["thing"] = "hello"; 461 | std::ostringstream oss; 462 | oss << ini; 463 | CHECK(oss.str() == "thing = hello\n"); 464 | } 465 | 466 | SECTION("Can create multiple sections"){ 467 | ini[""]["global"] = "section"; 468 | ini["sub"]["section"] = 99; 469 | std::ostringstream ss; 470 | ss << ini; 471 | CHECK(ss.str() == "global = section\n\n[sub]\nsection = 99\n"); 472 | } 473 | 474 | SECTION("Can create multiple things in multiple sections"){ 475 | ini[""]["global"] = "section"; 476 | ini["sub"]["section"] = 99; 477 | std::ostringstream ss; 478 | ss << ini; 479 | CHECK(ss.str() == "global = section\n\n[sub]\nsection = 99\n"); 480 | 481 | ini[""]["hello"] = "world"; 482 | ini["sub"]["terfuge"] = 666.666; 483 | ss.str(std::string{}); 484 | ss << ini; 485 | 486 | // because of the comparison operator I can be sure the items are in alphabetical order 487 | CHECK(ss.str() == "global = section\nhello = world\n\n[sub]\nsection = 99\nterfuge = 666.6660000000001\n"); 488 | } 489 | 490 | SECTION("Skips UTF-8 BOM"){ 491 | std::stringstream ss; 492 | ss << "\xEF\xBB\xBF""foo=bar\r\nworks=true\r\n"; 493 | std::istringstream iss(ss.str()); 494 | iss >> ini; 495 | CHECK((ini[""]["foo"] | "nope") == "bar"); 496 | CHECK((ini[""]["works"] | false) == true); 497 | } 498 | } 499 | 500 | TEST_CASE("Iterating an INI") { 501 | tortellini::ini ini; 502 | std::istringstream("[foo]\nfoo1 = true\nfoo2 = 1234\n\n[bar]\nbar1 = hello\nbar2 = 12345") >> ini; 503 | 504 | SECTION("Can iterate sections") { 505 | for (const auto §ion_pair : ini) { 506 | const auto &name = section_pair.name; 507 | const auto §ion = section_pair.section; 508 | 509 | if (name == "foo") { 510 | CHECK((section["foo1"] | false) == true); 511 | CHECK((section["foo2"] | 0) == 1234); 512 | } else if (name == "bar") { 513 | CHECK((section["bar1"] | "") == "hello"); 514 | CHECK((section["bar2"] | 0) == 12345); 515 | } else { 516 | FAIL("too many sections"); 517 | break; 518 | } 519 | } 520 | } 521 | } 522 | --------------------------------------------------------------------------------