Source file bcfg_validate.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
let error_msgf fmt = Fmt.kstr (fun msg -> Error (`Msg msg)) fmt
let rchop ~len str =
if len > String.length str then invalid_arg "rchop";
String.sub str 0 (String.length str - len)
let rchop_if_lf str =
if str = "" then ""
else
match str.[String.length str - 1] with '\n' -> rchop ~len:1 str | _ -> str
let escape str =
let buf = Buffer.create (String.length str) in
let fn = function
| '\x07' -> Buffer.add_string buf "\\a"
| '\x08' -> Buffer.add_string buf "\\b"
| '\x09' -> Buffer.add_string buf "\\t"
| '\x0a' -> Buffer.add_string buf "\\n"
| '\x0b' -> Buffer.add_string buf "\\v"
| '\x0c' -> Buffer.add_string buf "\\f"
| '\x0d' -> Buffer.add_string buf "\\r"
| '\x20' .. '\x7e' as chr -> Buffer.add_char buf chr
| chr ->
let hex = Fmt.str "\\x%02x" (Char.code chr) in
Buffer.add_string buf hex
in
String.iter fn str;
Buffer.contents buf
let read_all ic =
let buf = Buffer.create 0x10000 in
let chunk = Bytes.create 0x10000 in
let rec go () =
let n = input ic chunk 0 (Bytes.length chunk) in
if n > 0 then begin
Buffer.add_substring buf (Bytes.unsafe_to_string chunk) 0 n;
go ()
end
in
go ();
Buffer.contents buf
let read_input = function
| None -> read_all stdin
| Some filepath ->
let ic = open_in_bin filepath in
Fun.protect ~finally:(fun () -> close_in_noerr ic) (fun () -> read_all ic)
let has_newline str =
if str = "" then false else str.[String.length str - 1] = '\n'
let max_line_number lines =
let fn acc (line_number, _) = max acc line_number in
List.fold_left fn 0 lines
type cfg = { length_of_line_numbers : int }
let cfg lines =
let v = max_line_number lines in
let v = Float.of_int v |> Float.log10 |> Float.to_int |> succ in
{ length_of_line_numbers = v }
let pp_line_number ~cfg ppf line_number =
let pp ppf line_number =
Fmt.pf ppf "%*d" cfg.length_of_line_numbers line_number
in
Fmt.pf ppf "%a" Fmt.(styled `Faint pp) line_number
let pp_error ppf str =
let str = escape str in
Fmt.pf ppf "%a" Fmt.(styled (`Fg `Red) string) str
let pp_newline ppf = function
| false -> ()
| true -> Fmt.pf ppf "%a" Fmt.(styled `Faint string) "\\n"
let pp_line_with_ansi ~cfg ppf = function
| line_number, Bcfg.Txtloc.Context_line line ->
Fmt.pf ppf " %a %a@\n" (pp_line_number ~cfg) line_number
Fmt.(styled `Faint string)
(rchop_if_lf line)
| line_number, Bcfg.Txtloc.Errored_line { err; around = pre, nxt } ->
let with_newline = has_newline nxt in
Fmt.pf ppf ">%a %s%a%s%a@\n" (pp_line_number ~cfg) line_number pre
Fmt.(styled (`Fg `Red) pp_error)
err (rchop_if_lf nxt) pp_newline with_newline
| _, Eof { errored = true } ->
Fmt.pf ppf ">%s %a@\n"
(String.make cfg.length_of_line_numbers ' ')
Fmt.(styled (`Fg `Red) string)
"EOF"
| _, Eof { errored = false } ->
Fmt.pf ppf " %s %a@\n"
(String.make cfg.length_of_line_numbers ' ')
Fmt.(styled `Faint string)
"EOF"
let pp_symbol : type a. a Bcfg.Error.symbol -> a Fmt.t =
fun symbol ppf v ->
match symbol with
| Terminal Error -> Fmt.string ppf "#error"
| Terminal Eof -> Fmt.string ppf "#eof"
| Terminal Newline -> Fmt.string ppf "#newline"
| Terminal Word -> Fmt.pf ppf "%S" v
| Terminal LBrace -> Fmt.pf ppf "{"
| Terminal RBrace -> Fmt.pf ppf "}"
| Non_terminal Directives ->
Fmt.pf ppf "@[<1>(Directives@ @[<hov>%a@])@]" Bcfg.pp_as_ocaml_value v
| Non_terminal Newlines -> Fmt.string ppf "#newlines"
| Non_terminal Directive ->
Fmt.pf ppf "@[<1>(Directive @[<hov>%a@])@]" Bcfg.pp_as_ocaml_value [ v ]
| Non_terminal Parameters ->
Fmt.pf ppf "@[<hov>%a@]" Fmt.(Dump.list (fmt "%S")) v
| Non_terminal Children ->
Fmt.pf ppf "@[<1>(Children@ ]@[<hov>%a@])@]" Bcfg.pp_as_ocaml_value v
| Non_terminal Top ->
Fmt.pf ppf "@[<1>(Top@ @[<hov>%a@]@]" Bcfg.pp_as_ocaml_value v
let pp_error_message ppf (state, Bcfg.Error.Error (symbol, v)) =
match (state, symbol) with
| 1, Terminal Newline -> Fmt.pf ppf "> Unexpected closing curly bracked.@\n"
| 4, Terminal Word ->
Fmt.pf ppf
"> The %a directive must end with a line break or an opening curly \
bracket.@\n"
Fmt.(styled (`Fg `Yellow) (fmt "%S"))
v
| 5, Terminal Word ->
Fmt.pf ppf "> Unexpected closing curly bracket after %a.@\n"
Fmt.(styled (`Fg `Yellow) (fmt "%S"))
v
| 8, Terminal Newline ->
Fmt.pf ppf
"> A directive (potentially with parameters) must always precede a \
opening curly bracket.@\n"
| 10, Terminal LBrace ->
Fmt.pf ppf
"> A opening curly bracket must always be followed by a line break.@\n"
| 11, Non_terminal Newlines -> Fmt.pf ppf "> Missing a subdirective.@\n"
| 12, Non_terminal Directives ->
Fmt.pf ppf
"> We are in a directive that does not end; a closing curly bracket \
should follow the subdirective %a.@\n"
Fmt.(styled (`Fg `Yellow) (fmt "%S"))
(List.hd v).Bcfg.name
| 13, Terminal RBrace ->
Fmt.pf ppf
"> A closing curly bracket must always be followed by a line break.@\n"
| 15, Non_terminal Directive ->
Fmt.pf ppf
"> We are in a directive that does not end; a closing curly bracket \
should follow the subdirective %a.@\n"
Fmt.(styled (`Fg `Yellow) (fmt "%S"))
v.Bcfg.name
| 19, Non_terminal Directives ->
Fmt.pf ppf
"> The directive %a was never opened, so there is no reason to close \
it.@\n"
Fmt.(styled (`Fg `Yellow) (fmt "%S"))
(List.hd v).Bcfg.name
| 21, Non_terminal Directive ->
Fmt.pf ppf
"> The directive %a was never opened, so there is no reason to close \
it.@\n"
Fmt.(styled (`Fg `Yellow) (fmt "%S"))
v.Bcfg.name
| state, symbol ->
Fmt.pf ppf "%d => @[<hov>%a@]@\n" state (pp_symbol symbol) v
let pp_error_with_source ~content ppf err =
let lines txtloc = Bcfg.Txtloc.lines_around_txtloc_string ~txtloc content in
match err with
| `Lexer_error (txtloc, `Message msg) ->
let lines = lines txtloc in
let cfg = cfg lines in
Fmt.pf ppf "Error at %a:@\n" Bcfg.Txtloc.pp txtloc;
Fmt.pf ppf "> %s@\n" msg;
let fn = Fmt.pf ppf "%a" (pp_line_with_ansi ~cfg) in
List.iter fn lines
| `Lexer_error (txtloc, `Invalid_character chr) ->
let lines = lines txtloc in
let cfg = cfg lines in
Fmt.pf ppf "Invalid character %S at %a:@\n" (String.make 1 chr)
Bcfg.Txtloc.pp txtloc;
let fn = Fmt.pf ppf "%a" (pp_line_with_ansi ~cfg) in
List.iter fn lines
| `Parser_error (txtloc, None) ->
let lines = lines txtloc in
let cfg = cfg lines in
Fmt.pf ppf "Invalid syntax at %a:@\n" Bcfg.Txtloc.pp txtloc;
let fn = Fmt.pf ppf "%a" (pp_line_with_ansi ~cfg) in
List.iter fn lines
| `Parser_error (txtloc, Some err) ->
let lines = lines txtloc in
let cfg = cfg lines in
Fmt.pf ppf "Error at %a:@\n" Bcfg.Txtloc.pp txtloc;
Fmt.pf ppf "%a" pp_error_message err;
let fn = Fmt.pf ppf "%a" (pp_line_with_ansi ~cfg) in
List.iter fn lines
| `Rejected -> Fmt.pf ppf "Rejected@\n"
let run quiet input =
let content = read_input input in
let lexbuf = Lexing.from_string ~with_positions:true content in
match Bcfg.parser lexbuf with
| Ok _ -> Ok 0
| Error err when not quiet ->
Fmt.epr "%a%!" (pp_error_with_source ~content) err;
Ok 1
| Error _ -> Ok 1
open Cmdliner
open Bcfg_cli
let input =
let doc = "The configuration file to validate." 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 0 input None & info [] ~doc ~docv:"FILE"
let term =
let open Term in
const run $ setup_logs $ input |> term_result ~usage:false
let cmd =
let doc = "$(tname) validates a configuration file." in
let man = [] in
let info = Cmd.info "validate" ~doc ~man in
Cmd.v info term