1use lexopt::Arg::*;
2use lexopt::ValueExt;
3use rustpython_vm::{Settings, vm::CheckHashPycsMode};
4use std::str::FromStr;
5use std::{cmp, env};
6
7pub enum RunMode {
8 Script(String),
9 Command(String),
10 Module(String),
11 InstallPip(InstallPipMode),
12 Repl,
13}
14
15pub enum InstallPipMode {
16 Ensurepip,
19 GetPip,
22}
23
24impl FromStr for InstallPipMode {
25 type Err = &'static str;
26
27 fn from_str(s: &str) -> Result<Self, Self::Err> {
28 match s {
29 "ensurepip" => Ok(Self::Ensurepip),
30 "get-pip" => Ok(Self::GetPip),
31 _ => Err("--install-pip takes ensurepip or get-pip as first argument"),
32 }
33 }
34}
35
36#[derive(Default)]
37struct CliArgs {
38 bytes_warning: u8,
39 dont_write_bytecode: bool,
40 debug: u8,
41 ignore_environment: bool,
42 inspect: bool,
43 isolate: bool,
44 optimize: u8,
45 safe_path: bool,
46 quiet: bool,
47 random_hash_seed: bool,
48 no_user_site: bool,
49 no_site: bool,
50 unbuffered: bool,
51 verbose: u8,
52 warning_control: Vec<String>,
53 implementation_option: Vec<String>,
54 check_hash_based_pycs: CheckHashPycsMode,
55
56 #[cfg(feature = "flame-it")]
57 profile_output: Option<std::ffi::OsString>,
58 #[cfg(feature = "flame-it")]
59 profile_format: Option<String>,
60}
61
62const USAGE_STRING: &str = "\
63usage: {PROG} [option] ... [-c cmd | -m mod | file | -] [arg] ...
64Options (and corresponding environment variables):
65-b : issue warnings about converting bytes/bytearray to str and comparing
66 bytes/bytearray with str or bytes with int. (-bb: issue errors)
67-B : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
68-c cmd : program passed in as string (terminates option list)
69-d : turn on parser debugging output (for experts only, only works on
70 debug builds); also PYTHONDEBUG=x
71-E : ignore PYTHON* environment variables (such as PYTHONPATH)
72-h : print this help message and exit (also -? or --help)
73-i : inspect interactively after running script; forces a prompt even
74 if stdin does not appear to be a terminal; also PYTHONINSPECT=x
75-I : isolate Python from the user's environment (implies -E and -s)
76-m mod : run library module as a script (terminates option list)
77-O : remove assert and __debug__-dependent statements; add .opt-1 before
78 .pyc extension; also PYTHONOPTIMIZE=x
79-OO : do -O changes and also discard docstrings; add .opt-2 before
80 .pyc extension
81-P : don't prepend a potentially unsafe path to sys.path; also
82 PYTHONSAFEPATH
83-q : don't print version and copyright messages on interactive startup
84-s : don't add user site directory to sys.path; also PYTHONNOUSERSITE=x
85-S : don't imply 'import site' on initialization
86-u : force the stdout and stderr streams to be unbuffered;
87 this option has no effect on stdin; also PYTHONUNBUFFERED=x
88-v : verbose (trace import statements); also PYTHONVERBOSE=x
89 can be supplied multiple times to increase verbosity
90-V : print the Python version number and exit (also --version)
91 when given twice, print more information about the build
92-W arg : warning control; arg is action:message:category:module:lineno
93 also PYTHONWARNINGS=arg
94-x : skip first line of source, allowing use of non-Unix forms of #!cmd
95-X opt : set implementation-specific option
96--check-hash-based-pycs always|default|never:
97 control how Python invalidates hash-based .pyc files
98--help-env: print help about Python environment variables and exit
99--help-xoptions: print help about implementation-specific -X options and exit
100--help-all: print complete help information and exit
101
102RustPython extensions:
103
104
105Arguments:
106file : program read from script file
107- : program read from stdin (default; interactive mode if a tty)
108arg ...: arguments passed to program in sys.argv[1:]
109";
110
111fn parse_args() -> Result<(CliArgs, RunMode, Vec<String>), lexopt::Error> {
112 let mut args = CliArgs::default();
113 let mut parser = lexopt::Parser::from_env();
114 fn argv(argv0: String, mut parser: lexopt::Parser) -> Result<Vec<String>, lexopt::Error> {
115 std::iter::once(Ok(argv0))
116 .chain(parser.raw_args()?.map(|arg| arg.string()))
117 .collect()
118 }
119 while let Some(arg) = parser.next()? {
120 match arg {
121 Short('b') => args.bytes_warning += 1,
122 Short('B') => args.dont_write_bytecode = true,
123 Short('c') => {
124 let cmd = parser.value()?.string()?;
125 return Ok((args, RunMode::Command(cmd), argv("-c".to_owned(), parser)?));
126 }
127 Short('d') => args.debug += 1,
128 Short('E') => args.ignore_environment = true,
129 Short('h' | '?') | Long("help") => help(parser),
130 Short('i') => args.inspect = true,
131 Short('I') => args.isolate = true,
132 Short('m') => {
133 let module = parser.value()?.string()?;
134 let argv = argv("PLACEHOLDER".to_owned(), parser)?;
135 return Ok((args, RunMode::Module(module), argv));
136 }
137 Short('O') => args.optimize += 1,
138 Short('P') => args.safe_path = true,
139 Short('q') => args.quiet = true,
140 Short('R') => args.random_hash_seed = true,
141 Short('S') => args.no_site = true,
142 Short('s') => args.no_user_site = true,
143 Short('u') => args.unbuffered = true,
144 Short('v') => args.verbose += 1,
145 Short('V') | Long("version") => version(),
146 Short('W') => args.warning_control.push(parser.value()?.string()?),
147 Short('X') => args.implementation_option.push(parser.value()?.string()?),
149
150 Long("check-hash-based-pycs") => {
151 args.check_hash_based_pycs = parser.value()?.parse()?
152 }
153
154 Long("help-env") => help(parser),
156 Long("help-xoptions") => help(parser),
157 Long("help-all") => help(parser),
158
159 #[cfg(feature = "flame-it")]
160 Long("profile-output") => args.profile_output = Some(parser.value()?),
161 #[cfg(feature = "flame-it")]
162 Long("profile-format") => args.profile_format = Some(parser.value()?.string()?),
163
164 Long("install-pip") => {
165 let (mode, argv) = if let Some(val) = parser.optional_value() {
166 (val.parse()?, vec![val.string()?])
167 } else if let Ok(argv0) = parser.value() {
168 let mode = argv0.parse()?;
169 (mode, argv(argv0.string()?, parser)?)
170 } else {
171 (
172 InstallPipMode::Ensurepip,
173 ["ensurepip", "--upgrade", "--default-pip"]
174 .map(str::to_owned)
175 .into(),
176 )
177 };
178 return Ok((args, RunMode::InstallPip(mode), argv));
179 }
180 Value(script_name) => {
181 let script_name = script_name.string()?;
182 let mode = if script_name == "-" {
183 RunMode::Repl
184 } else {
185 RunMode::Script(script_name.clone())
186 };
187 return Ok((args, mode, argv(script_name, parser)?));
188 }
189 _ => return Err(arg.unexpected()),
190 }
191 }
192 Ok((args, RunMode::Repl, vec![]))
193}
194
195fn help(parser: lexopt::Parser) -> ! {
196 let usage = USAGE_STRING.replace("{PROG}", parser.bin_name().unwrap_or("rustpython"));
197 print!("{usage}");
198 std::process::exit(0);
199}
200
201fn version() -> ! {
202 println!("Python {}", rustpython_vm::version::get_version());
203 std::process::exit(0);
204}
205
206pub fn parse_opts() -> Result<(Settings, RunMode), lexopt::Error> {
209 let (args, mode, argv) = parse_args()?;
210
211 let mut settings = Settings::default();
212 settings.isolated = args.isolate;
213 settings.ignore_environment = settings.isolated || args.ignore_environment;
214 settings.bytes_warning = args.bytes_warning.into();
215 settings.import_site = !args.no_site;
216
217 let ignore_environment = settings.ignore_environment;
218
219 if !ignore_environment {
220 settings.path_list.extend(get_paths("RUSTPYTHONPATH"));
221 settings.path_list.extend(get_paths("PYTHONPATH"));
222 }
223
224 let get_env = |env| (!ignore_environment).then(|| env::var_os(env)).flatten();
227
228 let env_count = |env| {
229 get_env(env).filter(|v| !v.is_empty()).map_or(0, |val| {
230 val.to_str().and_then(|v| v.parse::<u8>().ok()).unwrap_or(1)
231 })
232 };
233
234 settings.optimize = cmp::max(args.optimize, env_count("PYTHONOPTIMIZE"));
235 settings.verbose = cmp::max(args.verbose, env_count("PYTHONVERBOSE"));
236 settings.debug = cmp::max(args.debug, env_count("PYTHONDEBUG"));
237
238 let env_bool = |env| get_env(env).is_some_and(|v| !v.is_empty());
239
240 settings.user_site_directory =
241 !(settings.isolated || args.no_user_site || env_bool("PYTHONNOUSERSITE"));
242 settings.quiet = args.quiet;
243 settings.write_bytecode = !(args.dont_write_bytecode || env_bool("PYTHONDONTWRITEBYTECODE"));
244 settings.safe_path = settings.isolated || args.safe_path || env_bool("PYTHONSAFEPATH");
245 settings.inspect = args.inspect || env_bool("PYTHONINSPECT");
246 settings.interactive = args.inspect;
247 settings.buffered_stdio = !args.unbuffered;
248
249 if let Some(val) = get_env("PYTHONINTMAXSTRDIGITS") {
250 settings.int_max_str_digits = match val.to_str().and_then(|s| s.parse().ok()) {
251 Some(digits @ (0 | 640..)) => digits,
252 _ => {
253 error!(
254 "Fatal Python error: config_init_int_max_str_digits: PYTHONINTMAXSTRDIGITS: invalid limit; must be >= 640 or 0 for unlimited.\nPython runtime state: preinitialized"
255 );
256 std::process::exit(1);
257 }
258 };
259 }
260
261 settings.check_hash_pycs_mode = args.check_hash_based_pycs;
262
263 if let Some(val) = get_env("PYTHONUTF8")
264 && let Some(val_str) = val.to_str()
265 && !val_str.is_empty()
266 {
267 settings.utf8_mode = match val_str {
268 "1" => 1,
269 "0" => 0,
270 _ => {
271 error!(
272 "Fatal Python error: config_init_utf8_mode: \
273 PYTHONUTF8=N: N is missing or invalid\n\
274 Python runtime state: preinitialized"
275 );
276 std::process::exit(1);
277 }
278 };
279 }
280
281 let xopts = args.implementation_option.into_iter().map(|s| {
282 let (name, value) = match s.split_once('=') {
283 Some((name, value)) => (name.to_owned(), Some(value)),
284 None => (s, None),
285 };
286 match &*name {
287 "dev" => settings.dev_mode = true,
288 "faulthandler" => settings.faulthandler = true,
289 "warn_default_encoding" => settings.warn_default_encoding = true,
290 "utf8" => {
291 settings.utf8_mode = match value {
292 None => 1,
293 Some("1") => 1,
294 Some("0") => 0,
295 _ => {
296 error!(
297 "Fatal Python error: config_init_utf8_mode: \
298 -X utf8=n: n is missing or invalid\n\
299 Python runtime state: preinitialized"
300 );
301 std::process::exit(1);
302 }
303 };
304 }
305 "no_sig_int" => settings.install_signal_handlers = false,
306 "no_debug_ranges" => settings.code_debug_ranges = false,
307 "int_max_str_digits" => {
308 settings.int_max_str_digits = match value.unwrap().parse() {
309 Ok(digits) if digits == 0 || digits >= 640 => digits,
310 _ => {
311 error!(
312 "Fatal Python error: config_init_int_max_str_digits: \
313 -X int_max_str_digits: \
314 invalid limit; must be >= 640 or 0 for unlimited.\n\
315 Python runtime state: preinitialized"
316 );
317 std::process::exit(1);
318 }
319 };
320 }
321 "thread_inherit_context" => {
322 settings.thread_inherit_context = match value {
323 Some("1") => true,
324 Some("0") => false,
325 _ => {
326 error!(
327 "Fatal Python error: config_init_thread_inherit_context: \
328 -X thread_inherit_context=n: n is missing or invalid\n\
329 Python runtime state: preinitialized"
330 );
331 std::process::exit(1);
332 }
333 };
334 }
335 _ => {}
336 }
337 (name, value.map(str::to_owned))
338 });
339 settings.xoptions.extend(xopts);
340
341 if settings.utf8_mode < 0 {
345 settings.utf8_mode = 1;
346 }
347
348 settings.warn_default_encoding =
349 settings.warn_default_encoding || env_bool("PYTHONWARNDEFAULTENCODING");
350 settings.faulthandler = settings.faulthandler || env_bool("PYTHONFAULTHANDLER");
351 if env_bool("PYTHONNODEBUGRANGES") {
352 settings.code_debug_ranges = false;
353 }
354 if let Some(val) = get_env("PYTHON_THREAD_INHERIT_CONTEXT") {
355 settings.thread_inherit_context = match val.to_str() {
356 Some("1") => true,
357 Some("0") => false,
358 _ => {
359 error!(
360 "Fatal Python error: config_init_thread_inherit_context: \
361 PYTHON_THREAD_INHERIT_CONTEXT=N: N is missing or invalid\n\
362 Python runtime state: preinitialized"
363 );
364 std::process::exit(1);
365 }
366 };
367 }
368
369 if let Some(val) = get_env("PYTHONIOENCODING")
371 && let Some(val_str) = val.to_str()
372 && !val_str.is_empty()
373 {
374 if let Some((enc, err)) = val_str.split_once(':') {
375 if !enc.is_empty() {
376 settings.stdio_encoding = Some(enc.to_owned());
377 }
378 if !err.is_empty() {
379 settings.stdio_errors = Some(err.to_owned());
380 }
381 } else {
382 settings.stdio_encoding = Some(val_str.to_owned());
383 }
384 }
385
386 if settings.dev_mode {
387 settings.warnoptions.push("default".to_owned());
388 settings.faulthandler = true;
389 }
390 if settings.bytes_warning > 0 {
391 let warn = if settings.bytes_warning > 1 {
392 "error::BytesWarning"
393 } else {
394 "default::BytesWarning"
395 };
396 settings.warnoptions.push(warn.to_owned());
397 }
398 if let Some(val) = get_env("PYTHONWARNINGS")
399 && let Some(val_str) = val.to_str()
400 && !val_str.is_empty()
401 {
402 for warning in val_str.split(',') {
403 let warning = warning.trim();
404 if !warning.is_empty() {
405 settings.warnoptions.push(warning.to_owned());
406 }
407 }
408 }
409 settings.warnoptions.extend(args.warning_control);
410
411 settings.hash_seed = match (!args.random_hash_seed)
412 .then(|| get_env("PYTHONHASHSEED"))
413 .flatten()
414 {
415 Some(s) if s == "random" || s.is_empty() => None,
416 Some(s) => {
417 let seed = s.parse_with(|s| {
418 s.parse::<u32>().map_err(|_| {
419 "Fatal Python init error: PYTHONHASHSEED must be \
420 \"random\" or an integer in range [0; 4294967295]"
421 })
422 })?;
423 Some(seed)
424 }
425 None => None,
426 };
427
428 settings.argv = argv;
429
430 #[cfg(feature = "flame-it")]
431 {
432 settings.profile_output = args.profile_output;
433 settings.profile_format = args.profile_format;
434 }
435
436 Ok((settings, mode))
437}
438
439fn get_paths(env_variable_name: &str) -> impl Iterator<Item = String> + '_ {
441 env::var_os(env_variable_name)
442 .filter(|v| !v.is_empty())
443 .into_iter()
444 .flat_map(move |paths| {
445 split_paths(&paths)
446 .map(|path| {
447 path.into_os_string()
448 .into_string()
449 .unwrap_or_else(|_| panic!("{env_variable_name} isn't valid unicode"))
450 })
451 .collect::<Vec<_>>()
452 })
453}
454
455#[cfg(not(target_os = "wasi"))]
456pub(crate) use env::split_paths;
457#[cfg(target_os = "wasi")]
458pub(crate) fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(
459 s: &T,
460) -> impl Iterator<Item = std::path::PathBuf> + '_ {
461 let s = s.as_ref().as_encoded_bytes();
462 s.split(|b| *b == b':').map(|x| {
463 unsafe { std::ffi::OsStr::from_encoded_bytes_unchecked(x) }
464 .to_owned()
465 .into()
466 })
467}