Skip to content

Commit 6e3b50f

Browse files
authored
Merge pull request #579 from immunant/kkysen/c2rust-forward: Make c2rust just forward args to discovered subcommands
This changes `c2rust` to be a simpler wrapper around the other `c2rust-*` subcommand. Instead of `c2rust` having to know about all the subcommands and their arguments upfront, `c2rust $subcommand $args` just runs `c2rust-$subcommand $args`. This allows `c2rust instrument` to work as before (before #554), while also enabling `c2rust analyze` and `c2rust pdg` in the same way. The `clap` help messages are still preserved for the most part, except for the short help messages for the subcommands. Otherwise, `c2rust --help` works as before (while also suggesting the new subcommands), and `c2rust $subcommand --help` works by running `c2rust-$subcommand --help` (instead of `clap` intercepting the `--help`). The way this is implemented is, first the `c2rust` binary's directory is searched for executables named `c2rust-*` to discover subcommands. This is combined with the simple list of known subcommands (`["transpile", "instrument", "pdg", "analyze"]`) in case they're not discovered properly and we still want to suggest them. Then we check if the first argument is one of these subcommands. If it exists, we invoke it. If it doesn't exist, but is known, we suggest building it, and it doesn't exist and isn't known (or there was no subcommand given), then we run the `clap` parser and let it handle arg parsing and nice error/help messages. The reason we don't have everything go through `clap` is that I couldn't figure out a way to have `clap` just forward all arguments, even ones like `--metadata` with hyphens (`Arg::allow_hyphen_values` didn't work), without requiring a leading `--` argument.
2 parents 38d1803 + 3f58677 commit 6e3b50f

File tree

3 files changed

+151
-47
lines changed

3 files changed

+151
-47
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

c2rust/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ azure-devops = { project = "immunant/c2rust", pipeline = "immunant.c2rust", buil
1818

1919
[dependencies]
2020
anyhow = "1.0"
21-
clap = {version = "2.34", features = ["yaml"]}
22-
log = "0.4"
21+
camino = "1.0"
22+
clap = { version = "2.34", features = ["yaml"] }
2323
env_logger = "0.9"
2424
git-testament = "0.2.1"
25+
is_executable = "1.0"
26+
log = "0.4"
2527
regex = "1.3"
2628
shlex = "1.1"
2729
c2rust-transpile = { version = "0.16.0", path = "../c2rust-transpile" }

c2rust/src/main.rs

Lines changed: 136 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,147 @@
1-
use clap::{crate_authors, load_yaml, App, AppSettings, SubCommand};
1+
use anyhow::anyhow;
2+
use camino::Utf8Path;
3+
use clap::{crate_authors, App, AppSettings, Arg};
24
use git_testament::{git_testament, render_testament};
5+
use is_executable::IsExecutable;
6+
use std::borrow::Cow;
7+
use std::collections::HashMap;
38
use std::env;
49
use std::ffi::OsStr;
5-
use std::process::{exit, Command};
10+
use std::path::{Path, PathBuf};
11+
use std::process;
12+
use std::process::Command;
13+
use std::str;
614

715
git_testament!(TESTAMENT);
816

9-
fn main() {
10-
let subcommand_yamls = [load_yaml!("transpile.yaml")];
11-
let matches = App::new("C2Rust")
12-
.version(&*render_testament!(TESTAMENT))
13-
.author(crate_authors!(", "))
14-
.setting(AppSettings::SubcommandRequiredElseHelp)
15-
.subcommands(
16-
subcommand_yamls
17-
.iter()
18-
.map(|yaml| SubCommand::from_yaml(yaml)),
19-
)
20-
.get_matches();
17+
/// A `c2rust` sub-command.
18+
struct SubCommand {
19+
/// The path to the [`SubCommand`]'s executable,
20+
/// if it was found (see [`Self::find_all`]).
21+
/// Otherwise [`None`] if it is a known [`SubCommand`] (see [`Self::known`]).
22+
path: Option<PathBuf>,
23+
/// The name of the [`SubCommand`], i.e. in `c2rust-{name}`.
24+
name: Cow<'static, str>,
25+
}
2126

22-
let mut os_args = env::args_os();
23-
os_args.next(); // Skip executable name
24-
let arg_name = os_args.next().and_then(|name| name.into_string().ok());
25-
match (&arg_name, matches.subcommand_name()) {
26-
(Some(arg_name), Some(subcommand)) if arg_name == subcommand => {
27-
invoke_subcommand(subcommand, os_args);
28-
}
29-
_ => {
30-
eprintln!("{:?}", arg_name);
31-
panic!("Could not match subcommand");
27+
impl SubCommand {
28+
/// Find all [`SubCommand`]s adjacent to the current (`c2rust`) executable.
29+
/// They are of the form `c2rust-{name}`.
30+
pub fn find_all() -> anyhow::Result<Vec<Self>> {
31+
let c2rust = env::current_exe()?;
32+
let c2rust_name: &Utf8Path = c2rust
33+
.file_name()
34+
.map(Path::new)
35+
.ok_or_else(|| anyhow!("no file name: {}", c2rust.display()))?
36+
.try_into()?;
37+
let c2rust_name = c2rust_name.as_str();
38+
let dir = c2rust
39+
.parent()
40+
.ok_or_else(|| anyhow!("no directory: {}", c2rust.display()))?;
41+
let mut sub_commands = Vec::new();
42+
for entry in dir.read_dir()? {
43+
let entry = entry?;
44+
let file_type = entry.file_type()?;
45+
let path = entry.path();
46+
let name = path
47+
.file_name()
48+
.and_then(|name| name.to_str())
49+
.and_then(|name| name.strip_prefix(c2rust_name))
50+
.and_then(|name| name.strip_prefix('-'))
51+
.map(|name| name.to_owned())
52+
.map(Cow::from)
53+
.filter(|_| file_type.is_file() || file_type.is_symlink())
54+
.filter(|_| path.is_executable());
55+
if let Some(name) = name {
56+
sub_commands.push(Self {
57+
path: Some(path),
58+
name,
59+
});
60+
}
3261
}
33-
};
62+
Ok(sub_commands)
63+
}
64+
65+
/// Get all known [`SubCommand`]s. These have no [`SubCommand::path`].
66+
/// Even if the subcommand executables aren't there, we can still suggest them.
67+
pub fn known() -> impl Iterator<Item = Self> {
68+
["transpile", "instrument", "pdg", "analyze"]
69+
.into_iter()
70+
.map(|name| Self {
71+
path: None,
72+
name: name.into(),
73+
})
74+
}
75+
76+
/// Get all known ([`Self::known`]) and actual, found ([`Self::find_all`]) subcommands,
77+
/// putting the known ones first so that the found ones overwrite them and take precedence.
78+
pub fn all() -> anyhow::Result<impl Iterator<Item = Self>> {
79+
Ok(Self::known().chain(Self::find_all()?))
80+
}
81+
82+
pub fn invoke<I, S>(&self, args: I) -> anyhow::Result<()>
83+
where
84+
I: IntoIterator<Item = S>,
85+
S: AsRef<OsStr>,
86+
{
87+
let path = self.path.as_ref().ok_or_else(|| {
88+
anyhow!(
89+
"known subcommand not found (probably not built): {}",
90+
self.name
91+
)
92+
})?;
93+
let status = Command::new(&path).args(args).status()?;
94+
process::exit(status.code().unwrap_or(1));
95+
}
3496
}
3597

36-
fn invoke_subcommand<I, S>(subcommand: &str, args: I)
37-
where
38-
I: IntoIterator<Item = S>,
39-
S: AsRef<OsStr>,
40-
{
41-
// Assumes the subcommand executable is in the same directory as this driver
42-
// program.
43-
let cmd_path = std::env::current_exe().expect("Cannot get current executable path");
44-
let mut cmd_path = cmd_path.as_path().canonicalize().unwrap();
45-
cmd_path.pop(); // remove current executable
46-
cmd_path.push(format!("c2rust-{}", subcommand));
47-
assert!(cmd_path.exists(), "{:?} is missing", cmd_path);
48-
exit(
49-
Command::new(cmd_path.into_os_string())
50-
.args(args)
51-
.status()
52-
.expect("SubCommand failed to start")
53-
.code()
54-
.unwrap_or(-1),
55-
);
98+
fn main() -> anyhow::Result<()> {
99+
let sub_commands = SubCommand::all()?.collect::<Vec<_>>();
100+
let sub_commands = sub_commands
101+
.iter()
102+
.map(|cmd| (cmd.name.as_ref(), cmd))
103+
.collect::<HashMap<_, _>>();
104+
105+
// If the subcommand matches, don't use `clap` at all.
106+
//
107+
// I can't seem to get `clap` to pass through all arguments as is,
108+
// like the ones with hyphens like `--metadata`,
109+
// even though I've set [`Arg::allow_hyphen_values`].
110+
// This is faster anyways.
111+
// I also tried a single "subcommand" argument with [`Arg::possible_values`],
112+
// but that had the same problem passing through all arguments as well.
113+
//
114+
// Furthermore, doing it this way correctly forwards `--help` through to the subcommand
115+
// instead of `clap` intercepting it and displaying the top-level `--help`.
116+
let mut args = env::args_os();
117+
let sub_command = args.nth(1);
118+
let sub_command = sub_command
119+
.as_ref()
120+
.and_then(|arg| arg.to_str())
121+
.and_then(|name| sub_commands.get(name));
122+
123+
if let Some(sub_command) = sub_command {
124+
return sub_command.invoke(args);
125+
}
126+
127+
// If we didn't get a subcommand, then use `clap` for parsing and error/help messages.
128+
let matches = App::new("C2Rust")
129+
.version(&*render_testament!(TESTAMENT))
130+
.author(crate_authors!(", "))
131+
.settings(&[
132+
AppSettings::SubcommandRequiredElseHelp,
133+
AppSettings::AllowExternalSubcommands,
134+
])
135+
.subcommands(sub_commands.keys().map(|name| {
136+
clap::SubCommand::with_name(name).arg(
137+
Arg::with_name("args")
138+
.multiple(true)
139+
.allow_hyphen_values(true),
140+
)
141+
}))
142+
.get_matches();
143+
let sub_command_name = matches
144+
.subcommand_name()
145+
.ok_or_else(|| anyhow!("no subcommand"))?;
146+
sub_commands[sub_command_name].invoke(args)
56147
}

0 commit comments

Comments
 (0)