use std::{
cmp,
collections::HashMap,
env,
fmt::Write,
fs::File,
io::{BufRead, BufReader, Read},
iter::FromIterator,
path::{Path, PathBuf},
str::FromStr,
string::ToString,
};
use crate::{ExactVersion, RequestedVersion};
#[derive(Debug, PartialEq)]
pub enum Action {
Help(String, PathBuf),
List(String),
Execute {
launcher_path: PathBuf,
executable: PathBuf,
args: Vec<String>,
},
}
impl Action {
pub fn from_main(argv: &[String]) -> crate::Result<Self> {
let mut args = argv.to_owned();
let mut requested_version = RequestedVersion::Any;
let launcher_path = PathBuf::from(args.remove(0));
if !args.is_empty() {
let flag = &args[0];
if flag == "-h" || flag == "--help" {
if let Some(executable_path) = crate::find_executable(RequestedVersion::Any) {
return Ok(Action::Help(
help_message(&launcher_path, &executable_path),
executable_path,
));
} else {
return Err(crate::Error::NoExecutableFound(RequestedVersion::Any));
}
} else if flag == "--list" {
let executables = crate::all_executables();
return match list_executables(&executables) {
Ok(list) => Ok(Action::List(list)),
Err(message) => Err(message),
};
} else if let Some(version) = version_from_flag(&flag) {
args.remove(0);
requested_version = version;
}
}
match find_executable(requested_version, &args) {
Ok(executable) => Ok(Action::Execute {
launcher_path,
executable,
args,
}),
Err(message) => Err(message),
}
}
}
fn help_message(launcher_path: &Path, executable_path: &Path) -> String {
let mut message = String::new();
writeln!(
message,
include_str!("HELP.txt"),
env!("CARGO_PKG_VERSION"),
launcher_path.to_string_lossy(),
executable_path.to_string_lossy()
)
.unwrap();
message
}
fn version_from_flag(arg: &str) -> Option<RequestedVersion> {
if !arg.starts_with('-') {
None
} else {
RequestedVersion::from_str(&arg[1..]).ok()
}
}
fn list_executables(executables: &HashMap<ExactVersion, PathBuf>) -> crate::Result<String> {
if executables.is_empty() {
return Err(crate::Error::NoExecutableFound(RequestedVersion::Any));
}
let mut executable_pairs = Vec::from_iter(executables);
executable_pairs.sort_unstable();
let max_version_length = executable_pairs.iter().fold(0, |max_so_far, pair| {
cmp::max(max_so_far, pair.0.to_string().len())
});
let left_column_width = cmp::max(max_version_length, "Version".len());
let mut help_string = String::new();
writeln!(help_string, "{:<1$} Path", "Version", left_column_width).unwrap();
writeln!(help_string, "{:<1$} ====", "=======", left_column_width).unwrap();
for (version, path) in executable_pairs {
writeln!(
help_string,
"{:<2$} {}",
version.to_string(),
path.to_string_lossy(),
left_column_width
)
.unwrap();
}
Ok(help_string)
}
fn venv_executable(venv_root: &str) -> PathBuf {
let mut path = PathBuf::new();
path.push(venv_root);
path.push("bin");
path.push("python");
path
}
fn parse_python_shebang(reader: &mut impl Read) -> Option<RequestedVersion> {
let mut shebang_buffer = [0; 2];
if reader.read(&mut shebang_buffer).is_err() || shebang_buffer != [0x23, 0x21] {
return None;
}
let mut buffered_reader = BufReader::new(reader);
let mut first_line = String::new();
if buffered_reader.read_line(&mut first_line).is_err() {
return None;
};
let line = first_line.trim();
let accepted_paths = [
"python",
"/usr/bin/python",
"/usr/local/bin/python",
"/usr/bin/env python",
];
for acceptable_path in &accepted_paths {
if !line.starts_with(acceptable_path) {
continue;
}
let version = line[acceptable_path.len()..].to_string();
return match RequestedVersion::from_str(&version) {
Ok(version) => Some(version),
Err(_) => None,
};
}
None
}
fn find_executable(version: RequestedVersion, args: &[String]) -> crate::Result<PathBuf> {
let mut requested_version = version;
let mut chosen_path: Option<PathBuf> = None;
if requested_version == RequestedVersion::Any {
if let Some(venv_root) = env::var_os("VIRTUAL_ENV") {
chosen_path = Some(venv_executable(&venv_root.to_string_lossy()));
} else if !args.is_empty() {
if let Ok(mut open_file) = File::open(&args[0]) {
if let Some(shebang_version) = parse_python_shebang(&mut open_file) {
requested_version = shebang_version;
}
}
}
}
if chosen_path.is_none() {
if let Some(env_var) = requested_version.env_var() {
if let Ok(env_var_value) = env::var(env_var) {
if !env_var_value.is_empty() {
if let Ok(env_requested_version) = RequestedVersion::from_str(&env_var_value) {
requested_version = env_requested_version;
}
}
};
}
if let Some(executable_path) = crate::find_executable(requested_version) {
chosen_path = Some(executable_path);
}
}
match chosen_path {
Some(path) => Ok(path),
None => Err(crate::Error::NoExecutableFound(requested_version)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_from_flag() {
assert!(version_from_flag(&"-S".to_string()).is_none());
assert!(version_from_flag(&"--something".to_string()).is_none());
assert_eq!(
version_from_flag(&"-3".to_string()),
Some(RequestedVersion::MajorOnly(3))
);
assert_eq!(
version_from_flag(&"-3.6".to_string()),
Some(RequestedVersion::Exact(3, 6))
);
assert_eq!(
version_from_flag(&"-42.13".to_string()),
Some(RequestedVersion::Exact(42, 13))
);
assert!(version_from_flag(&"-3.6.4".to_string()).is_none());
}
#[test]
fn test_help_message() {
let launcher_path = "/some/path/to/launcher";
let python_path = "/a/path/to/python";
let help = help_message(&PathBuf::from(launcher_path), &PathBuf::from(python_path));
assert!(help.contains(env!("CARGO_PKG_VERSION")));
assert!(help.contains(launcher_path));
assert!(help.contains(python_path));
}
#[test]
fn test_list_executables() {
let mut executables: HashMap<ExactVersion, PathBuf> = HashMap::new();
assert_eq!(
list_executables(&executables),
Err(crate::Error::NoExecutableFound(RequestedVersion::Any))
);
let python27_path = "/path/to/2/7/python";
executables.insert(
ExactVersion { major: 2, minor: 7 },
PathBuf::from(python27_path),
);
let python36_path = "/path/to/3/6/python";
executables.insert(
ExactVersion { major: 3, minor: 6 },
PathBuf::from(python36_path),
);
let python37_path = "/path/to/3/7/python";
executables.insert(
ExactVersion { major: 3, minor: 7 },
PathBuf::from(python37_path),
);
let executables_list = list_executables(&executables).unwrap();
assert!(executables_list.contains("2.7"));
assert!(executables_list.contains(python27_path));
assert!(executables_list.contains("3.6"));
assert!(executables_list.contains(python36_path));
assert!(executables_list.contains("3.7"));
assert!(executables_list.contains(python37_path));
assert!(executables_list.find("2.7").unwrap() < executables_list.find("3.6").unwrap());
assert!(executables_list.find("3.6").unwrap() < executables_list.find("3.7").unwrap());
assert!(
executables_list.find("3.6").unwrap() < executables_list.find(python36_path).unwrap()
);
assert!(
executables_list.find(python36_path).unwrap() < executables_list.find("3.7").unwrap()
);
}
#[test]
fn test_venv_executable() {
let venv_root = "/path/to/venv";
assert_eq!(
venv_executable(&venv_root),
PathBuf::from("/path/to/venv/bin/python")
);
}
#[test]
fn test_parse_python_shebang() {
let parameters = [
("/usr/bin/python", None),
("# /usr/bin/python", None),
("! /usr/bin/python", None),
("#! /usr/bin/env python", Some(RequestedVersion::Any)),
("#! /usr/bin/python", Some(RequestedVersion::Any)),
("#! /usr/local/bin/python", Some(RequestedVersion::Any)),
("#! python", Some(RequestedVersion::Any)),
(
"#! /usr/bin/env python3.7",
Some(RequestedVersion::Exact(3, 7)),
),
("#! /usr/bin/python3.7", Some(RequestedVersion::Exact(3, 7))),
("#! python3.7", Some(RequestedVersion::Exact(3, 7))),
("#!/usr/bin/python", Some(RequestedVersion::Any)),
];
for arg in parameters.iter() {
let result = parse_python_shebang(&mut arg.0.as_bytes());
assert_eq!(
result, arg.1,
"{:?} lead to {:?}, not {:?}",
arg.0, result, arg.1
);
}
}
}