Skip to main content

zsh/
init.rs

1//! Shell initialization for zshrs
2//!
3//! Port from zsh/Src/init.c
4//!
5//! Provides shell initialization, startup script sourcing, and main loop.
6
7use std::env;
8use std::path::{Path, PathBuf};
9
10/// Shell initialization options
11#[derive(Clone, Debug, Default)]
12pub struct ShellOptions {
13    pub interactive: bool,
14    pub login: bool,
15    pub shin_stdin: bool,
16    pub use_zle: bool,
17    pub monitor: bool,
18    pub hash_dirs: bool,
19    pub privileged: bool,
20    pub single_command: bool,
21    pub rcs: bool,
22    pub global_rcs: bool,
23}
24
25/// Global shell state
26pub struct ShellState {
27    pub options: ShellOptions,
28    pub argv0: String,
29    pub argzero: String,
30    pub posixzero: String,
31    pub shell_name: String,
32    pub pwd: String,
33    pub oldpwd: String,
34    pub home: String,
35    pub username: String,
36    pub mypid: i64,
37    pub ppid: i64,
38    pub shtty: i32,
39    pub sourcelevel: i32,
40    pub lineno: i64,
41    pub path: Vec<String>,
42    pub fpath: Vec<String>,
43    pub cdpath: Vec<String>,
44    pub module_path: Vec<String>,
45    pub term: String,
46    pub histsize: usize,
47    pub emulation: ShellEmulation,
48}
49
50/// Shell emulation mode
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
52pub enum ShellEmulation {
53    #[default]
54    Zsh,
55    Sh,
56    Ksh,
57    Csh,
58}
59
60impl ShellState {
61    pub fn new() -> Self {
62        let home = env::var("HOME").unwrap_or_else(|_| "/".to_string());
63        let pwd = env::current_dir()
64            .map(|p| p.to_string_lossy().to_string())
65            .unwrap_or_else(|_| home.clone());
66
67        ShellState {
68            options: ShellOptions {
69                rcs: true,
70                global_rcs: true,
71                ..Default::default()
72            },
73            argv0: String::new(),
74            argzero: String::new(),
75            posixzero: String::new(),
76            shell_name: "zsh".to_string(),
77            pwd: pwd.clone(),
78            oldpwd: pwd,
79            home,
80            username: env::var("USER").unwrap_or_default(),
81            mypid: std::process::id() as i64,
82            ppid: 0, // Would need libc to get parent pid
83            shtty: -1,
84            sourcelevel: 0,
85            lineno: 1,
86            path: vec![
87                "/bin".to_string(),
88                "/usr/bin".to_string(),
89                "/usr/local/bin".to_string(),
90            ],
91            fpath: Vec::new(),
92            cdpath: Vec::new(),
93            module_path: Vec::new(),
94            term: env::var("TERM").unwrap_or_default(),
95            histsize: 1000,
96            emulation: ShellEmulation::Zsh,
97        }
98    }
99
100    /// Determine shell emulation from name
101    pub fn emulate_from_name(&mut self, name: &str) {
102        let basename = Path::new(name)
103            .file_name()
104            .and_then(|s| s.to_str())
105            .unwrap_or(name);
106
107        let basename = basename.trim_start_matches('-');
108
109        self.emulation = match basename {
110            "sh" => ShellEmulation::Sh,
111            "ksh" | "ksh93" => ShellEmulation::Ksh,
112            "csh" | "tcsh" => ShellEmulation::Csh,
113            _ => ShellEmulation::Zsh,
114        };
115    }
116
117    /// Check if running in sh/ksh emulation
118    pub fn is_posix_emulation(&self) -> bool {
119        matches!(self.emulation, ShellEmulation::Sh | ShellEmulation::Ksh)
120    }
121}
122
123impl Default for ShellState {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129/// Loop result
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum LoopReturn {
132    Ok,
133    Empty,
134    Error,
135}
136
137/// Source result
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum SourceReturn {
140    Ok,
141    NotFound,
142    Error,
143}
144
145/// Parse command line arguments
146pub fn parseargs(args: &[String]) -> (ShellOptions, Option<String>, Vec<String>) {
147    let mut opts = ShellOptions::default();
148    let mut cmd = None;
149    let mut positional = Vec::new();
150    let mut iter = args.iter().skip(1).peekable();
151    let mut done_opts = false;
152
153    while let Some(arg) = iter.next() {
154        if done_opts || !arg.starts_with('-') && !arg.starts_with('+') {
155            positional.push(arg.clone());
156            done_opts = true;
157            continue;
158        }
159
160        if arg == "--" {
161            done_opts = true;
162            continue;
163        }
164
165        if arg == "--help" {
166            print_help();
167            std::process::exit(0);
168        }
169
170        if arg == "--version" {
171            println!("zshrs {}", env!("CARGO_PKG_VERSION"));
172            std::process::exit(0);
173        }
174
175        let is_set = arg.starts_with('-');
176        let flags: Vec<char> = arg[1..].chars().collect();
177
178        for flag in flags {
179            match flag {
180                'c' => {
181                    if let Some(c) = iter.next() {
182                        cmd = Some(c.clone());
183                        opts.interactive = false;
184                    }
185                }
186                'i' => opts.interactive = is_set,
187                'l' => opts.login = is_set,
188                's' => opts.shin_stdin = is_set,
189                'm' => opts.monitor = is_set,
190                'o' => {
191                    if let Some(opt_name) = iter.next() {
192                        set_option_by_name(&mut opts, opt_name, is_set);
193                    }
194                }
195                _ => {}
196            }
197        }
198    }
199
200    // Defaults based on tty
201    if atty::is(atty::Stream::Stdin) {
202        if !cmd.is_some() {
203            opts.interactive = true;
204        }
205        opts.use_zle = true;
206    }
207
208    (opts, cmd, positional)
209}
210
211fn set_option_by_name(opts: &mut ShellOptions, name: &str, value: bool) {
212    let name_lower = name.to_lowercase().replace('_', "");
213    match name_lower.as_str() {
214        "interactive" => opts.interactive = value,
215        "login" => opts.login = value,
216        "shinstdin" => opts.shin_stdin = value,
217        "zle" | "usezle" => opts.use_zle = value,
218        "monitor" => opts.monitor = value,
219        "hashdirs" => opts.hash_dirs = value,
220        "privileged" => opts.privileged = value,
221        "singlecommand" => opts.single_command = value,
222        "rcs" => opts.rcs = value,
223        "globalrcs" => opts.global_rcs = value,
224        _ => {}
225    }
226}
227
228fn print_help() {
229    println!("Usage: zshrs [<options>] [<argument> ...]");
230    println!();
231    println!("Special options:");
232    println!("  --help     show this message, then exit");
233    println!("  --version  show zshrs version number, then exit");
234    println!("  -c         take first argument as a command to execute");
235    println!("  -i         force interactive mode");
236    println!("  -l         treat as login shell");
237    println!("  -s         read commands from stdin");
238    println!("  -o OPTION  set an option by name");
239}
240
241/// Initialize shell I/O
242pub fn init_io(state: &mut ShellState) {
243    // Try to get tty
244    if atty::is(atty::Stream::Stdin) {
245        state.shtty = 0;
246    }
247
248    if state.options.interactive && state.shtty == -1 {
249        state.options.use_zle = false;
250    }
251}
252
253/// Set up shell values
254pub fn setupvals(state: &mut ShellState) {
255    // Set up PATH
256    if let Ok(path_env) = env::var("PATH") {
257        state.path = path_env.split(':').map(String::from).collect();
258    }
259
260    // Set up prompts based on emulation
261    // (In full implementation, these would be stored in params)
262
263    // Initialize history
264    state.histsize = env::var("HISTSIZE")
265        .ok()
266        .and_then(|s| s.parse().ok())
267        .unwrap_or(1000);
268}
269
270/// Source a file
271pub fn source(state: &mut ShellState, path: &str) -> SourceReturn {
272    let path = Path::new(path);
273
274    if !path.exists() {
275        return SourceReturn::NotFound;
276    }
277
278    state.sourcelevel += 1;
279
280    // In a full implementation, we would:
281    // 1. Open the file
282    // 2. Parse and execute commands
283    // 3. Handle errors
284
285    state.sourcelevel -= 1;
286    SourceReturn::Ok
287}
288
289/// Source a file from home directory
290pub fn sourcehome(state: &mut ShellState, filename: &str) -> SourceReturn {
291    let zdotdir = env::var("ZDOTDIR").unwrap_or_else(|_| state.home.clone());
292    let path = format!("{}/{}", zdotdir, filename);
293    source(state, &path)
294}
295
296/// Run initialization scripts
297pub fn run_init_scripts(state: &mut ShellState) {
298    if state.is_posix_emulation() {
299        // sh/ksh emulation
300        if state.options.login {
301            source(state, "/etc/profile");
302        }
303        if !state.options.privileged {
304            if state.options.login {
305                sourcehome(state, ".profile");
306            }
307            if state.options.interactive {
308                if let Ok(env_file) = env::var("ENV") {
309                    source(state, &env_file);
310                }
311            }
312        }
313    } else {
314        // zsh mode
315        if state.options.rcs && state.options.global_rcs {
316            source(state, "/etc/zshenv");
317        }
318        if state.options.rcs && !state.options.privileged {
319            sourcehome(state, ".zshenv");
320        }
321        if state.options.login {
322            if state.options.rcs && state.options.global_rcs {
323                source(state, "/etc/zprofile");
324            }
325            if state.options.rcs && !state.options.privileged {
326                sourcehome(state, ".zprofile");
327            }
328        }
329        if state.options.interactive {
330            if state.options.rcs && state.options.global_rcs {
331                source(state, "/etc/zshrc");
332            }
333            if state.options.rcs && !state.options.privileged {
334                sourcehome(state, ".zshrc");
335            }
336        }
337        if state.options.login {
338            if state.options.rcs && state.options.global_rcs {
339                source(state, "/etc/zlogin");
340            }
341            if state.options.rcs && !state.options.privileged {
342                sourcehome(state, ".zlogin");
343            }
344        }
345    }
346}
347
348/// Get the executable path of the current process
349pub fn get_exe_path() -> Option<PathBuf> {
350    #[cfg(target_os = "linux")]
351    {
352        std::fs::read_link("/proc/self/exe").ok()
353    }
354
355    #[cfg(target_os = "macos")]
356    {
357        use std::ffi::CStr;
358        let mut buf = [0u8; libc::PATH_MAX as usize];
359        let mut size = buf.len() as u32;
360        unsafe {
361            if libc::_NSGetExecutablePath(buf.as_mut_ptr() as *mut i8, &mut size) == 0 {
362                let path = CStr::from_ptr(buf.as_ptr() as *const i8);
363                Some(PathBuf::from(path.to_string_lossy().into_owned()))
364            } else {
365                None
366            }
367        }
368    }
369
370    #[cfg(not(any(target_os = "linux", target_os = "macos")))]
371    {
372        None
373    }
374}
375
376// ---------------------------------------------------------------------------
377// Missing functions from init.c
378// ---------------------------------------------------------------------------
379
380/// Initialize terminal settings (from init.c init_term)
381pub fn init_term(state: &ShellState) -> bool {
382    let term = &state.term;
383    if term.is_empty() {
384        return false;
385    }
386    // Terminal initialization is handled by the terminfo/termcap modules
387    // This function mainly validates the TERM value
388    !term.is_empty() && term != "dumb"
389}
390
391/// Set up the PWD variable (from init.c set_pwd_env)
392pub fn set_pwd_env(state: &mut ShellState) {
393    if let Ok(cwd) = env::current_dir() {
394        state.pwd = cwd.to_string_lossy().to_string();
395    }
396    env::set_var("PWD", &state.pwd);
397    env::set_var("OLDPWD", &state.oldpwd);
398}
399
400/// Run logout scripts (from init.c run_exit_scripts counterpart)
401pub fn run_exit_scripts(state: &mut ShellState) {
402    if state.options.login {
403        if state.options.rcs && state.options.global_rcs {
404            source(state, "/etc/zlogout");
405        }
406        if state.options.rcs && !state.options.privileged {
407            sourcehome(state, ".zlogout");
408        }
409    }
410}
411
412/// Close the shell (from init.c zexit)
413pub fn zexit(val: i32, from_where: i32) -> ! {
414    // from_where: 0=normal, 1=signal, 2=exec
415    std::process::exit(val)
416}
417
418/// Set up the tty (from init.c init_tty)
419pub fn init_tty(state: &mut ShellState) {
420    #[cfg(unix)]
421    {
422        // Check if stdin is a tty
423        if unsafe { libc::isatty(0) } == 1 {
424            state.shtty = 0;
425            state.options.interactive = true;
426        } else {
427            state.shtty = -1;
428        }
429    }
430}
431
432/// Set up the hash tables (from init.c init_hashtable equivalent)
433pub fn init_hashtable() {
434    // In Rust, hash tables are managed by the exec module
435    // This is a placeholder for compatibility
436}
437
438/// Set up options from emulation mode (from init.c setupvals emulation portion)
439pub fn setup_emulation_opts(state: &mut ShellState) {
440    match state.emulation {
441        ShellEmulation::Sh => {
442            // POSIX sh compatibility
443            state.options.monitor = state.options.interactive;
444        }
445        ShellEmulation::Ksh => {
446            // ksh compatibility
447            state.options.monitor = state.options.interactive;
448        }
449        ShellEmulation::Csh => {
450            // csh compatibility
451        }
452        ShellEmulation::Zsh => {
453            // Default zsh behavior
454            state.options.monitor = state.options.interactive;
455            state.options.hash_dirs = true;
456        }
457    }
458}
459
460/// Find a command in PATH (from init.c pathprog equivalent)
461pub fn pathprog(prog: &str, path: &[String]) -> Option<PathBuf> {
462    if prog.contains('/') {
463        let p = PathBuf::from(prog);
464        if p.exists() {
465            return Some(p);
466        }
467        return None;
468    }
469    for dir in path {
470        let candidate = PathBuf::from(dir).join(prog);
471        if candidate.exists() {
472            #[cfg(unix)]
473            {
474                use std::os::unix::fs::PermissionsExt;
475                if let Ok(meta) = std::fs::metadata(&candidate) {
476                    if meta.permissions().mode() & 0o111 != 0 {
477                        return Some(candidate);
478                    }
479                }
480            }
481            #[cfg(not(unix))]
482            {
483                return Some(candidate);
484            }
485        }
486    }
487    None
488}
489
490/// Determine if shell is a login shell from `argv[0]`
491pub fn is_login_shell(argv0: &str) -> bool {
492    argv0.starts_with('-')
493}
494
495/// Get the ZDOTDIR
496pub fn get_zdotdir() -> String {
497    env::var("ZDOTDIR").unwrap_or_else(|_| env::var("HOME").unwrap_or_else(|_| ".".to_string()))
498}
499
500/// Full initialization sequence (from init.c init_main)
501pub fn init_main(args: &[String]) -> ShellState {
502    let (opts, cmd, positional) = parseargs(args);
503    let mut state = ShellState::new();
504    state.options = opts;
505
506    // Determine shell name from argv[0]
507    if let Some(arg0) = args.first() {
508        if is_login_shell(arg0) {
509            state.options.login = true;
510        }
511        state.emulate_from_name(arg0);
512        state.argv0 = arg0.clone();
513        state.argzero = arg0.clone();
514        state.posixzero = arg0.clone();
515    }
516
517    // Set up tty
518    init_tty(&mut state);
519
520    // Set up values
521    setupvals(&mut state);
522
523    // Set up emulation-specific options
524    setup_emulation_opts(&mut state);
525
526    // Set PWD
527    set_pwd_env(&mut state);
528
529    // Run init scripts
530    run_init_scripts(&mut state);
531
532    state
533}
534
535#[cfg(test)]
536mod tests {
537    use super::*;
538
539    #[test]
540    fn test_shell_state_new() {
541        let state = ShellState::new();
542        assert!(!state.options.interactive);
543        assert!(state.options.rcs);
544    }
545
546    #[test]
547    fn test_emulate_from_name() {
548        let mut state = ShellState::new();
549
550        state.emulate_from_name("zsh");
551        assert_eq!(state.emulation, ShellEmulation::Zsh);
552
553        state.emulate_from_name("/bin/sh");
554        assert_eq!(state.emulation, ShellEmulation::Sh);
555
556        state.emulate_from_name("-ksh");
557        assert_eq!(state.emulation, ShellEmulation::Ksh);
558    }
559
560    #[test]
561    fn test_parseargs_basic() {
562        let args = vec!["zsh".to_string()];
563        let (opts, cmd, positional) = parseargs(&args);
564        assert!(cmd.is_none());
565        assert!(positional.is_empty());
566    }
567
568    #[test]
569    fn test_parseargs_command() {
570        let args = vec![
571            "zsh".to_string(),
572            "-c".to_string(),
573            "echo hello".to_string(),
574        ];
575        let (opts, cmd, _) = parseargs(&args);
576        assert_eq!(cmd, Some("echo hello".to_string()));
577        assert!(!opts.interactive);
578    }
579
580    #[test]
581    fn test_parseargs_interactive() {
582        let args = vec!["zsh".to_string(), "-i".to_string()];
583        let (opts, _, _) = parseargs(&args);
584        assert!(opts.interactive);
585    }
586
587    #[test]
588    fn test_is_posix_emulation() {
589        let mut state = ShellState::new();
590
591        state.emulation = ShellEmulation::Zsh;
592        assert!(!state.is_posix_emulation());
593
594        state.emulation = ShellEmulation::Sh;
595        assert!(state.is_posix_emulation());
596
597        state.emulation = ShellEmulation::Ksh;
598        assert!(state.is_posix_emulation());
599    }
600}