1use core::cmp;
2use core::num::NonZeroI32;
3use core::str::FromStr;
4use lexopt::Arg::{Long, Short, Value};
5use lexopt::ValueExt;
6use rustpython_vm::{Settings, vm::CheckHashPycsMode};
7use std::env;
8
9pub enum RunMode {
10 Script(String),
11 Command(String),
12 Module(String),
13 InstallPip(InstallPipMode),
14 Repl,
15}
16
17pub enum InstallPipMode {
18 /// Install pip using the ensurepip pip module. This has a higher chance of
19 /// success, but may not install the latest version of pip.
20 Ensurepip,
21 /// Install pip using the get-pip.py script, which retrieves the latest pip version.
22 /// This can be broken due to incompatibilities with cpython.
23 GetPip,
24}
25
26impl FromStr for InstallPipMode {
27 type Err = &'static str;
28
29 fn from_str(s: &str) -> Result<Self, Self::Err> {
30 match s {
31 "ensurepip" => Ok(Self::Ensurepip),
32 "get-pip" => Ok(Self::GetPip),
33 _ => Err("--install-pip takes ensurepip or get-pip as first argument"),
34 }
35 }
36}
37
38#[derive(Default)]
39struct CliArgs {
40 bytes_warning: u8,
41 dont_write_bytecode: bool,
42 debug: u8,
43 ignore_environment: bool,
44 inspect: bool,
45 isolate: bool,
46 optimize: u8,
47 safe_path: bool,
48 quiet: bool,
49 random_hash_seed: bool,
50 no_user_site: bool,
51 no_site: bool,
52 unbuffered: bool,
53 verbose: u8,
54 warning_control: Vec<String>,
55 implementation_option: Vec<String>,
56 check_hash_based_pycs: CheckHashPycsMode,
57
58 #[cfg(feature = "flame-it")]
59 profile_output: Option<std::ffi::OsString>,
60 #[cfg(feature = "flame-it")]
61 profile_format: Option<String>,
62}
63
64const USAGE_STRING: &str = "\
65usage: {PROG} [option] ... [-c cmd | -m mod | file | -] [arg] ...
66Options (and corresponding environment variables):
67-b : issue warnings about converting bytes/bytearray to str and comparing
68 bytes/bytearray with str or bytes with int. (-bb: issue errors)
69-B : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
70-c cmd : program passed in as string (terminates option list)
71-d : turn on parser debugging output (for experts only, only works on
72 debug builds); also PYTHONDEBUG=x
73-E : ignore PYTHON* environment variables (such as PYTHONPATH)
74-h : print this help message and exit (also -? or --help)
75-i : inspect interactively after running script; forces a prompt even
76 if stdin does not appear to be a terminal; also PYTHONINSPECT=x
77-I : isolate Python from the user's environment (implies -E and -s)
78-m mod : run library module as a script (terminates option list)
79-O : remove assert and __debug__-dependent statements; add .opt-1 before
80 .pyc extension; also PYTHONOPTIMIZE=x
81-OO : do -O changes and also discard docstrings; add .opt-2 before
82 .pyc extension
83-P : don't prepend a potentially unsafe path to sys.path; also
84 PYTHONSAFEPATH
85-q : don't print version and copyright messages on interactive startup
86-s : don't add user site directory to sys.path; also PYTHONNOUSERSITE=x
87-S : don't imply 'import site' on initialization
88-u : force the stdout and stderr streams to be unbuffered;
89 this option has no effect on stdin; also PYTHONUNBUFFERED=x
90-v : verbose (trace import statements); also PYTHONVERBOSE=x
91 can be supplied multiple times to increase verbosity
92-V : print the Python version number and exit (also --version)
93 when given twice, print more information about the build
94-W arg : warning control; arg is action:message:category:module:lineno
95 also PYTHONWARNINGS=arg
96-x : skip first line of source, allowing use of non-Unix forms of #!cmd
97-X opt : set implementation-specific option
98--check-hash-based-pycs always|default|never:
99 control how Python invalidates hash-based .pyc files
100--help-env: print help about Python environment variables and exit
101--help-xoptions: print help about implementation-specific -X options and exit
102--help-all: print complete help information and exit
103
104RustPython extensions:
105
106
107Arguments:
108file : program read from script file
109- : program read from stdin (default; interactive mode if a tty)
110arg ...: arguments passed to program in sys.argv[1:]
111";
112
113fn parse_args() -> Result<(CliArgs, RunMode, Vec<String>), lexopt::Error> {
114 let mut args = CliArgs::default();
115 let mut parser = lexopt::Parser::from_env();
116 fn argv(argv0: String, mut parser: lexopt::Parser) -> Result<Vec<String>, lexopt::Error> {
117 core::iter::once(Ok(argv0))
118 .chain(parser.raw_args()?.map(|arg| arg.string()))
119 .collect()
120 }
121 while let Some(arg) = parser.next()? {
122 match arg {
123 Short('b') => args.bytes_warning += 1,
124 Short('B') => args.dont_write_bytecode = true,
125 Short('c') => {
126 let cmd = parser.value()?.string()?;
127 return Ok((
128 args,
129 RunMode::Command(dedent(&cmd)),
130 argv("-c".to_owned(), parser)?,
131 ));
132 }
133 Short('d') => args.debug += 1,
134 Short('E') => args.ignore_environment = true,
135 Short('h' | '?') | Long("help") => help(parser),
136 Short('i') => args.inspect = true,
137 Short('I') => args.isolate = true,
138 Short('m') => {
139 let module = parser.value()?.string()?;
140 let argv = argv("-m".to_owned(), parser)?;
141 return Ok((args, RunMode::Module(module), argv));
142 }
143 Short('O') => args.optimize += 1,
144 Short('P') => args.safe_path = true,
145 Short('q') => args.quiet = true,
146 Short('R') => args.random_hash_seed = true,
147 Short('S') => args.no_site = true,
148 Short('s') => args.no_user_site = true,
149 Short('u') => args.unbuffered = true,
150 Short('v') => args.verbose += 1,
151 Short('V') | Long("version") => version(),
152 Short('W') => args.warning_control.push(parser.value()?.string()?),
153 // TODO: Short('x') =>
154 Short('X') => args.implementation_option.push(parser.value()?.string()?),
155
156 Long("check-hash-based-pycs") => {
157 args.check_hash_based_pycs = parser.value()?.parse()?
158 }
159
160 // TODO: make these more specific
161 Long("help-env") => help(parser),
162 Long("help-xoptions") => help(parser),
163 Long("help-all") => help(parser),
164
165 #[cfg(feature = "flame-it")]
166 Long("profile-output") => args.profile_output = Some(parser.value()?),
167 #[cfg(feature = "flame-it")]
168 Long("profile-format") => args.profile_format = Some(parser.value()?.string()?),
169
170 Long("install-pip") => {
171 let (mode, argv) = if let Some(val) = parser.optional_value() {
172 (val.parse()?, vec![val.string()?])
173 } else if let Ok(argv0) = parser.value() {
174 let mode = argv0.parse()?;
175 (mode, argv(argv0.string()?, parser)?)
176 } else {
177 (
178 InstallPipMode::Ensurepip,
179 ["ensurepip", "--upgrade", "--default-pip"]
180 .map(str::to_owned)
181 .into(),
182 )
183 };
184 return Ok((args, RunMode::InstallPip(mode), argv));
185 }
186 Value(script_name) => {
187 let script_name = script_name.string()?;
188 let mode = if script_name == "-" {
189 RunMode::Repl
190 } else {
191 RunMode::Script(script_name.clone())
192 };
193 return Ok((args, mode, argv(script_name, parser)?));
194 }
195 _ => return Err(arg.unexpected()),
196 }
197 }
198 Ok((args, RunMode::Repl, vec![]))
199}
200
201fn help(parser: lexopt::Parser) -> ! {
202 let usage = USAGE_STRING.replace("{PROG}", parser.bin_name().unwrap_or("rustpython"));
203 print!("{usage}");
204 std::process::exit(0);
205}
206
207fn version() -> ! {
208 println!("Python {}", rustpython_vm::version::RUSTPYTHON_VERSION);
209 std::process::exit(0);
210}
211
212/// Create settings by examining command line arguments and environment
213/// variables.
214pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> {
215 let (args, mode, argv) = parse_args()?;
216
217 let mut settings = Settings::default();
218 settings.isolated = args.isolate;
219 settings.ignore_environment = settings.isolated || args.ignore_environment;
220 settings.bytes_warning = args.bytes_warning.into();
221 settings.import_site = !args.no_site;
222
223 let ignore_environment = settings.ignore_environment;
224
225 if !ignore_environment {
226 settings.path_list.extend(get_paths("RUSTPYTHONPATH"));
227 settings.path_list.extend(get_paths("PYTHONPATH"));
228 }
229
230 // Now process command line flags:
231
232 let get_env = |env| (!ignore_environment).then(|| env::var_os(env)).flatten();
233
234 let env_count = |env| {
235 get_env(env).filter(|v| !v.is_empty()).map_or(0, |val| {
236 val.to_str().and_then(|v| v.parse::<u8>().ok()).unwrap_or(1)
237 })
238 };
239
240 settings.optimize = cmp::max(args.optimize, env_count("PYTHONOPTIMIZE"));
241 settings.verbose = cmp::max(args.verbose, env_count("PYTHONVERBOSE"));
242 settings.debug = cmp::max(args.debug, env_count("PYTHONDEBUG"));
243
244 let env_bool = |env| get_env(env).is_some_and(|v| !v.is_empty());
245
246 settings.user_site_directory =
247 !(settings.isolated || args.no_user_site || env_bool("PYTHONNOUSERSITE"));
248 settings.quiet = args.quiet;
249 settings.write_bytecode = !(args.dont_write_bytecode || env_bool("PYTHONDONTWRITEBYTECODE"));
250 settings.safe_path = settings.isolated || args.safe_path || env_bool("PYTHONSAFEPATH");
251 settings.inspect = args.inspect || env_bool("PYTHONINSPECT");
252 settings.interactive = args.inspect;
253 settings.buffered_stdio = !args.unbuffered;
254
255 if let Some(val) = get_env("PYTHONINTMAXSTRDIGITS") {
256 settings.int_max_str_digits = match val.to_str().and_then(|s| s.parse().ok()) {
257 Some(digits @ (0 | 640..)) => digits,
258 _ => {
259 error!(
260 "Fatal Python error: config_init_int_max_str_digits: PYTHONINTMAXSTRDIGITS: invalid limit; must be >= 640 or 0 for unlimited.\nPython runtime state: preinitialized"
261 );
262 std::process::exit(1);
263 }
264 };
265 }
266
267 if let Some(val) = get_env("PYTHON_CPU_COUNT") {
268 settings.cpu_count = match parse_cpu_count(val.to_str()) {
269 Ok(cpu_count) => cpu_count,
270 Err(()) => {
271 error!(
272 "Fatal Python error: config_init_cpu_count: \
273 -X cpu_count=n option: n is missing or an invalid number, \
274 n must be greater than 0\n\
275 Python runtime state: preinitialized"
276 );
277 std::process::exit(1);
278 }
279 };
280 }
281
282 settings.check_hash_pycs_mode = args.check_hash_based_pycs;
283
284 if let Some(val) = get_env("PYTHONUTF8")
285 && let Some(val_str) = val.to_str()
286 && !val_str.is_empty()
287 {
288 settings.utf8_mode = match val_str {
289 "1" => 1,
290 "0" => 0,
291 _ => {
292 error!(
293 "Fatal Python error: config_init_utf8_mode: \
294 PYTHONUTF8=N: N is missing or invalid\n\
295 Python runtime state: preinitialized"
296 );
297 std::process::exit(1);
298 }
299 };
300 }
301
302 let xopts = args.implementation_option.into_iter().map(|s| {
303 let (name, value) = match s.split_once('=') {
304 Some((name, value)) => (name.to_owned(), Some(value)),
305 None => (s, None),
306 };
307 match &*name {
308 "dev" => settings.dev_mode = true,
309 "faulthandler" => settings.faulthandler = true,
310 "warn_default_encoding" => settings.warn_default_encoding = true,
311 "utf8" => {
312 settings.utf8_mode = match value {
313 None => 1,
314 Some("1") => 1,
315 Some("0") => 0,
316 _ => {
317 error!(
318 "Fatal Python error: config_init_utf8_mode: \
319 -X utf8=n: n is missing or invalid\n\
320 Python runtime state: preinitialized"
321 );
322 std::process::exit(1);
323 }
324 };
325 }
326 "no_sig_int" => settings.install_signal_handlers = false,
327 "no_debug_ranges" => settings.code_debug_ranges = false,
328 "cpu_count" => {
329 settings.cpu_count = match parse_cpu_count(value) {
330 Ok(cpu_count) => cpu_count,
331 Err(()) => {
332 error!(
333 "Fatal Python error: config_init_cpu_count: \
334 -X cpu_count=n option: n is missing or an invalid \
335 number, n must be greater than 0\n\
336 Python runtime state: preinitialized"
337 );
338 std::process::exit(1);
339 }
340 };
341 }
342 "int_max_str_digits" => {
343 settings.int_max_str_digits = match value.and_then(|value| value.parse().ok()) {
344 Some(digits) if digits == 0 || digits >= 640 => digits,
345 _ => {
346 error!(
347 "Fatal Python error: config_init_int_max_str_digits: \
348 -X int_max_str_digits: \
349 invalid limit; must be >= 640 or 0 for unlimited.\n\
350 Python runtime state: preinitialized"
351 );
352 std::process::exit(1);
353 }
354 };
355 }
356 "thread_inherit_context" => {
357 settings.thread_inherit_context = match value {
358 Some("1") => true,
359 Some("0") => false,
360 _ => {
361 error!(
362 "Fatal Python error: config_init_thread_inherit_context: \
363 -X thread_inherit_context=n: n is missing or invalid\n\
364 Python runtime state: preinitialized"
365 );
366 std::process::exit(1);
367 }
368 };
369 }
370 _ => {}
371 }
372 (name, value.map(str::to_owned))
373 });
374 settings.xoptions.extend(xopts);
375
376 // Resolve utf8_mode if not explicitly set by PYTHONUTF8 or -X utf8.
377 // Default to UTF-8 mode since RustPython's locale encoding detection
378 // is incomplete. Users can set PYTHONUTF8=0 or -X utf8=0 to disable.
379 if settings.utf8_mode < 0 {
380 settings.utf8_mode = 1;
381 }
382
383 settings.warn_default_encoding =
384 settings.warn_default_encoding || env_bool("PYTHONWARNDEFAULTENCODING");
385 settings.faulthandler = settings.faulthandler || env_bool("PYTHONFAULTHANDLER");
386 if env_bool("PYTHONNODEBUGRANGES") {
387 settings.code_debug_ranges = false;
388 }
389 if let Some(val) = get_env("PYTHON_THREAD_INHERIT_CONTEXT") {
390 settings.thread_inherit_context = match val.to_str() {
391 Some("1") => true,
392 Some("0") => false,
393 _ => {
394 error!(
395 "Fatal Python error: config_init_thread_inherit_context: \
396 PYTHON_THREAD_INHERIT_CONTEXT=N: N is missing or invalid\n\
397 Python runtime state: preinitialized"
398 );
399 std::process::exit(1);
400 }
401 };
402 }
403
404 // Parse PYTHONIOENCODING=encoding[:errors]
405 if let Some(val) = get_env("PYTHONIOENCODING")
406 && let Some(val_str) = val.to_str()
407 && !val_str.is_empty()
408 {
409 if let Some((enc, err)) = val_str.split_once(':') {
410 if !enc.is_empty() {
411 settings.stdio_encoding = Some(enc.to_owned());
412 }
413 if !err.is_empty() {
414 settings.stdio_errors = Some(err.to_owned());
415 }
416 } else {
417 settings.stdio_encoding = Some(val_str.to_owned());
418 }
419 }
420
421 if settings.dev_mode {
422 settings.warnoptions.push("default".to_owned());
423 settings.faulthandler = true;
424 }
425 if settings.bytes_warning > 0 {
426 let warn = if settings.bytes_warning > 1 {
427 "error::BytesWarning"
428 } else {
429 "default::BytesWarning"
430 };
431 settings.warnoptions.push(warn.to_owned());
432 }
433 if let Some(val) = get_env("PYTHONWARNINGS")
434 && let Some(val_str) = val.to_str()
435 && !val_str.is_empty()
436 {
437 for warning in val_str.split(',') {
438 let warning = warning.trim();
439 if !warning.is_empty() {
440 settings.warnoptions.push(warning.to_owned());
441 }
442 }
443 }
444 settings.warnoptions.extend(args.warning_control);
445
446 settings.hash_seed = match (!args.random_hash_seed)
447 .then(|| get_env("PYTHONHASHSEED"))
448 .flatten()
449 {
450 Some(s) if s == "random" || s.is_empty() => None,
451 Some(s) => {
452 let seed = s.parse_with(|s| {
453 s.parse::<u32>().map_err(|_| {
454 "Fatal Python init error: PYTHONHASHSEED must be \
455 \"random\" or an integer in range [0; 4294967295]"
456 })
457 })?;
458 Some(seed)
459 }
460 None => None,
461 };
462
463 settings.argv = argv;
464
465 #[cfg(feature = "flame-it")]
466 {
467 settings.profile_output = args.profile_output;
468 settings.profile_format = args.profile_format;
469 }
470
471 Ok((settings, mode))
472}
473
474/// `-X cpu_count` / `PYTHON_CPU_COUNT`: a count above zero, or `"default"`.
475/// `Ok(None)` leaves the count to the host. Zero, negatives, and values above
476/// `i32::MAX` are rejected.
477/// = config_init_cpu_count
478fn parse_cpu_count(value: Option<&str>) -> Result<Option<NonZeroI32>, ()> {
479 match value {
480 None => Err(()),
481 Some("default") => Ok(None),
482 Some(number) => number
483 .parse()
484 .ok()
485 .filter(|count: &NonZeroI32| count.get() > 0)
486 .map(Some)
487 .ok_or(()),
488 }
489}
490
491fn get_paths(env_variable_name: &str) -> impl Iterator<Item = String> + '_ {
492 env::var_os(env_variable_name)
493 .filter(|v| !v.is_empty())
494 .into_iter()
495 .flat_map(move |paths| {
496 split_paths(&paths)
497 .map(|path| {
498 path.into_os_string()
499 .into_string()
500 .unwrap_or_else(|_| panic!("{env_variable_name} isn't valid unicode"))
501 })
502 .collect::<Vec<_>>()
503 })
504}
505
506#[cfg(not(target_os = "wasi"))]
507pub(crate) use env::split_paths;
508#[cfg(target_os = "wasi")]
509pub(crate) fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(
510 s: &T,
511) -> impl Iterator<Item = std::path::PathBuf> + '_ {
512 let s = s.as_ref().as_encoded_bytes();
513 s.split(|b| *b == b':').map(|x| {
514 unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(x) }
515 .to_owned()
516 .into()
517 })
518}
519
520/// Remove common whitespace prefix from all lines in a string.
521///
522/// This is like textwrap.dedent, and is used to process -c's code
523/// argument. It's different from ruff's dedent, which does not
524/// distinguish between tab and space characters when dedenting.
525fn dedent(input: &str) -> String {
526 let mut prefix: Option<String> = None;
527 let isspace = |c| c == ' ' || c == '\t';
528
529 // All-whitespace lines become empty.
530 let deblanked: Vec<&str> = input
531 .lines()
532 .map(|line| if line.chars().all(isspace) { "" } else { line })
533 .collect();
534
535 // Find maximum common whitespace prefix, if any.
536 for line in &deblanked {
537 if line.is_empty() {
538 continue;
539 }
540 if let Some(ref mut pstr) = prefix {
541 for (i, (c, pc)) in line.chars().zip(pstr.chars()).enumerate() {
542 // It's okay if `line` is shorter than `pstr`. At
543 // least one char in `line` must be non-whitespace,
544 // and if `line` is shorter than `pstr`, this
545 // non-whitespace char will be compared to a
546 // whitespace char, and loop will terminate.
547 if c != pc {
548 pstr.truncate(i);
549 break;
550 }
551 }
552 } else {
553 let mut pstr = String::new();
554 for c in line.chars() {
555 if isspace(c) {
556 pstr.push(c);
557 } else {
558 break;
559 }
560 }
561 prefix = Some(pstr);
562 continue;
563 }
564 }
565
566 if let Some(pstr) = prefix {
567 // Strip common prefix.
568 deblanked
569 .iter()
570 .map(|line| {
571 if line.is_empty() {
572 String::from("")
573 } else {
574 // All non-empty lines start with pstr, must be at
575 // least pstr.len() long.
576 String::from(line.get(pstr.len()..).unwrap())
577 }
578 })
579 .collect::<Vec<_>>()
580 .join("\n")
581 } else {
582 // No prefix found: all lines blank.
583 deblanked.join("\n")
584 }
585}