use clap::{App, AppSettings, Arg, ArgMatches}; use rustpython_vm::Settings; use std::{env, str::FromStr}; pub enum RunMode { ScriptInteractive(Option, bool), Command(String), Module(String), InstallPip(String), } pub fn opts_with_clap() -> (Settings, RunMode) { let app = App::new("RustPython"); let matches = parse_arguments(app); settings_from(&matches) } fn parse_arguments<'a>(app: App<'a, '_>) -> ArgMatches<'a> { let app = app .setting(AppSettings::TrailingVarArg) .version(crate_version!()) .author(crate_authors!()) .about("Rust implementation of the Python language") .usage("rustpython [OPTIONS] [-c CMD | -m MODULE | FILE] [PYARGS]...") .arg( Arg::with_name("script") .required(false) .allow_hyphen_values(true) .multiple(true) .value_name("script, args") .min_values(1), ) .arg( Arg::with_name("c") .short("c") .takes_value(true) .allow_hyphen_values(true) .multiple(true) .value_name("cmd, args") .min_values(1) .help("run the given string as a program"), ) .arg( Arg::with_name("m") .short("m") .takes_value(true) .allow_hyphen_values(true) .multiple(true) .value_name("module, args") .min_values(1) .help("run library module as script"), ) .arg( Arg::with_name("install_pip") .long("install-pip") .takes_value(true) .allow_hyphen_values(true) .multiple(true) .value_name("get-pip args") .min_values(0) .help("install the pip package manager for rustpython; \ requires rustpython be build with the ssl feature enabled." ), ) .arg( Arg::with_name("optimize") .short("O") .multiple(true) .help("Optimize. Set __debug__ to false. Remove debug statements."), ) .arg( Arg::with_name("verbose") .short("v") .multiple(true) .help("Give the verbosity (can be applied multiple times)"), ) .arg(Arg::with_name("debug").short("d").help("Debug the parser.")) .arg( Arg::with_name("quiet") .short("q") .help("Be quiet at startup."), ) .arg( Arg::with_name("inspect") .short("i") .help("Inspect interactively after running the script."), ) .arg( Arg::with_name("no-user-site") .short("s") .help("don't add user site directory to sys.path."), ) .arg( Arg::with_name("no-site") .short("S") .help("don't imply 'import site' on initialization"), ) .arg( Arg::with_name("dont-write-bytecode") .short("B") .help("don't write .pyc files on import"), ) .arg( Arg::with_name("safe-path") .short("P") .help("don’t prepend a potentially unsafe path to sys.path"), ) .arg( Arg::with_name("ignore-environment") .short("E") .help("Ignore environment variables PYTHON* such as PYTHONPATH"), ) .arg( Arg::with_name("isolate") .short("I") .help("isolate Python from the user's environment (implies -E and -s)"), ) .arg( Arg::with_name("implementation-option") .short("X") .takes_value(true) .multiple(true) .number_of_values(1) .help("set implementation-specific option"), ) .arg( Arg::with_name("warning-control") .short("W") .takes_value(true) .multiple(true) .number_of_values(1) .help("warning control; arg is action:message:category:module:lineno"), ) .arg( Arg::with_name("check-hash-based-pycs") .long("check-hash-based-pycs") .takes_value(true) .number_of_values(1) .default_value("default") .help("always|default|never\ncontrol how Python invalidates hash-based .pyc files"), ) .arg( Arg::with_name("bytes-warning") .short("b") .multiple(true) .help("issue warnings about using bytes where strings are usually expected (-bb: issue errors)"), ).arg( Arg::with_name("unbuffered") .short("u") .help( "force the stdout and stderr streams to be unbuffered; \ this option has no effect on stdin; also PYTHONUNBUFFERED=x", ), ); #[cfg(feature = "flame-it")] let app = app .arg( Arg::with_name("profile_output") .long("profile-output") .takes_value(true) .help("the file to output the profiling information to"), ) .arg( Arg::with_name("profile_format") .long("profile-format") .takes_value(true) .help("the profile format to output the profiling information in"), ); app.get_matches() } /// Create settings by examining command line arguments and environment /// variables. fn settings_from(matches: &ArgMatches) -> (Settings, RunMode) { let mut settings = Settings::default(); settings.isolated = matches.is_present("isolate"); settings.ignore_environment = matches.is_present("ignore-environment"); settings.interactive = !matches.is_present("c") && !matches.is_present("m") && (!matches.is_present("script") || matches.is_present("inspect")); settings.bytes_warning = matches.occurrences_of("bytes-warning"); settings.no_site = matches.is_present("no-site"); let ignore_environment = settings.ignore_environment || settings.isolated; if !ignore_environment { settings.path_list.extend(get_paths("RUSTPYTHONPATH")); settings.path_list.extend(get_paths("PYTHONPATH")); } // Now process command line flags: if matches.is_present("debug") || (!ignore_environment && env::var_os("PYTHONDEBUG").is_some()) { settings.debug = true; } if matches.is_present("inspect") || (!ignore_environment && env::var_os("PYTHONINSPECT").is_some()) { settings.inspect = true; } if matches.is_present("optimize") { settings.optimize = matches.occurrences_of("optimize").try_into().unwrap(); } else if !ignore_environment { if let Ok(value) = get_env_var_value("PYTHONOPTIMIZE") { settings.optimize = value; } } if matches.is_present("verbose") { settings.verbose = matches.occurrences_of("verbose").try_into().unwrap(); } else if !ignore_environment { if let Ok(value) = get_env_var_value("PYTHONVERBOSE") { settings.verbose = value; } } if matches.is_present("no-user-site") || matches.is_present("isolate") || (!ignore_environment && env::var_os("PYTHONNOUSERSITE").is_some()) { settings.no_user_site = true; } if matches.is_present("quiet") { settings.quiet = true; } if matches.is_present("dont-write-bytecode") || (!ignore_environment && env::var_os("PYTHONDONTWRITEBYTECODE").is_some()) { settings.dont_write_bytecode = true; } if !ignore_environment && env::var_os("PYTHONINTMAXSTRDIGITS").is_some() { settings.int_max_str_digits = match env::var("PYTHONINTMAXSTRDIGITS").unwrap().parse() { Ok(digits) if digits == 0 || digits >= 640 => digits, _ => { error!("Fatal Python error: config_init_int_max_str_digits: PYTHONINTMAXSTRDIGITS: invalid limit; must be >= 640 or 0 for unlimited.\nPython runtime state: preinitialized"); std::process::exit(1); } }; } if matches.is_present("safe-path") || (!ignore_environment && env::var_os("PYTHONSAFEPATH").is_some()) { settings.safe_path = true; } settings.check_hash_based_pycs = matches .value_of("check-hash-based-pycs") .unwrap_or("default") .to_owned(); let mut dev_mode = false; let mut warn_default_encoding = false; if let Some(xopts) = matches.values_of("implementation-option") { settings.xopts.extend(xopts.map(|s| { let mut parts = s.splitn(2, '='); let name = parts.next().unwrap().to_owned(); let value = parts.next().map(ToOwned::to_owned); if name == "dev" { dev_mode = true } if name == "warn_default_encoding" { warn_default_encoding = true } if name == "no_sig_int" { settings.no_sig_int = true; } if name == "int_max_str_digits" { settings.int_max_str_digits = match value.as_ref().unwrap().parse() { Ok(digits) if digits == 0 || digits >= 640 => digits, _ => { error!("Fatal Python error: config_init_int_max_str_digits: -X int_max_str_digits: invalid limit; must be >= 640 or 0 for unlimited.\nPython runtime state: preinitialized"); std::process::exit(1); }, }; } (name, value) })); } settings.dev_mode = dev_mode; if warn_default_encoding || (!ignore_environment && env::var_os("PYTHONWARNDEFAULTENCODING").is_some()) { settings.warn_default_encoding = true; } if dev_mode { settings.warnopts.push("default".to_owned()) } if settings.bytes_warning > 0 { let warn = if settings.bytes_warning > 1 { "error::BytesWarning" } else { "default::BytesWarning" }; settings.warnopts.push(warn.to_owned()); } if let Some(warnings) = matches.values_of("warning-control") { settings.warnopts.extend(warnings.map(ToOwned::to_owned)); } let (mode, argv) = if let Some(mut cmd) = matches.values_of("c") { let command = cmd.next().expect("clap ensure this exists"); let argv = std::iter::once("-c".to_owned()) .chain(cmd.map(ToOwned::to_owned)) .collect(); (RunMode::Command(command.to_owned()), argv) } else if let Some(mut cmd) = matches.values_of("m") { let module = cmd.next().expect("clap ensure this exists"); let argv = std::iter::once("PLACEHOLDER".to_owned()) .chain(cmd.map(ToOwned::to_owned)) .collect(); (RunMode::Module(module.to_owned()), argv) } else if let Some(get_pip_args) = matches.values_of("install_pip") { settings.isolated = true; let mut args: Vec<_> = get_pip_args.map(ToOwned::to_owned).collect(); if args.is_empty() { args.push("ensurepip".to_owned()); args.push("--upgrade".to_owned()); args.push("--default-pip".to_owned()); } let installer = args[0].clone(); let mode = match installer.as_str() { "ensurepip" | "get-pip" => RunMode::InstallPip(installer), _ => panic!("--install-pip takes ensurepip or get-pip as first argument"), }; (mode, args) } else if let Some(argv) = matches.values_of("script") { let argv: Vec<_> = argv.map(ToOwned::to_owned).collect(); let script = argv[0].clone(); ( RunMode::ScriptInteractive(Some(script), matches.is_present("inspect")), argv, ) } else { (RunMode::ScriptInteractive(None, true), vec!["".to_owned()]) }; let hash_seed = match env::var("PYTHONHASHSEED") { Ok(s) if s == "random" => Some(None), Ok(s) => s.parse::().ok().map(Some), Err(_) => Some(None), }; settings.hash_seed = hash_seed.unwrap_or_else(|| { error!("Fatal Python init error: PYTHONHASHSEED must be \"random\" or an integer in range [0; 4294967295]"); // TODO: Need to change to ExitCode or Termination std::process::exit(1) }); settings.argv = argv; (settings, mode) } /// Get environment variable and turn it into integer. fn get_env_var_value(name: &str) -> Result { env::var(name).map(|value| { if let Ok(value) = u8::from_str(&value) { value } else { 1 } }) } /// Helper function to retrieve a sequence of paths from an environment variable. fn get_paths(env_variable_name: &str) -> impl Iterator + '_ { env::var_os(env_variable_name) .into_iter() .flat_map(move |paths| { split_paths(&paths) .map(|path| { path.into_os_string() .into_string() .unwrap_or_else(|_| panic!("{env_variable_name} isn't valid unicode")) }) .collect::>() }) } #[cfg(not(target_os = "wasi"))] pub(crate) use env::split_paths; #[cfg(target_os = "wasi")] pub(crate) fn split_paths + ?Sized>( s: &T, ) -> impl Iterator + '_ { use std::os::wasi::ffi::OsStrExt; let s = s.as_ref().as_bytes(); s.split(|b| *b == b':') .map(|x| std::ffi::OsStr::from_bytes(x).to_owned().into()) }