1use std::path::PathBuf;
4
5use clap::Parser;
6use knf_dotted::{KeyPath, PathLeaf};
7
8use crate::format::Format;
9
10#[derive(Parser, Debug)]
11#[command(
12 name = "knf",
13 version,
14 about = "Merge layered configuration files and print the result",
15 long_about = "\
16Merge layered configuration files and print the result.
17
18Files are layers, merged left to right in argument order. JSON and TOML may be
19mixed freely. Exactly one document goes to stdout.
20
21 knf base.toml prod.toml
22 knf base.json - --input-format json # stdin as a layer
23 knf defaults.json --set server.port=8080 -f toml
24 knf base.toml prod.toml --append plugins # concatenate one array
25
26Objects merge key by key. Arrays, scalars and null all replace wholesale —
27null is an ordinary value that overwrites, not a delete instruction.
28
29--append, --replace and --fail change that at the paths they name, and only
30there. A path may be named by at most one of them, and since all three consume
31the whole value at their path, no rule may sit below another. Rules are a set:
32their order never affects the output."
33)]
34pub struct Cli {
35 #[arg(value_name = "FILE")]
37 pub files: Vec<PathBuf>,
38
39 #[arg(
41 long,
42 value_name = "FORMAT",
43 long_help = "\
44Treat every input as this format, overriding extension inference.
45
46Required for `-`, which has no extension. Note that it applies to all inputs,
47not only stdin, so it cannot be used to mix a stdin layer of one format with
48files of another."
49 )]
50 pub input_format: Option<Format>,
51
52 #[arg(
54 long = "set",
55 value_name = "KEY.PATH=VALUE",
56 long_help = "\
57Inline terminal layer, applied after all files. Repeatable; multiple --set apply
58left to right.
59
60The value is parsed as JSON, falling back to a string when that fails:
61
62 port=8080 -> 8080 (number)
63 debug=true -> true (bool)
64 name=foo -> \"foo\" (not valid JSON, so a string)
65 proxy=null -> null (an error under -f toml, like any other null)
66 tags=[\"a\",\"b\"] -> array
67 tags=[a,b] -> \"[a,b]\" (not valid JSON, so a string)
68
69Sharp edge: version=1.0 is the number 1.0, not the string \"1.0\". Force a string
70by quoting into JSON: --set version='\"1.0\"'.
71
72Dotted paths nest, so keys containing a literal dot are not addressable from
73--set; use a file."
74 )]
75 pub set: Vec<PathLeaf<String>>,
76
77 #[arg(
79 long,
80 value_name = "KEY.PATH",
81 long_help = "\
82Concatenate arrays at this path instead of replacing them. Repeatable.
83
84Both sides must be arrays; anything else is an error. The path is only combined
85where the merge already has a value for it, so a lone layer's array is inserted
86as-is rather than doubled:
87
88 knf base.toml prod.toml --append plugins # base's plugins ++ prod's
89
90Dotted paths address nested keys, so a key containing a literal dot cannot be
91named."
92 )]
93 pub append: Vec<KeyPath>,
94
95 #[arg(
97 long,
98 value_name = "KEY.PATH",
99 long_help = "\
100Replace the value at this path wholesale, without merging into it. Repeatable.
101
102Object over object stops recursing, so the later layer's table is taken whole
103and keys it omits are dropped:
104
105 knf base.toml prod.toml --replace db # db is prod's db, entirely
106
107This applies to --set layers too, which are ordinary layers: --replace db
108--set db.host=x leaves db with nothing but host.
109
110Dotted paths address nested keys, so a key containing a literal dot cannot be
111named."
112 )]
113 pub replace: Vec<KeyPath>,
114
115 #[arg(
117 long,
118 value_name = "KEY.PATH",
119 long_help = "\
120Error if a later layer sets this path again. Repeatable.
121
122The first layer to define the path pins it; the path may still be absent from
123every layer. Use it to protect a value that later layers must not override:
124
125 knf base.toml prod.toml --fail db.host
126
127Dotted paths address nested keys, so a key containing a literal dot cannot be
128named."
129 )]
130 pub fail: Vec<KeyPath>,
131
132 #[arg(short = 'f', long, value_name = "FORMAT")]
134 pub format: Option<Format>,
135
136 #[arg(
138 long,
139 value_name = "STRING",
140 long_help = "\
141Write this string in place of null when emitting TOML.
142
143TOML has no null, so a null reaching TOML output is an error by default. This
144substitutes a value of your choosing instead:
145
146 knf base.toml override.json -f toml --null-placeholder=none
147
148It applies to TOML output only. JSON can hold a null, so under -f json the flag
149has nothing to rescue and is ignored rather than corrupting a document that was
150never in trouble.
151
152The substitution writes a value that appeared in none of the inputs, which is
153why it is opt-in and why the string is yours to pick. It also applies inside
154arrays, where a null cannot simply be dropped without shifting every index
155after it."
156 )]
157 pub null_placeholder: Option<String>,
158
159 #[arg(long)]
161 pub strict: bool,
162
163 #[arg(long)]
165 pub compact: bool,
166}