Add support for `--help` flag.

This CL switches from using `getopts` to using `clap`:

- `clap` has built-in support for `--help`
- `clap` in general seems to have a nicer API (e.g. `getopts` required
  somewhat repetitive/redundant calls to `reqopt` and `opt_str`, passing
  the same `H_OUT` in both cases).

PiperOrigin-RevId: 477774149
diff --git a/cc_bindings_from_rs/BUILD b/cc_bindings_from_rs/BUILD
index ea0702e..ded6e94 100644
--- a/cc_bindings_from_rs/BUILD
+++ b/cc_bindings_from_rs/BUILD
@@ -35,8 +35,8 @@
         "//common:rust_allocator_shims",
         "//common:token_stream_printer",
         "@crate_index//:anyhow",
+        "@crate_index//:clap",
         "@crate_index//:either",
-        "@crate_index//:getopts",
         "@crate_index//:itertools",
         "@crate_index//:proc-macro2",
         "@crate_index//:quote",
diff --git a/cc_bindings_from_rs/README.md b/cc_bindings_from_rs/README.md
index ee30e5f..52a2174 100644
--- a/cc_bindings_from_rs/README.md
+++ b/cc_bindings_from_rs/README.md
@@ -21,7 +21,7 @@
 
 $ bazel run \
     //cc_bindings_from_rs:cc_bindings_from_rs_legacy_toolchain_runner -- \
-    --h_out=$(pwd)/test.h -- \
+    --h-out=$(pwd)/test.h -- \
     $(pwd)/test.rs \
     --crate-type=lib \
     --sysroot $(pwd)/third_party/unsupported_toolchains/rust/toolchains/nightly
diff --git a/cc_bindings_from_rs/cc_bindings_from_rs.rs b/cc_bindings_from_rs/cc_bindings_from_rs.rs
index f503d63..23abcdf 100644
--- a/cc_bindings_from_rs/cc_bindings_from_rs.rs
+++ b/cc_bindings_from_rs/cc_bindings_from_rs.rs
@@ -147,7 +147,7 @@
 
 fn run_with_tcx(cmdline: &Cmdline, tcx: TyCtxt) -> anyhow::Result<()> {
     let bindings = GeneratedBindings::generate(tcx);
-    write_file(cmdline.h_out(), tokens_to_string(bindings.h_body)?.as_str())
+    write_file(&cmdline.h_out, tokens_to_string(bindings.h_body)?.as_str())
 }
 
 /// Main entrypoint that (unlike `main`) doesn't do any intitializations that
@@ -155,7 +155,7 @@
 /// `install_ice_hook`) and therefore can be used from the tests module below.
 fn run_with_cmdline_args(args: &[String]) -> anyhow::Result<()> {
     let cmdline = Cmdline::new(args)?;
-    bindings_driver::run_after_analysis_and_stop(cmdline.rustc_args(), |tcx| {
+    bindings_driver::run_after_analysis_and_stop(&cmdline.rustc_args, |tcx| {
         run_with_tcx(&cmdline, tcx)
     })
 }
@@ -268,7 +268,7 @@
 
             let mut args = vec![
                 "cc_bindings_from_rs_unittest_executable".to_string(),
-                format!("--h_out={}", h_path.display()),
+                format!("--h-out={}", h_path.display()),
             ];
             args.extend(self.extra_crubit_args.iter().cloned());
             args.extend([
@@ -317,7 +317,11 @@
             .expect_err("--unrecognized_crubit_flag should trigger an error");
 
         let msg = err.to_string();
-        assert_eq!("Unrecognized option: 'unrecognized-crubit-flag'", msg);
+        assert!(
+            msg.contains("Found argument '--unrecognized-crubit-flag' which wasn't expected"),
+            "msg = {}",
+            msg,
+        );
         Ok(())
     }
 
@@ -336,12 +340,12 @@
 
     #[test]
     fn test_invalid_h_out_path() -> anyhow::Result<()> {
-        // Tests not only the specific problem of an invalid `--h_out` argument, but
+        // Tests not only the specific problem of an invalid `--h-out` argument, but
         // also tests that errors from `bindings_main::main` are propagated.
         let err = TestArgs::default_args()?
             .with_h_path("../..")
             .run()
-            .expect_err("Unwriteable --h_out should trigger an error");
+            .expect_err("Unwriteable --h-out should trigger an error");
 
         let msg = err.to_string();
         assert_eq!("Error when writing to ../..", msg);
diff --git a/cc_bindings_from_rs/cmdline.rs b/cc_bindings_from_rs/cmdline.rs
index 8c81174..8a793a5 100644
--- a/cc_bindings_from_rs/cmdline.rs
+++ b/cc_bindings_from_rs/cmdline.rs
@@ -2,37 +2,46 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-use getopts::{Fail, Options};
-use std::path::Path;
+use anyhow::Result;
+use clap::Parser;
+use std::path::PathBuf;
 
-#[derive(Debug)]
+#[derive(Debug, Parser)]
+#[clap(name = "cc_bindings_from_rs")]
+#[clap(about = "Generates C++ bindings for a Rust crate", long_about = None)]
 pub struct Cmdline {
-    h_out: String,
-    rustc_args: Vec<String>,
+    /// Output path for C++ header file with bindings.
+    #[clap(long, value_parser, value_name = "FILE")]
+    pub h_out: PathBuf,
+
+    /// Command line arguments of the Rust compiler.
+    #[clap(last = true, value_parser)]
+    pub rustc_args: Vec<String>,
 }
 
-const H_OUT: &str = "h_out";
-
 impl Cmdline {
-    pub fn new(args: &[String]) -> Result<Self, Fail> {
+    pub fn new(args: &[String]) -> Result<Self> {
+        assert_ne!(
+            0,
+            args.len(),
+            "`args` should include the name of the executable (i.e. argsv[0])"
+        );
+        let exe_name = args[0].clone();
+
         // Ensure that `@file` expansion also covers *our* args.
         let args = rustc_driver::args::arg_expand_all(args);
 
-        let matches = Options::new()
-            .reqopt("", H_OUT, "output path for C++ header file with bindings", "FILE")
-            .parse(&args)?;
-        let h_out =
-            matches.opt_str(H_OUT).expect("getopts should enforce presence of --h_out `reqopt`");
+        // Parse `args` using the parser `derive`d by the `clap` crate.
+        let mut cmdline = Self::try_parse_from(args)?;
 
-        Ok(Self { h_out, rustc_args: matches.free })
-    }
+        // For compatibility with `rustc_driver` expectations, we prepend `exe_name` to
+        // `rustc_args.  This is needed, because `rustc_driver::RunCompiler::new`
+        // expects that its `at_args` includes the name of the executable -
+        // `handle_options` in `rustc_driver/src/lib.rs` throws away the first
+        // element.
+        cmdline.rustc_args.insert(0, exe_name);
 
-    pub fn h_out(&self) -> &Path {
-        Path::new(&self.h_out)
-    }
-
-    pub fn rustc_args(&self) -> &[String] {
-        &self.rustc_args
+        Ok(cmdline)
     }
 }
 
@@ -41,9 +50,10 @@
     use super::*;
 
     use itertools::Itertools;
+    use std::path::Path;
     use tempfile::tempdir;
 
-    fn new_cmdline<'a>(args: impl IntoIterator<Item = &'a str>) -> Result<Cmdline, Fail> {
+    fn new_cmdline<'a>(args: impl IntoIterator<Item = &'a str>) -> Result<Cmdline> {
         // When `Cmdline::new` is invoked from `main.rs`, it includes not only the
         // "real" cmdline arguments, but also the name of the executable.
         let args = std::iter::once("cc_bindings_from_rs_unittest_executable")
@@ -54,47 +64,54 @@
     }
 
     #[test]
-    fn test_h_out_happy_path() -> Result<(), Fail> {
-        let cmdline = new_cmdline(["--h_out=foo.h"])?;
-        assert_eq!(Path::new("foo.h"), cmdline.h_out());
-        Ok(())
+    fn test_h_out_happy_path() {
+        let cmdline = new_cmdline(["--h-out=foo.h"]).expect("This is a happy path");
+        assert_eq!(Path::new("foo.h"), cmdline.h_out);
     }
 
     #[test]
-    fn test_h_out_missing() {
-        match new_cmdline([]) {
-            Err(Fail::OptionMissing(arg)) if arg == H_OUT => (),
-            other => panic!("Unexpected success or unrecognized error: {:?}", other),
-        }
-    }
-
-    #[test]
-    fn test_h_out_without_arg() {
-        match new_cmdline(["--h_out"]) {
-            Err(Fail::ArgumentMissing(arg)) if arg == H_OUT => (),
-            other => panic!("Unexpected success or unrecognized error: {:?}", other),
-        }
-    }
-
-    #[test]
-    fn test_h_out_duplicated() {
-        match new_cmdline(["--h_out=foo.h", "--h_out=bar.h"]) {
-            Err(Fail::OptionDuplicated(arg)) if arg == H_OUT => (),
-            other => panic!("Unexpected success or unrecognized error: {:?}", other),
-        }
-    }
-
-    #[test]
-    fn test_rustc_args_happy_path() -> Result<(), Fail> {
+    fn test_rustc_args_happy_path() {
         // Note that this test would fail without the `--` separator.
-        let cmdline = new_cmdline(["--h_out=foo.h", "--", "test.rs", "--crate-type=lib"])?;
-        let rustc_args = cmdline.rustc_args();
+        let cmdline = new_cmdline(["--h-out=foo.h", "--", "test.rs", "--crate-type=lib"])
+            .expect("This is a happy path");
+        let rustc_args = &cmdline.rustc_args;
         assert!(
-            itertools::equal(&["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"], rustc_args),
+            itertools::equal(
+                ["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"],
+                rustc_args
+            ),
             "rustc_args = {:?}",
-            rustc_args
+            rustc_args,
         );
-        Ok(())
+    }
+
+    #[test]
+    fn test_help() {
+        // This test has multiple purposes:
+        // - Direct/obvious purpose: testing that `--help` works
+        // - Double-checking the overall shape of our cmdline "API" (i.e. verification
+        //   that the way we use `clap` attributes results in the desired cmdline
+        //   "API").
+        // - Exhaustively checking runtime asserts (assumming that tests run in a debug
+        //   build; other tests also trigger these asserts).  See also:
+        //     - https://github.com/clap-rs/clap/issues/2740#issuecomment-907240414
+        //     - `clap::builder::App::debug_assert`
+        let anyhow_err = new_cmdline(["--help"]).expect_err("--help should trigger an error");
+        let clap_err = anyhow_err.downcast::<clap::Error>().expect("Expecting `clap` error");
+        let expected_msg = r#"cc_bindings_from_rs 
+Generates C++ bindings for a Rust crate
+
+USAGE:
+    cc_bindings_from_rs_unittest_executable --h-out <FILE> [-- <RUSTC_ARGS>...]
+
+ARGS:
+    <RUSTC_ARGS>...    Command line arguments of the Rust compiler
+
+OPTIONS:
+        --h-out <FILE>    Output path for C++ header file with bindings
+    -h, --help            Print help information
+"#;
+        assert_eq!(expected_msg, clap_err.to_string());
     }
 
     #[test]
@@ -103,16 +120,19 @@
         let tmpfile = tmpdir.path().join("herefile");
         std::fs::write(
             &tmpfile,
-            &["--h_out=foo.h", "--", "test.rs", "--crate-type=lib"].join("\n"),
+            &["--h-out=foo.h", "--", "test.rs", "--crate-type=lib"].join("\n"),
         )?;
 
-        let cmdline = Cmdline::new(&[format!("@{}", tmpfile.display())])?;
-        assert_eq!(Path::new("foo.h"), cmdline.h_out());
-        let rustc_args = cmdline.rustc_args();
+        let flag_file_arg = format!("@{}", tmpfile.display());
+        let cmdline = new_cmdline([flag_file_arg.as_str()]).expect("No errors expected");
+        assert_eq!(Path::new("foo.h"), cmdline.h_out);
+        let rustc_args = &cmdline.rustc_args;
         assert!(
-            itertools::equal(&["test.rs", "--crate-type=lib"], rustc_args),
+            itertools::equal(
+                ["cc_bindings_from_rs_unittest_executable", "test.rs", "--crate-type=lib"],
+                rustc_args),
             "rustc_args = {:?}",
-            rustc_args
+            rustc_args,
         );
         Ok(())
     }