shell/
builtins.rs

1use crate::command_data::Arg;
2use crate::jobs::Jobs;
3use crate::platform::{Platform, RLimit, RLimitVals, Sys};
4use std::collections::HashSet;
5use std::env;
6use std::ffi::{OsStr, OsString};
7use std::path::{Path, PathBuf};
8
9fn set_arg_flags<S: AsRef<str>>(
10    args: &mut HashSet<char>,
11    allowed: &str,
12    arg: S,
13) -> Result<(), String> {
14    let mut arg = arg.as_ref().chars();
15    arg.next(); // Skip over the leading '-'
16    for c in arg {
17        if allowed.contains(c) {
18            args.insert(c);
19        } else {
20            return Err(format!("invalid arg {c}"));
21        }
22    }
23    Ok(())
24}
25
26fn ulimit_parm_list(args: &HashSet<char>) -> Result<Vec<RLimit>, String> {
27    let mut limits = Vec::new();
28    for ch in args {
29        match ch {
30            'H' | 'S' | 'a' => {}
31            'b' => limits.push(RLimit::SocketBufferSize),
32            'c' => limits.push(RLimit::CoreSize),
33            'd' => limits.push(RLimit::DataSize),
34            'e' => limits.push(RLimit::Nice),
35            'f' => limits.push(RLimit::FileSize),
36            'i' => limits.push(RLimit::SigPending),
37            'k' => limits.push(RLimit::KQueues),
38            'l' => limits.push(RLimit::MemLock),
39            'm' => limits.push(RLimit::RSS),
40            'n' => limits.push(RLimit::MaxFiles),
41            //'p' => limits.push(RLimit::SocketBufferSize),
42            'q' => limits.push(RLimit::MessageQueueByte),
43            'r' => limits.push(RLimit::RealTimePriority),
44            's' => limits.push(RLimit::StackSize),
45            't' => limits.push(RLimit::CpuTime),
46            'u' => limits.push(RLimit::MaxProcs),
47            'v' => limits.push(RLimit::MaxMemory),
48            'x' => limits.push(RLimit::MaxFileLocks),
49            'P' => limits.push(RLimit::MaxPtty),
50            'R' => limits.push(RLimit::MaxRealTime),
51            'T' => limits.push(RLimit::MaxThreads),
52            _ => return Err(format!("unknown option {ch}")),
53        }
54    }
55    Ok(limits)
56}
57
58fn ulimit<I>(args: I, _jobs: &mut Jobs) -> i32
59where
60    I: Iterator<Item = OsString>,
61{
62    let mut args_flags = HashSet::new();
63    let mut limit = None;
64    for arg in args {
65        if arg.to_string_lossy().starts_with('-') {
66            if let Err(err) = set_arg_flags(
67                &mut args_flags,
68                "SHabcdefiklmnpqrstuvxPT",
69                arg.to_string_lossy(),
70            ) {
71                eprintln!("ulimit: {err}");
72                eprintln!("ulimit: usage: ulimit [-SHabcdefiklmnpqrstuvxPRT] [limit]");
73                return 1;
74            }
75        } else if limit.is_none() {
76            limit = Some(arg.to_string_lossy().to_string());
77        } else {
78            eprintln!("ulimit: invalid parameters");
79            eprintln!("ulimit: usage: ulimit [-SHabcdefiklmnpqrstuvxPT] [limit]");
80            return 1;
81        }
82    }
83    let limits = match ulimit_parm_list(&args_flags) {
84        Ok(limits) => limits,
85        Err(err) => {
86            eprintln!("ulimit: {err}");
87            eprintln!("ulimit: usage: ulimit [-SHabcdefiklmnpqrstuvxPRT] [limit]");
88            return 1;
89        }
90    };
91    if let Some(limit) = limit {
92        let limit = match limit.as_ref() {
93            "unlimited" => u64::MAX,
94            _ => match limit.parse() {
95                Ok(limit) => limit,
96                Err(_err) => {
97                    eprintln!("ulimit: invalid limit");
98                    return 1;
99                }
100            },
101        };
102        let vals = RLimitVals {
103            current: limit,
104            max: limit,
105        };
106        for l in limits {
107            if let Err(err) = Sys::set_rlimit(l, vals) {
108                eprintln!("ulimit: {err}");
109                return 1;
110            }
111        }
112    } else {
113        for l in limits {
114            match Sys::get_rlimit(l) {
115                Ok(lim) => {
116                    match (lim.current, lim.max) {
117                        (u64::MAX, u64::MAX) => println!("unlimited"),
118                        (u64::MAX, max) => println!("unlimited/{max}"), // WTF...
119                        (current, u64::MAX) => println!("{current}/unlimited"),
120                        (current, max) if current == max => println!("{current}"),
121                        (current, max) => println!("{current}/{max}"),
122                    }
123                }
124                Err(err) => eprintln!("ulimit: {err}"),
125            }
126        }
127    }
128    0
129}
130
131pub fn cd(arg: Option<PathBuf>) -> i32 {
132    let home: OsString = match env::var_os("HOME") {
133        Some(val) => val,
134        None => "/".into(),
135    };
136    let old_dir: OsString = match env::var_os("OLDPWD") {
137        Some(val) => val,
138        None => home.clone(),
139    };
140    let new_dir: PathBuf = match arg {
141        Some(arg) => arg,
142        None => home.into(),
143    };
144    let new_dir = if new_dir.as_os_str() == "-" {
145        old_dir.into()
146    } else {
147        new_dir
148    };
149    let new_dir = cd_expand_all_dots(new_dir);
150    let root = Path::new(&new_dir);
151    if let Ok(oldpwd) = env::current_dir() {
152        unsafe {
153            env::set_var("OLDPWD", oldpwd);
154        }
155    }
156    if let Err(e) = env::set_current_dir(root) {
157        eprintln!("cd: Error changing to {}, {}", root.display(), e);
158        -1
159    } else {
160        match env::current_dir() {
161            Ok(dir) => unsafe { env::set_var("PWD", dir) },
162            Err(err) => eprintln!("cd: error setting PWD: {err}"),
163        }
164        0
165    }
166}
167
168/// If path start with HOME then replace with ~.
169pub fn compress_tilde(path: &str) -> Option<String> {
170    if let Ok(mut home) = env::var("HOME") {
171        if home.ends_with('/') {
172            home.pop();
173        }
174        if path.starts_with(&home) {
175            Some(path.replace(&home, "~"))
176        } else {
177            None
178        }
179    } else {
180        None
181    }
182}
183
184fn cd_expand_all_dots(cd: PathBuf) -> PathBuf {
185    let mut all_dots = false;
186    let cd_ref = cd.to_string_lossy();
187    if cd_ref.len() > 2 {
188        all_dots = true;
189        for ch in cd_ref.chars() {
190            if ch != '.' {
191                all_dots = false;
192                break;
193            }
194        }
195    }
196    if all_dots {
197        let mut new_cd = OsString::new();
198        let paths_up = cd_ref.len() - 2;
199        new_cd.push("../");
200        for _i in 0..paths_up {
201            new_cd.push("../");
202        }
203        new_cd.into()
204    } else {
205        cd
206    }
207}
208
209fn export(key: OsString, val: OsString) -> i32 {
210    let key_str = key.to_string_lossy();
211    if key.is_empty() || key_str.contains('=') || key_str.contains('\0') {
212        eprintln!("export: Invalid key");
213        return 1;
214    }
215    let val_str = val.to_string_lossy();
216    if val_str.contains('\0') {
217        eprintln!("export: Invalid val (contains NUL character ('\\0')'");
218        return 1;
219    }
220    unsafe {
221        env::set_var(key, val);
222    }
223    0
224}
225
226fn umask(mask_str: Option<OsString>) -> i32 {
227    let umask = Sys::get_and_clear_umask();
228    if let Some(arg) = mask_str {
229        match Sys::merge_and_set_umask(umask, &arg.to_string_lossy()) {
230            Ok(_umask) => 0,
231            Err(e) => {
232                eprintln!("umask: {e}");
233                // Set the umask back.
234                if let Err(e) = Sys::set_umask(umask) {
235                    eprintln!("umask: {e}");
236                }
237                1
238            }
239        }
240    } else {
241        // put umask back, no change
242        if let Err(e) = Sys::set_umask(umask) {
243            eprintln!("umask: {e}");
244            1
245        } else {
246            match Sys::to_octal_string(umask) {
247                Ok(ostr) => {
248                    println!("{ostr}");
249                    0
250                }
251                Err(e) => {
252                    eprintln!("umask: {e}");
253                    1
254                }
255            }
256        }
257    }
258}
259
260fn set_var(jobs: &mut Jobs, key: OsString, val: OsString) -> i32 {
261    let key_str = key.to_string_lossy();
262    if key.is_empty() || key_str.contains('=') || key_str.contains('\0') {
263        eprintln!("export: Invalid key");
264        return 1;
265    }
266    let val_str = val.to_string_lossy();
267    if val_str.contains('\0') {
268        eprintln!("export: Invalid val (contains NUL character ('\\0')'");
269        return 1;
270    }
271    if env::var_os(&key).is_some() {
272        unsafe {
273            env::set_var(key, val);
274        }
275    } else {
276        jobs.set_local_var(key, val);
277    }
278    0
279}
280
281fn alias<I>(args: I, jobs: &mut Jobs) -> i32
282where
283    I: Iterator<Item = OsString>,
284{
285    let mut status = 0;
286    let mut empty = true;
287    let mut args = args.map(|a| a.to_string_lossy().to_string());
288    while let Some(a) = args.next() {
289        empty = false;
290        if a.contains('=') {
291            let mut key_val = a.split('=');
292            if let (Some(key), val, None) = (key_val.next(), key_val.next(), key_val.next()) {
293                let val = if let Some(val) = val {
294                    if val.is_empty() {
295                        args.next().unwrap_or_default()
296                    } else {
297                        val.to_string()
298                    }
299                } else {
300                    args.next().unwrap_or_default()
301                };
302                if let Err(e) = jobs.add_alias(key.to_string(), val) {
303                    eprintln!("alias: error setting {key}: {e}");
304                    status = 1;
305                }
306            } else {
307                eprintln!("alias: invalid arg {a}, use ALIAS_NAME=\"command\"");
308                status = 1;
309            }
310        } else {
311            jobs.print_alias(a.to_string());
312        }
313    }
314    if empty {
315        jobs.print_all_alias();
316    }
317    status
318}
319
320fn unalias<I>(args: I, jobs: &mut Jobs) -> i32
321where
322    I: Iterator<Item = OsString>,
323{
324    for arg in args {
325        let arg = arg.to_string_lossy();
326        if arg == "-a" {
327            jobs.clear_aliases();
328        } else if jobs.remove_alias(&arg).is_none() {
329            eprintln!("not found {}", arg);
330        }
331    }
332    0
333}
334
335pub fn run_builtin<'arg, I>(command: &OsStr, args: &mut I, jobs: &mut Jobs) -> Option<i32>
336where
337    I: Iterator<Item = &'arg Arg>,
338{
339    let mut args = args.map(|v| v.resolve_arg(jobs).unwrap_or_default());
340    let command_str = command.to_string_lossy();
341    // TODO #247 how can we document builtins?
342    let status = match &*command_str {
343        "cd" => {
344            let arg = args.next();
345            if args.next().is_none() {
346                cd(arg.map(|s| s.into()))
347            } else {
348                eprintln!("cd: too many arguments!");
349                1
350            }
351        }
352        "fg" => {
353            if let Some(arg) = args.next() {
354                if args.next().is_none() {
355                    if let Some(Ok(job_num)) = arg.to_str().map(|s| s.parse()) {
356                        jobs.foreground_job(job_num);
357                    }
358                    0
359                } else {
360                    eprintln!("fg: takes one argument!");
361                    1
362                }
363            } else {
364                eprintln!("fg: takes one argument!");
365                1
366            }
367        }
368        "bg" => {
369            if let Some(arg) = args.next() {
370                if args.next().is_none() {
371                    if let Some(Ok(job_num)) = arg.to_str().map(|s| s.parse()) {
372                        jobs.background_job(job_num);
373                    }
374                    0
375                } else {
376                    eprintln!("fg: takes one argument!");
377                    1
378                }
379            } else {
380                eprintln!("fg: takes one argument!");
381                1
382            }
383        }
384        "jobs" => {
385            if args.next().is_some() {
386                eprintln!("jobs: too many arguments!");
387                1
388            } else {
389                println!("{jobs}");
390                0
391            }
392        }
393        "export" => {
394            fn split_export<S: AsRef<str>>(arg: S) -> i32 {
395                let arg = arg.as_ref();
396                let mut key_val = arg.splitn(2, '=');
397                if let (Some(key), Some(val)) = (key_val.next(), key_val.next()) {
398                    export(key.into(), val.into())
399                } else {
400                    eprintln!("export: VAR_NAME=VALUE");
401                    1
402                }
403            }
404            let ceq: OsString = "=".into();
405            match (args.next(), args.next(), args.next(), args.next()) {
406                (Some(arg), None, None, None) => {
407                    if arg.to_string_lossy().contains('=') {
408                        split_export(arg.to_string_lossy())
409                    } else {
410                        if let Some(val) = jobs.remove_local_var(&arg) {
411                            unsafe { env::set_var(arg, val) }
412                        }
413                        0
414                    }
415                }
416                (Some(arg1), Some(arg2), None, None) => {
417                    let arg = arg1.to_string_lossy() + arg2.to_string_lossy();
418                    split_export(arg)
419                }
420                (Some(arg), Some(eq), Some(val), None) if eq == ceq => export(arg, val),
421                _ => {
422                    eprintln!("export: VAR_NAME=VALUE");
423                    1
424                }
425            }
426        }
427        "umask" => match (args.next(), args.next()) {
428            (arg, None) => umask(arg),
429            _ => {
430                eprintln!("umask: takes one optional argument");
431                1
432            }
433        },
434        "unset" => match (args.next(), args.next()) {
435            (Some(key), None) => {
436                let key_str = key.to_string_lossy();
437                if key.is_empty() || key_str.contains('=') || key_str.contains('\x00') {
438                    eprintln!("unset: Invalid key");
439                    1
440                } else {
441                    jobs.remove_local_var(&key);
442                    unsafe {
443                        env::remove_var(key);
444                    }
445                    0
446                }
447            }
448            _ => {
449                eprintln!("unset: VAR_NAME");
450                1
451            }
452        },
453        "alias" => {
454            let args: Vec<OsString> = args.collect();
455            alias(args.into_iter(), jobs)
456        }
457        "unalias" => {
458            let args: Vec<OsString> = args.collect();
459            unalias(args.into_iter(), jobs)
460        }
461        "ulimit" => {
462            let args: Vec<OsString> = args.collect();
463            ulimit(args.into_iter(), jobs)
464        }
465        // Check for VAR_NAME=val before returning.
466        _ => match (args.next(), args.next()) {
467            (None, None) if command_str.contains('=') => {
468                let mut key_val = command_str.split('=');
469                if let (Some(key), Some(val), None) =
470                    (key_val.next(), key_val.next(), key_val.next())
471                {
472                    set_var(jobs, key.into(), val.into())
473                } else {
474                    return None;
475                }
476            }
477            (Some(val), None) if command_str.ends_with('=') => {
478                if let Some(key) = command_str.strip_suffix('=') {
479                    set_var(jobs, key.into(), val)
480                } else {
481                    return None;
482                }
483            }
484            _ => return None,
485        },
486    };
487    Some(status)
488}
489
490/// Takes a PathBuf, if it contains ~ then replaces it with HOME and returns the new PathBuf, else
491/// returns path.  If path is not utf-8 then it will not expand ~.
492pub fn expand_tilde(path: PathBuf) -> PathBuf {
493    if let Some(path_str) = path.to_str() {
494        if path_str.contains('~') {
495            let home: PathBuf = match env::var_os("HOME") {
496                Some(val) => val.into(),
497                None => "/".into(),
498            };
499            let mut new_path = OsString::new();
500            let mut last_ch = ' ';
501            let mut buf = [0_u8; 4];
502            let mut quoted = false;
503            for ch in path_str.chars() {
504                if ch == '\'' && last_ch != '\\' {
505                    quoted = !quoted;
506                }
507                if ch == '~' && !quoted && (last_ch == ' ' || last_ch == ':' || last_ch == '=') {
508                    // Strip the trailing / if it exists.
509                    new_path.push(home.components().as_path().as_os_str());
510                } else {
511                    new_path.push(ch.encode_utf8(&mut buf));
512                }
513                last_ch = ch;
514            }
515            new_path.into()
516        } else {
517            path
518        }
519    } else {
520        path
521    }
522}