{
  "id": "P111",
  "slug": "config-merger",
  "key": "P111-config-merger",
  "title": "Config merger",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P111 - Config merger\n\nSettings in three layers, defaults, environment and command line, each\nlater layer over the earlier ones. The deep merge is set against a\nshallow merge and against a merge that tests values for truth.\n\n- `main.eml` - the menu, the layers and the merge with each value's\n  source, the two comparisons, setting a key, and where a key comes from\n- `layers.eml` - paths, the deep, shallow and truth-testing merges, the\n  merge of all layers with sources, and reading paths and values\n\nHow each part works:\n\n- Each layer is a set of leaves written as dotted paths, such as\n  server.tls.cert. A value is true, false, a whole number, or text.\n- The deep merge, as in the corpus case `nested-config-merge`: a later\n  layer replaces exactly the leaves it sets. A leaf it sets removes the\n  same path, every path under it, and any value above it that was not a\n  table. It is the same as merging nested tables recursively, which the\n  check confirms.\n- The shallow merge replaces each top-level key a later layer names, whole.\n  Setting server.tls.enabled in the environment loses server.host,\n  server.port and server.tls.cert.\n- The truth-testing merge, as in the corpus case `config-defaults-merge`,\n  makes the mistake of asking whether a value is true instead of whether\n  it is set. A later false, 0 or '' then falls through to the layer below.\n- Each merged value names the last layer that set it.\n\nWhat is checked: menu choices 1 to 6; one of the three layers; a setting\nas path=value or path=unset; a path of lower-case words, digits, _ and -\njoined by dots; a value of true, false, a whole number of up to nine\ndigits without a leading zero, 'text', or a word of letters, digits and\n- _ . / :. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the three layers and the merge, with each value's source;\n- the shallow merge, which loses four leaves;\n- the truth-testing merge, which loses log.verbose=false, retries=0 and\n  name='';\n- where server.tls.cert and retries come from;\n- server set to 'off' on the command line, which replaces the whole\n  server table; server.port explained; then server removed again;\n- log.level set in the environment, and the shallow merge again.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- settings: a cancel, an unknown layer, a cancel, no =, no path, upper\n  case, two dots, a quote inside a word, a space in a value, and removing\n  a key that is not there;\n- paths: a cancel, a bad one, and one no layer sets;\n- server.port=007, kept as text because of its leading zeros.\n\nBuilt on the verified corpus cases `nested-config-merge` (if both values\nare tables, recurse instead of replacing) and `config-defaults-merge`\n(presence is not truthiness).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P111 config merger: settings in three layers merged deeply, against a\n# shallow merge and against a merge that tests values for truth.\nimport layers\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef show_layers(ls):\n    for l in ls:\n        (\"  \" + l[0] + \":\") ^0\n        layers.sorted_paths(l[1]) => ps\n        if len(ps) == 0:\n            \"    (nothing)\" ^0\n        for p in ps:\n            (\"    \" + pad(p, 22) + layers.show(l[1][p])) ^0\n    layers.merged(ls, \"deep\") => m\n    \"Merged, each later layer over the earlier ones:\" ^0\n    for p in layers.sorted_paths(m[0]):\n        (\"    \" + pad(p, 22) + pad(layers.show(m[0][p]), 20) + \"from \" + m[1][p]) ^0\n\ndef compare(ls, mode, what):\n    layers.merged(ls, \"deep\") => good\n    layers.merged(ls, mode) => other\n    [] => lines\n    for p in layers.sorted_paths(good[0]):\n        if not (p in other[0]):\n            lines + [\"  \" + pad(p, 22) + pad(layers.show(good[0][p]), 20) + \"lost by the \" + what + \" merge\"] => lines\n        elif not layers.same(good[0][p], other[0][p]):\n            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\n    for p in layers.sorted_paths(other[0]):\n        if not (p in good[0]):\n            lines + [\"  \" + pad(p, 22) + pad(\"(not set)\", 20) + \"the \" + what + \" merge has \" + layers.show(other[0][p])] => lines\n    if len(lines) == 0:\n        (\"The \" + what + \" merge gives the same settings here.\") ^0\n        return\n    (\"Where the \" + what + \" merge differs from the deep one:\") ^0\n    for line in lines:\n        line ^0\n    if mode == \"shallow\":\n        \"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\n    else:\n        \"Testing a value for truth instead of asking whether it was set lets false, 0 and '' fall through to the layer below.\" ^0\n\ndef ask_layer(ls):\n    plain(input(\"layer (defaults, environment or command line)> \")) => n\n    if n == \"\":\n        return -1\n    for k in [0:len(ls) - 1]:\n        if ls[k][0] == n:\n            return k\n    \"Type defaults, environment or command line.\" ^0\n    return -2\n\ndef set_key(ls):\n    ask_layer(ls) => k\n    if k == -1:\n        \"Cancelled.\" ^0\n        return ls\n    if k == -2:\n        return ls\n    \"As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\" ^0\n    plain(input(\"setting> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return ls\n    -1 => eq\n    for i in [0:len(t) - 1]:\n        if eq == -1 and t[i] == \"=\":\n            i => eq\n    if eq < 1:\n        \"Type path=value.\" ^0\n        return ls\n    t[0:eq] => p\n    t[eq + 1:len(t)] => v\n    if not layers.path_ok(p):\n        \"A path is lower-case words, digits, _ and - joined by dots.\" ^0\n        return ls\n    ls[k][1] => d\n    if v == \"unset\":\n        if not (p in d):\n            (p + \" is not set in \" + ls[k][0] + \".\") ^0\n            return ls\n        {} => nd\n        for q in d:\n            if q != p:\n                d[q] => nd[q]\n        nd => ls[k][1]\n        (p + \" removed from \" + ls[k][0] + \".\") ^0\n        return ls\n    layers.parse_value(v) => val\n    if len(val) == 0:\n        \"A value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :.\" ^0\n        return ls\n    {} => nd\n    [] => gone\n    for q in d:\n        if layers.shadows(p, q) and q != p:\n            gone + [q] => gone\n        elif q != p:\n            d[q] => nd[q]\n    val => nd[p]\n    nd => ls[k][1]\n    (p + \" = \" + layers.show(val) + \" in \" + ls[k][0] + \".\") ^0\n    if len(gone) > 0:\n        \"\" => line\n        for q in gone:\n            if line != \"\":\n                line + \", \" => line\n            line + q => line\n        (\"  It replaces \" + line + \" in the same layer.\") ^0\n    return ls\n\ndef explain(ls):\n    plain(input(\"path> \")) => p\n    if p == \"\":\n        \"Cancelled.\" ^0\n        return\n    if not layers.path_ok(p):\n        \"A path is lower-case words, digits, _ and - joined by dots.\" ^0\n        return\n    False => any\n    for l in ls:\n        if p in l[1]:\n            True => any\n            (\"  \" + pad(l[0], 14) + layers.show(l[1][p])) ^0\n        else:\n            (\"  \" + pad(l[0], 14) + \"-\") ^0\n    layers.merged(ls, \"deep\") => m\n    if p in m[0]:\n        (\"Merged: \" + layers.show(m[0][p]) + \", from \" + m[1][p] + \": the last layer that sets it.\") ^0\n    elif any:\n        (\"Merged: not set - a later layer replaced it.\") ^0\n    else:\n        \"No layer sets it.\" ^0\n\n[[\"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\n\n\"== Config merger ==\" ^0\n\"Settings in three layers - defaults, environment, command line - each later one over the earlier ones.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"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\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_layers(ls)\n    elif choice == \"2\":\n        compare(ls, \"shallow\", \"shallow\")\n    elif choice == \"3\":\n        compare(ls, \"truthy\", \"truth-testing\")\n    elif choice == \"4\":\n        set_key(ls) => ls\n    elif choice == \"5\":\n        explain(ls)\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import layers\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef show_layers(ls):\n    for l in ls:\n        print(\"  \" + l[0] + \":\")\n        ps = layers.sorted_paths(l[1])\n        if len(ps) == 0:\n            print(\"    (nothing)\")\n        for p in ps:\n            print(\"    \" + pad(p, 22) + layers.show(l[1][p]))\n    m = layers.merged(ls, \"deep\")\n    print(\"Merged, each later layer over the earlier ones:\")\n    for p in layers.sorted_paths(m[0]):\n        print(\"    \" + pad(p, 22) + pad(layers.show(m[0][p]), 20) + \"from \" + m[1][p])\n\ndef compare(ls, mode, what):\n    good = layers.merged(ls, \"deep\")\n    other = layers.merged(ls, mode)\n    lines = []\n    for p in layers.sorted_paths(good[0]):\n        if not p in other[0]:\n            lines = lines + [\"  \" + pad(p, 22) + pad(layers.show(good[0][p]), 20) + \"lost by the \" + what + \" merge\"]\n        elif not layers.same(good[0][p], other[0][p]):\n            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]]\n    for p in layers.sorted_paths(other[0]):\n        if not p in good[0]:\n            lines = lines + [\"  \" + pad(p, 22) + pad(\"(not set)\", 20) + \"the \" + what + \" merge has \" + layers.show(other[0][p])]\n    if len(lines) == 0:\n        print(\"The \" + what + \" merge gives the same settings here.\")\n        return\n    print(\"Where the \" + what + \" merge differs from the deep one:\")\n    for line in lines:\n        print(line)\n    if mode == \"shallow\":\n        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.\")\n    else:\n        print(\"Testing a value for truth instead of asking whether it was set lets false, 0 and '' fall through to the layer below.\")\n\ndef ask_layer(ls):\n    n = plain(input(\"layer (defaults, environment or command line)> \"))\n    if n == \"\":\n        return -1\n    for k in range(0, len(ls)):\n        if ls[k][0] == n:\n            return k\n    print(\"Type defaults, environment or command line.\")\n    return -2\n\ndef set_key(ls):\n    k = ask_layer(ls)\n    if k == -1:\n        print(\"Cancelled.\")\n        return ls\n    if k == -2:\n        return ls\n    print(\"As path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\")\n    t = plain(input(\"setting> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return ls\n    eq = -1\n    for i in range(0, len(t)):\n        if eq == -1 and t[i] == \"=\":\n            eq = i\n    if eq < 1:\n        print(\"Type path=value.\")\n        return ls\n    p = t[0:eq]\n    v = t[eq + 1:len(t)]\n    if not layers.path_ok(p):\n        print(\"A path is lower-case words, digits, _ and - joined by dots.\")\n        return ls\n    d = ls[k][1]\n    if v == \"unset\":\n        if not p in d:\n            print(p + \" is not set in \" + ls[k][0] + \".\")\n            return ls\n        nd = {}\n        for q in d:\n            if q != p:\n                nd[q] = d[q]\n        ls[k][1] = nd\n        print(p + \" removed from \" + ls[k][0] + \".\")\n        return ls\n    val = layers.parse_value(v)\n    if len(val) == 0:\n        print(\"A value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :.\")\n        return ls\n    nd = {}\n    gone = []\n    for q in d:\n        if layers.shadows(p, q) and q != p:\n            gone = gone + [q]\n        elif q != p:\n            nd[q] = d[q]\n    nd[p] = val\n    ls[k][1] = nd\n    print(p + \" = \" + layers.show(val) + \" in \" + ls[k][0] + \".\")\n    if len(gone) > 0:\n        line = \"\"\n        for q in gone:\n            if line != \"\":\n                line = line + \", \"\n            line = line + q\n        print(\"  It replaces \" + line + \" in the same layer.\")\n    return ls\n\ndef explain(ls):\n    p = plain(input(\"path> \"))\n    if p == \"\":\n        print(\"Cancelled.\")\n        return\n    if not layers.path_ok(p):\n        print(\"A path is lower-case words, digits, _ and - joined by dots.\")\n        return\n    any = False\n    for l in ls:\n        if p in l[1]:\n            any = True\n            print(\"  \" + pad(l[0], 14) + layers.show(l[1][p]))\n        else:\n            print(\"  \" + pad(l[0], 14) + \"-\")\n    m = layers.merged(ls, \"deep\")\n    if p in m[0]:\n        print(\"Merged: \" + layers.show(m[0][p]) + \", from \" + m[1][p] + \": the last layer that sets it.\")\n    elif any:\n        print(\"Merged: not set - a later layer replaced it.\")\n    else:\n        print(\"No layer sets it.\")\n\nls = [[\"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\", \"\"]}]]\nprint(\"== Config merger ==\")\nprint(\"Settings in three layers - defaults, environment, command line - each later one over the earlier ones.\")\nrunning = True\nwhile running:\n    print(\"\")\n    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\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_layers(ls)\n    elif choice == \"2\":\n        compare(ls, \"shallow\", \"shallow\")\n    elif choice == \"3\":\n        compare(ls, \"truthy\", \"truth-testing\")\n    elif choice == \"4\":\n        ls = set_key(ls)\n    elif choice == \"5\":\n        explain(ls)\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "layers.eml",
      "eml": "# P111 config merger - settings in three layers (defaults, environment,\n# command line), each a set of leaves written as dotted paths such as\n# server.tls.cert. A value is [kind, text]: kind \"bool\", \"int\" or \"str\".\n#\n# A deep merge, as in the corpus case nested-config-merge, lets a later\n# layer replace exactly the leaves it sets: a leaf it sets removes the\n# same path, every path under it, and any value above it that was not a\n# table. A shallow merge replaces each top-level key wholesale, so setting\n# server.tls.enabled loses server.tls.cert. And as in the corpus case\n# config-defaults-merge, a value is set when its key is present, not when\n# it is true: false, 0 and '' must still win.\n\ndef starts(s, p):\n    return len(s) >= len(p) and s[0:len(p)] == p\n\ndef first(path):\n    for i in [0:len(path) - 1]:\n        if path[i] == \".\":\n            return path[0:i]\n    return path\n\ndef sorted_paths(d):\n    [] => out\n    for k in d:\n        out + [k] => out\n    for i in [1:len(out) - 1]:\n        i => j\n        while j > 0 and out[j] < out[j - 1]:\n            out[j] => t\n            out[j - 1] => out[j]\n            t => out[j - 1]\n            j - 1 => j\n    return out\n\ndef copy(d):\n    {} => out\n    for k in d:\n        d[k] => out[k]\n    return out\n\ndef shadows(p, q):\n    # setting leaf p removes leaf q\n    return q == p or starts(q, p + \".\") or starts(p, q + \".\")\n\ndef deep(base, over):\n    {} => out\n    for q in base:\n        True => keep\n        for p in over:\n            if shadows(p, q):\n                False => keep\n        if keep:\n            base[q] => out[q]\n    for p in over:\n        over[p] => out[p]\n    return out\n\ndef shallow(base, over):\n    # every top-level key the later layer names is replaced whole\n    {} => tops\n    for p in over:\n        True => tops[first(p)]\n    {} => out\n    for q in base:\n        if not (first(q) in tops):\n            base[q] => out[q]\n    for p in over:\n        over[p] => out[p]\n    return out\n\ndef truthy(v):\n    if v[0] == \"bool\":\n        return v[1] == \"true\"\n    if v[0] == \"int\":\n        return v[1] != \"0\"\n    return v[1] != \"\"\n\ndef truthy_deep(base, over):\n    # the mistake: a later value counts only when it is true\n    {} => kept\n    for p in over:\n        if truthy(over[p]):\n            over[p] => kept[p]\n    return deep(base, kept)\n\ndef merged(layers, mode):\n    # mode \"deep\", \"shallow\" or \"truthy\"; [result, source of each leaf]\n    {} => out\n    {} => source\n    for k in [0:len(layers) - 1]:\n        layers[k][1] => over\n        if mode == \"shallow\":\n            shallow(out, over) => out\n        elif mode == \"truthy\":\n            truthy_deep(out, over) => out\n        else:\n            deep(out, over) => out\n        for p in out:\n            if p in over and (mode != \"truthy\" or truthy(over[p])):\n                layers[k][0] => source[p]\n    return [out, source]\n\ndef show(v):\n    if v[0] == \"str\":\n        return \"'\" + v[1] + \"'\"\n    return v[1]\n\ndef same(a, b):\n    return a[0] == b[0] and a[1] == b[1]\n\ndef path_ok(p):\n    if len(p) == 0 or len(p) > 40 or p[0] == \".\" or p[len(p) - 1] == \".\":\n        return False\n    \"\" => prev\n    for c in p:\n        if c == \".\" and prev == \".\":\n            return False\n        if not ((c >= \"a\" and c <= \"z\") or (c >= \"0\" and c <= \"9\") or c == \"_\" or c == \"-\" or c == \".\"):\n            return False\n        c => prev\n    return True\n\ndef parse_value(t):\n    # [kind, text] or []: true, false, a whole number, 'text', or a bare word\n    if t == \"true\" or t == \"false\":\n        return [\"bool\", t]\n    if len(t) >= 2 and t[0] == \"'\" and t[len(t) - 1] == \"'\":\n        t[1:len(t) - 1] => inner\n        for c in inner:\n            if c == \"'\":\n                return []\n        return [\"str\", inner]\n    t => digits\n    if len(t) > 0 and t[0] == \"-\":\n        t[1:len(t)] => digits\n    if len(digits) > 0 and len(digits) <= 9:\n        True => num\n        for c in digits:\n            if not (c >= \"0\" and c <= \"9\"):\n                False => num\n        if num and (len(digits) == 1 or digits[0] != \"0\") and not (t[0] == \"-\" and digits == \"0\"):\n            return [\"int\", t]\n    if len(t) == 0 or len(t) > 40:\n        return []\n    for c in t:\n        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 == \":\"):\n            return []\n    return [\"str\", t]\n",
      "python": "def starts(s, p):\n    return len(s) >= len(p) and s[0:len(p)] == p\n\ndef first(path):\n    for i in range(0, len(path)):\n        if path[i] == \".\":\n            return path[0:i]\n    return path\n\ndef sorted_paths(d):\n    out = []\n    for k in d:\n        out = out + [k]\n    for i in range(1, len(out)):\n        j = i\n        while j > 0 and out[j] < out[j - 1]:\n            t = out[j]\n            out[j] = out[j - 1]\n            out[j - 1] = t\n            j = j - 1\n    return out\n\ndef copy(d):\n    out = {}\n    for k in d:\n        out[k] = d[k]\n    return out\n\ndef shadows(p, q):\n    return q == p or starts(q, p + \".\") or starts(p, q + \".\")\n\ndef deep(base, over):\n    out = {}\n    for q in base:\n        keep = True\n        for p in over:\n            if shadows(p, q):\n                keep = False\n        if keep:\n            out[q] = base[q]\n    for p in over:\n        out[p] = over[p]\n    return out\n\ndef shallow(base, over):\n    tops = {}\n    for p in over:\n        tops[first(p)] = True\n    out = {}\n    for q in base:\n        if not first(q) in tops:\n            out[q] = base[q]\n    for p in over:\n        out[p] = over[p]\n    return out\n\ndef truthy(v):\n    if v[0] == \"bool\":\n        return v[1] == \"true\"\n    if v[0] == \"int\":\n        return v[1] != \"0\"\n    return v[1] != \"\"\n\ndef truthy_deep(base, over):\n    kept = {}\n    for p in over:\n        if truthy(over[p]):\n            kept[p] = over[p]\n    return deep(base, kept)\n\ndef merged(layers, mode):\n    out = {}\n    source = {}\n    for k in range(0, len(layers)):\n        over = layers[k][1]\n        if mode == \"shallow\":\n            out = shallow(out, over)\n        elif mode == \"truthy\":\n            out = truthy_deep(out, over)\n        else:\n            out = deep(out, over)\n        for p in out:\n            if p in over and (mode != \"truthy\" or truthy(over[p])):\n                source[p] = layers[k][0]\n    return [out, source]\n\ndef show(v):\n    if v[0] == \"str\":\n        return \"'\" + v[1] + \"'\"\n    return v[1]\n\ndef same(a, b):\n    return a[0] == b[0] and a[1] == b[1]\n\ndef path_ok(p):\n    if len(p) == 0 or len(p) > 40 or p[0] == \".\" or p[len(p) - 1] == \".\":\n        return False\n    prev = \"\"\n    for c in p:\n        if c == \".\" and prev == \".\":\n            return False\n        if not (c >= \"a\" and c <= \"z\" or c >= \"0\" and c <= \"9\" or c == \"_\" or c == \"-\" or c == \".\"):\n            return False\n        prev = c\n    return True\n\ndef parse_value(t):\n    if t == \"true\" or t == \"false\":\n        return [\"bool\", t]\n    if len(t) >= 2 and t[0] == \"'\" and t[len(t) - 1] == \"'\":\n        inner = t[1:len(t) - 1]\n        for c in inner:\n            if c == \"'\":\n                return []\n        return [\"str\", inner]\n    digits = t\n    if len(t) > 0 and t[0] == \"-\":\n        digits = t[1:len(t)]\n    if len(digits) > 0 and len(digits) <= 9:\n        num = True\n        for c in digits:\n            if not (c >= \"0\" and c <= \"9\"):\n                num = False\n        if num and (len(digits) == 1 or digits[0] != \"0\") and not (t[0] == \"-\" and digits == \"0\"):\n            return [\"int\", t]\n    if len(t) == 0 or len(t) > 40:\n        return []\n    for c in t:\n        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 == \":\"):\n            return []\n    return [\"str\", t]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n\n4\nprod\n4\ndefaults\n\n4\ndefaults\nserver.port\n4\ndefaults\n=3\n4\ndefaults\nServer.Port=1\n4\ndefaults\na..b=1\n4\ndefaults\nx=it's\n4\ndefaults\nx=a b\n4\ndefaults\nnothing=unset\n5\n\n5\nBad Path\n5\ncolour\n4\ndefaults\nserver.port=007\n1\n6\n",
      "screen": "== Config merger ==\nSettings in three layers - defaults, environment, command line - each later one over the earlier ones.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> \nCancelled.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> prod\nType defaults, environment or command line.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> \nCancelled.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> server.port\nType path=value.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> =3\nType path=value.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> Server.Port=1\nA path is lower-case words, digits, _ and - joined by dots.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> a..b=1\nA path is lower-case words, digits, _ and - joined by dots.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> x=it's\nA value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> x=a b\nA value is true, false, a whole number, 'text', or a word of letters, digits and - _ . / :.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> nothing=unset\nnothing is not set in defaults.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 5\npath> \nCancelled.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 5\npath> Bad Path\nA path is lower-case words, digits, _ and - joined by dots.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 5\npath> colour\n  defaults      -\n  environment   -\n  command line  -\nNo layer sets it.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> defaults\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> server.port=007\nserver.port = '007' in defaults.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 1\n  defaults:\n    log.level             'info'\n    log.verbose           true\n    name                  'app'\n    retries               3\n    server.host           'app.example'\n    server.port           '007'\n    server.tls.cert       'certs/app.pem'\n    server.tls.enabled    false\n  environment:\n    log.verbose           false\n    server.tls.enabled    true\n  command line:\n    name                  ''\n    retries               0\nMerged, each later layer over the earlier ones:\n    log.level             'info'              from defaults\n    log.verbose           false               from environment\n    name                  ''                  from command line\n    retries               0                   from command line\n    server.host           'app.example'       from defaults\n    server.port           '007'               from defaults\n    server.tls.cert       'certs/app.pem'     from defaults\n    server.tls.enabled    true                from environment\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n5\nserver.tls.cert\n5\nretries\n4\ncommand line\nserver=off\n1\n5\nserver.port\n4\ncommand line\nserver=unset\n4\nenvironment\nlog.level=debug\n2\n6\n",
      "screen": "== Config merger ==\nSettings in three layers - defaults, environment, command line - each later one over the earlier ones.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 1\n  defaults:\n    log.level             'info'\n    log.verbose           true\n    name                  'app'\n    retries               3\n    server.host           'app.example'\n    server.port           8080\n    server.tls.cert       'certs/app.pem'\n    server.tls.enabled    false\n  environment:\n    log.verbose           false\n    server.tls.enabled    true\n  command line:\n    name                  ''\n    retries               0\nMerged, each later layer over the earlier ones:\n    log.level             'info'              from defaults\n    log.verbose           false               from environment\n    name                  ''                  from command line\n    retries               0                   from command line\n    server.host           'app.example'       from defaults\n    server.port           8080                from defaults\n    server.tls.cert       'certs/app.pem'     from defaults\n    server.tls.enabled    true                from environment\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 2\nWhere the shallow merge differs from the deep one:\n  log.level             'info'              lost by the shallow merge\n  server.host           'app.example'       lost by the shallow merge\n  server.port           8080                lost by the shallow merge\n  server.tls.cert       'certs/app.pem'     lost by the shallow merge\nA 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.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 3\nWhere the truth-testing merge differs from the deep one:\n  log.verbose           false               the truth-testing merge has true, from defaults\n  name                  ''                  the truth-testing merge has 'app', from defaults\n  retries               0                   the truth-testing merge has 3, from defaults\nTesting a value for truth instead of asking whether it was set lets false, 0 and '' fall through to the layer below.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 5\npath> server.tls.cert\n  defaults      'certs/app.pem'\n  environment   -\n  command line  -\nMerged: 'certs/app.pem', from defaults: the last layer that sets it.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 5\npath> retries\n  defaults      3\n  environment   -\n  command line  0\nMerged: 0, from command line: the last layer that sets it.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> command line\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> server=off\nserver = 'off' in command line.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 1\n  defaults:\n    log.level             'info'\n    log.verbose           true\n    name                  'app'\n    retries               3\n    server.host           'app.example'\n    server.port           8080\n    server.tls.cert       'certs/app.pem'\n    server.tls.enabled    false\n  environment:\n    log.verbose           false\n    server.tls.enabled    true\n  command line:\n    name                  ''\n    retries               0\n    server                'off'\nMerged, each later layer over the earlier ones:\n    log.level             'info'              from defaults\n    log.verbose           false               from environment\n    name                  ''                  from command line\n    retries               0                   from command line\n    server                'off'               from command line\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 5\npath> server.port\n  defaults      8080\n  environment   -\n  command line  -\nMerged: not set - a later layer replaced it.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> command line\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> server=unset\nserver removed from command line.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 4\nlayer (defaults, environment or command line)> environment\nAs path=value, such as server.port=9000, log.verbose=false, name='' - or path=unset to remove it.\nsetting> log.level=debug\nlog.level = 'debug' in environment.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 2\nWhere the shallow merge differs from the deep one:\n  server.host           'app.example'       lost by the shallow merge\n  server.port           8080                lost by the shallow merge\n  server.tls.cert       'certs/app.pem'     lost by the shallow merge\nA 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.\n\n1) layers and merge  2) deep against shallow  3) set against true  4) set a key  5) where a key comes from  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "nested-config-merge",
      "caseId": "192-nested-config-merge",
      "title": "Deep config merge: the sibling that disappears"
    },
    {
      "slug": "config-defaults-merge",
      "caseId": "158-config-defaults-merge",
      "title": "Presence is not truthiness"
    }
  ],
  "updated": "2026-10-16"
}
