├── .nojekyll ├── CNAME ├── _media ├── shoal.png └── shoal_dark.png ├── _sidebar.md ├── index.html ├── specification.md └── README.md /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | shoal.eelnet.org -------------------------------------------------------------------------------- /_media/shoal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamchatka-volcano/shoal/master/_media/shoal.png -------------------------------------------------------------------------------- /_media/shoal_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamchatka-volcano/shoal/master/_media/shoal_dark.png -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | - [Introduction](README.md#shoal---an-ergonomic-configuration-file-format "shoal - Introduction") 2 | - [Motivation](/README.md#motivation "shoal - Introduction") 3 | - [Implementations](/README.md#implementations "shoal - Introduction") 4 | - [Specification](specification.md#specification "shoal - Specification") 5 | - [Comments](specification.md#comments "shoal - Specification") 6 | - [Parameters](specification.md#parameters "shoal - Specification") 7 | - [Arrays](specification.md#arrays "shoal - Specification") 8 | - [Structures](specification.md#structures "shoal - Specification") 9 | - [Arrays of structures](specification.md#arrays-of-structures "shoal - Specification") 10 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | shoal - Introduction 6 | 7 | 8 | 9 | 10 | 16 | 29 | 40 | 41 | 42 |
43 | 44 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /specification.md: -------------------------------------------------------------------------------- 1 | # Specification 2 | 3 | 4 | 5 | `shoal` format only describes the configuration structure, it's not suited for data serialization, so details like file encoding or representation of various data types are unspecified and left for interpretation to client applications or parsing libraries implementers. 6 | 7 | ## Comments 8 | 9 | Comment sections are started with `;` character and closed by the end of line. 10 | 11 | 12 | ```shoal 13 | ;This is a comment 14 | foo = hello world ;this is also a comment 15 | ``` 16 | 17 | ```json 18 | { 19 | "foo" : "hello world" 20 | } 21 | ``` 22 | 23 | 24 | ## Parameters 25 | 26 | 27 | Parameters have the format: ` = ` 28 | Whitespaces around parameter name and value are trimmed. 29 | Parameter name and `=` must be on the same line. 30 | Unquoted parameter value must be on the same line with `=`. 31 | Quoted parameter value can be multiline, but must start on the same line as `=`. 32 | `'`, `"` and backticks can be used to create a quoted parameter value. 33 | If multiline values start with a newline character after the quotation mark, the first empty line is skipped. (See `param4` in the example). 34 | Quoted parameter values can contain separator symbols used by `shoal` without affecting a configuration structure. 35 | 36 | 37 | 38 | ```shoal 39 | param1=42 40 | param2 = Hello world 41 | param3 ="Hello world" 42 | param4 = " 43 | Hello world 44 | Test 45 | " 46 | param5 = ";this is not a comment" 47 | param6 = "'quoted'" 48 | param7 = '"quoted"' 49 | param8 = `'quoted' and "quoted"` 50 | param9 = "'quoted' and `quoted`" 51 | ``` 52 | 53 | ```json 54 | { 55 | "param1": 42, 56 | "param2": "Hello world", 57 | "param3": "Hello world", 58 | "param4": " Hello world\n Test\n", 59 | "param5": ";this is not a comment", 60 | "param6": "'quoted'", 61 | "param7": "\"quoted\"", 62 | "param8": "'quoted` and \"quoted\"", 63 | "param9": "'quoted` and `quoted`", 64 | } 65 | ``` 66 | 67 | ## Arrays 68 | 69 | 70 | Arrays have the format: ` = , , ...` or ` = [, ...]` 71 | Whitespaces around array name and array elements are trimmed. 72 | Array name and `=` must be on the same line. 73 | When array elements aren't enclosed with brackets, they must be placed on the same line with `=`. 74 | When array elements aren't enclosed with brackets, an array must have at least two elements. 75 | When brackets are used, the opening bracket must be placed on the same line with `=`, while elements and closing bracket can be placed on multiple lines. 76 | When brackets are used, an array can be empty or have a single element. 77 | 78 | 79 | 80 | ```shoal 81 | array1 = apple, orange, pineapple 82 | array2 = [apple, orange, pineapple] 83 | array3 = [apple, 84 | orange, 85 | pineapple] 86 | array4 = [ 87 | apple, 88 | orange, 89 | pineapple 90 | ] 91 | array5 = [apple] 92 | array6 = [] 93 | param = "apple, orange, pineapple" ;this is not a list 94 | ``` 95 | 96 | ```json 97 | { 98 | "array1": ["apple","orange","pineapple"], 99 | "array2": ["apple","orange","pineapple"], 100 | "array3": ["apple","orange","pineapple"], 101 | "array4": ["apple","orange","pineapple"], 102 | "array5": ["apple"], 103 | "array6": [], 104 | "param": "apple, orange, pineapple" 105 | } 106 | ``` 107 | 108 | ## Structures 109 | 110 | 111 | Structures have the format: 112 | ``` 113 | #(structure name): 114 | (parameters) 115 | and/or 116 | (arrays) 117 | and/or 118 | (structures) 119 | and/or 120 | (arrays of structures) 121 | --- or - or --(structure name) or the end of file 122 | ``` 123 | Lines with a structure opening token can contain nothing besides whitespaces or a comment. 124 | Lines with a structure closing token can contain nothing besides whitespaces or a comment. 125 | `---` marks the end of the current structure and all its parents and returns the context back to the configuration root. 126 | `-` marks the end of the current structure and returns the context to its parent structure. 127 | `--` marks the end of the current structure and all its parents up to the specified by the provided name and returns the context to its parent structure. 128 | A structure can be empty. 129 | 130 | 131 | 132 | ```shoal 133 | #struct1: 134 | foo=Hello world 135 | bar=42 136 | --- 137 | 138 | #struct2: 139 | foo=Hello world 140 | bar=42 141 | - 142 | 143 | #struct3: 144 | foo=Hello world 145 | bar=42 146 | --struct3 147 | 148 | #struct4: 149 | --- 150 | 151 | #struct5: 152 | foo=Hello world 153 | bar=42 154 | #nestedStruct: 155 | baz = 0 156 | --- 157 | 158 | #struct6: 159 | foo=Hello world 160 | #nestedStruct: 161 | baz = 0 162 | - 163 | bar=42 164 | --- 165 | 166 | #struct7: 167 | foo=Hello world 168 | #nestedStruct: 169 | baz = 0 170 | --nestedStruct 171 | bar=42 172 | --- 173 | 174 | #struct8: 175 | foo=Hello world 176 | #nestedStruct: 177 | baz = 0 178 | #nestedStruct2: 179 | abc = test 180 | --nestedStruct 181 | bar=42 182 | --- 183 | 184 | #struct9: 185 | foo=Hello world 186 | #nestedStruct: 187 | baz = 0 188 | #nestedStruct2: 189 | abc = test 190 | - 191 | - 192 | bar=42 193 | --- 194 | ``` 195 | 196 | ```json 197 | { 198 | "struct1" : { 199 | "foo":"Hello world", 200 | "bar":42 201 | }, 202 | "struct2" : { 203 | "foo":"Hello world", 204 | "bar":42 205 | }, 206 | "struct3" : { 207 | "foo":"Hello world", 208 | "bar":42 209 | }, 210 | "struct4" : {}, 211 | "struct5" : { 212 | "foo":"Hello world", 213 | "bar":42, 214 | "nestedStruct": { 215 | "baz": 0 216 | } 217 | }, 218 | "struct6" : { 219 | "foo":"Hello world", 220 | "bar":42, 221 | "nestedStruct": { 222 | "baz": 0 223 | } 224 | }, 225 | "struct7" : { 226 | "foo":"Hello world", 227 | "bar":42, 228 | "nestedStruct": { 229 | "baz": 0 230 | } 231 | }, 232 | "struct8" : { 233 | "foo":"Hello world", 234 | "bar":42, 235 | "nestedStruct": { 236 | "baz": 0, 237 | "nestedStruct2": { 238 | "abc": "test" 239 | } 240 | } 241 | }, 242 | "struct9" : { 243 | "foo":"Hello world", 244 | "bar":42, 245 | "nestedStruct": { 246 | "baz": 0, 247 | "nestedStruct2": { 248 | "abc": "test" 249 | } 250 | } 251 | } 252 | } 253 | ``` 254 | 255 | ## Arrays of structures 256 | 257 | 258 | Arrays of structures have the format: 259 | ``` 260 | #(name of array of structures): 261 | ### 262 | (element of array - a structure without opening and closing token) 263 | ... zero or more elements separated by ### 264 | ### 265 | (element of array) 266 | ... 267 | --- or - or --(name of array of structures) or the end of file 268 | ``` 269 | 270 | Lines with a structure opening token can contain nothing besides whitespaces or a comment. 271 | The next line after the opening token must contain an array elements separator `###` even when the array is empty. 272 | Lines with array elements separator can contain nothing besides whitespaces or a comment. 273 | Lines with a structure closing token can contain nothing besides whitespaces or a comment. 274 | 275 | `---` marks the end of the current array of structures and all its parents and returns the context back to the configuration root. 276 | `-` marks the end of the current array of structures and returns the context to its parent structure. 277 | `--` marks the end of the current array of structures and all its parents up to the specified by the provided name and returns the context to its parent structure. 278 | An array of structures can be empty. 279 | 280 | 281 | 282 | ```shoal 283 | #aos1: 284 | ### 285 | foo=Hello world 286 | bar=42 287 | ### 288 | foo=Test 289 | bar=9 290 | --- 291 | 292 | #aos2: 293 | ### 294 | foo=Hello world 295 | bar=42 296 | ### 297 | foo=Test 298 | bar=9 299 | - 300 | 301 | #aos3: 302 | ### 303 | foo=Hello world 304 | bar=42 305 | ### 306 | foo=Test 307 | bar=9 308 | --aos3 309 | 310 | #aos4: 311 | ### 312 | --- 313 | 314 | #aos5: 315 | ### 316 | foo=Hello world 317 | bar=42 318 | #nestedAos: 319 | ### 320 | baz = 0 321 | --- 322 | 323 | #aos6: 324 | ### 325 | foo=Hello world 326 | #nestedAos: 327 | ### 328 | baz = 0 329 | - 330 | bar=42 331 | ### 332 | foo=Hello 333 | #nestedAos: 334 | ### 335 | --nestedAos 336 | bar=9 337 | --- 338 | ``` 339 | 340 | ```json 341 | { 342 | "aos1" : [ 343 | { 344 | "foo":"Hello world", 345 | "bar":42 346 | }, 347 | { 348 | "foo":"Test", 349 | "bar":9 350 | } 351 | ], 352 | "aos2" : [ 353 | { 354 | "foo":"Hello world", 355 | "bar":42 356 | }, 357 | { 358 | "foo":"Test", 359 | "bar":9 360 | } 361 | ], 362 | "aos3" : [ 363 | { 364 | "foo":"Hello world", 365 | "bar":42 366 | }, 367 | { 368 | "foo": "Test", 369 | "bar": 9 370 | } 371 | ], 372 | "aos4" : [], 373 | "aos5" : [ 374 | { 375 | "foo":"Hello world", 376 | "bar":42, 377 | "nestedAos": [ 378 | { 379 | "baz": 0 380 | } 381 | ] 382 | } 383 | ], 384 | "aos6" : [ 385 | { 386 | "foo":"Hello world", 387 | "bar":42, 388 | "nestedAos": [ 389 | { 390 | "baz": 0 391 | } 392 | ] 393 | }, 394 | { 395 | "foo":"Hello", 396 | "bar":9, 397 | "nestedAos": [] 398 | } 399 | ] 400 | } 401 | 402 | ``` 403 | 404 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shoal - an ergonomic configuration file format 2 | 3 | 4 | 5 | `shoal` is a simple and readable `INI`-like format, supporting hierarchical structures with an arbitrary number of nesting levels. 6 | 7 | ```shoal 8 | ;shoal supports comments, 9 | 10 | root_dir = ~/Photos ;parameters, 11 | supported_files = [*.jpg, *.png] ;arrays, 12 | #thumbnails: ;structures, 13 | enabled = true 14 | max_width = 128 15 | max_height = 128 16 | --- 17 | #shared_albums: ;and arrays of structures 18 | ### 19 | dir = summer_2019 20 | title = "Summer (2019)" 21 | ### 22 | dir = misc 23 | title = Misc 24 | ``` 25 | 26 | 27 | 28 | 29 | `shoal` structure can be described in terms of a document tree: nodes can contain named values (parameters), named lists of values (arrays), named nodes (structures) and named lists of nodes (arrays of structures). The top level of the config file acts as the unnamed root node. 30 | To represent levels of this tree `shoal` uses opening and closing tokens similar to opening and closing tags in `XML` or braces in `JSON`, indenting levels is recommended, but not required. When node is created with an opening token `#nodeName:` the context descents on its level and all the following elements end up being the children of this node until it's closed: 31 | 32 | 33 | 34 | 35 | ```shoal 36 | #photos: 37 | root_dir = ~/Photos 38 | 39 | #videos: 40 | root_dir = ~/Videos 41 | ``` 42 | 43 | 44 | #### **JSON** 45 | ```json 46 | { 47 | "photos" : { 48 | "root_dir" : "~/Photos", 49 | "videos" : { 50 | "root_dir" : "~/Videos" 51 | } 52 | } 53 | } 54 | ``` 55 | #### **YAML** 56 | ```yaml 57 | photos: 58 | root_dir: ~/Photos 59 | videos: 60 | root_dir: ~/Videos 61 | ``` 62 | #### **TOML** 63 | ```toml 64 | [photos] 65 | root_dir = "~/Photos" 66 | [photos.videos] 67 | root_dir = "~/Videos" 68 | ``` 69 | 70 | 71 | 72 | 73 | 74 | Here `videos` is a child node of `photos`, because the `photos` node is closed implicitly at the end of the document. 75 | 76 | To fix this oversight and add `videos` node on the same level as `photos` we need to close the `photos` node first. This can be done with either of 3 possible closing tokens: 77 | * `---` which closes the node and returns the context back to the root node; 78 | * `-` which closes the node and returns the context to its parent node; 79 | * `--photos` which closes the node specified by the provided name and returns the context to its parent node; 80 | 81 | 82 | 83 | 84 | ```shoal 85 | #photos: 86 | root_dir = ~/Photos 87 | --- 88 | #videos: 89 | root_dir = ~/Videos 90 | ``` 91 | 92 | 93 | #### **JSON** 94 | ```json 95 | { 96 | "photos": { 97 | "root_dir" : "~/Photos" 98 | }, 99 | "videos": { 100 | "root_dir" : "~/Videos" 101 | } 102 | } 103 | ``` 104 | #### **YAML** 105 | ```yaml 106 | photos: 107 | root_dir: ~/Photos 108 | videos: 109 | root_dir: ~/Videos 110 | ``` 111 | #### **TOML** 112 | ```toml 113 | [photos] 114 | root_dir = "~/Photos" 115 | [videos] 116 | root_dir = "~/Videos" 117 | ``` 118 | 119 | 120 | 121 | 122 | 123 | Simple configs can be written with only `-` or `---` closing tokens. In multilevel configs `--nodeName` format can be used when you need to close several nodes at the same time without returning to the root node: 124 | 125 | 126 | 127 | 128 | ```shoal 129 | #photo_server: 130 | #security: 131 | password_strength = 8 132 | #password_policy: 133 | length = 4 134 | - 135 | #user-lock-policy: 136 | attempts = 0 137 | password_expiry_days = 0 138 | --security 139 | #host: 140 | address=127.0.0.1 141 | port = 8080 142 | ``` 143 | 144 | 145 | #### **JSON** 146 | ```json 147 | { 148 | "photo_server":{ 149 | "security":{ 150 | "password_strength": 8, 151 | "password_policy":{ 152 | "length" : 4 153 | }, 154 | "user-lock-policy":{ 155 | "attempts": 0, 156 | "password_expiry_days": 0 157 | } 158 | }, 159 | "host":{ 160 | "address":"127.0.0.1", 161 | "port": 8080 162 | } 163 | } 164 | } 165 | ``` 166 | #### **YAML** 167 | ```yaml 168 | photo_server: 169 | security: 170 | password_strength : 8 171 | password_policy: 172 | length : 4 173 | 174 | user-lock-policy: 175 | attempts: 0 176 | password_expiry_days: 0 177 | host: 178 | address: 127.0.0.1 179 | port: 8080 180 | ``` 181 | #### **TOML** 182 | ```toml 183 | [photo_server] 184 | [photo_server.security] 185 | password_strength = 8 186 | [photo_server.security.password_policy] 187 | length = 4 188 | 189 | [photo_server.security.user-lock-policy] 190 | attempts = 0 191 | password_expiry_days = 0 192 | [photo_server.host] 193 | address = "127.0.0.1" 194 | port = 8080 195 | ``` 196 | 197 | 198 | 199 | 200 | 201 | If possible it's better to minimize the usage of different closing tokens to keep the structure uniform. For example, the same config can be reordered to avoid the explicit closing of `security` node: 202 | 203 | 204 | 205 | 206 | ```shoal 207 | #photo_server: 208 | #host: 209 | address = 127.0.0.1 210 | port = 8080 211 | - 212 | #security: 213 | password_strength = 8 214 | #password_policy: 215 | length = 4 216 | - 217 | #user-lock-policy: 218 | attempts = 0 219 | password-expiry-days = 0 220 | ``` 221 | 222 | 223 | #### **JSON** 224 | ```json 225 | { 226 | "photo_server":{ 227 | "host":{ 228 | "address":"127.0.0.1", 229 | "port": 8080 230 | }, 231 | "security":{ 232 | "password_strength": 8, 233 | "password_policy":{ 234 | "length" : 4 235 | }, 236 | "user-lock-policy":{ 237 | "attempts": 0, 238 | "password_expiry_days": 0 239 | } 240 | } 241 | } 242 | } 243 | ``` 244 | #### **YAML** 245 | ```yaml 246 | photo_server: 247 | host: 248 | address: 127.0.0.1 249 | port: 8080 250 | security: 251 | password_strength : 8 252 | password_policy: 253 | length : 4 254 | 255 | user-lock-policy: 256 | attempts: 0 257 | password_expiry_days: 0 258 | ``` 259 | #### **TOML** 260 | ```toml 261 | [photo_server] 262 | [photo_server.host] 263 | address = "127.0.0.1" 264 | port = 8080 265 | [photo_server.security] 266 | password_strength = 8 267 | [photo_server.security.password_policy] 268 | length = 4 269 | 270 | [photo_server.security.user-lock-policy] 271 | attempts = 0 272 | password_expiry_days = 0 273 | ``` 274 | 275 | 276 | 277 | 278 | 279 | The last fairly unique looking feature of `shoal` is the arrays of structs. To create them add `###` on the next line after the opening token before the first element, and before all following elements as a separator. To close them it's possible to use all the closing tokens described earlier. 280 | 281 | 282 | 283 | 284 | ```shoal 285 | #video_server: 286 | #hosts: 287 | ### 288 | address = 127.0.0.1 289 | port = 8081 290 | ### 291 | address = 127.0.0.1 292 | port = 8082 293 | - 294 | root_dir=~/Videos 295 | ``` 296 | 297 | 298 | #### **JSON** 299 | ```json 300 | { 301 | "video_server":{ 302 | "hosts":[ 303 | { 304 | "address" : "127.0.0.1", 305 | "port": 8081 306 | }, 307 | { 308 | "address": "127.0.0.1", 309 | "port": 8082 310 | } 311 | ], 312 | "root_dir" : "~/Videos" 313 | } 314 | } 315 | ``` 316 | #### **YAML** 317 | ```yaml 318 | video_server: 319 | hosts: 320 | - address : 127.0.0.1 321 | port: 8081 322 | 323 | - address: 127.0.0.1 324 | port: 8082 325 | root_dir: ~/Videos 326 | ``` 327 | #### **TOML** 328 | ```toml 329 | [video_server] 330 | root_dir = "~/Videos" 331 | [[video_server.hosts]] 332 | address = "127.0.0.1" 333 | port = 8081 334 | [[video_server.hosts]] 335 | address = "127.0.0.1" 336 | port = "8082" 337 | ``` 338 | 339 | 340 | 341 | ## Motivation 342 | 343 | 344 | `shoal` was designed with the following goals in mind: 345 | * `(A)` the declaration of parameters and structures shouldn't use the same syntax; 346 | * `(B)` multilevel structures should be supported with direct representation of hierarchy; 347 | * `(C)` the indentation of config levels should be optional; 348 | * `(D)` the syntax noise level should be as low as possible (this of course is subjective). 349 | 350 | Let's see how `shoal` covers these requirements in comparison to popular solutions for software configuration: 351 | 352 | | | shoal | JSON | YAML | TOML | INI | XML | 353 | |-----|-------|-------|-------|-------|-------|-------| 354 | | A | + | - | - | + | + | + | 355 | | B | + | + | + | - | - | + | 356 | | C | + | + | - | + | + | + | 357 | | D | +/- | - | + | +/- | + | - | 358 | 359 | These are the personal requirements of the author of `shoal` who not surprisingly belongs to the tribe of people preferring `XML` to `JSON`. As you can see the closest contenders are the `INI` format that doesn't match the important functional requirement, and `XML` which is too verbose and feels like an overkill for simple config files. 360 | 361 | In other words, `shoal` aims to be an `INI` replacement with nesting structures support. Like `INI` it's intended solely for software configuration, `shoal` is implementation-dependent and its [specification](/specification) is too vague to be used as a general data exchange format reliably. 362 | 363 | 364 | 365 | ## Implementations 366 | * C++ 367 | * [`figcone`](https://github.com/kamchatka-volcano/figcone) 368 | --------------------------------------------------------------------------------