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(); 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 '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}"), (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
168pub 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 if let Err(e) = Sys::set_umask(umask) {
235 eprintln!("umask: {e}");
236 }
237 1
238 }
239 }
240 } else {
241 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 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 _ => 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
490pub 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 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}