Skip to content

Commit d3e7a33

Browse files
authored
feat(grainfmt): Allow directory input & output (#1274)
feat(grainfmt)!: Replace `--in-place` flag with `-o` flag chore(grainfmt)!: Remove stdin formatting support fix(cli): Ensure parent flags are inherited by the format command chore(cli): Update grain doc command chore: Update doc strings chore(stdlib): Format the Regex module
1 parent acec7ff commit d3e7a33

File tree

6 files changed

+1923
-938
lines changed

6 files changed

+1923
-938
lines changed

cli/bin/exec.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,12 @@ function execGrainformat(
8383
execOpts = { stdio: "pipe" }
8484
) {
8585
const flags = [];
86-
const options = program.opts();
87-
program.options.forEach((option) => {
86+
// Inherit compiler flags passed to the parent
87+
const options = program.parent.options.concat(program.options);
88+
const opts = { ...program.parent.opts(), ...program.opts() };
89+
options.forEach((option) => {
8890
if (!option.forward) return;
89-
const flag = option.toFlag(options);
91+
const flag = option.toFlag(opts);
9092
if (flag) flags.push(flag);
9193
});
9294

cli/bin/grain.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ program
207207
);
208208

209209
program
210-
.command("doc <file>")
210+
.command("doc <file|dir>")
211211
.description("generate documentation for a grain file")
212212
.forwardOption(
213213
"--current-version <version>",
@@ -220,9 +220,8 @@ program
220220
);
221221

222222
program
223-
.command("format [file]")
223+
.command("format <file|dir>")
224224
.description("format a grain file")
225-
.forwardOption("--in-place", "format in place")
226225
.action(
227226
wrapAction(function (file, options, program) {
228227
format(file, program);

compiler/graindoc/graindoc.re

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ let () =
2929

3030
[@deriving cmdliner]
3131
type io_params = {
32-
/** Grain source file for which to extract documentation */
32+
/** Grain source file or directory of source files to document */
3333
[@pos 0] [@docv "FILE"]
3434
input: ExistingFileOrDirectory.t,
35-
/** Output filename */
35+
/** Output file or directory */
3636
[@name "o"] [@docv "FILE"]
3737
output: option(MaybeExistingFileOrDirectory.t),
3838
};

compiler/grainformat/dune

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,7 @@
1515
(:include ./config/flags.sexp)))
1616
(libraries cmdliner grain grain_utils grain_parsing grainformat.format
1717
binaryen dune-build-info)
18+
(preprocess
19+
(pps ppx_deriving_cmdliner))
1820
(js_of_ocaml
1921
(flags --no-sourcemap --no-extern-fs --quiet --disable share)))

compiler/grainformat/grainformat.re

Lines changed: 104 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,29 @@ open Grain;
33
open Compile;
44
open Grain_parsing;
55
open Grain_utils;
6-
open Filename;
6+
open Grain_utils.Filepath.Args;
7+
8+
[@deriving cmdliner]
9+
type io_params = {
10+
/** Grain source file or directory of source files to format */
11+
[@pos 0] [@docv "FILE"]
12+
input: ExistingFileOrDirectory.t,
13+
/** Output file or directory */
14+
[@name "o"] [@docv "FILE"]
15+
output: option(MaybeExistingFileOrDirectory.t),
16+
};
717

818
let get_program_string = filename => {
9-
switch (filename) {
10-
| None =>
11-
let source_buffer = Buffer.create(1024);
12-
set_binary_mode_in(stdin, true);
13-
/* read from stdin until we get end of buffer */
14-
try(
15-
while (true) {
16-
let c = input_char(stdin);
17-
Buffer.add_char(source_buffer, c);
18-
}
19-
) {
20-
| exn => ()
21-
};
22-
Buffer.contents(source_buffer);
23-
| Some(filename) =>
24-
let ic = open_in_bin(filename);
25-
let n = in_channel_length(ic);
26-
let source_buffer = Buffer.create(n);
27-
Buffer.add_channel(source_buffer, ic, n);
28-
close_in(ic);
29-
Buffer.contents(source_buffer);
30-
};
19+
let ic = open_in_bin(filename);
20+
let n = in_channel_length(ic);
21+
let source_buffer = Buffer.create(n);
22+
Buffer.add_channel(source_buffer, ic, n);
23+
close_in(ic);
24+
Buffer.contents(source_buffer);
3125
};
3226

33-
let compile_parsed = (filename: option(string)) => {
27+
let compile_parsed = filename => {
28+
let filename = Filepath.to_string(filename);
3429
switch (
3530
{
3631
let program_str = get_program_string(filename);
@@ -42,7 +37,7 @@ let compile_parsed = (filename: option(string)) => {
4237
Compile.compile_string(
4338
~is_root_file=true,
4439
~hook=stop_after_parse,
45-
~name=?filename,
40+
~name=filename,
4641
program_str,
4742
);
4843

@@ -59,110 +54,123 @@ let compile_parsed = (filename: option(string)) => {
5954
Grain_parsing.Location.report_exception(Stdlib.Format.err_formatter, exn);
6055
Option.iter(
6156
s =>
62-
if (Grain_utils.Config.debug^) {
57+
if (Config.debug^) {
6358
prerr_string("Backtrace:\n");
6459
prerr_string(s);
6560
prerr_string("\n");
6661
},
6762
bt,
6863
);
6964
exit(2);
70-
| ({cstate_desc: Parsed(parsed_program)}, lines, eol) =>
71-
`Ok((parsed_program, Array.of_list(lines), eol))
72-
| _ => `Error((false, "Invalid compilation state"))
65+
| ({cstate_desc: Parsed(parsed_program)}, lines, eol) => (
66+
parsed_program,
67+
Array.of_list(lines),
68+
eol,
69+
)
70+
| _ => failwith("Invalid compilation state")
7371
};
7472
};
7573

7674
let format_code =
7775
(
7876
~eol,
79-
srcfile: option(string),
77+
~output=?,
78+
~original_source: array(string),
8079
program: Parsetree.parsed_program,
81-
outfile,
82-
original_source: array(string),
83-
format_in_place: bool,
8480
) => {
8581
let formatted_code = Format.format_ast(~original_source, ~eol, program);
8682

87-
// return the file to its format
88-
8983
let buf = Buffer.create(0);
9084
Buffer.add_string(buf, formatted_code);
9185

9286
let contents = Buffer.to_bytes(buf);
93-
switch (outfile) {
87+
switch (output) {
9488
| Some(outfile) =>
89+
let outfile = Filepath.to_string(outfile);
90+
// TODO: This crashes if you do something weird like `-o stdout/map.gr/foo`
91+
// because `foo` doesn't exist so it tries to mkdir it and raises
92+
Fs_access.ensure_parent_directory_exists(outfile);
9593
let oc = Fs_access.open_file_for_writing(outfile);
9694
output_bytes(oc, contents);
9795
close_out(oc);
9896
| None =>
99-
switch (srcfile, format_in_place) {
100-
| (Some(src), true) =>
101-
let oc = Fs_access.open_file_for_writing(src);
102-
output_bytes(oc, contents);
103-
close_out(oc);
104-
| _ =>
105-
set_binary_mode_out(stdout, true);
106-
print_bytes(contents);
107-
}
97+
set_binary_mode_out(stdout, true);
98+
print_bytes(contents);
10899
};
109-
110-
`Ok();
111100
};
112101

113-
let grainformat =
114-
(
115-
srcfile: option(string),
116-
outfile,
117-
format_in_place: bool,
118-
(program, lines: array(string), eol),
119-
) =>
120-
try(format_code(~eol, srcfile, program, outfile, lines, format_in_place)) {
121-
| e => `Error((false, Printexc.to_string(e)))
122-
};
123-
124-
let input_file_conv = {
125-
open Arg;
126-
let (prsr, prntr) = non_dir_file;
127-
(filename => prsr(filename), prntr);
128-
};
129-
130-
/** Converter which checks that the given output filename is valid */
131-
let output_file_conv = {
132-
let parse = s => {
133-
let s_dir = dirname(s);
134-
Sys.file_exists(s_dir)
135-
? if (Sys.is_directory(s_dir)) {
136-
`Ok(s);
137-
} else {
138-
`Error(Stdlib.Format.sprintf("`%s' is not a directory", s_dir));
139-
}
140-
: `Error(Stdlib.Format.sprintf("no `%s' directory", s_dir));
141-
};
142-
(parse, Stdlib.Format.pp_print_string);
102+
type run = {
103+
input_path: Fp.t(Fp.absolute),
104+
output_path: option(Fp.t(Fp.absolute)),
143105
};
144106

145-
let output_filename = {
146-
let doc = "Output filename";
147-
let docv = "FILE";
148-
Arg.(
149-
value & opt(some(output_file_conv), None) & info(["o"], ~docv, ~doc)
107+
let enumerate_directory = (input_dir_path, output_dir_path) => {
108+
let all_files = Array.to_list(Fs_access.readdir(input_dir_path));
109+
let grain_files =
110+
List.filter(
111+
filepath => Filename.extension(Fp.toString(filepath)) == ".gr",
112+
all_files,
113+
);
114+
List.map(
115+
filepath => {
116+
// We relativize between the input directory and the full filepath
117+
// such that we can reconstruct the directory structure of the input directory
118+
let relative_path =
119+
Fp.relativizeExn(~source=input_dir_path, ~dest=filepath);
120+
let gr_basename = Option.get(Fp.baseName(relative_path));
121+
let dirname = Fp.dirName(relative_path);
122+
let md_relative_path = Fp.join(dirname, Fp.relativeExn(gr_basename));
123+
let output_path = Fp.join(output_dir_path, md_relative_path);
124+
{input_path: filepath, output_path: Some(output_path)};
125+
},
126+
grain_files,
150127
);
151128
};
152129

153-
let format_in_place = {
154-
let doc = "Format in place";
155-
let docv = "";
156-
Arg.(value & flag & info(["in-place"], ~docv, ~doc));
157-
};
130+
let enumerate_runs = opts =>
131+
switch (opts.input, opts.output) {
132+
| (File(input_file_path), None) =>
133+
`Ok([{input_path: input_file_path, output_path: None}])
134+
| (File(input_file_path), Some(Exists(File(output_file_path)))) =>
135+
`Ok([
136+
{input_path: input_file_path, output_path: Some(output_file_path)},
137+
])
138+
| (File(input_file_path), Some(NotExists(output_file_path))) =>
139+
`Ok([
140+
{input_path: input_file_path, output_path: Some(output_file_path)},
141+
])
142+
| (Directory(_), None) =>
143+
`Error((
144+
false,
145+
"Directory input must be used with `-o` flag to specify output directory",
146+
))
147+
| (Directory(input_dir_path), Some(Exists(Directory(output_dir_path)))) =>
148+
`Ok(enumerate_directory(input_dir_path, output_dir_path))
149+
| (Directory(input_dir_path), Some(NotExists(output_dir_path))) =>
150+
`Ok(enumerate_directory(input_dir_path, output_dir_path))
151+
| (File(input_file_path), Some(Exists(Directory(output_dir_path)))) =>
152+
`Error((
153+
false,
154+
"Using a file as input cannot be combined with directory output",
155+
))
156+
| (Directory(_), Some(Exists(File(_)))) =>
157+
`Error((
158+
false,
159+
"Using a directory as input cannot be written as a single file output",
160+
))
161+
};
158162

159-
let input_filename = {
160-
let doc = "Grain source file to format";
161-
let docv = "FILE";
162-
Arg.(
163-
value
164-
& pos(~rev=true, 0, some(~none="", input_file_conv), None)
165-
& info([], ~docv, ~doc)
163+
let grainformat = runs => {
164+
List.iter(
165+
({input_path, output_path}) => {
166+
let (program, original_source, eol) = compile_parsed(input_path);
167+
try(format_code(~eol, ~output=?output_path, ~original_source, program)) {
168+
| exn =>
169+
Stdlib.Format.eprintf("@[%s@]@.", Printexc.to_string(exn));
170+
exit(2);
171+
};
172+
},
173+
runs,
166174
);
167175
};
168176

@@ -178,18 +186,8 @@ let cmd = {
178186

179187
Cmd.v(
180188
Cmd.info(Sys.argv[0], ~version, ~doc),
181-
Term.(
182-
ret(
183-
const(grainformat)
184-
$ input_filename
185-
$ output_filename
186-
$ format_in_place
187-
$ ret(
188-
Grain_utils.Config.with_cli_options(compile_parsed)
189-
$ input_filename,
190-
),
191-
)
192-
),
189+
Config.with_cli_options(grainformat)
190+
$ ret(const(enumerate_runs) $ io_params_cmdliner_term()),
193191
);
194192
};
195193

0 commit comments

Comments
 (0)