Features¶
In this section, we’ll talk briefly about the features of CFG and how they meet the requirements listed in the section entitled What, another new configuration format?.
High-Level Description¶
CFG is a superset of JSON, which means that a syntactically valid JSON configuration can serve unchanged as a CFG configuration. This meets the following requirements:
- Supports a hierarchical structure with no arbitrary limit on depth of nesting.
- Syntactically valid JSON is accepted, aiding transition from JSON to CFG.
- The full range of Unicode characters can be used.
- Order independence, except in lists.
The file naming convention is to use the .cfg
extension for files
containing CFG.
More details are provided in the sections below.
Elements¶
A CFG configuration consists of a number of types of elements:
- Mappings: these map strings to other elements, like a Python dictionary. The root (starting point) of a configuration is always a mapping.
- Lists: these hold arbitrary elements and are heterogeneous (i.e. there is no need for all elements in a list to be of the same type).
- Scalar values: these fall into one or more of the following categories:
- Strings
- Identifiers
- Integers
- Floating-point numbers
- Complex numbers
- Boolean values
- Null
- Includes: these allow configurations to contain other configurations.
- References: these allow referring to parts of a configuration from another part.
- Special values: these represent values available to the application using the configuration, such as environment variables and internal program values.
- Expressions: these allow combining parts of a configuration using other parts of it.
- Comments: these allow a configuration to be documented.
These elements are described in separate sections below.
Example¶
The following example illustrates a simple configuration, showing instances of all the elements described above:
# You can have comments anywhere in a configuration. Only line comments are
# supported, as you can easily comment and uncomment multiple lines using
# a modern editor or IDE.
{
# You can have standard JSON-like key-value mapping.
"writer": "Oscar Fingal O'Flahertie Wills Wilde",
# But also use single-quotes for keys and values.
'a dimension': 'length: 5"',
# You can use identifiers for the keys.
string_value: 'a string value',
integer_value: 3,
# you can use = instead of : as a key-value separator
float_value = 2.71828,
# these values are just like in JSON
boolean_value: true,
opposite_boolean_value: false,
null_value: null
list_value: [
123,
4.5 # note the absence of a comma - a newline acts as a separator, too.
2j, # a complex number with just an imaginary part
1 + 3j # another one with both real and imaginary parts
[
1,
'A',
2,
'b', # note the trailing comma - doesn't cause errors
]
] # a comma isn't needed here.
nested_mapping: {
integer_as_hex: 0x123
float_value: .14159, # note the trailing comma - doesn't cause errors
} # no comma needed here either.
# You can use escape sequences in strings ...
snowman_escaped: '\u2603'
# or not, and use e.g. utf-8 encoding.
snowman_unescaped: '☃'
# You can refer to code points outside the Basic Multilingual Plane
face_with_tears_of_joy: '\U0001F602'
unescaped_face_with_tears_of_joy: '😂'
# Include sub-configurations.
logging: @'logging.cfg',
# Refer to other values in this configuration.
refer_1: ${string_value}, # -> 'a string value'
refer_2: ${list_value[1]}, # -> 4.5
refer_3: ${nested_mapping.float_value}, # -> 0.14159
# Special values are implementation-dependent. On Python, for example:
s_val_1: `sys:stderr`, # -> module attribute sys.stderr
s_val_2: `$LANG|en_GB.UTF-8` # -> environment var with default
s_val_3: `2019-03-28T23:27:04.314159` # -> date/time value
# Expressions.
# N.B. backslash immediately followed by newline is seen as a continuation:
pi_approx: ${integer_value} + \
${nested_mapping.float_value} # -> 3.14159
sept_et_demi: ${integer_value} + \
${list_value[1]} # -> 7.5
}
The individual elements in the example are discussed in more detail below.
Mappings¶
Mappings are like Python dictionaries or JavaScript objects – they map keys to values. Keys must be string values, but they can be given either as string literals or as identifiers (starting with a letter or underscore, and followed by any number of alphanumeric characters or underscores). Values can be any element type.
A configuration loaded by a program must be a mapping or a mapping body – i.e.
a mapping without the {
and }
characters which normally bracket it.
Thus, a configuration could be expressed as either
{
foo: 'bar',
bar: 'baz',
}
or equivalently as
foo = 'bar'
bar = 'baz'
Note that you can use either a colon :
or an equals sign =
to separate
key from value. This can help when migrating from other configuration formats
which use =
as a key/value separator.
Either commas or newlines can be used to separate elements in a mapping. You can use multiple newlines between two elements, but not multiple commas. A trailing comma is allowed, and ignored.
Keys and Values¶
Mapping keys are always considered as literal strings, whether they are present in the source as identifiers or string literals. Thus, the following:
foo = 'bar'
bar = 'baz'
is the same as:
'foo' = 'bar'
'bar' = 'baz'
However, identifiers seen in mapping values are not necessarily treated as literal strings. In the following:
foo = 'bar'
bar = baz
the value of bar
could be interpreted as either the string value baz
,
or the result of looking up a value in some context using baz
as the
key. This allows us to consider passing in a context mapping where certain
specific items will be looked up based on keys. This makes baz
effectively
a variable in the above configuration, where the variable’s value is
determined by the software that uses the configuration.
Here’s an example. Let the configuration be:
foo: fizz
bar: buzz
You can then pass in a variables
context when the Config
instance
is initialized.
Note
The examples in this page use Python, but you can use one of the other languages for which a CFG library exists.
>>> variables = {'fizz': 'Fizz Fizz', 'buzz': 'Buzz Buzz'}
>>> with open('test0b.cfg') as f: cfg = config.Config(f, context=variables)
...
>>> cfg['foo']
'Fizz Fizz'
>>> cfg['bar']
'Buzz Buzz'
In cfg['foo']
, the configuration was queried to get the identifier fizz
,
which, as it’s not a literal string and a context was provided, was used as a
lookup key in the context to get the final result, 'Fizz Fizz'
.
Variables can be used to provide application specific information which is then usable in the configuration. An example of such information would be the current user’s home directory. The following configuration, for example:
bin: home + '/bin'
lib: home + '/lib'
could be used like this:
>>> import os
>>> variables = {'home': os.path.expanduser('~')}
>>> with open('test0c.cfg') as f: cfg = config.Config(f, context=variables)
...
>>> cfg['bin'] == os.path.expanduser('~/bin')
True
>>> cfg['lib'] == os.path.expanduser('~/lib')
True
Of course, identifiers in values can be configured to be treated as literal strings, but this is less useful than the recommended convention – which is to use identifier for keys wherever possible, and use literal strings for string values.
Paths¶
Along with mappings, you have the concept of a path which is used to access something in the mapping. The simplest path is just a key which allows you to access the corresponding value, but you can also extend the path to refer to elements in nested mappings and/or lists.
A path consists of a sequence of segments, starting with an identifier. The following are all syntactically valid paths:
first_part
: this path has just one segment; it’s the key into the mapping.first_part.second_part
: this is semantically valid if thefirst_part
key’s value is a mapping, andsecond_part
is a key in that mapping. The result is the value associated with thesecond_part
key.first_part['second_part']
: this is equivalent to the path just above.first_part[2]
: this is semantically valid iffirst_part
refers to a list which has at least three elements, and the result is the third element.first_part[0].second_part['foo'].third_path
: this would traverse tofirst_part
, which should be a list, then to its first element (i.e. at index0
), which should be a mapping, then get the value there at key'second_part'
, which should also be a mapping, then get the value there at key'foo'
, which should be a mapping, and then fetch the value at key'third_path'
as the final result.
Note
Paths start with identifiers, which means that to use them, you generally need to arrange your root configurations with keys that are identifiers, rather than say, keys which can’t be represented by identifiers. In the following example
identifier_key: {
sub_key: {
sub_sub_key: 'foo'
}
},
'hyphenated-key': {
sub_key: {
sub_sub_key: 'bar'
}
}
you can access the 'foo'
value via path
identifier_key.sub_key.sub_sub_key
(and other equivalent forms) but you
can’t do that for the bar
value – you have to use a slightly more
verbose form.
>>> cfg['identifier_key.sub_key.sub_sub_key']
'foo'
>>> cfg['hyphenated-key']['sub_key']['sub_sub_key']
'bar'
Slices¶
As well as integer indices into lists, CFG also supports the concept of
slices. A slice index is written like this: first_part[start:stop:step]
where first_part
must be a list and start
, stop
and step
must
either be absent, or else be integer values (Python developers should be
familiar with slices). When applied to a list, the slice index produces
another list (the result) whose first element is the start
-th element of
the source list, whose last element is just before the stop
-th element of
the source list, and every step
-th element between start
and stop
is picked from the source list into the result. If start
is omitted, it’s
taken to mean the start of the source list. If end
is omitted, it’s taken
to mean the end of the source list. If step
is omitted, it is taken as the
value 1
, meaning every element is taken between start and stop from the
source list. Negative values for start
and stop
mean they are computed
from the end of the source list. A negative value for step
means count
backwards: in this case, start
should be greater than stop
, whereas
normally start
is expected to be smaller than stop
.
Examples of slices are as follows, assuming foo
is the list
['a', 'b', 'c', 'd', 'e', 'f', 'g']
:
Path expression | Result |
---|---|
foo[:] |
['a', 'b', 'c', 'd', 'e', 'f', 'g'] |
foo[::] |
['a', 'b', 'c', 'd', 'e', 'f', 'g'] |
foo[:20] |
['a', 'b', 'c', 'd', 'e', 'f', 'g'] |
foo[-20:4] |
['a', 'b', 'c', 'd'] |
foo[2:] |
['c', 'd', 'e', 'f', 'g'] |
foo[-3:] |
['e', 'f', 'g'] |
foo[-2:2:-1] |
['f', 'e', 'd'] |
foo[::-1] |
['g', 'f', 'e', 'd', 'c', 'b', 'a'] |
foo[2:-2:2] |
['c', 'e'] |
foo[::2] |
['a', 'c', 'e', 'g'] |
foo[::3] |
['a', 'd', 'g'] |
Invalid paths¶
The following are examples of paths which would be invalid:
foo[]
: this path does not specify an index intofoo
, so it can’t be used.foo[1, 2]
: since 2-dimensional arrays aren’t supported,1, 2
is not a usable index intofoo
.foo.
: since there is nothing following the dot, it’s not clear how to access any value beyondfoo
.foo.123
: since there is not an identifier that follows the dot, it’s not clear how to access any value beyondfoo
.foo[1] bar
: There is extraneous textbar
following a valid path, which makes the path as a whole invalid.foo[:::]
: There is an extraneous:
in the slice.
Paths are used both in the Application Programming Interfaces and in the configuration itself, via References.
Lists¶
CFG lists are like Python lists or JavaScript arrays. They are inherently ordered, and also heterogeneous (i.e. elements in a list don’t all have to be of the same type).
Either commas or newlines can be used to separate elements in a list. You can use multiple newlines between two elements, but not multiple commas. A trailing comma is allowed, and ignored. A comma followed by newlines is accepted.
In paths, lists can be indexed by integers or by slices. See the section on slices for more information.
Scalar Values¶
If a configuration is like a tree, then scalar values are leaves of the tree. The following subsections describe the different kinds of scalar values you can have.
Strings¶
Strings can be single- or double-quoted, and can span multiple lines using triple-quoted forms, as in Python:
{
strings: [
"Oscar Fingal O'Flahertie Wills Wilde"
'size: 5"'
"""Triple quoted form
can span
'multiple' lines"""
'''with "either"
kind of 'quote' embedded within'''
]
}
In the triple-quoted forms, newlines are other whitespace are preserved exactly in the source.
Unicode can be entered using escape sequences, just as in JSON:
snowman_escaped: '\u2603'
but also using literal Unicode, as in the following example:
snowman_unescaped: '☃'
If you do this, you should encode the configuration file using UTF-8, as this
is the default encoding used to read .cfg
files.
You can include characters outside the Basic Multilingual Plane:
face_with_tears_of_joy: '\U0001F602'
unescaped_face_with_tears_of_joy: '😂'
Again, if you use the latter form, make sure to encode the configuration file using UTF-8.
Note
There is no currently no provision, as there is in some formats, for
raw strings – i.e. strings which don’t use escapes. In such strings,
backslashes are treated literally: for example, 'c:\Users\Me'
would be
valid for a Windows path. In CFG, you would need to write this as
'c:\\Users\\Me'
. The need to provide unescaped strings is generally in
two areas – Windows paths and regular expressions.
Identifiers¶
The CFG format allows you to specify identifiers as values. How they are interpreted is implementation specific:
- They could be interpreted as literal strings – the identifier
foo
is interpreted as the literal string'foo'
. - They could raise errors if used.
- They could be used as keys to lookup values in some context.
Identifiers can be used in paths:
some_stuff: {
foo: 'a value',
bar: 'another value'
}
ref_foo_1: ${some_stuff.foo} # -> 'a value'
ref_foo_2: ${some_stuff['foo']} # behaves the same as the line above
ref_foo_3: ${some_stuff[foo]} # behaviour is implementation-dependent.
When the reference at ref_foo_3
comes across the identifier foo
, then
what happens is determined by which of the three methods above is used to
resolve identifiers:
- In the first case, the value of the reference is the same as the values of
ref_foo_1
andref_foo_2
. - In the second case, an error will be raised.
- In the third case, the identifier
foo
is used as a lookup key in some implementation-defined context, and the resulting value is the value of the reference.
See the section entitled Application Programming Interfaces for more information on how different implementations deal with identifiers.
Integers¶
You can specify integer values using a range of notations:
decimal_integer = 123
hexadecimal_integer = 0x123
octal_integer = 0o123 # Python-style. C-style octal literals not supported!
binary_integer = 0b000100100011
You can also use underscores in numbers to improve readability. Numbers may contain single underscores as separators between digits. They should not end in an underscore (they can’t start with one, as that would be interpreted as an identifier). Thus, the following forms are allowed:
decimal_integer = 1234_5678
hexadecimal_integer = 0x789A_BCDE_F012
octal_integer = 0o123_321
binary_integer = 0b0001_0010_0011
Floating-point numbers¶
Floating-point values can be expressed in a number of ways:
common_or_garden = 123.456
leading_zero_not_needed = .123
trailing_zero_not_needed = 123.
scientific_large = 1.e6
scientific_small = .1e-6
negated = -.1e-6
The precision with which floating-point values are stored internally is implementation-specific. Current implementations support 64-bit precision (sometimes called double precision).
You can also use single underscores as digit separators for readability, as for integers. For example:
common_or_garden = 123_456.78_90
leading_zero_not_needed = .12_3_4
trailing_zero_not_needed = 1_2_3.
scientific_large = 1_0.e6_2
scientific_small = .1_0e-6_0
Complex numbers¶
Complex numbers are represented by an integer or floating-point value
immediately followed by a j
. There must be no space between the number
value and the j
. This represents just the imaginary part of the number –
you can make complex values with both real and imaginary parts using a form such
as 1 + 2j
(see the section on Expressions, below.)
Boolean values¶
The Boolean values are represented in CFG using false
and true
, just as
in JSON.
Null¶
The null value is represented in CFG using null
, just as in JSON. Note that
in implementations which don’t support null
(such as Rust), the value will
be a specific one representing a null value. In Python, the corresponding value
is None
.
Includes¶
Includes are a mechanism for breaking up a configuration into smaller sub-configurations. While not needed at all for small configurations, they can be very useful when a configuration gets large, or for sharing common elements of configuration across related projects. This has some other benefits:
- The responsibility for maintaining the configuration as a whole could be shared between different people.
- Parts of the configuration could be put under change control and digitally signed to check against changes/tampering.
- An include is described by the unary @ operator: the operand should resolve to
a literal string, which is interpreted as the location of the included
configuration. The literal string can be interpreted in a way which is
implementation-specific:
- It could be parsed as a URL and fetched from a local or remote location in a standardised way.
- If not a URL, it could be interpreted as a filename, and if a relative filename, it could be looked for relative to the directory of the including configuration (if known).
See the section on Application Programming Interfaces more information.
References¶
References allow a part of the configuration to refer to another part. This is very useful to avoid unnecessary repetition.
They have the syntax ${ ... }
where the thing between the curly brackets is
a path (see the Paths section).
References can refer to things in sub-configurations that they include, but they cannot refer to anything in “parent” configurations that include them. That’s because multiple places might point to a particular sub-configuration.
So for example, if we have
# webapp.cfg
# ... stuff
routes: @'routes.cfg',
# ... more stuff
and
# routes.cfg
# ... stuff
admin_routes: [
# ...
],
# ... more stuff
then elements in webapp.cfg
could refer to e.g.
${routes.admin_routes[0]}
. However, if there is a main.cfg
which
contains a
webapp: @'webapp.cfg'
then nothing in webapp.cfg
can refer to anything in main.cfg
. Of
course, elements in main.cfg
can refer to elements in webapp.cfg
using a path starting with webapp.
as well as to elements in
routes.cfg
using a path starting with webapp.routes.
– and so on
for further levels of nesting.
See also the section on Expressions, which often use references.
Special values¶
Special values allow a configuration to specify values which are available to the program using the configuration, but are not necessarily stored in the configuration itself. The most common example of this is probably environment variables.
Special values are indicated using a special type of string notation: instead of using single or double quotes, backtick characters (`) are used to delimit special values. A special value can contain any character other than a backtick or non-printable character, and the interpretation of special values is entirely up to the program using the configuration.
Examples:
- Programs in multiple languages could interpret a special value string such as
`abc ${foo} ${bar} ${baz.quux} xyz`
as a string value which interpolates values from other parts of the configuration. In this example,foo
andbar
are keys to values in the configuration, andbaz.quux
could be interpreted as a path. The corresponding values would be interpolated into the result string, which would begin withabc
and end withxyz
but have the inner values determined by other values in the configuration. - Programs in multiple languages could interpret a special value string such as
`$VARNAME|default_value`
to be the value of the environment variableVARNAME
, but which returns thedefault_value
ifVARNAME
isn’t set in the environment. - A Python program could interpret a special value string such as
`logging:DEFAULT_TCP_LOGGING_PORT`
by using the part before the:
as a module to import and the part after the:
as an attribute of the module, and resolve to the actual port number as defined in that module. - A .NET program could interpret a special value string such as
`A.B.C,D.E.F:G`
such as the value of the static field or propertyG
in a class with fully-qualified nameD.E.F
found in an assembly with nameA.B.C
. - A Kotlin or Java program could interpret a special value string such as
`A.B.C:d`
as the value of the static fieldd
in a class on the classpath with fully-qualified nameA.B.C
, or the return value of a static methodd
in that class which takes no arguments.
Platforms which don’t offer powerful run-time reflection facilities (such as Go, Rust and D) can’t take advantage of special strings in the way that Python, Kotlin/Java, .NET or JavaScript can. However, there is a set of special values which are available across platforms, as described below.
Special values – cross-platform¶
The following special string formats are used by convention across current implementations:
Native date/times¶
You can use `YYYY-mm-ddTHH:mm:ss.NNNNNN+HH:mm:ss.nnnnnn`
— this is an ISO-like
format which allows precise description of a datetime including a timezone offset.
Points to note:
- The date/time separator can be a space instead of a
T
. - The timezone offset can be a
-
instead of a+
. - The
.NNNNNN
and.nnnnnn
values are microseconds (expressed as fractions of a second) and can be omitted or less precise, but not more precise than six digits. - The time must be provided to a precision of at least seconds, though the timezone offset need only be precise up to hours and minutes.
This format is converted to a date-time object appropriate to the platform.
Note
Some platforms do not support timezone offsets to high resolution:
- Kotlin/Java, Rust, Go – in the respective standard libraries, timezone offsets in date/times are only accurate to the nearest second – fractional seconds aren’t available, and if they are present in the CFG source, any fractional value is truncated to zero.
- D – in the D standard library, timeone offsets are only accurate to the nearest minute – fractional minutes aren’t available, and if they are present in the CFG source, any such fractional value is truncated to zero.
Environment Variables¶
You can use `$VARNAME|default`
— this is a format to access environment
variables with an optional default value specifiable if the environment variable
doesn’t exist. Points to note:
- The pipe character and default can be omitted, or the pipe character provided by itself, making the default value the empty string.
- If no default is provided and the environment variable doesn’t exist, a suitable
platform-specific value is provided (e.g.
None
for Python,null
for Kotlin / Java, .NET and D,nil
for Go and astd::Option
None
value for Rust).
Interpolated Strings¶
You can use `${foo.bar} ${bar.baz}`
— this format allows string interpolation to
be used to build up configuration values from other values. Anything bracketed between
${
and }
is assumed to be a path and resolved in the usual way, and the
resulting value interpolated into the string. If any path resolution fails, the
special value conversion as a whole fails. Note that rendering of certain values (e.g.
floating-point values) might differ across platforms.
New in version 0.1.1/0.5.1: The string interpolation facility was added on all current platforms - Python, the JVM, .NET, Rust, Go, JavaScript and D. (In Python, the release version is 0.5.1, and on the other platforms, it’s 0.1.1.)
Special values – Python¶
In addition to the formats that work across implementations, described above, below are the formats specific to the Python implementation:
`logging.handlers:`
– this is an example of a format to access a Python module. It’s a specific case of the more general format which follows.`logging.handlers:SysLogHandler.LOG_EMERG`
– this is an example of a format to access a value inside a Python module.`logging.handlers.SysLogHandler.LOG_EMERG`
– an older variant of the above, which is only provided for backward compatibility. The form with a colon is preferred for new projects, as it is more computationally efficient – there’s no need to guess where the module stops and an object within it starts.
Special values – Kotlin / Java¶
In addition to the formats that work across implementations, described above, below are the formats specific to the Kotlin/Java implementation:
`A.B.C.D`
whereA.B.C.D
is a fully-qualified class name for some class found on the classpath. The returned value is the class. An example of this would be`java.io.File`
.`A.B.C.D:e`
whereA.B.C.D
is a fully-qualified class name for some class found on the classpath, ande
is either a public static field of that class, or a public static method which takes no parameters. Examples of these are`java.lang.System:out`
(public static field) or`java.time.LocalDate:now`
(public static method with no parameters). The returned value is either the value of the field or the return value of the method.
Special values – .NET¶
In addition to the formats that work across implementations, described above, below are the formats specific to the .NET implementation:
`asmname,A.B.C.D`
whereasmname
is the name of a .NET assembly available on the path, andA.B.C.D
is a fully-qualified class name for some class found in that assembly. The returned value is the class. An example of this would be`mscorlib,System.IO.FileAccess`
.`asmname,A.B.C.D:e`
whereasmname
is the name of a .NET assembly available on the path, andA.B.C.D
is a fully-qualified class name for some class found in that assembly, ande
is either a public static field or property of that class, or a public static method of that class which takes no parameters. Examples of these would be`mscorlib,System.IO.FileAccess:ReadWrite`
(public field of an enum) or`mscorlib,System.DateTime:Today`
(public static method with no parameters). The returned value is either the value of the field or the return value of the method.
Special values – JavaScript¶
In addition to the formats that work across implementations, described above, below are the formats specific to the JavaScript implementation:
`A.B.C.D`
whereA.B.C.D
is a path to an object accessed via the JavaScript global object (globalThis
). That object is searched forA
, which is then searched forB
, etc. If at any point the search fails, the special value conversion fails as a whole.
Expressions¶
Expressions are used to compose values from other values. In the following example,
base_path: '/var/www/foo'
html_path: ${base_path} + '/static/html'
css_path: ${base_path} + '/static/css'
js_path: ${base_path} + '/static/js'
if the base_path
changes, the change only needs to be made in one place.
Operators¶
Expressions make use of a number of operators applied to AST nodes:
A + B
is used to:- Add numbers
- Concatenate strings and lists
- Deep-merge mappings (in which case, values in
B
override those inA
, with nested mappings being recursively merged).
A - B
is used to:- Subtract numbers
- Make a copy of a mapping
A
excluding the keys inB
.
A * B
is used to multiply numbers.A / B
is used to divide numbers.A % B
is used to compute the modulo of one number with respect to another.A | B
is used to compute a bitwise-OR of integers.A & B
is used to compute a bitwise-AND of integers.A ^ B
is used to compute a bitwise-XOR of integers.A << B
is used to left-shift one integer by another.A >> B
is used to right-shift one integer by another.A ** B
is used to raiseA
to the power ofB
.A or B
is used to do short-circuit Boolean evaluation ofA
orB
. You can also useA || B
for this.A and B
is used to do short-circuit Boolean evaluation ofA
andB
. You can also useA && B
for this.
The above are binary operators, but there are also some unary operators:
- A
is used to negate numbers in a numerical sense.~ A
is used to compute a bitwise complement of an integer.not A
is used to do logical negation ofA
. You can also use the!A
notation for this.
Operators follow standard precedence rules. You can use parentheses to force a particular order of evaluation.
Continuation lines¶
Because newlines are used to delineate elements in mappings and lists, they cannot normally be used to break up long lines in the middle of a list or mapping, where newlines aren’t expected – e.g. in an expression value. Consider the lines
key1 = ${ref1} + ${ref2}
key2 = ${ref3} + ${ref4} + ${ref5} + ${ref6} ... + ${ref10}
where the second line is quite long. If you want to break the line after say the
${ref4}
, then the parser wouldn’t automatically know that the line needed to
be continued, unless there’s a way of indicating this. There is such a way –
you indicate a continuation by specifying a backslash immediately followed by a
newline (i.e. with no intervening whitespace). When the tokenizer sees such a
combination, it swallows both characters and acts as neither one had been there
– treating the following line as a continuation of the previous line. Thus the
key2
line could be written as
key2 = ${ref3} + ${ref4} \
+ ${ref5} + ${ref6} + \
... + ${ref10}
thereby making the configuration more readable.