Repository: dtolnay/cxx (language: rust)

Issue:
Consider allowing one cxxbridge cli invocation to emit both header and cc
As currently implemented, two invocations of the cli command are required and the output goes to stdout.

```bash
$ cxxbridge path/to/lib.rs --header > generated.h
$ cxxbridge path/to/lib.rs > generated.cc
```

In some Starlark-based environments (or others) it turns out to be desirable to emit both outputs from a single invocation.

```python
genrule(
    name = "gen",
    src = "...",
    cmd = "$(exe //path/to:cxxbridge-cmd) ${SRC}",
    outs = {
        "header": ["generated.h"],
        "source": ["generated.cc"],
    },
)
```


Discussion / hints:

Fix patch:
diff --git a/gen/cmd/src/app.rs b/gen/cmd/src/app.rs
index 939bb4465..152f11ad8 100644
--- a/gen/cmd/src/app.rs
+++ b/gen/cmd/src/app.rs
@@ -65,18 +65,25 @@ pub(super) fn from_args() -> Opt {
         .unwrap_or_default()
         .map(str::to_owned)
         .collect();
-    let output = match matches.value_of_os(OUTPUT) {
-        None => Output::Stdout,
-        Some(path) if path == "-" => Output::Stdout,
-        Some(path) => Output::File(PathBuf::from(path)),
-    };
+
+    let mut outputs = Vec::new();
+    for path in matches.values_of_os(OUTPUT).unwrap_or_default() {
+        outputs.push(if path == "-" {
+            Output::Stdout
+        } else {
+            Output::File(PathBuf::from(path))
+        });
+    }
+    if outputs.is_empty() {
+        outputs.push(Output::Stdout);
+    }
 
     Opt {
         input,
         cxx_impl_annotations,
         header,
         include,
-        output,
+        outputs,
     }
 }
 
@@ -142,6 +149,7 @@ not specified.
         .long(OUTPUT)
         .short("o")
         .takes_value(true)
+        .multiple(true)
         .validator_os(validate_utf8)
         .help(HELP)
 }
diff --git a/gen/cmd/src/main.rs b/gen/cmd/src/main.rs
index 12e173ec0..f0fd9b443 100644
--- a/gen/cmd/src/main.rs
+++ b/gen/cmd/src/main.rs
@@ -25,7 +25,7 @@ struct Opt {
     header: bool,
     cxx_impl_annotations: Option<String>,
     include: Vec<String>,
-    output: Output,
+    outputs: Vec<Output>,
 }
 
 fn main() {
@@ -35,35 +35,54 @@ fn main() {
     }
 }
 
+enum Kind {
+    GeneratedHeader,
+    GeneratedImplementation,
+    Header,
+}
+
 fn try_main() -> Result<()> {
     let opt = app::from_args();
 
-    let gen_header = opt.header || opt.output.ends_with(".h");
+    let mut outputs = Vec::new();
+    let mut gen_header = false;
+    let mut gen_implementation = false;
+    for output in opt.outputs {
+        let kind = if opt.input.is_none() {
+            Kind::Header
+        } else if opt.header || output.ends_with(".h") {
+            gen_header = true;
+            Kind::GeneratedHeader
+        } else {
+            gen_implementation = true;
+            Kind::GeneratedImplementation
+        };
+        outputs.push((output, kind));
+    }
 
     let gen = gen::Opt {
         include: opt.include,
         cxx_impl_annotations: opt.cxx_impl_annotations,
         gen_header,
-        gen_implementation: !gen_header,
+        gen_implementation,
     };
 
-    let content;
-    let content = match (opt.input, gen_header) {
-        (Some(input), true) => {
-            content = gen::generate_from_path(&input, &gen).header;
-            content.as_slice()
-        }
-        (Some(input), false) => {
-            content = gen::generate_from_path(&input, &gen).implementation;
-            content.as_slice()
-        }
-        (None, true) => include::HEADER.as_bytes(),
-        (None, false) => unreachable!(), // enforced by required_unless
+    let generated_code = if let Some(input) = opt.input {
+        gen::generate_from_path(&input, &gen)
+    } else {
+        Default::default()
     };
 
-    match opt.output {
-        Output::Stdout => drop(io::stdout().write_all(content)),
-        Output::File(path) => fs::write(path, content)?,
+    for (output, kind) in outputs {
+        let content = match kind {
+            Kind::GeneratedHeader => &generated_code.header,
+            Kind::GeneratedImplementation => &generated_code.implementation,
+            Kind::Header => include::HEADER.as_bytes(),
+        };
+        match output {
+            Output::Stdout => drop(io::stdout().write_all(content)),
+            Output::File(path) => fs::write(path, content)?,
+        }
     }
 
     Ok(())
diff --git a/gen/src/mod.rs b/gen/src/mod.rs
index 9578fe867..85227152e 100644
--- a/gen/src/mod.rs
+++ b/gen/src/mod.rs
@@ -50,6 +50,7 @@ pub struct Opt {
 }
 
 /// Results of code generation.
+#[derive(Default)]
 pub struct GeneratedCode {
     /// The bytes of a C++ header file.
     pub header: Vec<u8>,


Test patch:
diff --git a/gen/cmd/src/test.rs b/gen/cmd/src/test.rs
index 2e85fac90..17023e4dd 100644
--- a/gen/cmd/src/test.rs
+++ b/gen/cmd/src/test.rs
@@ -32,7 +32,7 @@ OPTIONS:
             parse or even require the given paths to exist; they simply go
             into the generated C++ code as #include lines.
                \x20
-    -o, --output <output>
+    -o, --output <output>...
             Path of file to write as output. Output goes to stdout if -o is
             not specified.
                \x20