1use std::env;
8use std::path::{Path, PathBuf};
9
10#[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
25pub 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#[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, 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 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
131pub enum LoopReturn {
132 Ok,
133 Empty,
134 Error,
135}
136
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum SourceReturn {
140 Ok,
141 NotFound,
142 Error,
143}
144
145pub 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 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
241pub fn init_io(state: &mut ShellState) {
243 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
253pub fn setupvals(state: &mut ShellState) {
255 if let Ok(path_env) = env::var("PATH") {
257 state.path = path_env.split(':').map(String::from).collect();
258 }
259
260 state.histsize = env::var("HISTSIZE")
265 .ok()
266 .and_then(|s| s.parse().ok())
267 .unwrap_or(1000);
268}
269
270pub 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 state.sourcelevel -= 1;
286 SourceReturn::Ok
287}
288
289pub 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
296pub fn run_init_scripts(state: &mut ShellState) {
298 if state.is_posix_emulation() {
299 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 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
348pub 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
376pub fn init_term(state: &ShellState) -> bool {
382 let term = &state.term;
383 if term.is_empty() {
384 return false;
385 }
386 !term.is_empty() && term != "dumb"
389}
390
391pub 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
400pub 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
412pub fn zexit(val: i32, from_where: i32) -> ! {
414 std::process::exit(val)
416}
417
418pub fn init_tty(state: &mut ShellState) {
420 #[cfg(unix)]
421 {
422 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
432pub fn init_hashtable() {
434 }
437
438pub fn setup_emulation_opts(state: &mut ShellState) {
440 match state.emulation {
441 ShellEmulation::Sh => {
442 state.options.monitor = state.options.interactive;
444 }
445 ShellEmulation::Ksh => {
446 state.options.monitor = state.options.interactive;
448 }
449 ShellEmulation::Csh => {
450 }
452 ShellEmulation::Zsh => {
453 state.options.monitor = state.options.interactive;
455 state.options.hash_dirs = true;
456 }
457 }
458}
459
460pub 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
490pub fn is_login_shell(argv0: &str) -> bool {
492 argv0.starts_with('-')
493}
494
495pub fn get_zdotdir() -> String {
497 env::var("ZDOTDIR").unwrap_or_else(|_| env::var("HOME").unwrap_or_else(|_| ".".to_string()))
498}
499
500pub 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 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 init_tty(&mut state);
519
520 setupvals(&mut state);
522
523 setup_emulation_opts(&mut state);
525
526 set_pwd_env(&mut state);
528
529 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}