|
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}; |
2 | 4 | use git_testament::{git_testament, render_testament};
|
| 5 | +use is_executable::IsExecutable; |
| 6 | +use std::borrow::Cow; |
| 7 | +use std::collections::HashMap; |
3 | 8 | use std::env;
|
4 | 9 | 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; |
6 | 14 |
|
7 | 15 | git_testament!(TESTAMENT);
|
8 | 16 |
|
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 | +} |
21 | 26 |
|
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 | + } |
32 | 61 | }
|
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 | + } |
34 | 96 | }
|
35 | 97 |
|
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) |
56 | 147 | }
|
0 commit comments