<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P111-config-merger/ | updated: 2026-10-16 -->

# P111 Config merger

Settings in three layers - defaults, environment, command line - merged deeply with the source of every value, against a shallow merge that loses sibling settings and a merge that tests values for truth and lets false, 0 and empty text fall through.

EML-P project `projects/config-merger` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/config-merger` runs it and `eml project verify projects/config-merger` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: nested-config-merge (https://efficientnewlanguage.org/cases/192-nested-config-merge/), config-defaults-merge (https://efficientnewlanguage.org/cases/158-config-defaults-merge/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
4

4
prod
4
defaults

4
defaults
server.port
4
defaults
=3
4
defaults
Server.Port=1
4
defaults
a..b=1
4
defaults
x=it's
4
defaults
x=a b
4
defaults
nothing=unset
5

5
Bad Path
5
colour
4
defaults
server.port=007
1
6
```

Screen:

```text
== Config merger ==
Settings in three layers - defaults, environment, command line - each later one over the earlier ones.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 0
Pick a number from 1 to 6.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> x
Pick a number from 1 to 6.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> 
Cancelled.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> prod
Type defaults, environment or command line.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> 
Cancelled.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> server.port
Type path=value.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> =3
Type path=value.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> Server.Port=1
A path is lower-case words, digits, _ and - joined by dots.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> a..b=1
A path is lower-case words, digits, _ and - joined by dots.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> x=it's
A value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> x=a b
A value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> nothing=unset
nothing is not set in defaults.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 5
path> 
Cancelled.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 5
path> Bad Path
A path is lower-case words, digits, _ and - joined by dots.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 5
path> colour
  defaults      -
  environment   -
  command line  -
No layer sets it.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> defaults
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> server.port=007
server.port = '007' in defaults.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 1
  defaults:
    log.level             'info'
    log.verbose           true
    name                  'app'
    retries               3
    server.host           'app.example'
    server.port           '007'
    server.tls.cert       'certs/app.pem'
    server.tls.enabled    false
  environment:
    log.verbose           false
    server.tls.enabled    true
  command line:
    name                  ''
    retries               0
Merged, each later layer over the earlier ones:
    log.level             'info'              from defaults
    log.verbose           false               from environment
    name                  ''                  from command line
    retries               0                   from command line
    server.host           'app.example'       from defaults
    server.port           '007'               from defaults
    server.tls.cert       'certs/app.pem'     from defaults
    server.tls.enabled    true                from environment

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 6
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
3
5
server.tls.cert
5
retries
4
command line
server=off
1
5
server.port
4
command line
server=unset
4
environment
log.level=debug
2
6
```

Screen:

```text
== Config merger ==
Settings in three layers - defaults, environment, command line - each later one over the earlier ones.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 1
  defaults:
    log.level             'info'
    log.verbose           true
    name                  'app'
    retries               3
    server.host           'app.example'
    server.port           8080
    server.tls.cert       'certs/app.pem'
    server.tls.enabled    false
  environment:
    log.verbose           false
    server.tls.enabled    true
  command line:
    name                  ''
    retries               0
Merged, each later layer over the earlier ones:
    log.level             'info'              from defaults
    log.verbose           false               from environment
    name                  ''                  from command line
    retries               0                   from command line
    server.host           'app.example'       from defaults
    server.port           8080                from defaults
    server.tls.cert       'certs/app.pem'     from defaults
    server.tls.enabled    true                from environment

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 2
Where the shallow merge differs from the deep one:
  log.level             'info'              lost by the shallow merge
  server.host           'app.example'       lost by the shallow merge
  server.port           8080                lost by the shallow merge
  server.tls.cert       'certs/app.pem'     lost by the shallow merge
A shallow merge replaces every top-level key a later layer names, so setting one leaf under a top-level key loses every other leaf under that key.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 3
Where the truth-testing merge differs from the deep one:
  log.verbose           false               the truth-testing merge has true, from defaults
  name                  ''                  the truth-testing merge has 'app', from defaults
  retries               0                   the truth-testing merge has 3, from defaults
Testing a value for truth instead of asking whether it was set lets false, 0 and '' fall through to the layer below.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 5
path> server.tls.cert
  defaults      'certs/app.pem'
  environment   -
  command line  -
Merged: 'certs/app.pem', from defaults: the last layer that sets it.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 5
path> retries
  defaults      3
  environment   -
  command line  0
Merged: 0, from command line: the last layer that sets it.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> command line
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> server=off
server = 'off' in command line.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 1
  defaults:
    log.level             'info'
    log.verbose           true
    name                  'app'
    retries               3
    server.host           'app.example'
    server.port           8080
    server.tls.cert       'certs/app.pem'
    server.tls.enabled    false
  environment:
    log.verbose           false
    server.tls.enabled    true
  command line:
    name                  ''
    retries               0
    server                'off'
Merged, each later layer over the earlier ones:
    log.level             'info'              from defaults
    log.verbose           false               from environment
    name                  ''                  from command line
    retries               0                   from command line
    server                'off'               from command line

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 5
path> server.port
  defaults      8080
  environment   -
  command line  -
Merged: not set - a later layer replaced it.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> command line
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> server=unset
server removed from command line.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 4
layer (defaults, environment or command line)> environment
As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.
setting> log.level=debug
log.level = 'debug' in environment.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 2
Where the shallow merge differs from the deep one:
  server.host           'app.example'       lost by the shallow merge
  server.port           8080                lost by the shallow merge
  server.tls.cert       'certs/app.pem'     lost by the shallow merge
A shallow merge replaces every top-level key a later layer names, so setting one leaf under a top-level key loses every other leaf under that key.

1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit
choice> 6
Bye.
```

## Modules

### main.eml

```eml
# P111 config merger: settings in three layers merged deeply, against a
# shallow merge and against a merge that tests values for truth.
import layers

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    while len(s) < n:
        s + " " => s
    return s

def show_layers(ls):
    for l in ls:
        ("  " + l[0] + ":") ^0
        layers.sorted_paths(l[1]) => ps
        if len(ps) == 0:
            "    (nothing)" ^0
        for p in ps:
            ("    " + pad(p, 22) + layers.show(l[1][p])) ^0
    layers.merged(ls, "deep") => m
    "Merged, each later layer over the earlier ones:" ^0
    for p in layers.sorted_paths(m[0]):
        ("    " + pad(p, 22) + pad(layers.show(m[0][p]), 20) + "from " + m[1][p]) ^0

def compare(ls, mode, what):
    layers.merged(ls, "deep") => good
    layers.merged(ls, mode) => other
    [] => lines
    for p in layers.sorted_paths(good[0]):
        if not (p in other[0]):
            lines + ["  " + pad(p, 22) + pad(layers.show(good[0][p]), 20) + "lost by the " + what + " merge"] => lines
        elif not layers.same(good[0][p], other[0][p]):
            lines + ["  " + pad(p, 22) + pad(layers.show(good[0][p]), 20) + "the " + what + " merge has " + layers.show(other[0][p]) + ", from " + other[1][p]] => lines
    for p in layers.sorted_paths(other[0]):
        if not (p in good[0]):
            lines + ["  " + pad(p, 22) + pad("(not set)", 20) + "the " + what + " merge has " + layers.show(other[0][p])] => lines
    if len(lines) == 0:
        ("The " + what + " merge gives the same settings here.") ^0
        return
    ("Where the " + what + " merge differs from the deep one:") ^0
    for line in lines:
        line ^0
    if mode == "shallow":
        "A shallow merge replaces every top-level key a later layer names, so setting one leaf under a top-level key loses every other leaf under that key." ^0
    else:
        "Testing a value for truth instead of asking whether it was set lets false, 0 and '' fall through to the layer below." ^0

def ask_layer(ls):
    plain(input("layer (defaults, environment or command line)> ")) => n
    if n == "":
        return -1
    for k in [0:len(ls) - 1]:
        if ls[k][0] == n:
            return k
    "Type defaults, environment or command line." ^0
    return -2

def set_key(ls):
    ask_layer(ls) => k
    if k == -1:
        "Cancelled." ^0
        return ls
    if k == -2:
        return ls
    "As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it." ^0
    plain(input("setting> ")) => t
    if t == "":
        "Cancelled." ^0
        return ls
    -1 => eq
    for i in [0:len(t) - 1]:
        if eq == -1 and t[i] == "=":
            i => eq
    if eq < 1:
        "Type path=value." ^0
        return ls
    t[0:eq] => p
    t[eq + 1:len(t)] => v
    if not layers.path_ok(p):
        "A path is lower-case words, digits, _ and - joined by dots." ^0
        return ls
    ls[k][1] => d
    if v == "unset":
        if not (p in d):
            (p + " is not set in " + ls[k][0] + ".") ^0
            return ls
        {} => nd
        for q in d:
            if q != p:
                d[q] => nd[q]
        nd => ls[k][1]
        (p + " removed from " + ls[k][0] + ".") ^0
        return ls
    layers.parse_value(v) => val
    if len(val) == 0:
        "A value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :." ^0
        return ls
    {} => nd
    [] => gone
    for q in d:
        if layers.shadows(p, q) and q != p:
            gone + [q] => gone
        elif q != p:
            d[q] => nd[q]
    val => nd[p]
    nd => ls[k][1]
    (p + " = " + layers.show(val) + " in " + ls[k][0] + ".") ^0
    if len(gone) > 0:
        "" => line
        for q in gone:
            if line != "":
                line + ", " => line
            line + q => line
        ("  It replaces " + line + " in the same layer.") ^0
    return ls

def explain(ls):
    plain(input("path> ")) => p
    if p == "":
        "Cancelled." ^0
        return
    if not layers.path_ok(p):
        "A path is lower-case words, digits, _ and - joined by dots." ^0
        return
    False => any
    for l in ls:
        if p in l[1]:
            True => any
            ("  " + pad(l[0], 14) + layers.show(l[1][p])) ^0
        else:
            ("  " + pad(l[0], 14) + "-") ^0
    layers.merged(ls, "deep") => m
    if p in m[0]:
        ("Merged: " + layers.show(m[0][p]) + ", from " + m[1][p] + ": the last layer that sets it.") ^0
    elif any:
        ("Merged: not set - a later layer replaced it.") ^0
    else:
        "No layer sets it." ^0

[["defaults", {"server.host": ["str", "app.example"], "server.port": ["int", "8080"], "server.tls.enabled": ["bool", "false"], "server.tls.cert": ["str", "certs/app.pem"], "log.level": ["str", "info"], "log.verbose": ["bool", "true"], "retries": ["int", "3"], "name": ["str", "app"]}], ["environment", {"server.tls.enabled": ["bool", "true"], "log.verbose": ["bool", "false"]}], ["command line", {"retries": ["int", "0"], "name": ["str", ""]}]] => ls

"== Config merger ==" ^0
"Settings in three layers - defaults, environment, command line - each later one over the earlier ones." ^0
True => running
while running:
    "" ^0
    "1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        show_layers(ls)
    elif choice == "2":
        compare(ls, "shallow", "shallow")
    elif choice == "3":
        compare(ls, "truthy", "truth-testing")
    elif choice == "4":
        set_key(ls) => ls
    elif choice == "5":
        explain(ls)
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import layers

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    while len(s) < n:
        s = s + " "
    return s

def show_layers(ls):
    for l in ls:
        print("  " + l[0] + ":")
        ps = layers.sorted_paths(l[1])
        if len(ps) == 0:
            print("    (nothing)")
        for p in ps:
            print("    " + pad(p, 22) + layers.show(l[1][p]))
    m = layers.merged(ls, "deep")
    print("Merged, each later layer over the earlier ones:")
    for p in layers.sorted_paths(m[0]):
        print("    " + pad(p, 22) + pad(layers.show(m[0][p]), 20) + "from " + m[1][p])

def compare(ls, mode, what):
    good = layers.merged(ls, "deep")
    other = layers.merged(ls, mode)
    lines = []
    for p in layers.sorted_paths(good[0]):
        if not p in other[0]:
            lines = lines + ["  " + pad(p, 22) + pad(layers.show(good[0][p]), 20) + "lost by the " + what + " merge"]
        elif not layers.same(good[0][p], other[0][p]):
            lines = lines + ["  " + pad(p, 22) + pad(layers.show(good[0][p]), 20) + "the " + what + " merge has " + layers.show(other[0][p]) + ", from " + other[1][p]]
    for p in layers.sorted_paths(other[0]):
        if not p in good[0]:
            lines = lines + ["  " + pad(p, 22) + pad("(not set)", 20) + "the " + what + " merge has " + layers.show(other[0][p])]
    if len(lines) == 0:
        print("The " + what + " merge gives the same settings here.")
        return
    print("Where the " + what + " merge differs from the deep one:")
    for line in lines:
        print(line)
    if mode == "shallow":
        print("A shallow merge replaces every top-level key a later layer names, so setting one leaf under a top-level key loses every other leaf under that key.")
    else:
        print("Testing a value for truth instead of asking whether it was set lets false, 0 and '' fall through to the layer below.")

def ask_layer(ls):
    n = plain(input("layer (defaults, environment or command line)> "))
    if n == "":
        return -1
    for k in range(0, len(ls)):
        if ls[k][0] == n:
            return k
    print("Type defaults, environment or command line.")
    return -2

def set_key(ls):
    k = ask_layer(ls)
    if k == -1:
        print("Cancelled.")
        return ls
    if k == -2:
        return ls
    print("As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.")
    t = plain(input("setting> "))
    if t == "":
        print("Cancelled.")
        return ls
    eq = -1
    for i in range(0, len(t)):
        if eq == -1 and t[i] == "=":
            eq = i
    if eq < 1:
        print("Type path=value.")
        return ls
    p = t[0:eq]
    v = t[eq + 1:len(t)]
    if not layers.path_ok(p):
        print("A path is lower-case words, digits, _ and - joined by dots.")
        return ls
    d = ls[k][1]
    if v == "unset":
        if not p in d:
            print(p + " is not set in " + ls[k][0] + ".")
            return ls
        nd = {}
        for q in d:
            if q != p:
                nd[q] = d[q]
        ls[k][1] = nd
        print(p + " removed from " + ls[k][0] + ".")
        return ls
    val = layers.parse_value(v)
    if len(val) == 0:
        print("A value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :.")
        return ls
    nd = {}
    gone = []
    for q in d:
        if layers.shadows(p, q) and q != p:
            gone = gone + [q]
        elif q != p:
            nd[q] = d[q]
    nd[p] = val
    ls[k][1] = nd
    print(p + " = " + layers.show(val) + " in " + ls[k][0] + ".")
    if len(gone) > 0:
        line = ""
        for q in gone:
            if line != "":
                line = line + ", "
            line = line + q
        print("  It replaces " + line + " in the same layer.")
    return ls

def explain(ls):
    p = plain(input("path> "))
    if p == "":
        print("Cancelled.")
        return
    if not layers.path_ok(p):
        print("A path is lower-case words, digits, _ and - joined by dots.")
        return
    any = False
    for l in ls:
        if p in l[1]:
            any = True
            print("  " + pad(l[0], 14) + layers.show(l[1][p]))
        else:
            print("  " + pad(l[0], 14) + "-")
    m = layers.merged(ls, "deep")
    if p in m[0]:
        print("Merged: " + layers.show(m[0][p]) + ", from " + m[1][p] + ": the last layer that sets it.")
    elif any:
        print("Merged: not set - a later layer replaced it.")
    else:
        print("No layer sets it.")

ls = [["defaults", {"server.host": ["str", "app.example"], "server.port": ["int", "8080"], "server.tls.enabled": ["bool", "false"], "server.tls.cert": ["str", "certs/app.pem"], "log.level": ["str", "info"], "log.verbose": ["bool", "true"], "retries": ["int", "3"], "name": ["str", "app"]}], ["environment", {"server.tls.enabled": ["bool", "true"], "log.verbose": ["bool", "false"]}], ["command line", {"retries": ["int", "0"], "name": ["str", ""]}]]
print("== Config merger ==")
print("Settings in three layers - defaults, environment, command line - each later one over the earlier ones.")
running = True
while running:
    print("")
    print("1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        show_layers(ls)
    elif choice == "2":
        compare(ls, "shallow", "shallow")
    elif choice == "3":
        compare(ls, "truthy", "truth-testing")
    elif choice == "4":
        ls = set_key(ls)
    elif choice == "5":
        explain(ls)
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")
```

### layers.eml

```eml
# P111 config merger - settings in three layers (defaults, environment,
# command line), each a set of leaves written as dotted paths such as
# server.tls.cert. A value is [kind, text]: kind "bool", "int" or "str".
#
# A deep merge, as in the corpus case nested-config-merge, lets a later
# layer replace exactly the leaves it sets: a leaf it sets removes the
# same path, every path under it, and any value above it that was not a
# table. A shallow merge replaces each top-level key wholesale, so setting
# server.tls.enabled loses server.tls.cert. And as in the corpus case
# config-defaults-merge, a value is set when its key is present, not when
# it is true: false, 0 and '' must still win.

def starts(s, p):
    return len(s) >= len(p) and s[0:len(p)] == p

def first(path):
    for i in [0:len(path) - 1]:
        if path[i] == ".":
            return path[0:i]
    return path

def sorted_paths(d):
    [] => out
    for k in d:
        out + [k] => out
    for i in [1:len(out) - 1]:
        i => j
        while j > 0 and out[j] < out[j - 1]:
            out[j] => t
            out[j - 1] => out[j]
            t => out[j - 1]
            j - 1 => j
    return out

def copy(d):
    {} => out
    for k in d:
        d[k] => out[k]
    return out

def shadows(p, q):
    # setting leaf p removes leaf q
    return q == p or starts(q, p + ".") or starts(p, q + ".")

def deep(base, over):
    {} => out
    for q in base:
        True => keep
        for p in over:
            if shadows(p, q):
                False => keep
        if keep:
            base[q] => out[q]
    for p in over:
        over[p] => out[p]
    return out

def shallow(base, over):
    # every top-level key the later layer names is replaced whole
    {} => tops
    for p in over:
        True => tops[first(p)]
    {} => out
    for q in base:
        if not (first(q) in tops):
            base[q] => out[q]
    for p in over:
        over[p] => out[p]
    return out

def truthy(v):
    if v[0] == "bool":
        return v[1] == "true"
    if v[0] == "int":
        return v[1] != "0"
    return v[1] != ""

def truthy_deep(base, over):
    # the mistake: a later value counts only when it is true
    {} => kept
    for p in over:
        if truthy(over[p]):
            over[p] => kept[p]
    return deep(base, kept)

def merged(layers, mode):
    # mode "deep", "shallow" or "truthy"; [result, source of each leaf]
    {} => out
    {} => source
    for k in [0:len(layers) - 1]:
        layers[k][1] => over
        if mode == "shallow":
            shallow(out, over) => out
        elif mode == "truthy":
            truthy_deep(out, over) => out
        else:
            deep(out, over) => out
        for p in out:
            if p in over and (mode != "truthy" or truthy(over[p])):
                layers[k][0] => source[p]
    return [out, source]

def show(v):
    if v[0] == "str":
        return "'" + v[1] + "'"
    return v[1]

def same(a, b):
    return a[0] == b[0] and a[1] == b[1]

def path_ok(p):
    if len(p) == 0 or len(p) > 40 or p[0] == "." or p[len(p) - 1] == ".":
        return False
    "" => prev
    for c in p:
        if c == "." and prev == ".":
            return False
        if not ((c >= "a" and c <= "z") or (c >= "0" and c <= "9") or c == "_" or c == "-" or c == "."):
            return False
        c => prev
    return True

def parse_value(t):
    # [kind, text] or []: true, false, a whole number, 'text', or a bare word
    if t == "true" or t == "false":
        return ["bool", t]
    if len(t) >= 2 and t[0] == "'" and t[len(t) - 1] == "'":
        t[1:len(t) - 1] => inner
        for c in inner:
            if c == "'":
                return []
        return ["str", inner]
    t => digits
    if len(t) > 0 and t[0] == "-":
        t[1:len(t)] => digits
    if len(digits) > 0 and len(digits) <= 9:
        True => num
        for c in digits:
            if not (c >= "0" and c <= "9"):
                False => num
        if num and (len(digits) == 1 or digits[0] != "0") and not (t[0] == "-" and digits == "0"):
            return ["int", t]
    if len(t) == 0 or len(t) > 40:
        return []
    for c in t:
        if not ((c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or (c >= "0" and c <= "9") or c == "-" or c == "_" or c == "." or c == "/" or c == ":"):
            return []
    return ["str", t]
```

Python projection of layers.eml:

```python
def starts(s, p):
    return len(s) >= len(p) and s[0:len(p)] == p

def first(path):
    for i in range(0, len(path)):
        if path[i] == ".":
            return path[0:i]
    return path

def sorted_paths(d):
    out = []
    for k in d:
        out = out + [k]
    for i in range(1, len(out)):
        j = i
        while j > 0 and out[j] < out[j - 1]:
            t = out[j]
            out[j] = out[j - 1]
            out[j - 1] = t
            j = j - 1
    return out

def copy(d):
    out = {}
    for k in d:
        out[k] = d[k]
    return out

def shadows(p, q):
    return q == p or starts(q, p + ".") or starts(p, q + ".")

def deep(base, over):
    out = {}
    for q in base:
        keep = True
        for p in over:
            if shadows(p, q):
                keep = False
        if keep:
            out[q] = base[q]
    for p in over:
        out[p] = over[p]
    return out

def shallow(base, over):
    tops = {}
    for p in over:
        tops[first(p)] = True
    out = {}
    for q in base:
        if not first(q) in tops:
            out[q] = base[q]
    for p in over:
        out[p] = over[p]
    return out

def truthy(v):
    if v[0] == "bool":
        return v[1] == "true"
    if v[0] == "int":
        return v[1] != "0"
    return v[1] != ""

def truthy_deep(base, over):
    kept = {}
    for p in over:
        if truthy(over[p]):
            kept[p] = over[p]
    return deep(base, kept)

def merged(layers, mode):
    out = {}
    source = {}
    for k in range(0, len(layers)):
        over = layers[k][1]
        if mode == "shallow":
            out = shallow(out, over)
        elif mode == "truthy":
            out = truthy_deep(out, over)
        else:
            out = deep(out, over)
        for p in out:
            if p in over and (mode != "truthy" or truthy(over[p])):
                source[p] = layers[k][0]
    return [out, source]

def show(v):
    if v[0] == "str":
        return "'" + v[1] + "'"
    return v[1]

def same(a, b):
    return a[0] == b[0] and a[1] == b[1]

def path_ok(p):
    if len(p) == 0 or len(p) > 40 or p[0] == "." or p[len(p) - 1] == ".":
        return False
    prev = ""
    for c in p:
        if c == "." and prev == ".":
            return False
        if not (c >= "a" and c <= "z" or c >= "0" and c <= "9" or c == "_" or c == "-" or c == "."):
            return False
        prev = c
    return True

def parse_value(t):
    if t == "true" or t == "false":
        return ["bool", t]
    if len(t) >= 2 and t[0] == "'" and t[len(t) - 1] == "'":
        inner = t[1:len(t) - 1]
        for c in inner:
            if c == "'":
                return []
        return ["str", inner]
    digits = t
    if len(t) > 0 and t[0] == "-":
        digits = t[1:len(t)]
    if len(digits) > 0 and len(digits) <= 9:
        num = True
        for c in digits:
            if not (c >= "0" and c <= "9"):
                num = False
        if num and (len(digits) == 1 or digits[0] != "0") and not (t[0] == "-" and digits == "0"):
            return ["int", t]
    if len(t) == 0 or len(t) > 40:
        return []
    for c in t:
        if not (c >= "a" and c <= "z" or c >= "A" and c <= "Z" or c >= "0" and c <= "9" or c == "-" or c == "_" or c == "." or c == "/" or c == ":"):
            return []
    return ["str", t]
```

## README

# P111 - Config merger

Settings in three layers, defaults, environment and command line, each
later layer over the earlier ones. The deep merge is set against a
shallow merge and against a merge that tests values for truth.

- `main.eml` - the menu, the layers and the merge with each value's
  source, the two comparisons, setting a key, and where a key comes from
- `layers.eml` - paths, the deep, shallow and truth-testing merges, the
  merge of all layers with sources, and reading paths and values

How each part works:

- Each layer is a set of leaves written as dotted paths, such as
  server.tls.cert. A value is true, false, a whole number, or text.
- The deep merge, as in the corpus case `nested-config-merge`: a later
  layer replaces exactly the leaves it sets. A leaf it sets removes the
  same path, every path under it, and any value above it that was not a
  table. It is the same as merging nested tables recursively, which the
  check confirms.
- The shallow merge replaces each top-level key a later layer names, whole.
  Setting server.tls.enabled in the environment loses server.host,
  server.port and server.tls.cert.
- The truth-testing merge, as in the corpus case `config-defaults-merge`,
  makes the mistake of asking whether a value is true instead of whether
  it is set. A later false, 0 or '' then falls through to the layer below.
- Each merged value names the last layer that set it.

What is checked: menu choices 1 to 6; one of the three layers; a setting
as path=value or path=unset; a path of lower-case words, digits, _ and -
joined by dots; a value of true, false, a whole number of up to nine
digits without a leading zero, 'text', or a word of letters, digits and
- _ . / :. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the three layers and the merge, with each value's source;
- the shallow merge, which loses four leaves;
- the truth-testing merge, which loses log.verbose=false, retries=0 and
  name='';
- where server.tls.cert and retries come from;
- server set to 'off' on the command line, which replaces the whole
  server table; server.port explained; then server removed again;
- log.level set in the environment, and the shallow merge again.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- settings: a cancel, an unknown layer, a cancel, no =, no path, upper
  case, two dots, a quote inside a word, a space in a value, and removing
  a key that is not there;
- paths: a cancel, a bad one, and one no layer sets;
- server.port=007, kept as text because of its leading zeros.

Built on the verified corpus cases `nested-config-merge` (if both values
are tables, recurse instead of replacing) and `config-defaults-merge`
(presence is not truthiness).
