Add "--list" option to cargo, that shows lists of installed (sub)commands by searching

directories for executables with name cargo-*.
This commit is contained in:
xanxys 2014-08-09 14:37:50 +09:00
parent 07b9c7028a
commit a3f6a404be
3 changed files with 169 additions and 21 deletions

View file

@ -7,7 +7,10 @@ extern crate cargo;
extern crate docopt;
#[phase(plugin)] extern crate docopt_macros;
use std::collections::TreeSet;
use std::os;
use std::io;
use std::io::fs;
use std::io::process::{Command,InheritFd,ExitStatus,ExitSignal};
use serialize::Encodable;
use docopt::FlagParser;
@ -28,10 +31,12 @@ Usage:
cargo <command> [<args>...]
cargo -h | --help
cargo -V | --version
cargo --list
Options:
-h, --help Display this message
-V, --version Print version info and exit
--list List installed commands
-v, --verbose Use verbose output
Some common cargo commands are:
@ -54,6 +59,14 @@ See 'cargo help <command>' for more information on a specific command.
fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
debug!("executing; cmd=cargo; args={}", os::args());
shell.set_verbose(flags.flag_verbose);
if flags.flag_list {
println!("Installed Commands:");
for command in list_commands().iter() {
println!(" {}", command);
// TODO: it might be helpful to add result of -h to each command.
};
return Ok(None)
}
let mut args = flags.arg_args.clone();
args.insert(0, flags.arg_command.clone());
match flags.arg_command.as_slice() {
@ -82,29 +95,25 @@ fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
let r = cargo::call_main_without_stdin(execute, shell,
["-h".to_string()], false);
cargo::process_executed(r, shell)
}
},
orig_cmd => {
let cmd = if orig_cmd == "help" {
let is_help = orig_cmd == "help";
let cmd = if is_help {
flags.arg_args[0].as_slice()
} else {
orig_cmd
};
let command = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX);
let mut command = match os::self_exe_path() {
Some(path) => {
let p1 = path.join("../lib/cargo").join(command.as_slice());
let p2 = path.join(command.as_slice());
if p1.exists() {
Command::new(p1)
} else if p2.exists() {
Command::new(p2)
} else {
Command::new(command)
}
}
None => Command::new(command),
};
let command = if orig_cmd == "help" {
execute_subcommand(cmd, is_help, &flags, shell)
}
}
Ok(None)
}
fn execute_subcommand(cmd: &str, is_help: bool, flags: &Flags, shell: &mut MultiShell) -> () {
match find_command(cmd) {
Some(command) => {
let mut command = Command::new(command);
let command = if is_help {
command.arg("-h")
} else {
command.args(flags.arg_args.as_slice())
@ -124,12 +133,81 @@ fn execute(flags: Flags, shell: &mut MultiShell) -> CliResult<Option<()>> {
let msg = format!("subcommand failed with signal: {}", i);
handle_error(CliError::new(msg, i as uint), shell)
}
Err(_) => handle_error(CliError::new("No such subcommand", 127),
shell)
Err(io::IoError{kind, ..}) if kind == io::FileNotFound =>
handle_error(CliError::new("No such subcommand", 127), shell),
Err(err) => handle_error(
CliError::new(
format!("Subcommand failed to run: {}", err), 127),
shell)
}
},
None => handle_error(CliError::new("No such subcommand", 127), shell)
}
}
/// List all runnable commands. find_command should always succeed
/// if given one of returned command.
fn list_commands() -> TreeSet<String> {
let command_prefix = "cargo-";
let mut commands = TreeSet::new();
for dir in list_command_directory().iter() {
let entries = match fs::readdir(dir) {
Ok(entries) => entries,
_ => continue
};
for entry in entries.iter() {
let filename = match entry.filename_str() {
Some(filename) => filename,
_ => continue
};
if filename.starts_with(command_prefix) &&
filename.ends_with(os::consts::EXE_SUFFIX) &&
is_executable(entry) {
let command = filename.slice(
command_prefix.len(),
filename.len() - os::consts::EXE_SUFFIX.len());
commands.insert(String::from_str(command));
}
}
}
Ok(None)
commands
}
fn is_executable(path: &Path) -> bool {
match fs::stat(path) {
Ok(io::FileStat{kind, perm, ..}) =>
(kind == io::TypeFile) && perm.contains(io::OtherExecute),
_ => false
}
}
/// Get `Command` to run given command.
fn find_command(cmd: &str) -> Option<Path> {
let command_exe = format!("cargo-{}{}", cmd, os::consts::EXE_SUFFIX);
let dirs = list_command_directory();
let mut command_paths = dirs.iter().map(|dir| dir.join(command_exe.as_slice()));
command_paths.find(|path| path.exists())
}
/// List candidate locations where subcommands might be installed.
fn list_command_directory() -> Vec<Path> {
let mut dirs = vec![];
match os::self_exe_path() {
Some(path) => {
dirs.push(path.join("../lib/cargo"));
dirs.push(path);
},
None => {}
};
match std::os::getenv("PATH") {
Some(val) => {
for dir in os::split_paths(val).iter() {
dirs.push(Path::new(dir))
}
},
None => {}
};
dirs
}
#[deriving(Encodable)]

69
tests/test_cargo.rs Normal file
View file

@ -0,0 +1,69 @@
use cargo::util::{process, ProcessBuilder};
use hamcrest::{assert_that};
use std::io;
use std::io::fs;
use std::os;
use support::paths;
use support::{project, execs, cargo_dir, mkdir_recursive, ProjectBuilder, ResultTest};
fn setup() {
}
/// Add an empty file with executable flags (and platform-dependent suffix).
/// TODO: move this to `ProjectBuilder` if other cases using this emerge.
fn fake_executable(proj: ProjectBuilder, dir: &Path, name: &str) -> ProjectBuilder {
let path = proj.root().join(dir).join(format!("{}{}", name, os::consts::EXE_SUFFIX));
mkdir_recursive(&Path::new(path.dirname())).assert();
fs::File::create(&path).assert();
let io::FileStat{perm, ..} = fs::stat(&path).assert();
fs::chmod(&path, io::OtherExecute | perm).assert();
proj
}
/// Copy real cargo exeutable just built to specified location, and
/// prepare to run it.
fn copied_executable_process(proj: &ProjectBuilder, name: &str, dir: &Path) -> ProcessBuilder {
let name = format!("{}{}", name, os::consts::EXE_SUFFIX);
let path_src = cargo_dir().join(name.clone());
let path_dst = proj.root().join(dir).join(name);
mkdir_recursive(&Path::new(path_dst.dirname())).assert();
fs::copy(&path_src, &path_dst).assert();
process(path_dst)
.cwd(proj.root())
.env("HOME", Some(paths::home().as_vec()))
}
test!(list_commands_empty {
let proj = project("list-runs");
let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list");
assert_that(pr, execs()
.with_status(0)
.with_stdout("Installed Commands:\n"));
})
test!(list_commands_non_overlapping {
// lib/cargo | cargo-3
// bin/ | cargo-2
// PATH | cargo-1
// Check if --list searches all 3 targets.
// Also checks that results are in lexicographic order.
let proj = project("list-non-overlapping");
let proj = fake_executable(proj, &Path::new("lib/cargo"), "cargo-3");
let proj = fake_executable(proj, &Path::new("bin"), "cargo-2");
let proj = fake_executable(proj, &Path::new("path-test"), "cargo-1");
let pr = copied_executable_process(&proj, "cargo", &Path::new("bin")).arg("--list");
let path_test = proj.root().join("path-test");
// On Windows, cargo.exe seems to require some directory (
// I don't know which) to run properly.
// That's why we append to $PATH here, instead of overwriting.
let path = os::getenv_as_bytes("PATH").unwrap();
let mut components = os::split_paths(path);
components.push(path_test);
let path_var = os::join_paths(components.as_slice()).assert();
assert_that(
pr.env("PATH", Some(path_var.as_slice())),
execs()
.with_status(0)
.with_stdout("Installed Commands:\n 1\n 2\n 3\n"));
})

View file

@ -20,6 +20,7 @@ macro_rules! test(
)
)
mod test_cargo;
mod test_cargo_clean;
mod test_cargo_compile;
mod test_cargo_compile_git_deps;