Skip to main content

rustpython/
settings.rs

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    /// Install pip using the ensurepip pip module. This has a higher chance of
17    /// success, but may not install the latest version of pip.
18    Ensurepip,
19    /// Install pip using the get-pip.py script, which retrieves the latest pip version.
20    /// This can be broken due to incompatibilities with cpython.
21    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            // TODO: Short('x') =>
148            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            // TODO: make these more specific
155            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
206/// Create settings by examining command line arguments and environment
207/// variables.
208pub 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    // Now process command line flags:
225
226    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    // Resolve utf8_mode if not explicitly set by PYTHONUTF8 or -X utf8.
342    // Default to UTF-8 mode since RustPython's locale encoding detection
343    // is incomplete. Users can set PYTHONUTF8=0 or -X utf8=0 to disable.
344    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    // Parse PYTHONIOENCODING=encoding[:errors]
370    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
439/// Helper function to retrieve a sequence of paths from an environment variable.
440fn 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}