Source file bcfg_query.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
let ( let@ ) finally fn = Fun.protect ~finally fn
let ( let* ) = Result.bind

let run _quiet cfg output_format query input =
  let* query = Bcfgq.of_string query in
  let ic, finally =
    match input with
    | None -> (stdin, ignore)
    | Some filepath ->
        let ic = open_in_bin filepath in
        (ic, fun () -> close_in ic)
  in
  let@ () = finally in
  let lexbuf = Lexing.from_channel ic in
  let* result =
    if Bcfgq.is_streamable query then begin
      (* No [@(...)] substitution: evaluate one top-level directive at a time,
         without materialising the whole document. *)
      let exception E of string in
      let chunks = ref [] in
      try
        Seq.iter
          (function
            | Ok d -> chunks := Bcfgq.eval query [ d ] :: !chunks
            | Error e -> raise (E (Format.asprintf "%a" Bcfg.Stream.pp_error e)))
          (Bcfg.Stream.to_directives lexbuf);
        Ok (List.concat (List.rev !chunks))
      with E msg -> Error (`Msg msg)
    end
    else
      let* bcfg = Bcfg.parser lexbuf in
      Ok (Bcfgq.eval query bcfg)
  in
  (match output_format with
  | `Bcfg -> Seq.iter (output_string stdout) (Bcfg.emitter ~cfg result)
  | `Json ->
      print_string (Bcfg_json.to_string (Bcfg_json.of_config result));
      print_newline ());
  Ok 0

let to_msg = function
  | `Msg _ as m -> m
  | #Bcfg.error -> `Msg "Invalid bcfg file"

open Cmdliner
open Bcfg_cli

let input =
  let doc = "The configuration file to query ($(b,-) for standard input)." in
  let parser str =
    match str with
    | "-" -> Ok None
    | filepath when Sys.file_exists filepath && is_regular_file filepath ->
        Ok (Some filepath)
    | filepath ->
        error_msgf "%S does not exist or is not a regular file" filepath
  in
  let pp ppf = function
    | None -> Fmt.string ppf "-"
    | Some filepath -> Fmt.string ppf filepath
  in
  let input = Arg.conv (parser, pp) in
  let open Arg in
  value & pos 1 input None & info [] ~doc ~docv:"FILE"

let query =
  let doc = "The $(b,bcfg) query." in
  let open Arg in
  required & pos 0 (some string) None & info [] ~doc ~docv:"QUERY"

let output_format =
  let doc = "The output format of the query result ($(b,bcfg) or $(b,json))." in
  let open Arg in
  value
  & opt (enum [ ("bcfg", `Bcfg); ("json", `Json) ]) `Bcfg
  & info [ "o"; "output-format" ] ~doc ~docv:"FORMAT"

let term =
  let open Term in
  const run $ setup_logs $ setup_output_configuration $ output_format $ query
  $ input
  |> map (Result.map_error to_msg)
  |> term_result ~usage:false

let cmd =
  let doc =
    "$(tname) applies a query to the given $(b,bcfg) configuration file."
  in
  let man =
    [
      `S Manpage.s_description;
      `P
        "$(tname) selects directives from a $(b,bcfg) configuration using a \
         small query language and prints them back, either as $(b,bcfg) or as \
         JSON (to be piped into tools such as $(b,jq)).";
      `P
        "A $(b,bcfg) document is a list of directives, and a directive is a \
         name, a list of parameters and a list of children (themselves \
         directives). A query works on that shape only: it starts from the \
         list of top-level directives and every step of the query turns a list \
         of directives into another list of directives. The result is \
         therefore always a list of directives, which is what $(tname) prints. \
         There is no other kind of value: even a single parameter such as a \
         port number comes out as a directive whose name is that parameter.";
      `P
        "All the examples below run on the configuration file shipped in the \
         $(b,example/) directory of the distribution:";
      `Pre
        "# The virtual host used when no other one matches.\n\
         default www.example.org\n\n\
         service www.example.org public {\n\
        \  memory 128\n\
        \  weight 1.5\n\
        \  log debug\n\
        \  listen 80\n\
        \  listen 443\n\
        \  upstream {\n\
        \    kind tcp\n\
        \    host 10.0.0.2\n\
        \    port 8080\n\
        \  }\n\
        \  tls {\n\
        \    certificate /etc/ssl/www.example.org.pem\n\
        \    key /etc/ssl/www.example.org.key\n\
        \  }\n\
         }\n\n\
         service git.example.org internal {\n\
        \  memory 256\n\
        \  weight 1.\n\
        \  log info\n\
        \  listen 22\n\
        \  upstream {\n\
        \    kind unix\n\
        \    path /run/git.sock\n\
        \  }\n\
         }\n\n\
         service static.example.org public {\n\
        \  memory 64\n\
        \  weight 0.5\n\
        \  log quiet\n\
        \  listen 80\n\
        \  upstream {\n\
        \    kind tcp\n\
        \    host 10.0.0.3\n\
        \    port 8081\n\
        \  }\n\
         }\n\n\
         group production {\n\
        \  member www.example.org\n\
        \  member git.example.org\n\n\
        \  group staging {\n\
        \    member static.example.org\n\
        \  }\n\
         }";
      `S "SELECTING DIRECTIVES";
      `P
        "These constructs choose $(i,which) directives are kept and how deep \
         the query goes.";
      `I
        ( "$(b,foo)",
          "Keeps the directives named \"foo\" at the current level. At the \
           beginning of a query, the current level is the list of top-level \
           directives, so $(b,service) selects the three services of our \
           example." );
      `I
        ( "$(b,*)",
          "Keeps every directive of the current level, whatever its name." );
      `I
        ( "$(b,foo.bar)",
          "Descends: $(b,bar) is applied to the children of the directives \
           selected by $(b,foo). It can be repeated, e.g. \
           $(b,service.upstream.port) reaches the port of every upstream. Note \
           that a query never descends by itself: what is not reached by a \
           $(b,.) is not looked at." );
      `I
        ( "$(b,foo[N])",
          "Takes the N-th parameter (counting from 0) of each selected \
           directive: the parameter becomes the name of the resulting \
           directive and the children are kept. Directives with fewer \
           parameters are dropped. This is how you extract a value: \
           $(b,service.listen) prints \"listen 80\" whereas \
           $(b,service.listen[0]) prints \"80\"." );
      `S "FILTERING DIRECTIVES";
      `P
        "A filter keeps or discards the directives of the current level \
         according to a $(b,pattern) (see below). It never descends and never \
         changes what is selected, so it can be inserted anywhere in a query \
         and repeated: $(b,service\\(public\\)\\(:^tls\\)) keeps the public \
         services that have no $(b,tls) block.";
      `I
        ( "$(b,foo\\(PAT\\))",
          "Keeps the directives having $(i,at least one) parameter matching \
           $(i,PAT), e.g. $(b,service\\(public\\))." );
      `I
        ( "$(b,foo\\(^PAT\\))",
          "The anti-join: keeps the directives having $(i,no) parameter \
           matching $(i,PAT), e.g. $(b,service\\(^public\\)) selects the \
           services that are not public." );
      `I
        ( "$(b,foo\\(:PAT\\))",
          "Keeps the directives having at least one child whose $(i,name) \
           matches $(i,PAT), e.g. $(b,service\\(:tls\\)) selects the services \
           that define a $(b,tls) block." );
      `I
        ( "$(b,foo\\(:^PAT\\))",
          "Keeps the directives having no child whose name matches $(i,PAT), \
           e.g. $(b,service\\(:^tls\\)) selects the services that do $(i,not) \
           define a $(b,tls) block." );
      `I
        ( "$(b,\\(PAT\\)foo)",
          "Keeps the directives whose $(i,name) matches $(i,PAT). Alone, \
           $(b,\\(PAT\\)) filters the names of the current level: \
           $(b,\\(&,!service,!group\\)) selects the top-level directives that \
           are neither a service nor a group, and $(b,group.\\(!member\\)*) \
           selects the children of $(b,group) that are not members." );
      `S "PATTERNS";
      `P
        "A pattern says how a single word (a parameter, or the name of a \
         directive) is matched. The comparison is a plain string equality: \
         there is no globbing and no regular expression.";
      `I ("$(i,word)", "Matches that exact word.");
      `I ("$(b,*)", "Matches any word.");
      `I ("$(b,!PAT)", "Matches when the pattern $(i,PAT) does not match.");
      `I
        ( "$(b,PAT|PAT)",
          "Matches when either side matches. $(b,\\(|,A,B,C\\)) is the same \
           thing with more than two alternatives." );
      `I
        ( "$(b,PAT&PAT)",
          "Matches when both sides match. $(b,\\(&,A,B,C\\)) is the n-ary \
           form. Parentheses group patterns, as in \
           $(b,service\\(\\(public|internal\\)&!internal\\))." );
      `P
        "Beware of the difference between $(b,!) and $(b,^). A filter asks \
         whether $(i,some) parameter matches, so $(b,service\\(!public\\)) \
         keeps a service as soon as it has one parameter that is not \
         \"public\" (its host name always is), which selects everything. To \
         express \"is not public\", use the anti-join \
         $(b,service\\(^public\\)), which requires that $(i,no) parameter \
         matches.";
      `S "SUBSTITUTIONS";
      `P
        "$(b,@\\(QUERY\\)) is a pattern whose words are computed from the \
         document itself. The sub-query is evaluated against the $(i,whole) \
         document (not against the current level) and each resulting directive \
         gives one word: its first parameter, or its name when it has none, \
         exactly as $(b,QUERY[0]) would print it. The pattern matches when the \
         tested word is one of them.";
      `P
        "For instance $(b,service\\(@\\(default[0]\\)\\)) selects the service \
         designated by the $(b,default) directive, and \
         $(b,service\\(@\\(group.member[0]\\)\\)) selects the services that \
         are listed in a group. Since it is a pattern, it composes with the \
         operators above: $(b,service\\(!@\\(group.member[0]\\)\\)) selects \
         the services that no group mentions.";
      `P
        "$(b,\\$\\(QUERY\\)) is an alias of $(b,@\\(QUERY\\)) for those used \
         to $(b,jq). $(b,@) is preferred because it is not special inside \
         shell double quotes. Note that a substitution is only a pattern: it \
         appears where a pattern is expected, never as a query on its own.";
      `S "QUOTING";
      `P
        "Words follow the same lexical rules as the configuration format \
         itself: a value containing characters that are meaningful to the \
         query language (dots, brackets, parentheses, $(b,!), $(b,|), $(b,&), \
         $(b,:), $(b,^), $(b,*), $(b,@)) must be quoted with $(b,'...') or \
         $(b,\"...\"), e.g. $(b,service\\('www.example.org'\\).listen). \
         Unquoted, $(b,www.example.org) would be read as three names separated \
         by dots. Inside quotes, $(b,\\\\xNN) denotes an arbitrary byte, which \
         is the way to name a value that is not valid UTF-8. Single quotes are \
         handy inside shell double quotes, and vice versa.";
      `S "OUTPUT";
      `P
        "By default the result is printed as $(b,bcfg), using the output \
         configuration options (indentation, margin, escaping) described \
         below. The result is a real configuration file: it can be fed back to \
         $(tname).";
      `P
        "With $(b,-o json), the result is printed as JSON. A list of \
         directives becomes an object keyed by directive names; a name \
         appearing several times becomes an array (which means that the shape \
         of the output depends on the document: one $(b,listen) directive \
         gives a string, two give an array). A directive with children becomes \
         an object, and its own parameters, if any, are stored under the \
         $(b,\\$params) key. A directive without children is its parameters: \
         $(b,null) when there is none, a string when there is one, an array \
         otherwise. Values are always strings, since $(b,bcfg) has no notion \
         of number or boolean.";
      `S "LARGE FILES";
      `P
        "A query that contains no substitution never looks outside the \
         top-level directive being examined. Such a query is evaluated in a \
         streaming fashion, one top-level directive at a time, and $(tname) \
         does not need to hold the whole document in memory. As soon as an \
         $(b,@\\(...\\)) appears, the document must be fully parsed first, \
         because the sub-query may refer to any part of it.";
      `S Manpage.s_examples;
      `P "Get the certificate of a given service:";
      `Pre
        "\\$ bcfg query \"service\\('www.example.org'\\).tls.certificate[0]\" \
         services.cfg\n\
         /etc/ssl/www.example.org.pem";
      `P "List the ports of the public services:";
      `Pre
        "\\$ bcfg query 'service\\(public\\).listen[0]' services.cfg\n\
         80\n\
         443\n\
         80";
      `P "Show the services that are not public:";
      `Pre
        "\\$ bcfg query 'service\\(^public\\)[0]' services.cfg\n\
         git.example.org {\n\
        \  memory 256\n\
        \  weight 1.\n\
        \  log info\n\
        \  listen 22\n\
        \  upstream {\n\
        \    kind unix\n\
        \    path /run/git.sock\n\
        \  }\n\
         }";
      `P "Ask for the service designated by another directive of the file:";
      `Pre
        "\\$ bcfg query 'service\\(@\\(default[0]\\)\\).listen[0]' services.cfg\n\
         80\n\
         443";
      `P "Produce a CSV summary with $(b,jq):";
      `Pre
        "\\$ bcfg query -o json 'service[0]' services.cfg \\\\\n\
        \  | jq -r 'to_entries[]\n\
        \           | [ .key, \\([.value.listen] | flatten | join\\(\" \
         \"\\)\\) ]\n\
        \           | @csv'\n\
         \"www.example.org\",\"80 443\"\n\
         \"git.example.org\",\"22\"\n\
         \"static.example.org\",\"80\"";
      `S Manpage.s_see_also;
      `P "$(b,bcfg-validate)(1), $(b,bcfg-lint)(1), $(b,bcfg-iso)(1)";
    ]
  in
  let info = Cmd.info "query" ~doc ~man in
  Cmd.v info term