diff --git a/Cargo.lock b/Cargo.lock index e290f7f..157f691 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,7 +353,7 @@ dependencies = [ [[package]] name = "python-launcher" -version = "0.14.3" +version = "0.15.0" dependencies = [ "comfy-table", "exitcode", diff --git a/Cargo.toml b/Cargo.toml index 0f5c9db..307b63f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "python-launcher" description = "The Python launcher for Unix" -version = "0.14.3" +version = "0.15.0" authors = ["Brett Cannon "] repository = "https://github.com/brettcannon/python-launcher" readme = "README.md" diff --git a/README.md b/README.md index ed2686a..7aee5b1 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,25 @@ An implementation of the `py` command for Unix-based platforms (with some potential experimentation for good measure 😉) -The goal is to have `py` become the cross-platform command that all Python users -use when executing a Python interpreter. By having a version-agnostic command -it side-steps the "what should the `python` command point to?" debate by -clearly specifying that upfront (i.e. the newest version of Python that can be -found). This also unifies the suggested command to document for launching -Python on both Windows as Unix as `py` which has existed as the preferred -[command on Windows](https://docs.python.org/3/using/windows.html#launcher) for -some time. - -See the top section of `py --help` for instructions. +The goal is to have `py` become the cross-platform command that Python users +typically use to launch an interpreter. By having a command that is +version-agnostic command when it comes to Python, it side-steps the "what should +the `python` command point to?" debate by clearly specifying that upfront (i.e. +the newest version of Python that can be found). This also unifies the suggested +command to document for launching Python on both Windows as Unix as `py` has +existed as the preferred +[command on Windows](https://docs.python.org/3/using/windows.html#launcher) +since 2012 with the release of Python 3.3. + +A non-goal of this project is to become the way to launch the Python +interpreter _all the time_. If you know the exact interpreter you want to launch +then you should launch it directly; same goes for when you have +requirements on the type of interpreter you want (e.g. 32-bit, framework build +on macOS, etc.). The Python Launcher should be viewed as a tool of convenience, +not necessity. + +For instructions on how to use the Python Launcher, see the top section of +`py --help`. ## Installation diff --git a/src/HELP.txt b/src/HELP.txt index e124d46..b154d77 100644 --- a/src/HELP.txt +++ b/src/HELP.txt @@ -2,8 +2,9 @@ Python Launcher for Unix {} usage: {} [launcher-args] [python-args] Launcher arguments: --h/--help: This output ---list : List all known interpreters (except activated virtual environment). +-h/--help: This output; must be specified on its own. +--list : List all known interpreters (except activated virtual environment); + must be specified on its own. -[X] : Launch the latest Python `X` version (e.g. `-3` for the latest Python 3); PY_PYTHON[X] overrides what is considered the latest (e.g. `PY_PYTHON3=3.6` will cause `-3` to search for Python 3.6). diff --git a/src/cli.rs b/src/cli.rs index b965b59..65b04ad 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -41,18 +41,24 @@ impl Action { let launcher_path = PathBuf::from(&argv[0]); // Strip the path to this executable. match argv.get(1) { - Some(help) if help == "-h" || help == "--help" => { - crate::find_executable(RequestedVersion::Any) - .ok_or(crate::Error::NoExecutableFound(RequestedVersion::Any)) - .map(|executable_path| { - Action::Help( - help_message(&launcher_path, &executable_path), - executable_path, - ) - }) - } - Some(list) if list == "--list" => { - Ok(Action::List(list_executables(&crate::all_executables())?)) + Some(flag) if flag == "-h" || flag == "--help" || flag == "--list" => { + if argv.len() > 2 { + Err(crate::Error::IllegalArgument( + launcher_path, + flag.to_string(), + )) + } else if flag == "--list" { + Ok(Action::List(list_executables(&crate::all_executables())?)) + } else { + crate::find_executable(RequestedVersion::Any) + .ok_or(crate::Error::NoExecutableFound(RequestedVersion::Any)) + .map(|executable_path| { + Action::Help( + help_message(&launcher_path, &executable_path), + executable_path, + ) + }) + } } // TODO: Figure out how to store the result of the version_from_flag() call. Some(version) if version_from_flag(version).is_some() => { @@ -118,7 +124,6 @@ fn list_executables(executables: &HashMap) -> crate::Resu Ok(table.to_string() + "\n") } -// XXX Expose publicly? /// Returns the path to the activated virtual environment's executable. /// /// A virtual environment is determined to be activated based on the @@ -156,7 +161,6 @@ fn venv_executable() -> Option { activated_venv().or_else(venv_in_dir) } -// XXX Expose publicly? // https://en.m.wikipedia.org/wiki/Shebang_(Unix) fn parse_python_shebang(reader: &mut impl Read) -> Option { let mut shebang_buffer = [0; 2]; @@ -199,7 +203,6 @@ fn parse_python_shebang(reader: &mut impl Read) -> Option { None } -// XXX Expose publicly? fn find_executable(version: RequestedVersion, args: &[String]) -> crate::Result { let mut requested_version = version; let mut chosen_path: Option = None; @@ -252,6 +255,12 @@ mod tests { use super::*; + #[test_case(&["py".to_string(), "--help".to_string(), "--list".to_string()] => Err(crate::Error::IllegalArgument(PathBuf::from("py"), "--help".to_string())))] + #[test_case(&["py".to_string(), "--list".to_string(), "--help".to_string()] => Err(crate::Error::IllegalArgument(PathBuf::from("py"), "--list".to_string())))] + fn from_main_illegal_argument_tests(argv: &[String]) -> crate::Result { + Action::from_main(argv) + } + #[test_case("-S" => None ; "unrecognized short flag is None")] #[test_case("--something" => None ; "unrecognized long flag is None")] #[test_case("-3" => Some(RequestedVersion::MajorOnly(3)) ; "major version")] diff --git a/src/lib.rs b/src/lib.rs index 7e6ba5b..56153e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,10 @@ pub enum Error { /// No Python executable could be found based on the [`RequestedVersion`]. // cli::{list_executables, find_executable, help} NoExecutableFound(RequestedVersion), + /// Multiple CLI flags given when the first flag that is expected to be specified + /// on its own. + // cli::Action::from_main + IllegalArgument(PathBuf, String), } #[cfg(not(tarpaulin_include))] @@ -47,6 +51,14 @@ impl fmt::Display for Error { "No executable found for {}", requested_version.to_string() ), + Self::IllegalArgument(launcher_path, flag) => { + write!( + f, + "The `{}` flag must be specified on its own; see `{} --help` for details", + flag, + launcher_path.to_string_lossy() + ) + } } } } @@ -61,6 +73,7 @@ impl std::error::Error for Error { Self::FileNameToStrError => None, Self::PathFileNameError => None, Self::NoExecutableFound(_) => None, + Self::IllegalArgument(_, _) => None, } } } @@ -76,6 +89,7 @@ impl Error { Self::FileNameToStrError => exitcode::SOFTWARE, Self::PathFileNameError => exitcode::SOFTWARE, Self::NoExecutableFound(_) => exitcode::USAGE, + Self::IllegalArgument(_, _) => exitcode::USAGE, } } }