├── .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 |
--------------------------------------------------------------------------------