1use std::collections::HashMap;
9use std::env;
10use std::process::{Command, Stdio};
11
12pub mod prefork {
14 pub const SINGLE: u32 = 1; pub const SPLIT: u32 = 2; pub const SHWORDSPLIT: u32 = 4; pub const NOSHWORDSPLIT: u32 = 8; pub const ASSIGN: u32 = 16; pub const TYPESET: u32 = 32; pub const SUBEXP: u32 = 64; pub const KEY_VALUE: u32 = 128; }
23
24pub fn subst_string(
26 s: &str,
27 params: &HashMap<String, String>,
28 opts: &SubstOptions,
29) -> Result<String, String> {
30 let mut result = s.to_string();
31
32 result = tilde_expand(&result, opts)?;
34
35 result = param_expand(&result, params, opts)?;
37
38 result = command_subst(&result, opts)?;
40
41 result = arith_expand(&result, params, opts)?;
43
44 Ok(result)
45}
46
47#[derive(Clone, Debug, Default)]
49pub struct SubstOptions {
50 pub noglob: bool,
51 pub noexec: bool,
52 pub nounset: bool,
53 pub word_split: bool,
54 pub ignore_braces: bool,
55}
56
57pub fn tilde_expand(s: &str, _opts: &SubstOptions) -> Result<String, String> {
59 if !s.starts_with('~') {
60 return Ok(s.to_string());
61 }
62
63 let rest = &s[1..];
64
65 let (user, suffix) = match rest.find('/') {
67 Some(pos) => (&rest[..pos], &rest[pos..]),
68 None => (rest, ""),
69 };
70
71 let expanded = if user.is_empty() {
72 env::var("HOME").unwrap_or_else(|_| "/".to_string())
74 } else if user.starts_with('+') {
75 env::var("PWD").unwrap_or_else(|_| ".".to_string())
77 } else if user.starts_with('-') {
78 env::var("OLDPWD").unwrap_or_else(|_| ".".to_string())
80 } else {
81 #[cfg(unix)]
83 {
84 get_user_home(user).unwrap_or_else(|| format!("~{}", user))
85 }
86 #[cfg(not(unix))]
87 {
88 format!("~{}", user)
89 }
90 };
91
92 Ok(format!("{}{}", expanded, suffix))
93}
94
95#[cfg(unix)]
96fn get_user_home(user: &str) -> Option<String> {
97 use std::ffi::CString;
98 unsafe {
99 let c_user = CString::new(user).ok()?;
100 let pw = libc::getpwnam(c_user.as_ptr());
101 if pw.is_null() {
102 return None;
103 }
104 let dir = std::ffi::CStr::from_ptr((*pw).pw_dir);
105 dir.to_str().ok().map(|s| s.to_string())
106 }
107}
108
109pub fn param_expand(
111 s: &str,
112 params: &HashMap<String, String>,
113 opts: &SubstOptions,
114) -> Result<String, String> {
115 let mut result = String::new();
116 let mut chars = s.chars().peekable();
117
118 while let Some(c) = chars.next() {
119 if c == '$' {
120 match chars.peek() {
121 Some(&'{') => {
122 chars.next();
124 let expanded = parse_brace_param(&mut chars, params, opts)?;
125 result.push_str(&expanded);
126 }
127 Some(&'(') => {
128 chars.next();
130 if chars.peek() == Some(&'(') {
131 chars.next();
133 let expr = collect_until(&mut chars, ')');
134 if chars.next() != Some(')') {
135 return Err("Missing )) in arithmetic expansion".to_string());
136 }
137 let value = eval_arith(&expr, params)?;
138 result.push_str(&value.to_string());
139 } else {
140 let cmd = collect_balanced(&mut chars, '(', ')');
142 if !opts.noexec {
143 let output = run_command(&cmd)?;
144 result.push_str(output.trim_end_matches('\n'));
145 }
146 }
147 }
148 Some(&c) if c.is_ascii_alphabetic() || c == '_' => {
149 chars.next();
151 let name = collect_varname(&mut chars);
152 let full_name = format!("{}{}", c, name);
153
154 if let Some(value) = params.get(&full_name) {
155 result.push_str(value);
156 } else if let Ok(value) = env::var(&full_name) {
157 result.push_str(&value);
158 } else if opts.nounset {
159 return Err(format!("{}: parameter not set", full_name));
160 }
161 }
162 Some(&c) if c.is_ascii_digit() => {
163 let mut num = String::new();
165 while let Some(&c) = chars.peek() {
166 if c.is_ascii_digit() {
167 num.push(chars.next().unwrap());
168 } else {
169 break;
170 }
171 }
172 }
174 Some(&'?') => {
175 chars.next();
176 result.push_str("0"); }
178 Some(&'$') => {
179 chars.next();
180 result.push_str(&std::process::id().to_string());
181 }
182 Some(&'#') => {
183 chars.next();
184 result.push_str("0"); }
186 Some(&'*') | Some(&'@') => {
187 chars.next();
188 }
190 _ => result.push('$'),
191 }
192 } else {
193 result.push(c);
194 }
195 }
196
197 Ok(result)
198}
199
200fn parse_brace_param(
201 chars: &mut std::iter::Peekable<std::str::Chars>,
202 params: &HashMap<String, String>,
203 _opts: &SubstOptions,
204) -> Result<String, String> {
205 let mut name = String::new();
206 let mut operator = None;
207 let mut operand = String::new();
208
209 let prefix = match chars.peek() {
211 Some(&'#') => {
212 chars.next();
213 if chars.peek() == Some(&'}') {
214 chars.next();
216 return Ok("0".to_string());
217 }
218 Some('#') }
220 Some(&'!') => {
221 chars.next();
222 Some('!') }
224 _ => None,
225 };
226
227 while let Some(&c) = chars.peek() {
229 if c.is_ascii_alphanumeric() || c == '_' {
230 name.push(chars.next().unwrap());
231 } else {
232 break;
233 }
234 }
235
236 match chars.peek() {
238 Some(&':') => {
239 chars.next();
240 match chars.peek() {
241 Some(&'-') => {
242 chars.next();
243 operator = Some(":-");
244 }
245 Some(&'=') => {
246 chars.next();
247 operator = Some(":=");
248 }
249 Some(&'+') => {
250 chars.next();
251 operator = Some(":+");
252 }
253 Some(&'?') => {
254 chars.next();
255 operator = Some(":?");
256 }
257 _ => operator = Some(":"),
258 }
259 }
260 Some(&'-') => {
261 chars.next();
262 operator = Some("-");
263 }
264 Some(&'=') => {
265 chars.next();
266 operator = Some("=");
267 }
268 Some(&'+') => {
269 chars.next();
270 operator = Some("+");
271 }
272 Some(&'?') => {
273 chars.next();
274 operator = Some("?");
275 }
276 Some(&'#') => {
277 chars.next();
278 operator = Some("#");
279 }
280 Some(&'%') => {
281 chars.next();
282 operator = Some("%");
283 }
284 Some(&'/') => {
285 chars.next();
286 operator = Some("/");
287 }
288 Some(&'^') => {
289 chars.next();
290 operator = Some("^");
291 }
292 Some(&',') => {
293 chars.next();
294 operator = Some(",");
295 }
296 _ => {}
297 }
298
299 let mut depth = 1;
301 while depth > 0 {
302 match chars.next() {
303 Some('{') => depth += 1,
304 Some('}') => depth -= 1,
305 Some(c) if depth > 0 => operand.push(c),
306 None => return Err("Missing } in parameter expansion".to_string()),
307 _ => {}
308 }
309 }
310
311 let value = params.get(&name).cloned().or_else(|| env::var(&name).ok());
313
314 if let Some('#') = prefix {
316 return Ok(value.map(|v| v.len()).unwrap_or(0).to_string());
318 }
319
320 match operator {
322 Some(":-") | Some("-") => {
323 if value.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
324 Ok(operand)
325 } else {
326 Ok(value.unwrap_or_default())
327 }
328 }
329 Some(":+") | Some("+") => {
330 if value.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
331 Ok(operand)
332 } else {
333 Ok(String::new())
334 }
335 }
336 Some(":?") | Some("?") => {
337 if value.as_ref().map(|v| v.is_empty()).unwrap_or(true) {
338 Err(if operand.is_empty() {
339 format!("{}: parameter null or not set", name)
340 } else {
341 operand
342 })
343 } else {
344 Ok(value.unwrap_or_default())
345 }
346 }
347 Some("#") => {
348 if let Some(v) = value {
350 Ok(remove_prefix(&v, &operand, false))
351 } else {
352 Ok(String::new())
353 }
354 }
355 Some("##") => {
356 if let Some(v) = value {
358 Ok(remove_prefix(&v, &operand, true))
359 } else {
360 Ok(String::new())
361 }
362 }
363 Some("%") => {
364 if let Some(v) = value {
366 Ok(remove_suffix(&v, &operand, false))
367 } else {
368 Ok(String::new())
369 }
370 }
371 Some("%%") => {
372 if let Some(v) = value {
374 Ok(remove_suffix(&v, &operand, true))
375 } else {
376 Ok(String::new())
377 }
378 }
379 Some("^") => {
380 if let Some(v) = value {
382 let mut c = v.chars();
383 match c.next() {
384 Some(first) => Ok(first.to_uppercase().collect::<String>() + c.as_str()),
385 None => Ok(String::new()),
386 }
387 } else {
388 Ok(String::new())
389 }
390 }
391 Some("^^") => {
392 Ok(value.map(|v| v.to_uppercase()).unwrap_or_default())
394 }
395 Some(",") => {
396 if let Some(v) = value {
398 let mut c = v.chars();
399 match c.next() {
400 Some(first) => Ok(first.to_lowercase().collect::<String>() + c.as_str()),
401 None => Ok(String::new()),
402 }
403 } else {
404 Ok(String::new())
405 }
406 }
407 Some(",,") => {
408 Ok(value.map(|v| v.to_lowercase()).unwrap_or_default())
410 }
411 Some("/") => {
412 if let Some(v) = value {
414 let parts: Vec<&str> = operand.splitn(2, '/').collect();
415 if parts.len() == 2 {
416 Ok(v.replacen(parts[0], parts[1], 1))
417 } else {
418 Ok(v.replacen(parts[0], "", 1))
419 }
420 } else {
421 Ok(String::new())
422 }
423 }
424 Some("//") => {
425 if let Some(v) = value {
427 let parts: Vec<&str> = operand.splitn(2, '/').collect();
428 if parts.len() == 2 {
429 Ok(v.replace(parts[0], parts[1]))
430 } else {
431 Ok(v.replace(parts[0], ""))
432 }
433 } else {
434 Ok(String::new())
435 }
436 }
437 _ => Ok(value.unwrap_or_default()),
438 }
439}
440
441fn remove_prefix(s: &str, pattern: &str, greedy: bool) -> String {
442 if greedy {
444 for i in (0..=s.len()).rev() {
445 if s[..i].ends_with(pattern) || (pattern == "*" && i > 0) {
446 return s[i..].to_string();
447 }
448 }
449 } else {
450 for i in 0..=s.len() {
451 if s[..i].ends_with(pattern) || (pattern == "*" && i > 0) {
452 return s[i..].to_string();
453 }
454 }
455 }
456 s.to_string()
457}
458
459fn remove_suffix(s: &str, pattern: &str, greedy: bool) -> String {
460 if greedy {
462 for i in 0..=s.len() {
463 if s[i..].starts_with(pattern) || (pattern == "*" && i < s.len()) {
464 return s[..i].to_string();
465 }
466 }
467 } else {
468 for i in (0..=s.len()).rev() {
469 if s[i..].starts_with(pattern) || (pattern == "*" && i < s.len()) {
470 return s[..i].to_string();
471 }
472 }
473 }
474 s.to_string()
475}
476
477fn collect_varname(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
478 let mut name = String::new();
479 while let Some(&c) = chars.peek() {
480 if c.is_ascii_alphanumeric() || c == '_' {
481 name.push(chars.next().unwrap());
482 } else {
483 break;
484 }
485 }
486 name
487}
488
489fn collect_until(chars: &mut std::iter::Peekable<std::str::Chars>, end: char) -> String {
490 let mut result = String::new();
491 while let Some(&c) = chars.peek() {
492 if c == end {
493 break;
494 }
495 result.push(chars.next().unwrap());
496 }
497 result
498}
499
500fn collect_balanced(
501 chars: &mut std::iter::Peekable<std::str::Chars>,
502 open: char,
503 close: char,
504) -> String {
505 let mut result = String::new();
506 let mut depth = 1;
507
508 while depth > 0 {
509 match chars.next() {
510 Some(c) if c == open => {
511 depth += 1;
512 result.push(c);
513 }
514 Some(c) if c == close => {
515 depth -= 1;
516 if depth > 0 {
517 result.push(c);
518 }
519 }
520 Some(c) => result.push(c),
521 None => break,
522 }
523 }
524
525 result
526}
527
528pub fn command_subst(s: &str, opts: &SubstOptions) -> Result<String, String> {
530 if opts.noexec {
531 return Ok(s.to_string());
532 }
533
534 let mut result = String::new();
535 let mut chars = s.chars().peekable();
536
537 while let Some(c) = chars.next() {
538 if c == '`' {
539 let cmd = collect_until(&mut chars, '`');
541 chars.next(); let output = run_command(&cmd)?;
543 result.push_str(output.trim_end_matches('\n'));
544 } else {
545 result.push(c);
546 }
547 }
548
549 Ok(result)
550}
551
552fn run_command(cmd: &str) -> Result<String, String> {
553 let output = Command::new("sh")
554 .arg("-c")
555 .arg(cmd)
556 .stdin(Stdio::null())
557 .stdout(Stdio::piped())
558 .stderr(Stdio::inherit())
559 .output()
560 .map_err(|e| e.to_string())?;
561
562 String::from_utf8(output.stdout).map_err(|e| e.to_string())
563}
564
565pub fn arith_expand(
567 s: &str,
568 params: &HashMap<String, String>,
569 opts: &SubstOptions,
570) -> Result<String, String> {
571 let mut result = String::new();
572 let mut chars = s.chars().peekable();
573
574 while let Some(c) = chars.next() {
575 if c == '$' && chars.peek() == Some(&'[') {
576 chars.next();
578 let expr = collect_until(&mut chars, ']');
579 chars.next(); if !opts.noexec {
582 let value = eval_arith(&expr, params)?;
583 result.push_str(&value.to_string());
584 }
585 } else {
586 result.push(c);
587 }
588 }
589
590 Ok(result)
591}
592
593fn eval_arith(expr: &str, _params: &HashMap<String, String>) -> Result<i64, String> {
594 let expr = expr.trim();
597
598 if let Ok(n) = expr.parse::<i64>() {
600 return Ok(n);
601 }
602
603 if let Some(pos) = expr.find('+') {
605 let left = expr[..pos]
606 .trim()
607 .parse::<i64>()
608 .map_err(|e| e.to_string())?;
609 let right = expr[pos + 1..]
610 .trim()
611 .parse::<i64>()
612 .map_err(|e| e.to_string())?;
613 return Ok(left + right);
614 }
615 if let Some(pos) = expr.rfind('-') {
616 if pos > 0 {
617 let left = expr[..pos]
618 .trim()
619 .parse::<i64>()
620 .map_err(|e| e.to_string())?;
621 let right = expr[pos + 1..]
622 .trim()
623 .parse::<i64>()
624 .map_err(|e| e.to_string())?;
625 return Ok(left - right);
626 }
627 }
628 if let Some(pos) = expr.find('*') {
629 let left = expr[..pos]
630 .trim()
631 .parse::<i64>()
632 .map_err(|e| e.to_string())?;
633 let right = expr[pos + 1..]
634 .trim()
635 .parse::<i64>()
636 .map_err(|e| e.to_string())?;
637 return Ok(left * right);
638 }
639 if let Some(pos) = expr.find('/') {
640 let left = expr[..pos]
641 .trim()
642 .parse::<i64>()
643 .map_err(|e| e.to_string())?;
644 let right = expr[pos + 1..]
645 .trim()
646 .parse::<i64>()
647 .map_err(|e| e.to_string())?;
648 if right == 0 {
649 return Err("division by zero".to_string());
650 }
651 return Ok(left / right);
652 }
653
654 Err(format!("Invalid arithmetic expression: {}", expr))
655}
656
657pub fn brace_expand(s: &str) -> Vec<String> {
659 if !s.contains('{') {
660 return vec![s.to_string()];
661 }
662
663 let mut results = vec![String::new()];
664 let mut chars = s.chars().peekable();
665
666 while let Some(c) = chars.next() {
667 if c == '{' {
668 let content = collect_balanced(&mut chars, '{', '}');
669 let alternatives: Vec<&str> = content.split(',').collect();
670
671 if alternatives.len() > 1 {
672 results = results
673 .iter()
674 .flat_map(|prefix| {
675 alternatives
676 .iter()
677 .map(|alt| format!("{}{}", prefix, alt))
678 .collect::<Vec<_>>()
679 })
680 .collect();
681 } else if let Some((start, end)) = parse_range(&content) {
682 results = results
683 .iter()
684 .flat_map(|prefix| {
685 (start..=end)
686 .map(|n| format!("{}{}", prefix, n))
687 .collect::<Vec<_>>()
688 })
689 .collect();
690 } else {
691 for r in &mut results {
692 r.push('{');
693 r.push_str(&content);
694 r.push('}');
695 }
696 }
697 } else {
698 for r in &mut results {
699 r.push(c);
700 }
701 }
702 }
703
704 results
705}
706
707fn parse_range(s: &str) -> Option<(i32, i32)> {
708 let parts: Vec<&str> = s.splitn(2, "..").collect();
709 if parts.len() != 2 {
710 return None;
711 }
712 let start = parts[0].parse().ok()?;
713 let end = parts[1].parse().ok()?;
714 Some((start, end))
715}
716
717pub fn remtpath(path: &str, count: usize) -> String {
720 let mut result = path.to_string();
721 for _ in 0..count {
722 if let Some(pos) = result.rfind('/') {
723 if pos == 0 {
724 result = "/".to_string();
725 } else {
726 result = result[..pos].to_string();
727 }
728 } else {
729 result = ".".to_string();
730 break;
731 }
732 }
733 result
734}
735
736pub fn remlpaths(path: &str, count: usize) -> String {
739 let mut result = path;
740 for _ in 0..count {
741 if let Some(pos) = result.find('/') {
742 result = &result[pos + 1..];
743 } else {
744 return result.to_string();
745 }
746 }
747 result.to_string()
748}
749
750pub fn remtext(path: &str) -> String {
753 if let Some(slash_pos) = path.rfind('/') {
754 let filename = &path[slash_pos + 1..];
755 if let Some(dot_pos) = filename.rfind('.') {
756 if dot_pos > 0 {
757 return format!("{}{}", &path[..=slash_pos], &filename[..dot_pos]);
758 }
759 }
760 path.to_string()
761 } else if let Some(dot_pos) = path.rfind('.') {
762 if dot_pos > 0 {
763 return path[..dot_pos].to_string();
764 }
765 path.to_string()
766 } else {
767 path.to_string()
768 }
769}
770
771pub fn rembutext(path: &str) -> String {
774 let filename = if let Some(slash_pos) = path.rfind('/') {
775 &path[slash_pos + 1..]
776 } else {
777 path
778 };
779
780 if let Some(dot_pos) = filename.rfind('.') {
781 if dot_pos > 0 && dot_pos < filename.len() - 1 {
782 return filename[dot_pos + 1..].to_string();
783 }
784 }
785 String::new()
786}
787
788pub fn path_tail(path: &str) -> String {
790 if let Some(pos) = path.rfind('/') {
791 path[pos + 1..].to_string()
792 } else {
793 path.to_string()
794 }
795}
796
797pub fn path_head(path: &str) -> String {
799 remtpath(path, 1)
800}
801
802#[derive(Clone, Copy, PartialEq, Eq)]
805pub enum CaseMod {
806 Lower,
807 Upper,
808 Caps,
809}
810
811pub fn casemodify(s: &str, mode: CaseMod) -> String {
814 match mode {
815 CaseMod::Lower => s.to_lowercase(),
816 CaseMod::Upper => s.to_uppercase(),
817 CaseMod::Caps => {
818 let mut result = String::with_capacity(s.len());
819 let mut cap_next = true;
820 for c in s.chars() {
821 if c.is_whitespace() || !c.is_alphabetic() {
822 result.push(c);
823 cap_next = true;
824 } else if cap_next {
825 for uc in c.to_uppercase() {
826 result.push(uc);
827 }
828 cap_next = false;
829 } else {
830 for lc in c.to_lowercase() {
831 result.push(lc);
832 }
833 }
834 }
835 result
836 }
837 }
838}
839
840pub fn chabspath(path: &str) -> String {
843 if path.starts_with('/') {
844 return clean_path(path);
845 }
846
847 let cwd = env::current_dir()
848 .map(|p| p.to_string_lossy().to_string())
849 .unwrap_or_else(|_| "/".to_string());
850
851 clean_path(&format!("{}/{}", cwd, path))
852}
853
854fn clean_path(path: &str) -> String {
856 let mut components: Vec<&str> = Vec::new();
857
858 for part in path.split('/') {
859 match part {
860 "" | "." => continue,
861 ".." => {
862 if !components.is_empty() && components.last() != Some(&"..") {
863 components.pop();
864 } else if !path.starts_with('/') {
865 components.push("..");
866 }
867 }
868 p => components.push(p),
869 }
870 }
871
872 if path.starts_with('/') {
873 format!("/{}", components.join("/"))
874 } else if components.is_empty() {
875 ".".to_string()
876 } else {
877 components.join("/")
878 }
879}
880
881pub fn singsub(s: &str, params: &HashMap<String, String>) -> Result<String, String> {
884 let opts = SubstOptions::default();
885 subst_string(s, params, &opts)
886}
887
888pub fn multsub(s: &str, params: &HashMap<String, String>) -> Result<Vec<String>, String> {
891 let mut opts = SubstOptions::default();
892 opts.word_split = true;
893
894 let expanded = subst_string(s, params, &opts)?;
895
896 let ifs = params.get("IFS").map(|s| s.as_str()).unwrap_or(" \t\n");
898
899 Ok(expanded
900 .split(|c: char| ifs.contains(c))
901 .filter(|s| !s.is_empty())
902 .map(|s| s.to_string())
903 .collect())
904}
905
906pub fn untokenize(s: &str) -> String {
909 s.to_string()
910}
911
912pub fn remnulargs(s: &str) -> String {
915 s.to_string()
916}
917
918pub fn dopadding(
920 s: &str,
921 prenum: usize,
922 postnum: usize,
923 preone: Option<&str>,
924 postone: Option<&str>,
925 premul: Option<&str>,
926 postmul: Option<&str>,
927) -> String {
928 let default_pad = " ";
929 let preone = preone.unwrap_or("");
930 let postone = postone.unwrap_or("");
931 let premul = if premul.map(|s| s.is_empty()).unwrap_or(true) {
932 default_pad
933 } else {
934 premul.unwrap()
935 };
936 let postmul = if postmul.map(|s| s.is_empty()).unwrap_or(true) {
937 default_pad
938 } else {
939 postmul.unwrap()
940 };
941
942 let slen = s.chars().count();
943
944 if prenum + postnum == slen {
945 return s.to_string();
946 }
947
948 let mut result = String::new();
949
950 if prenum > 0 {
951 let f = prenum.saturating_sub(slen);
952 if f == 0 {
953 let skip = slen - prenum;
955 result.extend(s.chars().skip(skip));
956 } else {
957 let mut pad_needed = f.saturating_sub(preone.chars().count());
959
960 while pad_needed > 0 {
962 let plen = premul.chars().count();
963 if pad_needed >= plen {
964 result.push_str(premul);
965 pad_needed -= plen;
966 } else {
967 result.extend(premul.chars().take(pad_needed));
969 pad_needed = 0;
970 }
971 }
972
973 if !preone.is_empty() && f >= preone.chars().count() {
975 result.push_str(preone);
976 } else if !preone.is_empty() {
977 let skip = preone.chars().count() - f;
979 result.extend(preone.chars().skip(skip));
980 }
981
982 result.push_str(s);
984 }
985 } else if postnum > 0 {
986 let f = postnum.saturating_sub(slen);
987 if f == 0 {
988 result.extend(s.chars().take(postnum));
990 } else {
991 result.push_str(s);
993
994 if !postone.is_empty() {
996 if f >= postone.chars().count() {
997 result.push_str(postone);
998 } else {
999 result.extend(postone.chars().take(f));
1000 }
1001 }
1002
1003 let mut pad_needed = f.saturating_sub(postone.chars().count());
1005 while pad_needed > 0 {
1006 let plen = postmul.chars().count();
1007 if pad_needed >= plen {
1008 result.push_str(postmul);
1009 pad_needed -= plen;
1010 } else {
1011 result.extend(postmul.chars().take(pad_needed));
1012 pad_needed = 0;
1013 }
1014 }
1015 }
1016 } else {
1017 result.push_str(s);
1018 }
1019
1020 result
1021}
1022
1023pub fn get_strarg(s: &str) -> Option<(&str, char)> {
1025 let mut chars = s.chars();
1026 let delim = chars.next()?;
1027
1028 let end_delim = match delim {
1029 '(' => ')',
1030 '[' => ']',
1031 '{' => '}',
1032 '<' => '>',
1033 _ => delim,
1034 };
1035
1036 let rest: String = chars.collect();
1037 if let Some(pos) = rest.find(end_delim) {
1038 Some((&s[1..pos + 1], end_delim))
1039 } else {
1040 None
1041 }
1042}
1043
1044pub fn equalsubstr(cmd: &str) -> Option<String> {
1046 crate::utils::find_in_path(cmd).and_then(|p| p.to_str().map(|s| s.to_string()))
1047}
1048
1049pub fn filesubstr(name: &str, assign: bool) -> Option<String> {
1051 if name.starts_with('~') {
1052 let rest = &name[1..];
1053
1054 if rest.is_empty() || rest.starts_with('/') {
1056 let home = std::env::var("HOME").unwrap_or_default();
1057 return Some(format!("{}{}", home, rest));
1058 }
1059
1060 if rest.starts_with('+') && (rest.len() == 1 || rest.chars().nth(1) == Some('/')) {
1062 let pwd = std::env::var("PWD").unwrap_or_else(|_| ".".to_string());
1063 return Some(format!("{}{}", pwd, &rest[1..]));
1064 }
1065
1066 if rest.starts_with('-') && (rest.len() == 1 || rest.chars().nth(1) == Some('/')) {
1068 let oldpwd = std::env::var("OLDPWD").unwrap_or_else(|_| ".".to_string());
1069 return Some(format!("{}{}", oldpwd, &rest[1..]));
1070 }
1071
1072 let (user, suffix) = match rest.find('/') {
1074 Some(pos) => (&rest[..pos], &rest[pos..]),
1075 None => (rest, ""),
1076 };
1077
1078 #[cfg(unix)]
1079 {
1080 if let Some(home) = crate::subst::get_user_home(user) {
1081 return Some(format!("{}{}", home, suffix));
1082 }
1083 }
1084 } else if name.starts_with('=') && name.len() > 1 {
1085 if let Some(path) = equalsubstr(&name[1..]) {
1087 return Some(path);
1088 }
1089 }
1090
1091 None
1092}
1093
1094pub fn substevalchar(expr: &str) -> Option<char> {
1096 let value: i64 = expr.parse().ok()?;
1097 if value < 0 || value > 0x10FFFF {
1098 return None;
1099 }
1100 char::from_u32(value as u32)
1101}
1102
1103pub fn check_colon_subscript(s: &str) -> Option<(String, &str)> {
1105 if s.is_empty() || s.starts_with(|c: char| c.is_alphabetic()) || s.starts_with('&') {
1106 return None;
1107 }
1108
1109 if s.starts_with(':') {
1110 return Some(("0".to_string(), s));
1111 }
1112
1113 let end = s.find(':').unwrap_or(s.len());
1115 let expr = &s[..end];
1116 let rest = &s[end..];
1117
1118 Some((expr.to_string(), rest))
1119}
1120
1121pub fn array_slice(arr: &[String], offset: i64, length: Option<i64>) -> Vec<String> {
1123 let len = arr.len() as i64;
1124
1125 let offset = if offset < 0 {
1126 (len + offset).max(0) as usize
1127 } else {
1128 (offset as usize).min(arr.len())
1129 };
1130
1131 let length = match length {
1132 Some(l) if l < 0 => (len - offset as i64 + l).max(0) as usize,
1133 Some(l) => l.max(0) as usize,
1134 None => arr.len().saturating_sub(offset),
1135 };
1136
1137 arr.iter().skip(offset).take(length).cloned().collect()
1138}
1139
1140pub fn string_slice(s: &str, offset: i64, length: Option<i64>) -> String {
1142 let chars: Vec<char> = s.chars().collect();
1143 let len = chars.len() as i64;
1144
1145 let offset = if offset < 0 {
1146 (len + offset).max(0) as usize
1147 } else {
1148 (offset as usize).min(chars.len())
1149 };
1150
1151 let length = match length {
1152 Some(l) if l < 0 => (len - offset as i64 + l).max(0) as usize,
1153 Some(l) => l.max(0) as usize,
1154 None => chars.len().saturating_sub(offset),
1155 };
1156
1157 chars.iter().skip(offset).take(length).collect()
1158}
1159
1160pub fn array_union(arr1: &[String], arr2: &[String]) -> Vec<String> {
1162 use std::collections::HashSet;
1163 let set2: HashSet<_> = arr2.iter().collect();
1164
1165 let mut result: Vec<String> = arr1.to_vec();
1166 for item in arr2 {
1167 if !result.contains(item) {
1168 result.push(item.clone());
1169 }
1170 }
1171 result
1172}
1173
1174pub fn array_intersection(arr1: &[String], arr2: &[String]) -> Vec<String> {
1176 use std::collections::HashSet;
1177 let set2: HashSet<_> = arr2.iter().collect();
1178
1179 arr1.iter()
1180 .filter(|item| set2.contains(item))
1181 .cloned()
1182 .collect()
1183}
1184
1185pub fn array_difference(arr1: &[String], arr2: &[String]) -> Vec<String> {
1187 use std::collections::HashSet;
1188 let set2: HashSet<_> = arr2.iter().collect();
1189
1190 arr1.iter()
1191 .filter(|item| !set2.contains(item))
1192 .cloned()
1193 .collect()
1194}
1195
1196pub fn array_zip(arr1: &[String], arr2: &[String], shortest: bool) -> Vec<String> {
1198 let len = if shortest {
1199 arr1.len().min(arr2.len())
1200 } else {
1201 arr1.len().max(arr2.len())
1202 };
1203
1204 let mut result = Vec::with_capacity(len * 2);
1205 for i in 0..len {
1206 let v1 = arr1.get(i % arr1.len()).cloned().unwrap_or_default();
1207 let v2 = arr2.get(i % arr2.len()).cloned().unwrap_or_default();
1208 result.push(v1);
1209 result.push(v2);
1210 }
1211 result
1212}
1213
1214pub fn array_unique(arr: &[String]) -> Vec<String> {
1216 use std::collections::HashSet;
1217 let mut seen = HashSet::new();
1218 arr.iter()
1219 .filter(|item| seen.insert(item.as_str()))
1220 .cloned()
1221 .collect()
1222}
1223
1224pub fn array_reverse(arr: &[String]) -> Vec<String> {
1226 arr.iter().rev().cloned().collect()
1227}
1228
1229pub fn array_sort(arr: &[String], reverse: bool, numeric: bool) -> Vec<String> {
1231 let mut result = arr.to_vec();
1232 if numeric {
1233 result.sort_by(|a, b| {
1234 let na: f64 = a.parse().unwrap_or(0.0);
1235 let nb: f64 = b.parse().unwrap_or(0.0);
1236 na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
1237 });
1238 } else {
1239 result.sort();
1240 }
1241 if reverse {
1242 result.reverse();
1243 }
1244 result
1245}
1246
1247pub fn array_filter_pattern(arr: &[String], pattern: &str, invert: bool) -> Vec<String> {
1249 arr.iter()
1250 .filter(|item| {
1251 let matches = crate::glob::pattern_match(pattern, item, false, true);
1252 if invert {
1253 matches
1254 } else {
1255 !matches
1256 }
1257 })
1258 .cloned()
1259 .collect()
1260}
1261
1262pub fn array_replace(
1264 arr: &[String],
1265 pattern: &str,
1266 replacement: &str,
1267 global: bool,
1268) -> Vec<String> {
1269 arr.iter()
1270 .map(|item| {
1271 if global {
1272 item.replace(pattern, replacement)
1273 } else {
1274 item.replacen(pattern, replacement, 1)
1275 }
1276 })
1277 .collect()
1278}
1279
1280pub fn modify_case(s: &str, mode: CaseMode) -> String {
1282 match mode {
1283 CaseMode::Lower => s.to_lowercase(),
1284 CaseMode::Upper => s.to_uppercase(),
1285 CaseMode::Capitalize => {
1286 let mut chars = s.chars();
1287 match chars.next() {
1288 None => String::new(),
1289 Some(c) => c.to_uppercase().chain(chars).collect(),
1290 }
1291 }
1292 CaseMode::CapitalizeWords => s
1293 .split_whitespace()
1294 .map(|word| {
1295 let mut chars = word.chars();
1296 match chars.next() {
1297 None => String::new(),
1298 Some(c) => c.to_uppercase().chain(chars).collect(),
1299 }
1300 })
1301 .collect::<Vec<_>>()
1302 .join(" "),
1303 }
1304}
1305
1306#[derive(Clone, Copy, Debug)]
1307pub enum CaseMode {
1308 Lower,
1309 Upper,
1310 Capitalize,
1311 CapitalizeWords,
1312}
1313
1314pub fn param_type_info(value: &ParamValue) -> String {
1316 use crate::params::{flags, ParamValue};
1317 match value {
1318 ParamValue::Scalar(_) => "scalar".to_string(),
1319 ParamValue::Integer(_) => "integer".to_string(),
1320 ParamValue::Float(_) => "float".to_string(),
1321 ParamValue::Array(_) => "array".to_string(),
1322 ParamValue::Assoc(_) => "association".to_string(),
1323 ParamValue::Unset => "undefined".to_string(),
1324 }
1325}
1326
1327use crate::params::ParamValue;
1328
1329#[derive(Default, Clone, Debug)]
1331pub struct SubscriptFlags {
1332 pub reverse: bool, pub words: bool, pub chars: bool, pub match_once: bool, }
1337
1338pub fn apply_subscript_string(s: &str, start: i64, end: i64, flags: &SubscriptFlags) -> String {
1340 if flags.words {
1341 let words: Vec<&str> = s.split_whitespace().collect();
1342 return apply_subscript_array(
1343 &words.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
1344 start,
1345 end,
1346 )
1347 .join(" ");
1348 }
1349
1350 let chars: Vec<char> = s.chars().collect();
1351 let len = chars.len() as i64;
1352
1353 let (start, end) = normalize_indices(start, end, len);
1354
1355 chars[start..end].iter().collect()
1356}
1357
1358pub fn apply_subscript_array(arr: &[String], start: i64, end: i64) -> Vec<String> {
1360 let len = arr.len() as i64;
1361 let (start, end) = normalize_indices(start, end, len);
1362 arr[start..end].to_vec()
1363}
1364
1365fn normalize_indices(start: i64, end: i64, len: i64) -> (usize, usize) {
1366 let start = if start < 0 { len + start + 1 } else { start };
1367 let end = if end < 0 { len + end + 1 } else { end };
1368 let start = ((start.max(1) - 1) as usize).min(len as usize);
1369 let end = (end.max(0) as usize).min(len as usize);
1370 (start, end.max(start))
1371}
1372
1373#[cfg(test)]
1374mod tests {
1375 use super::*;
1376
1377 #[test]
1378 fn test_tilde_expand() {
1379 let opts = SubstOptions::default();
1380 let result = tilde_expand("~", &opts).unwrap();
1381 assert!(!result.starts_with('~'));
1382 }
1383
1384 #[test]
1385 fn test_param_expand_simple() {
1386 let mut params = HashMap::new();
1387 params.insert("FOO".to_string(), "bar".to_string());
1388
1389 let opts = SubstOptions::default();
1390 let result = param_expand("$FOO", ¶ms, &opts).unwrap();
1391 assert_eq!(result, "bar");
1392 }
1393
1394 #[test]
1395 fn test_param_expand_default() {
1396 let params = HashMap::new();
1397 let opts = SubstOptions::default();
1398
1399 let result = param_expand("${UNDEFINED:-default}", ¶ms, &opts).unwrap();
1400 assert_eq!(result, "default");
1401 }
1402
1403 #[test]
1404 fn test_brace_expand_alternatives() {
1405 let results = brace_expand("file.{txt,md,rs}");
1406 assert_eq!(results.len(), 3);
1407 assert!(results.contains(&"file.txt".to_string()));
1408 assert!(results.contains(&"file.md".to_string()));
1409 assert!(results.contains(&"file.rs".to_string()));
1410 }
1411
1412 #[test]
1413 fn test_brace_expand_range() {
1414 let results = brace_expand("file{1..3}");
1415 assert_eq!(results.len(), 3);
1416 assert!(results.contains(&"file1".to_string()));
1417 assert!(results.contains(&"file2".to_string()));
1418 assert!(results.contains(&"file3".to_string()));
1419 }
1420
1421 #[test]
1422 fn test_remtpath() {
1423 assert_eq!(remtpath("/a/b/c", 1), "/a/b");
1424 assert_eq!(remtpath("/a/b/c", 2), "/a");
1425 assert_eq!(remtpath("foo", 1), ".");
1426 }
1427
1428 #[test]
1429 fn test_remlpaths() {
1430 assert_eq!(remlpaths("/a/b/c", 1), "a/b/c");
1431 assert_eq!(remlpaths("a/b/c", 2), "c");
1432 }
1433
1434 #[test]
1435 fn test_remtext() {
1436 assert_eq!(remtext("file.txt"), "file");
1437 assert_eq!(remtext("/path/to/file.txt"), "/path/to/file");
1438 assert_eq!(remtext("noext"), "noext");
1439 }
1440
1441 #[test]
1442 fn test_rembutext() {
1443 assert_eq!(rembutext("file.txt"), "txt");
1444 assert_eq!(rembutext("/path/to/file.rs"), "rs");
1445 assert_eq!(rembutext("noext"), "");
1446 }
1447
1448 #[test]
1449 fn test_casemodify() {
1450 assert_eq!(casemodify("Hello World", CaseMod::Lower), "hello world");
1451 assert_eq!(casemodify("Hello World", CaseMod::Upper), "HELLO WORLD");
1452 assert_eq!(casemodify("hello world", CaseMod::Caps), "Hello World");
1453 }
1454
1455 #[test]
1456 fn test_clean_path() {
1457 assert_eq!(chabspath("/a/b/../c"), "/a/c");
1458 assert_eq!(chabspath("/a/./b/c"), "/a/b/c");
1459 }
1460}