1use std::borrow::Cow;
7use std::fmt;
8
9use crate::ast::*;
10use crate::lexer::ParseError;
11use crate::parser::Parser;
12
13struct Ctx {
15 in_subshell: bool,
16}
17
18impl Ctx {
19 fn new() -> Self {
20 Ctx {
21 in_subshell: false,
22 }
23 }
24}
25
26#[derive(Debug)]
32#[non_exhaustive]
33pub enum TranslateError {
34 Unsupported(&'static str),
36 Parse(ParseError),
38}
39
40impl fmt::Display for TranslateError {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 match self {
43 TranslateError::Unsupported(msg) => write!(f, "unsupported: {msg}"),
44 TranslateError::Parse(e) => write!(f, "{e}"),
45 }
46 }
47}
48
49impl std::error::Error for TranslateError {
50 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
51 match self {
52 TranslateError::Parse(e) => Some(e),
53 TranslateError::Unsupported(_) => None,
54 }
55 }
56}
57
58impl From<ParseError> for TranslateError {
59 fn from(e: ParseError) -> Self {
60 TranslateError::Parse(e)
61 }
62}
63
64type Res<T> = Result<T, TranslateError>;
66
67#[must_use = "translation produces a result that should be inspected"]
96pub fn translate_bash_to_fish(input: &str) -> Result<String, TranslateError> {
97 let cmds = Parser::new(input).parse()?;
98 let mut ctx = Ctx::new();
99 let mut out = String::with_capacity(input.len());
100 for (i, cmd) in cmds.iter().enumerate() {
101 if i > 0 {
102 out.push('\n');
103 }
104 emit_cmd(&mut ctx, cmd, &mut out)?;
105 }
106 Ok(out)
107}
108
109fn emit_cmd(ctx: &mut Ctx, cmd: &Cmd<'_>, out: &mut String) -> Res<()> {
114 match cmd {
115 Cmd::List(list) => emit_and_or(ctx, list, out),
116 Cmd::Job(list) => {
117 emit_and_or(ctx, list, out)?;
118 out.push_str(" &");
119 Ok(())
120 }
121 }
122}
123
124fn emit_and_or(ctx: &mut Ctx, list: &AndOrList<'_>, out: &mut String) -> Res<()> {
125 emit_pipeline(ctx, &list.first, out)?;
126 for and_or in &list.rest {
127 match and_or {
128 AndOr::And(p) => {
129 out.push_str("; and ");
130 emit_pipeline(ctx, p, out)?;
131 }
132 AndOr::Or(p) => {
133 out.push_str("; or ");
134 emit_pipeline(ctx, p, out)?;
135 }
136 }
137 }
138 Ok(())
139}
140
141fn emit_pipeline(ctx: &mut Ctx, pipeline: &Pipeline<'_>, out: &mut String) -> Res<()> {
142 match pipeline {
143 Pipeline::Single(exec) => emit_exec(ctx, exec, out),
144 Pipeline::Pipe(negated, cmds) => {
145 if *negated {
146 out.push_str("not ");
147 }
148 for (i, c) in cmds.iter().enumerate() {
149 if i > 0 {
150 out.push_str(" | ");
151 }
152 emit_exec(ctx, c, out)?;
153 }
154 Ok(())
155 }
156 }
157}
158
159fn emit_exec(ctx: &mut Ctx, exec: &Executable<'_>, out: &mut String) -> Res<()> {
160 match exec {
161 Executable::Simple(simple) => emit_simple(ctx, simple, out),
162 Executable::Compound(compound) => emit_compound(ctx, compound, out),
163 Executable::FuncDef(name, body) => {
164 out.push_str("function ");
165 out.push_str(name);
166 out.push('\n');
167 match &body.kind {
169 CompoundKind::Brace(cmds) => emit_body(ctx, cmds, out)?,
170 other => emit_compound_kind(ctx, other, out)?,
171 }
172 out.push_str("\nend");
173 Ok(())
174 }
175 }
176}
177
178fn emit_simple(ctx: &mut Ctx, cmd: &SimpleCmd<'_>, out: &mut String) -> Res<()> {
183 let mut env_vars: Vec<(&str, &Option<Word<'_>>)> = Vec::new();
184 let mut array_ops: Vec<&CmdPrefix<'_>> = Vec::new();
185 let mut cmd_words: Vec<&Word<'_>> = Vec::new();
186 let mut redirects: Vec<&Redir<'_>> = Vec::new();
187 let mut herestring: Option<&Word<'_>> = None;
188 let mut heredoc: Option<&HeredocBody<'_>> = None;
189
190 for item in &cmd.prefix {
191 match item {
192 CmdPrefix::Assign(name, val) => env_vars.push((name, val)),
193 CmdPrefix::ArrayAssign(..) | CmdPrefix::ArrayAppend(..) => array_ops.push(item),
194 CmdPrefix::Redirect(Redir::HereString(w)) => herestring = Some(w),
195 CmdPrefix::Redirect(Redir::Heredoc(body)) => heredoc = Some(body),
196 CmdPrefix::Redirect(r) => redirects.push(r),
197 }
198 }
199 for item in &cmd.suffix {
200 match item {
201 CmdSuffix::Word(w) => cmd_words.push(w),
202 CmdSuffix::Redirect(Redir::HereString(w)) => herestring = Some(w),
203 CmdSuffix::Redirect(Redir::Heredoc(body)) => heredoc = Some(body),
204 CmdSuffix::Redirect(r) => redirects.push(r),
205 }
206 }
207
208 if cmd_words.is_empty() {
210 if !array_ops.is_empty() {
211 return emit_array_assignments(ctx, &env_vars, &array_ops, out);
212 }
213 if !env_vars.is_empty() {
214 return emit_var_assignments(ctx, &env_vars, out);
215 }
216 }
217
218 let cmd_name = cmd_words.first().and_then(|w| word_as_str(w));
219
220 if matches!(cmd_name.as_deref(), Some("mapfile" | "readarray")) {
222 return emit_mapfile(ctx, &cmd_words, &redirects, herestring, out);
223 }
224
225 if let Some(hs_word) = herestring {
227 out.push_str("echo ");
228 emit_word(ctx, hs_word, out)?;
229 out.push_str(" | ");
230 }
231 if let Some(body) = heredoc {
232 emit_heredoc_body(ctx, body, out)?;
233 out.push_str(" | ");
234 }
235
236 if !env_vars.is_empty() && !cmd_words.is_empty() {
241 return Err(TranslateError::Unsupported("prefix assignment with command"));
242 }
243
244 if let Some(ref name) = cmd_name
246 && let Some(result) = dispatch_builtin(ctx, name, &cmd_words, &redirects, out)
247 {
248 return result;
249 }
250
251 if ctx.in_subshell && cmd_name.as_deref() == Some("exit") {
255 return Err(TranslateError::Unsupported("exit in subshell"));
256 }
257
258 for (i, word) in cmd_words.iter().enumerate() {
260 if i > 0 {
261 out.push(' ');
262 }
263 emit_word(ctx, word, out)?;
264 }
265
266 for redir in &redirects {
268 out.push(' ');
269 emit_redir(ctx, redir, out)?;
270 }
271
272 Ok(())
273}
274
275fn emit_array_assignments(ctx: &mut Ctx,
277 env_vars: &[(&str, &Option<Word<'_>>)],
278 array_ops: &[&CmdPrefix<'_>],
279 out: &mut String,
280) -> Res<()> {
281 let set_kw = if ctx.in_subshell { "set -l " } else { "set " };
282 let mut first = true;
283 for (i, (name, value)) in env_vars.iter().enumerate() {
284 if !first || i > 0 {
285 out.push('\n');
286 }
287 first = false;
288 out.push_str(set_kw);
289 out.push_str(name);
290 if let Some(val) = value {
291 out.push(' ');
292 emit_word(ctx, val, out)?;
293 }
294 }
295 for op in array_ops {
296 if !first {
297 out.push('\n');
298 }
299 first = false;
300 match op {
301 CmdPrefix::ArrayAssign(name, words) => {
302 out.push_str(set_kw);
303 out.push_str(name);
304 for w in words {
305 out.push(' ');
306 emit_word(ctx, w, out)?;
307 }
308 }
309 CmdPrefix::ArrayAppend(name, words) => {
310 out.push_str(if ctx.in_subshell { "set -la " } else { "set -a " });
311 out.push_str(name);
312 for w in words {
313 out.push(' ');
314 emit_word(ctx, w, out)?;
315 }
316 }
317 _ => unreachable!(),
318 }
319 }
320 Ok(())
321}
322
323fn emit_var_assignments(ctx: &mut Ctx,
325 env_vars: &[(&str, &Option<Word<'_>>)],
326 out: &mut String,
327) -> Res<()> {
328 let set_kw = if ctx.in_subshell { "set -l " } else { "set " };
329 for (i, (name, value)) in env_vars.iter().enumerate() {
330 if i > 0 {
331 out.push('\n');
332 }
333 out.push_str(set_kw);
334 out.push_str(name);
335 if let Some(val) = value {
336 out.push(' ');
337 emit_word(ctx, val, out)?;
338 }
339 }
340 Ok(())
341}
342
343fn dispatch_builtin(ctx: &mut Ctx,
345 name: &str,
346 cmd_words: &[&Word<'_>],
347 redirects: &[&Redir<'_>],
348 out: &mut String,
349) -> Option<Res<()>> {
350 match name {
351 "export" => Some(emit_export(ctx, &cmd_words[1..], out)),
352 "unset" => Some(emit_unset(ctx, &cmd_words[1..], out)),
353 "local" => Some(emit_local(ctx, &cmd_words[1..], out)),
354 "declare" | "typeset" => Some(emit_declare(ctx, &cmd_words[1..], out)),
355 "readonly" => Some(emit_readonly(ctx, &cmd_words[1..], out)),
356 "[[" => Some(emit_double_bracket(ctx, &cmd_words[1..], redirects, out)),
357 "let" => Some(emit_let(ctx, &cmd_words[1..], out)),
358 "shopt" => Some(Err(TranslateError::Unsupported("shopt"))),
359 "trap" => Some(emit_trap(ctx, &cmd_words[1..], out)),
360 "shift" => Some(emit_shift(ctx, &cmd_words[1..], out)),
361 "alias" => Some(emit_alias(ctx, &cmd_words[1..], out)),
362 "read" => Some(emit_read(ctx, cmd_words, redirects, out)),
363 "set" => Some(emit_bash_set(ctx, &cmd_words[1..], out)),
364 "select" => Some(Err(TranslateError::Unsupported("select loop"))),
365 "getopts" => Some(Err(TranslateError::Unsupported(
366 "getopts (use argparse in fish)",
367 ))),
368 "exec" if cmd_words.len() == 1 && !redirects.is_empty() => {
369 Some(Err(TranslateError::Unsupported("exec fd manipulation")))
370 }
371 "eval" => Some(emit_eval(ctx, &cmd_words[1..], out)),
372 "printf" => dispatch_printf(ctx, cmd_words, out),
373 _ => None,
374 }
375}
376
377fn dispatch_printf(ctx: &mut Ctx,
379 cmd_words: &[&Word<'_>],
380 out: &mut String,
381) -> Option<Res<()>> {
382 if cmd_words.len() >= 3
384 && let Some(fmt) = word_as_str(cmd_words[1])
385 && let Some(ch) = extract_printf_repeat_char(fmt.as_ref())
386 && let Some(count) = extract_brace_range_count(&cmd_words[2..])
387 {
388 out.push_str("string repeat -n ");
389 itoa(out, count);
390 out.push_str(" -- '");
391 out.push(ch);
392 out.push('\'');
393 return Some(Ok(()));
394 }
395 for w in &cmd_words[1..] {
397 let text: Cow<'_, str> = if let Some(s) = word_as_str(w) {
398 s
399 } else {
400 let mut buf = String::with_capacity(64);
401 let _ = emit_word(ctx, w, &mut buf);
402 Cow::Owned(buf)
403 };
404 if text.contains("%0.s") || text.contains("%.0s") {
405 return Some(Err(TranslateError::Unsupported(
406 "printf %0.s format (fish printf doesn't support this)",
407 )));
408 }
409 }
410 None
411}
412
413fn emit_export(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
419 let mut first = true;
420 for arg in args {
421 if let Some(s) = word_as_str(arg)
422 && s.starts_with('-')
423 {
424 continue;
425 }
426 if !first {
427 out.push('\n');
428 }
429 first = false;
430
431 if let Some((var_name, value_parts)) = split_word_at_equals(ctx, arg) {
432 out.push_str("set -gx ");
433 out.push_str(&var_name);
434 if !value_parts.is_empty() {
435 out.push(' ');
436 if var_name.ends_with("PATH") && value_parts.contains(':') {
438 out.push_str(&value_parts.replace(':', " "));
439 } else {
440 out.push_str(&value_parts);
441 }
442 }
443 } else if let Some(s) = word_as_str(arg) {
444 out.push_str("set -gx ");
445 out.push_str(&s);
446 out.push_str(" $");
447 out.push_str(&s);
448 } else {
449 out.push_str("set -gx ");
450 emit_word(ctx, arg, out)?;
451 }
452 }
453 Ok(())
454}
455
456fn split_word_at_equals(ctx: &mut Ctx, word: &Word<'_>) -> Option<(String, String)> {
458 let mut full = String::with_capacity(64);
459 if emit_word(ctx, word, &mut full).is_err() {
460 return None;
461 }
462 let eq_pos = full.find('=')?;
463 let value_part = full.split_off(eq_pos + 1);
464 full.pop(); let var_name = full;
466
467 let value = if value_part.len() >= 2
468 && ((value_part.starts_with('"') && value_part.ends_with('"'))
469 || (value_part.starts_with('\'') && value_part.ends_with('\'')))
470 {
471 let mut v = value_part;
472 v.pop();
473 v.remove(0);
474 v
475 } else {
476 value_part
477 };
478
479 Some((var_name, value))
480}
481
482fn emit_unset(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
484 let mut first = true;
485 for arg in args {
486 let s = word_as_str(arg);
487 if matches!(s.as_deref(), Some(f) if f.starts_with('-')) {
488 continue;
489 }
490 if !first {
491 out.push('\n');
492 }
493 first = false;
494 if let Some(ref s) = s
496 && let Some((name, idx_str)) = parse_array_index_str(s)
497 && let Ok(idx) = idx_str.parse::<i64>()
498 {
499 out.push_str("set -e ");
500 out.push_str(name);
501 out.push('[');
502 itoa(out, idx + 1);
503 out.push(']');
504 continue;
505 }
506 out.push_str("set -e ");
507 emit_word(ctx, arg, out)?;
508 }
509 Ok(())
510}
511
512fn parse_array_index_str(s: &str) -> Option<(&str, &str)> {
514 let bracket = s.find('[')?;
515 if !s.ends_with(']') {
516 return None;
517 }
518 let name = &s[..bracket];
519 let idx = &s[bracket + 1..s.len() - 1];
520 if name.is_empty() || idx.is_empty() {
521 return None;
522 }
523 Some((name, idx))
524}
525
526fn emit_local(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
528 let mut first = true;
529 for arg in args {
530 let s = word_as_str(arg);
531 if matches!(s.as_deref(), Some(f) if f.starts_with('-')) {
532 continue;
533 }
534 if !first {
535 out.push('\n');
536 }
537 first = false;
538
539 if let Some(s) = s {
540 out.push_str("set -l ");
541 if let Some(eq) = s.find('=') {
542 out.push_str(&s[..eq]);
543 out.push(' ');
544 out.push_str(&s[eq + 1..]);
545 } else {
546 out.push_str(&s);
547 }
548 } else if let Some((name, val)) = split_word_at_equals(ctx, arg) {
549 out.push_str("set -l ");
550 out.push_str(&name);
551 out.push(' ');
552 out.push_str(&val);
553 } else {
554 out.push_str("set -l ");
555 emit_word(ctx, arg, out)?;
556 }
557 }
558 Ok(())
559}
560
561fn emit_declare(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
564 let mut scope = "-g";
565 let mut print_mode = false;
566 let mut remaining = Vec::new();
567
568 for arg in args {
569 if let Some(s) = word_as_str(arg) {
570 match &*s {
571 "-n" => {
572 return Err(TranslateError::Unsupported("declare -n (nameref)"));
573 }
574 "-A" | "-Ag" | "-gA" => {
575 return Err(TranslateError::Unsupported(
576 "declare -A (associative array)",
577 ));
578 }
579 "-p" => print_mode = true,
580 "-x" => scope = "-gx",
581 "-g" => scope = "-g",
582 s if s.starts_with('-') => {}
583 _ => remaining.push(*arg),
584 }
585 } else {
586 remaining.push(*arg);
587 }
588 }
589
590 if print_mode {
591 if remaining.is_empty() {
592 out.push_str("set --show");
593 } else {
594 for (i, arg) in remaining.iter().enumerate() {
595 if i > 0 {
596 out.push('\n');
597 }
598 out.push_str("set --show ");
599 emit_word(ctx, arg, out)?;
600 }
601 }
602 return Ok(());
603 }
604
605 let mut first = true;
606 for arg in &remaining {
607 if !first {
608 out.push('\n');
609 }
610 first = false;
611
612 if let Some((var_name, value_parts)) = split_word_at_equals(ctx, arg) {
613 out.push_str("set ");
614 out.push_str(scope);
615 out.push(' ');
616 out.push_str(&var_name);
617 if !value_parts.is_empty() {
618 out.push(' ');
619 out.push_str(&value_parts);
620 }
621 } else if let Some(s) = word_as_str(arg) {
622 out.push_str("set ");
623 out.push_str(scope);
624 out.push(' ');
625 out.push_str(&s);
626 } else {
627 out.push_str("set ");
628 out.push_str(scope);
629 out.push(' ');
630 emit_word(ctx, arg, out)?;
631 }
632 }
633 Ok(())
634}
635
636fn emit_trap(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
641 if args.is_empty() {
642 return Err(TranslateError::Unsupported("bare trap"));
643 }
644
645 let handler_str = word_as_str(args[0]);
646
647 if handler_str.as_deref() == Some("-") {
648 for sig_word in &args[1..] {
649 let sig = word_as_str(sig_word)
650 .ok_or(TranslateError::Unsupported("trap with dynamic signal"))?;
651 let name = sig.strip_prefix("SIG").unwrap_or(&sig);
652 out.push_str("functions -e __reef_trap_");
653 out.push_str(name);
654 }
655 return Ok(());
656 }
657
658 if args.len() < 2 {
659 return Err(TranslateError::Unsupported("trap with missing signal"));
660 }
661
662 let fish_body = match &handler_str {
664 Some(h) if h.is_empty() => String::new(),
665 Some(h) => translate_bash_to_fish(h)?,
666 None => {
667 let mut body = String::with_capacity(128);
669 emit_word_unquoted(ctx, args[0], &mut body)?;
670 translate_bash_to_fish(&body)?
671 }
672 };
673
674 for (i, sig_word) in args[1..].iter().enumerate() {
675 if i > 0 {
676 out.push('\n');
677 }
678 let sig =
679 word_as_str(sig_word).ok_or(TranslateError::Unsupported("trap with dynamic signal"))?;
680 let name = sig.strip_prefix("SIG").unwrap_or(&sig);
681
682 if name == "ERR" {
684 return Err(TranslateError::Unsupported("trap ERR (no fish equivalent)"));
685 }
686
687 if (name == "EXIT" || name == "0") && ctx.in_subshell {
690 return Err(TranslateError::Unsupported(
691 "trap EXIT in subshell (no fish equivalent)",
692 ));
693 }
694
695 out.push_str("function __reef_trap_");
696 out.push_str(name);
697 if name == "EXIT" || name == "0" {
698 out.push_str(" --on-event fish_exit");
699 } else {
700 out.push_str(" --on-signal ");
701 out.push_str(name);
702 }
703 if fish_body.is_empty() {
704 out.push_str("; end");
705 } else {
706 out.push('\n');
707 out.push_str(&fish_body);
708 out.push_str("\nend");
709 }
710 }
711 Ok(())
712}
713
714fn emit_read(ctx: &mut Ctx,
715 cmd_words: &[&Word<'_>],
716 redirects: &[&Redir<'_>],
717 out: &mut String,
718) -> Res<()> {
719 out.push_str("read");
720 let mut skip_next = false;
721 for word in &cmd_words[1..] {
722 if skip_next {
723 skip_next = false;
724 out.push_str(" -P ");
726 emit_word(ctx, word, out)?;
727 continue;
728 }
729 if let Some(s) = word_as_str(word) {
730 if s == "-r" || s == "-ra" || s == "-ar" {
731 if s.contains('a') {
733 out.push_str(" --list");
734 }
735 continue;
736 }
737 if s == "-a" {
738 out.push_str(" --list");
740 continue;
741 }
742 if s == "-p" {
743 skip_next = true;
745 continue;
746 }
747 if s.as_bytes()[0] == b'-' && s.len() > 1 && s.as_bytes()[1] != b'-' {
749 let mut wrote_flags = false;
750 let mut needs_prompt = false;
751 for &b in &s.as_bytes()[1..] {
752 match b {
753 b'r' => {} b'a' => out.push_str(" --list"),
755 b'p' => needs_prompt = true,
756 _ => {
757 if !wrote_flags {
758 out.push_str(" -");
759 wrote_flags = true;
760 }
761 out.push(b as char);
762 }
763 }
764 }
765 if needs_prompt {
766 skip_next = true;
767 }
768 continue;
769 }
770 }
771 out.push(' ');
772 emit_word(ctx, word, out)?;
773 }
774 emit_redirects(ctx, redirects, out)?;
775 Ok(())
776}
777
778fn emit_mapfile(ctx: &mut Ctx,
781 cmd_words: &[&Word<'_>],
782 redirects: &[&Redir<'_>],
783 herestring: Option<&Word<'_>>,
784 out: &mut String,
785) -> Res<()> {
786 let mut var_name: Cow<'_, str> = Cow::Borrowed("MAPFILE"); let mut skip_next = false;
789 for word in &cmd_words[1..] {
790 if skip_next {
791 skip_next = false;
792 continue;
793 }
794 if let Some(s) = word_as_str(word) {
795 match s.as_bytes().first() {
796 Some(b'-') => {
797 match &*s {
800 "-O" | "-s" | "-c" | "-C" | "-d" | "-n" | "-u" => skip_next = true,
801 _ => {}
802 }
803 }
804 _ => var_name = s,
805 }
806 }
807 }
808 if let Some(hs_word) = herestring {
810 out.push_str("set ");
812 out.push_str(&var_name);
813 out.push_str(" (string split -- \\n ");
814 emit_word(ctx, hs_word, out)?;
815 out.push(')');
816 return Ok(());
817 }
818
819 let mut has_input_redir = false;
821 for redir in redirects {
822 match redir {
823 Redir::HereString(word) => {
824 out.push_str("set ");
825 out.push_str(&var_name);
826 out.push_str(" (string split -- \\n ");
827 emit_word(ctx, word, out)?;
828 out.push(')');
829 has_input_redir = true;
830 break;
831 }
832 Redir::Read(_, word) => {
833 out.push_str("set ");
835 out.push_str(&var_name);
836 out.push_str(" (");
837 if let Some(cmds) = extract_procsub_cmds(word) {
838 for (i, cmd) in cmds.iter().enumerate() {
839 if i > 0 {
840 out.push_str("; ");
841 }
842 emit_cmd(ctx, cmd, out)?;
843 }
844 } else {
845 out.push_str("cat ");
846 emit_word(ctx, word, out)?;
847 }
848 out.push(')');
849 has_input_redir = true;
850 break;
851 }
852 _ => {}
853 }
854 }
855 if !has_input_redir {
856 out.push_str("set ");
857 out.push_str(&var_name);
858 out.push_str(" (cat)");
859 }
860 Ok(())
861}
862
863fn emit_eval(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
867 let cmds = extract_eval_cmds(args).ok_or(TranslateError::Unsupported("eval"))?;
869 for (i, cmd) in cmds.iter().enumerate() {
870 if i > 0 {
871 out.push_str("; ");
872 }
873 emit_cmd(ctx, cmd, out)?;
874 }
875 out.push_str(" | source");
876 Ok(())
877}
878
879fn extract_eval_cmds<'a>(args: &[&'a Word<'a>]) -> Option<&'a [Cmd<'a>]> {
880 let [arg] = args else { return None };
881 let subst = match arg {
882 Word::Simple(WordPart::DQuoted(atoms)) => match atoms.as_slice() {
883 [Atom::Subst(s)] => s,
884 _ => return None,
885 },
886 Word::Simple(WordPart::Bare(Atom::Subst(s))) => s,
887 _ => return None,
888 };
889 match subst.as_ref() {
890 Subst::Cmd(cmds) => Some(cmds),
891 _ => None,
892 }
893}
894
895fn emit_bash_set(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
898 if args.is_empty() {
899 out.push_str("set");
900 return Ok(());
901 }
902 if let Some(first) = args.first().and_then(|w| word_as_str(w)) {
903 let fb = first.as_bytes();
904 if fb == b"--" {
905 out.push_str("set argv");
907 for arg in &args[1..] {
908 out.push(' ');
909 emit_word(ctx, arg, out)?;
910 }
911 return Ok(());
912 }
913 if fb.len() >= 2
915 && (fb[0] == b'-' || fb[0] == b'+')
916 && fb[1..]
917 .iter()
918 .all(|&b| matches!(b, b'e' | b'u' | b'x' | b'o'))
919 {
920 out.push_str("# set");
921 for arg in args {
922 out.push(' ');
923 emit_word(ctx, arg, out)?;
924 }
925 out.push_str(" # no fish equivalent");
926 return Ok(());
927 }
928 }
929 out.push_str("set");
931 for arg in args {
932 out.push(' ');
933 emit_word(ctx, arg, out)?;
934 }
935 Ok(())
936}
937
938fn emit_shift(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
939 let Some(first) = args.first() else {
940 out.push_str("set -e argv[1]");
941 return Ok(());
942 };
943 if let Some(s) = word_as_str(first)
944 && let Ok(n) = s.parse::<u32>()
945 {
946 if n <= 1 {
947 out.push_str("set -e argv[1]");
948 } else {
949 out.push_str("set argv $argv[");
950 itoa(out, i64::from(n + 1));
951 out.push_str("..]");
952 }
953 return Ok(());
954 }
955 out.push_str("set argv $argv[(math \"");
957 emit_word(ctx, first, out)?;
958 out.push_str(" + 1\")..]");
959 Ok(())
960}
961
962fn emit_alias(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
964 out.push_str("alias");
965 for arg in args {
966 out.push(' ');
967 if let Some(s) = word_as_str(arg) {
968 if let Some(eq_pos) = s.find('=') {
971 let name = &s[..eq_pos];
972 let value = &s[eq_pos + 1..];
973 let unquoted = if (value.starts_with('\'') && value.ends_with('\''))
975 || (value.starts_with('"') && value.ends_with('"'))
976 {
977 &value[1..value.len() - 1]
978 } else {
979 value
980 };
981 out.push_str(name);
982 out.push(' ');
983 out.push('\'');
984 out.push_str(unquoted);
985 out.push('\'');
986 continue;
987 }
988 }
989 emit_word(ctx, arg, out)?;
990 }
991 Ok(())
992}
993
994fn emit_readonly(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
996 let mut first = true;
997 for arg in args {
998 if let Some(s) = word_as_str(arg)
999 && s.starts_with('-')
1000 {
1001 continue;
1002 }
1003 if !first {
1004 out.push('\n');
1005 }
1006 first = false;
1007
1008 if let Some(s) = word_as_str(arg) {
1009 if let Some(eq) = s.find('=') {
1010 out.push_str("set -g ");
1011 out.push_str(&s[..eq]);
1012 out.push(' ');
1013 out.push_str(&s[eq + 1..]);
1014 } else {
1015 out.push_str("set -g ");
1016 out.push_str(&s);
1017 out.push_str(" $");
1018 out.push_str(&s);
1019 }
1020 } else if let Some((name, val)) = split_word_at_equals(ctx, arg) {
1021 out.push_str("set -g ");
1022 out.push_str(&name);
1023 out.push(' ');
1024 out.push_str(&val);
1025 } else {
1026 out.push_str("set -g ");
1027 emit_word(ctx, arg, out)?;
1028 }
1029 }
1030 Ok(())
1031}
1032
1033fn emit_let(ctx: &mut Ctx, args: &[&Word<'_>], out: &mut String) -> Res<()> {
1035 for (i, arg) in args.iter().enumerate() {
1036 if i > 0 {
1037 out.push('\n');
1038 }
1039 let mut arg_str = String::with_capacity(32);
1041 emit_word_unquoted(ctx, arg, &mut arg_str)?;
1042
1043 let mut parser = Parser::new(&arg_str);
1045 match parser.arith(0) {
1046 Ok(arith) => {
1047 emit_standalone_arith(ctx, &arith, out)?;
1048 }
1049 Err(_) => {
1050 return Err(TranslateError::Unsupported("'let' with complex expression"));
1051 }
1052 }
1053 }
1054 Ok(())
1055}
1056
1057fn emit_string_match(ctx: &mut Ctx,
1059 lhs: &[&Word<'_>],
1060 rhs: &[&Word<'_>],
1061 regex: bool,
1062 negated: bool,
1063 out: &mut String,
1064) -> Res<()> {
1065 if regex {
1066 if negated {
1069 out.push_str("not ");
1070 }
1071 out.push_str("set __bash_rematch (string match -r -- ");
1072 } else {
1073 if negated {
1074 out.push_str("not ");
1075 }
1076 out.push_str("string match -q -- ");
1077 }
1078 let mut pat_buf = String::with_capacity(32);
1079 for (i, w) in rhs.iter().enumerate() {
1080 if i > 0 {
1081 pat_buf.push(' ');
1082 }
1083 emit_word_unquoted(ctx, w, &mut pat_buf)?;
1084 }
1085 push_sq_escaped(out, &pat_buf);
1086 out.push(' ');
1087 for w in lhs {
1088 emit_word(ctx, w, out)?;
1089 }
1090 if regex {
1091 out.push(')');
1092 }
1093 Ok(())
1094}
1095
1096fn emit_double_bracket(ctx: &mut Ctx,
1098 args: &[&Word<'_>],
1099 redirects: &[&Redir<'_>],
1100 out: &mut String,
1101) -> Res<()> {
1102 let filtered = if args.last().and_then(|a| word_as_str(a)).as_deref() == Some("]]") {
1104 &args[..args.len() - 1]
1105 } else {
1106 args
1107 };
1108
1109 let (filtered, bang_negated) =
1111 if !filtered.is_empty() && word_as_str(filtered[0]).as_deref() == Some("!") {
1112 (&filtered[1..], true)
1113 } else {
1114 (filtered, false)
1115 };
1116
1117 let regex_pos = filtered
1119 .iter()
1120 .position(|a| word_as_str(a).as_deref() == Some("=~"));
1121
1122 let op_pos = filtered.iter().position(|a| {
1124 let s = word_as_str(a);
1125 matches!(s.as_deref(), Some("==" | "!="))
1126 });
1127
1128 if filtered.len() == 2
1130 && let Some(flag) = word_as_str(filtered[0])
1131 && flag.as_ref() == "-v"
1132 {
1133 if bang_negated {
1134 out.push_str("not ");
1135 }
1136 out.push_str("set -q ");
1137 emit_word(ctx, filtered[1], out)?;
1138 emit_redirects(ctx, redirects, out)?;
1139 return Ok(());
1140 }
1141
1142 if let Some(pos) = regex_pos {
1143 emit_string_match(ctx, &filtered[..pos], &filtered[pos + 1..], true, bang_negated, out)?;
1144 } else if let Some(pos) = op_pos {
1145 let negated = word_as_str(filtered[pos]).as_deref() == Some("!=");
1146 emit_string_match(ctx, &filtered[..pos], &filtered[pos + 1..], false, negated ^ bang_negated, out)?;
1148 } else {
1149 if bang_negated {
1150 out.push_str("not ");
1151 }
1152 out.push_str("test");
1153 for arg in filtered {
1154 out.push(' ');
1155 emit_word(ctx, arg, out)?;
1156 }
1157 }
1158
1159 emit_redirects(ctx, redirects, out)?;
1160 Ok(())
1161}
1162
1163fn emit_compound(ctx: &mut Ctx, cmd: &CompoundCmd<'_>, out: &mut String) -> Res<()> {
1168 let herestring = cmd.redirects.iter().find_map(|r| match r {
1169 Redir::HereString(w) => Some(w),
1170 _ => None,
1171 });
1172 let heredoc = cmd.redirects.iter().find_map(|r| match r {
1173 Redir::Heredoc(body) => Some(body),
1174 _ => None,
1175 });
1176 if let Some(hs_word) = herestring {
1177 out.push_str("echo ");
1178 emit_word(ctx, hs_word, out)?;
1179 out.push_str(" | ");
1180 }
1181 if let Some(body) = heredoc {
1182 emit_heredoc_body(ctx, body, out)?;
1183 out.push_str(" | ");
1184 }
1185 emit_compound_kind(ctx, &cmd.kind, out)?;
1186 for redir in &cmd.redirects {
1187 if matches!(redir, Redir::HereString(..) | Redir::Heredoc(..)) {
1188 continue;
1189 }
1190 out.push(' ');
1191 emit_redir(ctx, redir, out)?;
1192 }
1193 Ok(())
1194}
1195
1196fn get_bare_command_subst<'a>(word: &'a Word<'a>) -> Option<&'a [Cmd<'a>]> {
1199 match word {
1200 Word::Simple(WordPart::Bare(Atom::Subst(subst))) => match subst.as_ref() {
1201 Subst::Cmd(cmds) => Some(cmds),
1202 _ => None,
1203 },
1204 _ => None,
1205 }
1206}
1207
1208fn is_bare_var_ref(word: &Word<'_>) -> bool {
1210 matches!(word, Word::Simple(WordPart::Bare(Atom::Param(Param::Var(_)))))
1211}
1212
1213fn emit_command_subst_with_split(ctx: &mut Ctx, cmds: &[Cmd<'_>], out: &mut String) -> Res<()> {
1216 out.push('(');
1217 for (i, cmd) in cmds.iter().enumerate() {
1218 if i > 0 {
1219 out.push_str("; ");
1220 }
1221 emit_cmd(ctx, cmd, out)?;
1222 }
1223 out.push_str(" | string split -n ' ')");
1224 Ok(())
1225}
1226
1227fn emit_compound_kind(ctx: &mut Ctx, kind: &CompoundKind<'_>, out: &mut String) -> Res<()> {
1228 match kind {
1229 CompoundKind::For { var, words, body } => {
1230 out.push_str("for ");
1231 out.push_str(var);
1232 out.push_str(" in ");
1233 if let Some(words) = words {
1234 for (i, w) in words.iter().enumerate() {
1235 if i > 0 {
1236 out.push(' ');
1237 }
1238 if let Some(cmds) = get_bare_command_subst(w) {
1239 emit_command_subst_with_split(ctx, cmds, out)?;
1240 } else if is_bare_var_ref(w) {
1241 out.push_str("(string split -n -- ' ' ");
1244 emit_word(ctx, w, out)?;
1245 out.push(')');
1246 } else {
1247 emit_word(ctx, w, out)?;
1248 }
1249 }
1250 } else {
1251 out.push_str("$argv");
1252 }
1253 out.push('\n');
1254 emit_body(ctx, body, out)?;
1255 out.push_str("\nend");
1256 }
1257
1258 CompoundKind::While(guard_body) => {
1259 out.push_str("while ");
1260 emit_guard(ctx, &guard_body.guard, out)?;
1261 out.push('\n');
1262 emit_body(ctx, &guard_body.body, out)?;
1263 out.push_str("\nend");
1264 }
1265
1266 CompoundKind::Until(guard_body) => {
1267 out.push_str("while not ");
1268 emit_guard(ctx, &guard_body.guard, out)?;
1269 out.push('\n');
1270 emit_body(ctx, &guard_body.body, out)?;
1271 out.push_str("\nend");
1272 }
1273
1274 CompoundKind::If {
1275 conditionals,
1276 else_branch,
1277 } => {
1278 for (i, guard_body) in conditionals.iter().enumerate() {
1279 if i == 0 {
1280 out.push_str("if ");
1281 } else {
1282 out.push_str("\nelse if ");
1283 }
1284 emit_guard(ctx, &guard_body.guard, out)?;
1285 out.push('\n');
1286 emit_body(ctx, &guard_body.body, out)?;
1287 }
1288 if let Some(else_body) = else_branch {
1289 out.push_str("\nelse\n");
1290 emit_body(ctx, else_body, out)?;
1291 }
1292 out.push_str("\nend");
1293 }
1294
1295 CompoundKind::Case { word, arms } => {
1296 out.push_str("switch ");
1297 emit_word(ctx, word, out)?;
1298 out.push('\n');
1299 let mut pat_buf = String::with_capacity(32);
1300 for arm in arms {
1301 out.push_str("case ");
1302 for (i, pattern) in arm.patterns.iter().enumerate() {
1303 if i > 0 {
1304 out.push(' ');
1305 }
1306 pat_buf.clear();
1307 emit_word(ctx, pattern, &mut pat_buf)?;
1308
1309 if let Some(expanded) = expand_bracket_pattern(&pat_buf) {
1310 out.push_str(&expanded);
1311 } else if pat_buf.contains('*') || pat_buf.contains('?') {
1312 push_sq_escaped(out, &pat_buf);
1313 } else {
1314 out.push_str(&pat_buf);
1315 }
1316 }
1317 out.push('\n');
1318 emit_body(ctx, &arm.body, out)?;
1319 out.push('\n');
1320 }
1321 out.push_str("end");
1322 }
1323
1324 CompoundKind::CFor {
1325 init,
1326 cond,
1327 step,
1328 body,
1329 } => {
1330 if let Some(init_expr) = init {
1331 emit_standalone_arith(ctx, init_expr, out)?;
1332 out.push('\n');
1333 }
1334 out.push_str("while ");
1335 if let Some(cond_expr) = cond {
1336 emit_arith_condition(cond_expr, out)?;
1337 } else {
1338 out.push_str("true");
1339 }
1340 out.push('\n');
1341 emit_body(ctx, body, out)?;
1342 if let Some(step_expr) = step {
1343 out.push('\n');
1344 emit_standalone_arith(ctx, step_expr, out)?;
1345 }
1346 out.push_str("\nend");
1347 }
1348
1349 CompoundKind::Brace(cmds) => {
1350 out.push_str("begin\n");
1351 emit_body(ctx, cmds, out)?;
1352 out.push_str("\nend");
1353 }
1354
1355 CompoundKind::Subshell(cmds) => {
1356 if cmds.is_empty() {
1357 return Err(TranslateError::Unsupported("empty subshell"));
1358 }
1359 out.push_str("begin\n");
1360 out.push_str("set -l __reef_pwd (pwd)\n");
1361 let prev = ctx.in_subshell;
1362 ctx.in_subshell = true;
1363 emit_body(ctx, cmds, out)?;
1364 ctx.in_subshell = prev;
1365 out.push_str(
1366 "\nset -l __reef_rc $status; cd $__reef_pwd 2>/dev/null\nend",
1367 );
1368 }
1369
1370 CompoundKind::DoubleBracket(cmds) => {
1371 emit_body(ctx, cmds, out)?;
1372 }
1373
1374 CompoundKind::Arithmetic(arith) => {
1375 emit_standalone_arith(ctx, arith, out)?;
1376 }
1377 }
1378 Ok(())
1379}
1380
1381fn expand_bracket_pattern(pat: &str) -> Option<String> {
1383 if !pat.starts_with('[') || !pat.ends_with(']') || pat.len() < 3 {
1384 return None;
1385 }
1386 let inner = &pat[1..pat.len() - 1];
1387 if inner.contains('-') {
1388 return None;
1389 }
1390 let mut result = String::with_capacity(inner.len() * 4);
1391 for (i, &b) in inner.as_bytes().iter().enumerate() {
1392 if i > 0 {
1393 result.push(' ');
1394 }
1395 if b == b'\'' {
1396 result.push_str("'\\'''");
1397 } else {
1398 result.push('\'');
1399 result.push(b as char);
1400 result.push('\'');
1401 }
1402 }
1403 Some(result)
1404}
1405
1406fn emit_guard(ctx: &mut Ctx, guard: &[Cmd<'_>], out: &mut String) -> Res<()> {
1407 if guard.len() == 1 {
1408 emit_cmd(ctx, &guard[0], out)?;
1409 } else {
1410 out.push_str("begin; ");
1411 for (i, cmd) in guard.iter().enumerate() {
1412 if i > 0 {
1413 out.push_str("; ");
1414 }
1415 emit_cmd(ctx, cmd, out)?;
1416 }
1417 out.push_str("; end");
1418 }
1419 Ok(())
1420}
1421
1422fn emit_body(ctx: &mut Ctx, cmds: &[Cmd<'_>], out: &mut String) -> Res<()> {
1423 for (i, cmd) in cmds.iter().enumerate() {
1424 if i > 0 {
1425 out.push('\n');
1426 }
1427 emit_cmd(ctx, cmd, out)?;
1428 }
1429 Ok(())
1430}
1431
1432fn emit_word(ctx: &mut Ctx, word: &Word<'_>, out: &mut String) -> Res<()> {
1437 if word_has_nested_braces(word) {
1439 return Err(TranslateError::Unsupported(
1440 "nested brace expansion (fish expands in different order)",
1441 ));
1442 }
1443 if word_has_brace_range_concat(word) {
1447 return Err(TranslateError::Unsupported(
1448 "brace range with concatenated expansion",
1449 ));
1450 }
1451 match word {
1452 Word::Simple(p) => emit_word_part(ctx, p, out),
1453 Word::Concat(parts) => {
1454 for p in parts {
1455 emit_word_part(ctx, p, out)?;
1456 }
1457 Ok(())
1458 }
1459 }
1460}
1461
1462fn emit_word_unquoted(ctx: &mut Ctx, word: &Word<'_>, out: &mut String) -> Res<()> {
1464 match word {
1465 Word::Simple(WordPart::DQuoted(parts)) => {
1466 for part in parts {
1467 emit_atom(ctx, part, out)?;
1468 }
1469 Ok(())
1470 }
1471 Word::Simple(WordPart::SQuoted(s)) => {
1472 out.push_str(s);
1473 Ok(())
1474 }
1475 _ => emit_word(ctx, word, out),
1476 }
1477}
1478
1479fn emit_word_part(ctx: &mut Ctx, part: &WordPart<'_>, out: &mut String) -> Res<()> {
1480 match part {
1481 WordPart::Bare(atom) => emit_atom(ctx, atom, out),
1482 WordPart::DQuoted(parts) => {
1483 let mut in_quotes = true;
1484 out.push('"');
1485 for atom in parts {
1486 if let Atom::Subst(_) = atom {
1487 if in_quotes {
1488 out.push('"');
1489 in_quotes = false;
1490 }
1491 } else if !in_quotes {
1492 out.push('"');
1493 in_quotes = true;
1494 }
1495 emit_atom(ctx, atom, out)?;
1496 }
1497 if in_quotes {
1498 out.push('"');
1499 }
1500 Ok(())
1501 }
1502 WordPart::SQuoted(s) => {
1503 out.push('\'');
1504 out.push_str(s);
1505 out.push('\'');
1506 Ok(())
1507 }
1508 }
1509}
1510
1511fn emit_atom(ctx: &mut Ctx, atom: &Atom<'_>, out: &mut String) -> Res<()> {
1512 match atom {
1513 Atom::Lit(s) => {
1514 out.push_str(s);
1515 Ok(())
1516 }
1517 Atom::Escaped(s) => {
1518 out.push('\\');
1519 out.push_str(s);
1520 Ok(())
1521 }
1522 Atom::Param(param) => {
1523 check_untranslatable_var(param)?;
1524 emit_param(param, out);
1525 Ok(())
1526 }
1527 Atom::Subst(subst) => emit_subst(ctx, subst, out),
1528 Atom::Star => {
1529 out.push('*');
1530 Ok(())
1531 }
1532 Atom::Question => {
1533 out.push('?');
1534 Ok(())
1535 }
1536 Atom::SquareOpen => {
1537 out.push('[');
1538 Ok(())
1539 }
1540 Atom::SquareClose => {
1541 out.push(']');
1542 Ok(())
1543 }
1544 Atom::Tilde => {
1545 out.push('~');
1546 Ok(())
1547 }
1548 Atom::ProcSubIn(cmds) => {
1549 out.push('(');
1550 for (i, cmd) in cmds.iter().enumerate() {
1551 if i > 0 {
1552 out.push_str("; ");
1553 }
1554 emit_cmd(ctx, cmd, out)?;
1555 }
1556 out.push_str(" | psub)");
1557 Ok(())
1558 }
1559 Atom::AnsiCQuoted(s) => {
1560 emit_ansi_c_quoted(s, out);
1561 Ok(())
1562 }
1563 Atom::BraceRange { start, end, step } => {
1564 emit_brace_range(start, end, *step, out);
1565 Ok(())
1566 }
1567 }
1568}
1569
1570fn word_has_brace_range_concat(word: &Word<'_>) -> bool {
1574 let Word::Concat(parts) = word else { return false };
1575 let has_brace_range = parts.iter().any(|p| {
1576 matches!(p, WordPart::Bare(Atom::BraceRange { .. }))
1577 });
1578 if !has_brace_range {
1579 return false;
1580 }
1581 parts.iter().any(|p| match p {
1584 WordPart::Bare(Atom::Param(_) | Atom::Subst(_) | Atom::ProcSubIn(_)) => true,
1585 WordPart::DQuoted(atoms) => atoms.iter().any(|a| !matches!(a, Atom::Lit(_))),
1586 _ => false,
1587 })
1588}
1589
1590fn word_has_nested_braces(word: &Word<'_>) -> bool {
1594 let mut state = BraceState::default();
1595 let parts: &[WordPart<'_>] = match word {
1596 Word::Simple(p) => std::slice::from_ref(p),
1597 Word::Concat(parts) => parts,
1598 };
1599 for p in parts {
1600 if scan_part_braces(p, &mut state) {
1601 return true;
1602 }
1603 }
1604 state.count >= 2
1605}
1606
1607#[derive(Default)]
1608struct BraceState {
1609 count: u32,
1611 in_brace: bool,
1613 has_comma: bool,
1615}
1616
1617fn scan_part_braces(part: &WordPart<'_>, st: &mut BraceState) -> bool {
1620 let slice: &str = match part {
1621 WordPart::Bare(Atom::Lit(s)) | WordPart::SQuoted(s) => s,
1622 _ => return false, };
1624 for &b in slice.as_bytes() {
1625 if st.in_brace {
1626 match b {
1627 b',' => st.has_comma = true,
1628 b'}' => {
1629 st.in_brace = false;
1630 if st.has_comma {
1631 st.count += 1;
1632 if st.count >= 2 {
1633 return true;
1634 }
1635 } else {
1636 st.count = 0;
1637 }
1638 }
1639 _ => {}
1640 }
1641 } else if b == b'{' {
1642 st.in_brace = true;
1643 st.has_comma = false;
1644 } else {
1645 st.count = 0;
1646 }
1647 }
1648 false
1649}
1650
1651#[inline]
1656fn ensure_bare(in_dq: &mut bool, out: &mut String) {
1657 if *in_dq {
1658 out.push('"');
1659 *in_dq = false;
1660 }
1661}
1662
1663#[inline]
1665fn ensure_dquoted(in_dq: &mut bool, out: &mut String) {
1666 if !*in_dq {
1667 out.push('"');
1668 *in_dq = true;
1669 }
1670}
1671
1672fn emit_ansi_c_quoted(s: &str, out: &mut String) {
1673 let bytes = s.as_bytes();
1674 let mut i = 0;
1675 let mut in_dq = false;
1676
1677 while i < bytes.len() {
1678 if bytes[i] == b'\\' && i + 1 < bytes.len() {
1679 match bytes[i + 1] {
1680 b'n' | b't' | b'r' | b'a' | b'b' | b'e' | b'f' | b'v' => {
1682 ensure_bare(&mut in_dq, out);
1683 out.push('\\');
1684 out.push(bytes[i + 1] as char);
1685 i += 2;
1686 }
1687 b'E' => {
1688 ensure_bare(&mut in_dq, out);
1689 out.push_str("\\e");
1690 i += 2;
1691 }
1692 b'x' | b'0' => {
1693 ensure_bare(&mut in_dq, out);
1694 out.push('\\');
1695 i += 1;
1696 while i < bytes.len()
1697 && (bytes[i].is_ascii_hexdigit() || bytes[i] == b'x' || bytes[i] == b'0')
1698 {
1699 out.push(bytes[i] as char);
1700 i += 1;
1701 }
1702 }
1703 b'\'' => {
1704 ensure_dquoted(&mut in_dq, out);
1705 out.push('\'');
1706 i += 2;
1707 }
1708 b'\\' => {
1709 ensure_dquoted(&mut in_dq, out);
1710 out.push_str("\\\\");
1711 i += 2;
1712 }
1713 b'?' => {
1714 ensure_dquoted(&mut in_dq, out);
1715 out.push('?');
1716 i += 2;
1717 }
1718 _ => {
1719 ensure_dquoted(&mut in_dq, out);
1720 out.push(bytes[i + 1] as char);
1721 i += 2;
1722 }
1723 }
1724 } else {
1725 ensure_dquoted(&mut in_dq, out);
1726 match bytes[i] {
1727 b'$' => out.push_str("\\$"),
1728 b'"' => out.push_str("\\\""),
1729 _ => out.push(bytes[i] as char),
1730 }
1731 i += 1;
1732 }
1733 }
1734 if in_dq {
1735 out.push('"');
1736 }
1737}
1738
1739fn emit_brace_range(start: &str, end: &str, step: Option<&str>, out: &mut String) {
1740 let sc = start.as_bytes().first().copied().unwrap_or(0);
1742 let ec = end.as_bytes().first().copied().unwrap_or(0);
1743 if start.len() == 1 && end.len() == 1 && sc.is_ascii_alphabetic() && ec.is_ascii_alphabetic() {
1744 out.push_str(&expand_alpha_range(sc as char, ec as char));
1745 return;
1746 }
1747
1748 if let Some(step) = step {
1750 out.push_str("(seq ");
1751 out.push_str(start);
1752 out.push(' ');
1753 out.push_str(step);
1754 out.push(' ');
1755 out.push_str(end);
1756 out.push(')');
1757 } else if let (Ok(s), Ok(e)) = (start.parse::<i64>(), end.parse::<i64>()) {
1758 out.push_str("(seq ");
1759 out.push_str(start);
1760 if s > e {
1761 out.push_str(" -1 ");
1762 } else {
1763 out.push(' ');
1764 }
1765 out.push_str(end);
1766 out.push(')');
1767 } else {
1768 out.push_str("(seq ");
1769 out.push_str(start);
1770 out.push(' ');
1771 out.push_str(end);
1772 out.push(')');
1773 }
1774}
1775
1776fn expand_alpha_range(start: char, end: char) -> String {
1777 let (lo, hi) = if start <= end {
1778 (start as u8, end as u8)
1779 } else {
1780 (end as u8, start as u8)
1781 };
1782 let count = (hi - lo + 1) as usize;
1783 let mut result = String::with_capacity(count * 2);
1784 if start <= end {
1785 for c in lo..=hi {
1786 if !result.is_empty() {
1787 result.push(' ');
1788 }
1789 result.push(c as char);
1790 }
1791 } else {
1792 for c in (lo..=hi).rev() {
1793 if !result.is_empty() {
1794 result.push(' ');
1795 }
1796 result.push(c as char);
1797 }
1798 }
1799 result
1800}
1801
1802fn check_untranslatable_var(param: &Param<'_>) -> Res<()> {
1808 if let Param::Var(name) = param {
1809 match *name {
1810 "LINENO" => return Err(TranslateError::Unsupported("$LINENO")),
1811 "FUNCNAME" => return Err(TranslateError::Unsupported("$FUNCNAME")),
1812 "SECONDS" => return Err(TranslateError::Unsupported("$SECONDS")),
1813 "COMP_WORDS" | "COMP_CWORD" | "COMP_LINE" | "COMP_POINT" => {
1814 return Err(TranslateError::Unsupported("bash completion variable"));
1815 }
1816 _ => {}
1817 }
1818 }
1819 Ok(())
1820}
1821
1822fn emit_param(param: &Param<'_>, out: &mut String) {
1823 match param {
1824 Param::Var("RANDOM") => out.push_str("(random)"),
1825 Param::Var("HOSTNAME") => out.push_str("$hostname"),
1826 Param::Var("BASH_SOURCE" | "BASH_SOURCE[@]") => {
1827 out.push_str("(status filename)");
1828 }
1829 Param::Var("PIPESTATUS") => out.push_str("$pipestatus"),
1830 Param::Var(name) => {
1831 out.push('$');
1832 out.push_str(name);
1833 }
1834 Param::Positional(n) => {
1835 if *n == 0 {
1836 out.push_str("(status filename)");
1837 } else {
1838 out.push_str("$argv[");
1839 itoa(out, i64::from(*n));
1840 out.push(']');
1841 }
1842 }
1843 Param::At | Param::Star => out.push_str("$argv"),
1844 Param::Pound => out.push_str("(count $argv)"),
1845 Param::Status => out.push_str("$status"),
1846 Param::Pid => out.push_str("$fish_pid"),
1847 Param::Bang => out.push_str("$last_pid"),
1848 Param::Dash => out.push_str("\"\""),
1849 }
1850}
1851
1852fn emit_subst(ctx: &mut Ctx, subst: &Subst<'_>, out: &mut String) -> Res<()> {
1853 match subst {
1854 Subst::Cmd(cmds) => {
1855 out.push('(');
1856 for (i, cmd) in cmds.iter().enumerate() {
1857 if i > 0 {
1858 out.push_str("; ");
1859 }
1860 emit_cmd(ctx, cmd, out)?;
1861 }
1862 out.push(')');
1863 Ok(())
1864 }
1865
1866 Subst::Arith(Some(arith)) => {
1867 if arith_has_unsupported(arith) {
1868 return Err(TranslateError::Unsupported(
1869 "unsupported arithmetic (bitwise, increment, or assignment)",
1870 ));
1871 }
1872 if arith_needs_test(arith) {
1873 emit_arith_as_command(arith, out)
1874 } else {
1875 out.push_str("(math \"");
1876 emit_arith(arith, out);
1877 out.push_str("\")");
1878 Ok(())
1879 }
1880 }
1881 Subst::Arith(None) => {
1882 out.push_str("(math 0)");
1883 Ok(())
1884 }
1885
1886 Subst::Indirect(name) => {
1887 out.push_str("$$");
1889 out.push_str(name);
1890 Ok(())
1891 }
1892
1893 Subst::PrefixList(prefix) => {
1894 out.push_str("(set -n | string match '");
1896 out.push_str(prefix);
1897 out.push_str("*')");
1898 Ok(())
1899 }
1900
1901 Subst::Transform(name, op) => {
1902 match op {
1903 b'Q' => {
1904 out.push_str("(string escape -- $");
1906 out.push_str(name);
1907 out.push(')');
1908 Ok(())
1909 }
1910 b'U' => {
1911 out.push_str("(string upper -- $");
1913 out.push_str(name);
1914 out.push(')');
1915 Ok(())
1916 }
1917 b'u' => {
1918 out.push_str("(string sub -l 1 -- $");
1920 out.push_str(name);
1921 out.push_str(" | string upper)(string sub -s 2 -- $");
1922 out.push_str(name);
1923 out.push(')');
1924 Ok(())
1925 }
1926 b'L' => {
1927 out.push_str("(string lower -- $");
1929 out.push_str(name);
1930 out.push(')');
1931 Ok(())
1932 }
1933 b'E' => Err(TranslateError::Unsupported("${var@E} escape expansion")),
1934 b'P' => Err(TranslateError::Unsupported("${var@P} prompt expansion")),
1935 b'A' => Err(TranslateError::Unsupported("${var@A} assignment form")),
1936 b'K' => Err(TranslateError::Unsupported("${var@K} quoted key-value")),
1937 b'a' => Err(TranslateError::Unsupported("${var@a} attribute flags")),
1938 _ => Err(TranslateError::Unsupported(
1939 "unsupported parameter transformation",
1940 )),
1941 }
1942 }
1943
1944 Subst::Len(param) => {
1945 out.push_str("(string length -- \"");
1946 emit_param(param, out);
1947 out.push_str("\")");
1948 Ok(())
1949 }
1950
1951 Subst::Default(param, word) => {
1952 out.push_str("(set -q ");
1953 emit_param_name(param, out);
1954 out.push_str("; and echo $");
1955 emit_param_name(param, out);
1956 out.push_str("; or echo ");
1957 if let Some(w) = word {
1958 emit_word(ctx, w, out)?;
1959 }
1960 out.push(')');
1961 Ok(())
1962 }
1963
1964 Subst::Assign(param, word) => {
1965 out.push_str("(set -q ");
1966 emit_param_name(param, out);
1967 out.push_str("; or set ");
1968 emit_param_name(param, out);
1969 out.push(' ');
1970 if let Some(w) = word {
1971 emit_word(ctx, w, out)?;
1972 }
1973 out.push_str("; echo $");
1974 emit_param_name(param, out);
1975 out.push(')');
1976 Ok(())
1977 }
1978
1979 Subst::Error(param, word) => {
1980 out.push_str("(set -q ");
1981 emit_param_name(param, out);
1982 out.push_str("; and echo $");
1983 emit_param_name(param, out);
1984 out.push_str("; or begin; echo ");
1985 if let Some(w) = word {
1986 emit_word(ctx, w, out)?;
1987 } else {
1988 out.push_str("'parameter ");
1989 emit_param_name(param, out);
1990 out.push_str(" not set'");
1991 }
1992 out.push_str(" >&2; return 1; end)");
1993 Ok(())
1994 }
1995
1996 Subst::Alt(param, word) => {
1997 out.push_str("(set -q ");
1998 emit_param_name(param, out);
1999 out.push_str("; and echo ");
2000 if let Some(w) = word {
2001 emit_word(ctx, w, out)?;
2002 }
2003 out.push(')');
2004 Ok(())
2005 }
2006
2007 Subst::TrimSuffixSmall(param, pattern) => {
2008 emit_string_op(ctx, param, pattern.as_ref(), "suffix", false, out)
2009 }
2010 Subst::TrimSuffixLarge(param, pattern) => {
2011 emit_string_op(ctx, param, pattern.as_ref(), "suffix", true, out)
2012 }
2013 Subst::TrimPrefixSmall(param, pattern) => {
2014 emit_string_op(ctx, param, pattern.as_ref(), "prefix", false, out)
2015 }
2016 Subst::TrimPrefixLarge(param, pattern) => {
2017 emit_string_op(ctx, param, pattern.as_ref(), "prefix", true, out)
2018 }
2019
2020 Subst::Upper(all, param) => {
2021 if !all {
2022 out.push_str("(string sub -l 1 -- $");
2024 emit_param_name(param, out);
2025 out.push_str(" | string upper)(string sub -s 2 -- $");
2026 emit_param_name(param, out);
2027 out.push(')');
2028 return Ok(());
2029 }
2030 out.push_str("(string upper -- \"");
2031 emit_param(param, out);
2032 out.push_str("\")");
2033 Ok(())
2034 }
2035 Subst::Lower(all, param) => {
2036 if !all {
2037 out.push_str("(string sub -l 1 -- $");
2038 emit_param_name(param, out);
2039 out.push_str(" | string lower)(string sub -s 2 -- $");
2040 emit_param_name(param, out);
2041 out.push(')');
2042 return Ok(());
2043 }
2044 out.push_str("(string lower -- \"");
2045 emit_param(param, out);
2046 out.push_str("\")");
2047 Ok(())
2048 }
2049
2050 Subst::Replace(param, pattern, replacement) => {
2051 emit_string_replace(ctx, param, pattern.as_ref(), replacement.as_ref(), false, false, false, out)
2052 }
2053 Subst::ReplaceAll(param, pattern, replacement) => {
2054 emit_string_replace(ctx, param, pattern.as_ref(), replacement.as_ref(), true, false, false, out)
2055 }
2056 Subst::ReplacePrefix(param, pattern, replacement) => {
2057 emit_string_replace(ctx, param, pattern.as_ref(), replacement.as_ref(), false, true, false, out)
2058 }
2059 Subst::ReplaceSuffix(param, pattern, replacement) => {
2060 emit_string_replace(ctx, param, pattern.as_ref(), replacement.as_ref(), false, false, true, out)
2061 }
2062
2063 Subst::Substring(param, offset, length) => {
2064 out.push_str("(string sub -s (math \"");
2065 out.push_str(offset);
2066 out.push_str(" + 1\")");
2067 if let Some(len) = length {
2068 out.push_str(" -l (math \"");
2069 out.push_str(len);
2070 out.push_str("\")");
2071 }
2072 out.push_str(" -- \"");
2073 emit_param(param, out);
2074 out.push_str("\")");
2075 Ok(())
2076 }
2077
2078 Subst::ArrayElement(name, idx) => {
2080 if *name == "BASH_REMATCH" {
2081 out.push_str("$__bash_rematch[");
2082 emit_array_index(ctx, idx, out)?;
2083 out.push(']');
2084 } else if *name == "PIPESTATUS" {
2085 out.push_str("$pipestatus[");
2086 emit_array_index(ctx, idx, out)?;
2087 out.push(']');
2088 } else {
2089 out.push('$');
2091 out.push_str(name);
2092 out.push('[');
2093 emit_array_index(ctx, idx, out)?;
2094 out.push(']');
2095 }
2096 Ok(())
2097 }
2098 Subst::ArrayAll(name) => {
2099 if *name == "PIPESTATUS" {
2101 out.push_str("$pipestatus");
2102 } else {
2103 out.push('$');
2104 out.push_str(name);
2105 }
2106 Ok(())
2107 }
2108 Subst::ArrayLen(name) => {
2109 out.push_str("(count $");
2111 out.push_str(name);
2112 out.push(')');
2113 Ok(())
2114 }
2115 Subst::ArraySlice(name, offset, length) => {
2116 out.push('$');
2118 out.push_str(name);
2119 out.push_str("[(math \"");
2120 out.push_str(offset);
2121 out.push_str(" + 1\")..(math \"");
2122 if let Some(len) = length {
2123 out.push_str(offset);
2124 out.push_str(" + ");
2125 out.push_str(len);
2126 } else {
2127 out.push_str("(count $");
2129 out.push_str(name);
2130 out.push(')');
2131 }
2132 out.push_str("\")]");
2133 Ok(())
2134 }
2135 }
2136}
2137
2138fn emit_array_index(ctx: &mut Ctx, idx: &Word<'_>, out: &mut String) -> Res<()> {
2142 if let Some(s) = word_as_str(idx)
2144 && let Ok(n) = s.parse::<i64>()
2145 {
2146 itoa(out, n + 1);
2147 return Ok(());
2148 }
2149
2150 if let Word::Simple(WordPart::Bare(Atom::Subst(subst))) = idx
2152 && let Subst::Arith(Some(arith)) = subst.as_ref()
2153 {
2154 out.push_str("(math \"");
2155 emit_arith(arith, out);
2156 out.push_str(" + 1\")");
2157 return Ok(());
2158 }
2159
2160 out.push_str("(math \"");
2162 emit_word(ctx, idx, out)?;
2163 out.push_str(" + 1\")");
2164 Ok(())
2165}
2166
2167fn emit_string_op(ctx: &mut Ctx,
2169 param: &Param<'_>,
2170 pattern: Option<&Word<'_>>,
2171 kind: &str,
2172 greedy: bool,
2173 out: &mut String,
2174) -> Res<()> {
2175 let suffix_small = kind == "suffix" && !greedy;
2178
2179 out.push_str("(string replace -r -- '");
2180
2181 if suffix_small {
2182 out.push_str("^(.*)");
2183 } else if kind == "prefix" {
2184 out.push('^');
2185 }
2186
2187 if let Some(p) = pattern {
2188 let pat_greedy = if suffix_small { true } else { greedy };
2191 emit_word_as_pattern(ctx, p, out, pat_greedy)?;
2192 }
2193
2194 if kind == "suffix" {
2195 out.push('$');
2196 }
2197
2198 if suffix_small {
2199 out.push_str("' '$1' $");
2200 } else {
2201 out.push_str("' '' $");
2202 }
2203 emit_param_name(param, out);
2204 out.push(')');
2205 Ok(())
2206}
2207
2208#[allow(clippy::too_many_arguments)]
2213fn emit_string_replace(ctx: &mut Ctx,
2214 param: &Param<'_>,
2215 pattern: Option<&Word<'_>>,
2216 replacement: Option<&Word<'_>>,
2217 all: bool,
2218 prefix: bool,
2219 suffix: bool,
2220 out: &mut String,
2221) -> Res<()> {
2222 let needs_regex = prefix || suffix || pattern.is_some_and(word_has_glob);
2223
2224 out.push_str("(string replace ");
2225 if needs_regex {
2226 out.push_str("-r ");
2227 }
2228 if all {
2229 out.push_str("-a ");
2230 }
2231 out.push_str("-- '");
2232
2233 if prefix {
2234 out.push('^');
2235 }
2236 if let Some(p) = pattern {
2237 if needs_regex {
2238 emit_word_as_pattern(ctx, p, out, true)?;
2239 } else {
2240 emit_word_unquoted(ctx, p, out)?;
2241 }
2242 }
2243 if suffix {
2244 out.push('$');
2245 }
2246 out.push_str("' '");
2247 if let Some(r) = replacement {
2248 emit_word_unquoted(ctx, r, out)?;
2249 }
2250 out.push_str("' \"$");
2251 emit_param_name(param, out);
2252 out.push_str("\")");
2253 Ok(())
2254}
2255
2256fn emit_word_as_pattern(ctx: &mut Ctx,
2260 word: &Word<'_>,
2261 out: &mut String,
2262 greedy: bool,
2263) -> Res<()> {
2264 let mut pieces: Vec<PatPiece<'_>> = Vec::new();
2266 match word {
2267 Word::Simple(p) => collect_pattern_pieces(p, &mut pieces),
2268 Word::Concat(parts) => {
2269 for p in parts {
2270 collect_pattern_pieces(p, &mut pieces);
2271 }
2272 }
2273 }
2274
2275 for piece in &pieces {
2277 match piece {
2278 PatPiece::Lit(s) => {
2279 for &b in s.as_bytes() {
2280 match b {
2281 b'.' | b'+' | b'(' | b')' | b'{' | b'}' | b'|' | b'\\' | b'^' | b'$' => {
2282 out.push('\\');
2283 out.push(b as char);
2284 }
2285 _ => out.push(b as char),
2286 }
2287 }
2288 }
2289 PatPiece::Star => {
2290 if greedy {
2291 out.push_str(".*");
2292 } else {
2293 out.push_str(".*?");
2294 }
2295 }
2296 PatPiece::Question => out.push('.'),
2297 PatPiece::Other(atom) => emit_atom(ctx, atom, out)?,
2298 }
2299 }
2300 Ok(())
2301}
2302
2303enum PatPiece<'a> {
2304 Lit(&'a str),
2305 Star,
2306 Question,
2307 Other(&'a Atom<'a>),
2308}
2309
2310fn collect_pattern_pieces<'a>(part: &'a WordPart<'a>, pieces: &mut Vec<PatPiece<'a>>) {
2311 match part {
2312 WordPart::Bare(atom) => match atom {
2313 Atom::Lit(s) => pieces.push(PatPiece::Lit(s)),
2314 Atom::Star => pieces.push(PatPiece::Star),
2315 Atom::Question => pieces.push(PatPiece::Question),
2316 other => pieces.push(PatPiece::Other(other)),
2317 },
2318 WordPart::SQuoted(s) => pieces.push(PatPiece::Lit(s)),
2319 WordPart::DQuoted(atoms) => {
2320 for atom in atoms {
2321 match atom {
2322 Atom::Lit(s) => pieces.push(PatPiece::Lit(s)),
2323 other => pieces.push(PatPiece::Other(other)),
2324 }
2325 }
2326 }
2327 }
2328}
2329
2330fn emit_standalone_arith(ctx: &mut Ctx, arith: &Arith<'_>, out: &mut String) -> Res<()> {
2336 let set_kw = if ctx.in_subshell { "set -l " } else { "set " };
2337 match arith {
2338 Arith::PostInc(var) | Arith::PreInc(var) => {
2339 out.push_str(set_kw);
2340 out.push_str(var);
2341 out.push_str(" (math \"$");
2342 out.push_str(var);
2343 out.push_str(" + 1\")");
2344 Ok(())
2345 }
2346 Arith::PostDec(var) | Arith::PreDec(var) => {
2347 out.push_str(set_kw);
2348 out.push_str(var);
2349 out.push_str(" (math \"$");
2350 out.push_str(var);
2351 out.push_str(" - 1\")");
2352 Ok(())
2353 }
2354 Arith::Assign(var, expr) => {
2355 out.push_str(set_kw);
2356 out.push_str(var);
2357 out.push_str(" (math \"");
2358 emit_arith(expr, out);
2359 out.push_str("\")");
2360 Ok(())
2361 }
2362 Arith::Lt(..)
2363 | Arith::Le(..)
2364 | Arith::Gt(..)
2365 | Arith::Ge(..)
2366 | Arith::Eq(..)
2367 | Arith::Ne(..)
2368 | Arith::LogAnd(..)
2369 | Arith::LogOr(..)
2370 | Arith::LogNot(..) => emit_arith_condition(arith, out),
2371
2372 _ => Err(TranslateError::Unsupported(
2373 "unsupported standalone arithmetic expression",
2374 )),
2375 }
2376}
2377
2378fn emit_arith(arith: &Arith<'_>, out: &mut String) {
2379 match arith {
2380 Arith::Var(name) => {
2381 if name.as_bytes().first().is_some_and(u8::is_ascii_digit) {
2383 out.push_str("$argv[");
2384 out.push_str(name);
2385 out.push(']');
2386 } else {
2387 out.push('$');
2388 out.push_str(name);
2389 }
2390 }
2391 Arith::Lit(n) => {
2392 itoa(out, *n);
2393 }
2394
2395 Arith::Add(l, r) => emit_arith_binop(l, " + ", r, out),
2396 Arith::Sub(l, r) => emit_arith_binop(l, " - ", r, out),
2397 Arith::Mul(l, r) => emit_arith_binop(l, " * ", r, out),
2398 Arith::Div(l, r) => {
2399 out.push_str("floor(");
2404 emit_arith(l, out);
2405 out.push_str(" / ");
2406 emit_arith(r, out);
2407 out.push(')');
2408 }
2409 Arith::Rem(l, r) => emit_arith_binop(l, " % ", r, out),
2410 Arith::Pow(l, r) => emit_arith_binop(l, " ^ ", r, out),
2411 Arith::Lt(l, r) => emit_arith_binop(l, " < ", r, out),
2412 Arith::Le(l, r) => emit_arith_binop(l, " <= ", r, out),
2413 Arith::Gt(l, r) => emit_arith_binop(l, " > ", r, out),
2414 Arith::Ge(l, r) => emit_arith_binop(l, " >= ", r, out),
2415 Arith::Eq(l, r) => emit_arith_binop(l, " == ", r, out),
2416 Arith::Ne(l, r) => emit_arith_binop(l, " != ", r, out),
2417 Arith::BitAnd(l, r) => {
2418 out.push_str("bitand(");
2419 emit_arith(l, out);
2420 out.push_str(", ");
2421 emit_arith(r, out);
2422 out.push(')');
2423 }
2424 Arith::BitOr(l, r) => {
2425 out.push_str("bitor(");
2426 emit_arith(l, out);
2427 out.push_str(", ");
2428 emit_arith(r, out);
2429 out.push(')');
2430 }
2431 Arith::BitXor(l, r) => {
2432 out.push_str("bitxor(");
2433 emit_arith(l, out);
2434 out.push_str(", ");
2435 emit_arith(r, out);
2436 out.push(')');
2437 }
2438 Arith::LogAnd(l, r) => emit_arith_binop(l, " && ", r, out),
2439 Arith::LogOr(l, r) => emit_arith_binop(l, " || ", r, out),
2440 Arith::Shl(l, r) => {
2441 out.push('(');
2443 emit_arith(l, out);
2444 out.push_str(" * 2 ^ ");
2445 emit_arith(r, out);
2446 out.push(')');
2447 }
2448 Arith::Shr(l, r) => {
2449 out.push_str("floor(");
2451 emit_arith(l, out);
2452 out.push_str(" / 2 ^ ");
2453 emit_arith(r, out);
2454 out.push(')');
2455 }
2456
2457 Arith::Pos(e) => {
2458 out.push('+');
2459 emit_arith(e, out);
2460 }
2461 Arith::Neg(e) => {
2462 out.push('-');
2463 emit_arith(e, out);
2464 }
2465 Arith::LogNot(e) => {
2466 out.push('!');
2467 emit_arith(e, out);
2468 }
2469 Arith::BitNot(e) => {
2470 out.push_str("bitxor(");
2472 emit_arith(e, out);
2473 out.push_str(", -1)");
2474 }
2475
2476 Arith::PostInc(var) | Arith::PreInc(var) => {
2477 out.push_str("($");
2478 out.push_str(var);
2479 out.push_str(" + 1)");
2480 }
2481 Arith::PostDec(var) | Arith::PreDec(var) => {
2482 out.push_str("($");
2483 out.push_str(var);
2484 out.push_str(" - 1)");
2485 }
2486
2487 Arith::Ternary(cond, then_val, else_val) => {
2488 out.push('(');
2489 emit_arith(cond, out);
2490 out.push_str(" ? ");
2491 emit_arith(then_val, out);
2492 out.push_str(" : ");
2493 emit_arith(else_val, out);
2494 out.push(')');
2495 }
2496
2497 Arith::Assign(var, expr) => {
2498 out.push_str(var);
2499 out.push_str(" = ");
2500 emit_arith(expr, out);
2501 }
2502 }
2503}
2504
2505fn emit_arith_binop(l: &Arith<'_>, op: &str, r: &Arith<'_>, out: &mut String) {
2506 let l_needs_parens = is_arith_binop(l);
2507 let r_needs_parens = is_arith_binop(r);
2508
2509 if l_needs_parens {
2510 out.push('(');
2511 }
2512 emit_arith(l, out);
2513 if l_needs_parens {
2514 out.push(')');
2515 }
2516
2517 out.push_str(op);
2518
2519 if r_needs_parens {
2520 out.push('(');
2521 }
2522 emit_arith(r, out);
2523 if r_needs_parens {
2524 out.push(')');
2525 }
2526}
2527
2528fn is_arith_binop(arith: &Arith<'_>) -> bool {
2529 matches!(
2530 arith,
2531 Arith::Add(..)
2532 | Arith::Sub(..)
2533 | Arith::Mul(..)
2534 | Arith::Div(..)
2535 | Arith::Rem(..)
2536 | Arith::Pow(..)
2537 | Arith::Lt(..)
2538 | Arith::Le(..)
2539 | Arith::Gt(..)
2540 | Arith::Ge(..)
2541 | Arith::Eq(..)
2542 | Arith::Ne(..)
2543 | Arith::BitAnd(..)
2544 | Arith::BitOr(..)
2545 | Arith::BitXor(..)
2546 | Arith::LogAnd(..)
2547 | Arith::LogOr(..)
2548 | Arith::Shl(..)
2549 | Arith::Shr(..)
2550 )
2551}
2552
2553fn arith_has_unsupported(arith: &Arith<'_>) -> bool {
2555 match arith {
2556 Arith::PostInc(..)
2557 | Arith::PreInc(..)
2558 | Arith::PostDec(..)
2559 | Arith::PreDec(..)
2560 | Arith::Assign(..) => true,
2561
2562 Arith::Add(l, r)
2563 | Arith::Sub(l, r)
2564 | Arith::Mul(l, r)
2565 | Arith::Div(l, r)
2566 | Arith::Rem(l, r)
2567 | Arith::Pow(l, r)
2568 | Arith::Lt(l, r)
2569 | Arith::Le(l, r)
2570 | Arith::Gt(l, r)
2571 | Arith::Ge(l, r)
2572 | Arith::Eq(l, r)
2573 | Arith::Ne(l, r)
2574 | Arith::LogAnd(l, r)
2575 | Arith::LogOr(l, r)
2576 | Arith::BitAnd(l, r)
2577 | Arith::BitOr(l, r)
2578 | Arith::BitXor(l, r)
2579 | Arith::Shl(l, r)
2580 | Arith::Shr(l, r) => arith_has_unsupported(l) || arith_has_unsupported(r),
2581
2582 Arith::Pos(e) | Arith::Neg(e) | Arith::LogNot(e) | Arith::BitNot(e) => {
2583 arith_has_unsupported(e)
2584 }
2585
2586 Arith::Ternary(c, t, f) => {
2587 arith_has_unsupported(c) || arith_has_unsupported(t) || arith_has_unsupported(f)
2588 }
2589
2590 Arith::Var(_) | Arith::Lit(_) => false,
2591 }
2592}
2593
2594fn arith_needs_test(arith: &Arith<'_>) -> bool {
2596 matches!(
2597 arith,
2598 Arith::Lt(..)
2599 | Arith::Le(..)
2600 | Arith::Gt(..)
2601 | Arith::Ge(..)
2602 | Arith::Eq(..)
2603 | Arith::Ne(..)
2604 | Arith::LogAnd(..)
2605 | Arith::LogOr(..)
2606 | Arith::LogNot(..)
2607 | Arith::Ternary(..)
2608 )
2609}
2610
2611fn emit_arith_as_command(arith: &Arith<'_>, out: &mut String) -> Res<()> {
2612 if let Arith::Ternary(cond, then_val, else_val) = arith {
2613 out.push_str("(if ");
2614 emit_arith_condition(cond, out)?;
2615 out.push_str("; echo ");
2616 emit_arith_value(then_val, out)?;
2617 out.push_str("; else; echo ");
2618 emit_arith_value(else_val, out)?;
2619 out.push_str("; end)");
2620 } else {
2621 out.push('(');
2622 emit_arith_condition(arith, out)?;
2623 out.push_str("; and echo 1; or echo 0)");
2624 }
2625 Ok(())
2626}
2627
2628fn emit_arith_condition(arith: &Arith<'_>, out: &mut String) -> Res<()> {
2629 match arith {
2630 Arith::Lt(l, r) => emit_test_cmp(l, "-lt", r, out),
2631 Arith::Le(l, r) => emit_test_cmp(l, "-le", r, out),
2632 Arith::Gt(l, r) => emit_test_cmp(l, "-gt", r, out),
2633 Arith::Ge(l, r) => emit_test_cmp(l, "-ge", r, out),
2634 Arith::Eq(l, r) => emit_test_cmp(l, "-eq", r, out),
2635 Arith::Ne(l, r) => emit_test_cmp(l, "-ne", r, out),
2636 Arith::LogAnd(l, r) => {
2637 emit_arith_condition(l, out)?;
2638 out.push_str("; and ");
2639 emit_arith_condition(r, out)
2640 }
2641 Arith::LogOr(l, r) => {
2642 emit_arith_condition(l, out)?;
2643 out.push_str("; or ");
2644 emit_arith_condition(r, out)
2645 }
2646 Arith::LogNot(e) => {
2647 out.push_str("not ");
2648 emit_arith_condition(e, out)
2649 }
2650 _ => {
2651 out.push_str("test ");
2652 emit_arith_value(arith, out)?;
2653 out.push_str(" -ne 0");
2654 Ok(())
2655 }
2656 }
2657}
2658
2659fn emit_test_cmp(
2660 l: &Arith<'_>,
2661 op: &str,
2662 r: &Arith<'_>,
2663 out: &mut String,
2664) -> Res<()> {
2665 out.push_str("test ");
2666 emit_arith_value(l, out)?;
2667 out.push(' ');
2668 out.push_str(op);
2669 out.push(' ');
2670 emit_arith_value(r, out)
2671}
2672
2673fn emit_arith_value(arith: &Arith<'_>, out: &mut String) -> Res<()> {
2674 match arith {
2675 Arith::Var(name) => {
2676 out.push('$');
2677 out.push_str(name);
2678 Ok(())
2679 }
2680 Arith::Lit(n) => {
2681 itoa(out, *n);
2682 Ok(())
2683 }
2684 _ if arith_needs_test(arith) => emit_arith_as_command(arith, out),
2685 _ => {
2686 out.push_str("(math \"");
2687 emit_arith(arith, out);
2688 out.push_str("\")");
2689 Ok(())
2690 }
2691 }
2692}
2693
2694fn emit_redirects(ctx: &mut Ctx, redirects: &[&Redir<'_>], out: &mut String) -> Res<()> {
2699 for redir in redirects {
2700 out.push(' ');
2701 emit_redir(ctx, redir, out)?;
2702 }
2703 Ok(())
2704}
2705
2706fn emit_redir(ctx: &mut Ctx, redir: &Redir<'_>, out: &mut String) -> Res<()> {
2707 fn write_fd(fd: Option<u16>, out: &mut String) {
2708 if let Some(n) = fd {
2709 itoa(out, i64::from(n));
2710 }
2711 }
2712
2713 match redir {
2714 Redir::Read(fd, word) => {
2715 write_fd(*fd, out);
2716 out.push('<');
2717 emit_word(ctx, word, out)?;
2718 }
2719 Redir::Write(fd, word) => {
2720 write_fd(*fd, out);
2721 out.push('>');
2722 emit_word(ctx, word, out)?;
2723 }
2724 Redir::Append(fd, word) => {
2725 write_fd(*fd, out);
2726 out.push_str(">>");
2727 emit_word(ctx, word, out)?;
2728 }
2729 Redir::ReadWrite(fd, word) => {
2730 write_fd(*fd, out);
2731 out.push_str("<>");
2732 emit_word(ctx, word, out)?;
2733 }
2734 Redir::Clobber(fd, word) => {
2735 write_fd(*fd, out);
2736 out.push_str(">|");
2737 emit_word(ctx, word, out)?;
2738 }
2739 Redir::DupRead(fd, word) => {
2740 write_fd(*fd, out);
2741 out.push_str("<&");
2742 emit_word(ctx, word, out)?;
2743 }
2744 Redir::DupWrite(fd, word) => {
2745 write_fd(*fd, out);
2746 out.push_str(">&");
2747 emit_word(ctx, word, out)?;
2748 }
2749 Redir::HereString(_) | Redir::Heredoc(_) => {
2750 }
2752 Redir::WriteAll(word) => {
2753 out.push('>');
2754 emit_word(ctx, word, out)?;
2755 out.push_str(" 2>&1");
2756 }
2757 Redir::AppendAll(word) => {
2758 out.push_str(">>");
2759 emit_word(ctx, word, out)?;
2760 out.push_str(" 2>&1");
2761 }
2762 }
2763 Ok(())
2764}
2765
2766fn extract_printf_repeat_char(fmt: &str) -> Option<char> {
2773 let stripped = fmt.strip_prefix('%')?;
2775 let s_pos = stripped.find('s')?;
2777 let before_s = &stripped[..s_pos];
2778 if before_s.contains('0') && (before_s.contains('.') || before_s == "0") {
2780 let after_s = &stripped[s_pos + 1..];
2781 after_s.as_bytes().first().map(|&b| b as char)
2782 } else {
2783 None
2784 }
2785}
2786
2787fn extract_brace_range_count(args: &[&Word<'_>]) -> Option<i64> {
2790 for arg in args {
2792 if let Word::Simple(WordPart::Bare(Atom::BraceRange { start, end, step })) = arg {
2793 let s: i64 = start.parse().ok()?;
2794 let e: i64 = end.parse().ok()?;
2795 let st: i64 = step.and_then(|s| s.parse().ok()).unwrap_or(1);
2796 if st == 0 {
2797 return None;
2798 }
2799 let count = ((e - s).abs() / st.abs()) + 1;
2800 return Some(count);
2801 }
2802 }
2803 None
2804}
2805
2806fn extract_procsub_cmds<'a>(word: &'a Word<'a>) -> Option<&'a Vec<Cmd<'a>>> {
2808 match word {
2809 Word::Simple(WordPart::Bare(Atom::ProcSubIn(cmds))) => Some(cmds),
2810 _ => None,
2811 }
2812}
2813
2814fn word_as_str<'a>(word: &'a Word<'a>) -> Option<Cow<'a, str>> {
2815 if let Word::Simple(WordPart::Bare(Atom::Lit(s)) | WordPart::SQuoted(s)) = word {
2816 return Some(Cow::Borrowed(s));
2817 }
2818 let mut buf = String::with_capacity(64);
2819 if word_to_simple_string(word, &mut buf) {
2820 Some(Cow::Owned(buf))
2821 } else {
2822 None
2823 }
2824}
2825
2826#[inline]
2827fn word_has_glob(word: &Word<'_>) -> bool {
2828 match word {
2829 Word::Simple(p) => part_has_glob(p),
2830 Word::Concat(parts) => parts.iter().any(part_has_glob),
2831 }
2832}
2833
2834#[inline]
2835fn part_has_glob(part: &WordPart<'_>) -> bool {
2836 match part {
2837 WordPart::Bare(atom) => matches!(atom, Atom::Star | Atom::Question),
2838 WordPart::DQuoted(atoms) => atoms
2839 .iter()
2840 .any(|a| matches!(a, Atom::Star | Atom::Question)),
2841 WordPart::SQuoted(_) => false,
2842 }
2843}
2844
2845fn word_to_simple_string(word: &Word<'_>, out: &mut String) -> bool {
2846 match word {
2847 Word::Simple(p) => part_to_string(p, out),
2848 Word::Concat(parts) => {
2849 for p in parts {
2850 if !part_to_string(p, out) {
2851 return false;
2852 }
2853 }
2854 true
2855 }
2856 }
2857}
2858
2859fn part_to_string(part: &WordPart<'_>, out: &mut String) -> bool {
2860 match part {
2861 WordPart::Bare(atom) => atom_to_string(atom, out),
2862 WordPart::SQuoted(s) => {
2863 out.push_str(s);
2864 true
2865 }
2866 WordPart::DQuoted(atoms) => {
2867 for atom in atoms {
2868 if !atom_to_string(atom, out) {
2869 return false;
2870 }
2871 }
2872 true
2873 }
2874 }
2875}
2876
2877fn atom_to_string(atom: &Atom<'_>, out: &mut String) -> bool {
2878 match atom {
2879 Atom::Lit(s) => {
2880 out.push_str(s);
2881 true
2882 }
2883 Atom::Escaped(s) => {
2884 out.push_str(s);
2885 true
2886 }
2887 Atom::SquareOpen => {
2888 out.push('[');
2889 true
2890 }
2891 Atom::SquareClose => {
2892 out.push(']');
2893 true
2894 }
2895 Atom::Tilde => {
2896 out.push('~');
2897 true
2898 }
2899 Atom::Star => {
2900 out.push('*');
2901 true
2902 }
2903 Atom::Question => {
2904 out.push('?');
2905 true
2906 }
2907 _ => false,
2908 }
2909}
2910
2911#[inline]
2916fn itoa(out: &mut String, n: i64) {
2917 let mut buf = [0u8; 20]; let mut pos = buf.len();
2919 let negative = n < 0;
2920 let mut val = n.unsigned_abs();
2921 loop {
2922 pos -= 1;
2923 buf[pos] = b'0' + (val % 10) as u8;
2924 val /= 10;
2925 if val == 0 {
2926 break;
2927 }
2928 }
2929 if negative {
2930 pos -= 1;
2931 buf[pos] = b'-';
2932 }
2933 out.push_str(std::str::from_utf8(&buf[pos..]).expect("ASCII digits"));
2935}
2936
2937fn push_sq_escaped(out: &mut String, s: &str) {
2940 out.push('\'');
2941 for b in s.bytes() {
2942 if b == b'\'' {
2943 out.push_str("'\\''");
2944 } else {
2945 out.push(b as char);
2946 }
2947 }
2948 out.push('\'');
2949}
2950
2951fn emit_heredoc_body(ctx: &mut Ctx, body: &HeredocBody<'_>, out: &mut String) -> Res<()> {
2954 match body {
2955 HeredocBody::Literal(text) => {
2956 out.push_str("printf '%s\\n' ");
2957 push_sq_escaped(out, text.strip_suffix('\n').unwrap_or(text));
2958 Ok(())
2959 }
2960 HeredocBody::Interpolated(atoms) => {
2961 let mut body_str = String::with_capacity(256);
2964 for atom in atoms {
2965 match atom {
2966 Atom::Lit(s) => {
2967 for &b in s.as_bytes() {
2968 match b {
2969 b'"' => body_str.push_str("\\\""),
2970 b'\\' => body_str.push_str("\\\\"),
2971 b'$' => body_str.push_str("\\$"),
2972 _ => body_str.push(b as char),
2973 }
2974 }
2975 }
2976 Atom::Escaped(s) => {
2977 match s.as_ref() {
2979 "$" => body_str.push('$'),
2980 "\\" => body_str.push_str("\\\\"),
2981 "`" => body_str.push('`'),
2982 _ => body_str.push_str(s),
2983 }
2984 }
2985 Atom::Param(param) => emit_param(param, &mut body_str),
2986 Atom::Subst(subst) => {
2987 body_str.push('"');
2988 emit_subst(ctx, subst, &mut body_str)?;
2989 body_str.push('"');
2990 }
2991 _ => emit_atom(ctx, atom, &mut body_str)?,
2992 }
2993 }
2994 let trimmed = body_str.strip_suffix('\n').unwrap_or(&body_str);
2996 out.push_str("printf '%s\\n' \"");
2997 out.push_str(trimmed);
2998 out.push('"');
2999 Ok(())
3000 }
3001 }
3002}
3003
3004fn emit_param_name(param: &Param<'_>, out: &mut String) {
3005 match param {
3006 Param::Var("HOSTNAME") => out.push_str("hostname"),
3007 Param::Var("PIPESTATUS") => out.push_str("pipestatus"),
3008 Param::Var(name) => out.push_str(name),
3009 Param::Positional(n) => {
3010 out.push_str("argv[");
3011 itoa(out, i64::from(*n));
3012 out.push(']');
3013 }
3014 Param::At | Param::Star => out.push_str("argv"),
3015 Param::Pound => out.push_str("ARGC"),
3016 Param::Status => out.push_str("status"),
3017 Param::Pid => out.push_str("fish_pid"),
3018 Param::Bang => out.push_str("last_pid"),
3019 Param::Dash => out.push_str("FISH_FLAGS"),
3020 }
3021}
3022
3023#[cfg(test)]
3028mod tests {
3029 use super::*;
3030
3031 fn t(bash: &str) -> String {
3032 translate_bash_to_fish(bash).unwrap()
3033 }
3034
3035 fn t_unsupported(bash: &str) {
3036 assert!(matches!(translate_bash_to_fish(bash), Err(TranslateError::Unsupported(_))));
3037 }
3038
3039 #[test]
3042 fn simple_echo() {
3043 assert_eq!(t("echo hello world"), "echo hello world");
3044 }
3045
3046 #[test]
3047 fn simple_pipeline() {
3048 assert_eq!(
3049 t("cat file | grep foo | wc -l"),
3050 "cat file | grep foo | wc -l"
3051 );
3052 }
3053
3054 #[test]
3055 fn and_or_chain() {
3056 assert_eq!(
3057 t("mkdir -p foo && cd foo || echo fail"),
3058 "mkdir -p foo; and cd foo; or echo fail"
3059 );
3060 }
3061
3062 #[test]
3065 fn standalone_assignment() {
3066 assert_eq!(t("FOO=bar"), "set FOO bar");
3067 }
3068
3069 #[test]
3070 fn env_prefix_command() {
3071 t_unsupported("FOO=bar command");
3073 }
3074
3075 #[test]
3078 fn export_simple() {
3079 assert_eq!(t("export EDITOR=vim"), "set -gx EDITOR vim");
3080 }
3081
3082 #[test]
3083 fn export_path_splits_colons() {
3084 assert_eq!(
3085 t("export PATH=/usr/bin:$PATH"),
3086 "set -gx PATH /usr/bin $PATH"
3087 );
3088 assert_eq!(
3089 t("export PATH=$HOME/bin:/usr/local/bin:$PATH"),
3090 "set -gx PATH $HOME/bin /usr/local/bin $PATH"
3091 );
3092 assert_eq!(
3094 t("export FOO=a:b:c"),
3095 "set -gx FOO a:b:c"
3096 );
3097 }
3098
3099 #[test]
3102 fn for_loop_with_seq() {
3103 let result = t("for i in $(seq 5); do echo $i; done");
3104 assert!(result.contains("for i in (seq 5 | string split -n ' ')"));
3105 assert!(result.contains("echo $i"));
3106 assert!(result.contains("end"));
3107 }
3108
3109 #[test]
3110 fn for_loop_word_split_echo() {
3111 let result = t("for f in $(echo a b c); do echo $f; done");
3113 assert!(result.contains("for f in (echo a b c | string split -n ' ')"));
3114 }
3115
3116 #[test]
3117 fn for_loop_literal_words_no_split() {
3118 let result = t("for f in a b c; do echo $f; done");
3120 assert!(result.contains("for f in a b c"));
3121 assert!(!result.contains("string split"));
3122 }
3123
3124 #[test]
3125 fn for_loop_quoted_subst_no_split() {
3126 let result = t("for f in \"$(echo a b c)\"; do echo $f; done");
3128 assert!(!result.contains("string split"));
3129 }
3130
3131 #[test]
3132 fn for_loop_with_glob() {
3133 let result = t("for f in *.txt; do echo $f; done");
3134 assert!(result.contains("for f in *.txt"));
3135 assert!(result.contains("echo $f"));
3136 }
3137
3138 #[test]
3139 fn for_loop_bare_var_gets_split() {
3140 let result = t(r#"files="a b c"; for f in $files; do echo $f; done"#);
3141 assert!(result.contains("(string split -n -- ' ' $files)"));
3142 }
3143
3144 #[test]
3147 fn if_then_fi() {
3148 let result = t("if test -f foo; then echo exists; fi");
3149 assert!(result.contains("if test -f foo"));
3150 assert!(result.contains("echo exists"));
3151 assert!(result.contains("end"));
3152 }
3153
3154 #[test]
3155 fn if_else() {
3156 let result = t("if test -f foo; then echo yes; else echo no; fi");
3157 assert!(result.contains("if test -f foo"));
3158 assert!(result.contains("echo yes"));
3159 assert!(result.contains("else"));
3160 assert!(result.contains("echo no"));
3161 assert!(result.contains("end"));
3162 }
3163
3164 #[test]
3167 fn while_loop() {
3168 let result = t("while true; do echo loop; done");
3169 assert!(result.contains("while true"));
3170 assert!(result.contains("echo loop"));
3171 assert!(result.contains("end"));
3172 }
3173
3174 #[test]
3177 fn command_substitution() {
3178 assert_eq!(t("echo $(whoami)"), "echo (whoami)");
3179 }
3180
3181 #[test]
3184 fn arithmetic_substitution() {
3185 let result = t("echo $((2 + 2))");
3186 assert!(result.contains("math"));
3187 assert!(result.contains("2 + 2"));
3188 }
3189
3190 #[test]
3193 fn special_params() {
3194 assert_eq!(t("echo $?"), "echo $status");
3195 }
3196
3197 #[test]
3198 fn positional_params() {
3199 assert_eq!(t("echo $1"), "echo $argv[1]");
3200 }
3201
3202 #[test]
3203 fn all_args() {
3204 assert_eq!(t("echo $@"), "echo $argv");
3205 }
3206
3207 #[test]
3210 fn unset_var() {
3211 assert_eq!(t("unset FOO"), "set -e FOO");
3212 }
3213
3214 #[test]
3217 fn local_var() {
3218 assert_eq!(t("local FOO=bar"), "set -l FOO bar");
3219 }
3220
3221 #[test]
3224 fn background_job() {
3225 assert_eq!(t("sleep 10 &"), "sleep 10 &");
3226 }
3227
3228 #[test]
3231 fn negated_pipeline() {
3232 assert_eq!(t("! grep -q pattern file"), "not grep -q pattern file");
3233 }
3234
3235 #[test]
3238 fn case_statement() {
3239 let result = t("case $1 in foo) echo foo;; bar) echo bar;; esac");
3240 assert!(result.contains("switch"));
3241 assert!(result.contains("case foo"));
3242 assert!(result.contains("echo foo"));
3243 assert!(result.contains("case bar"));
3244 assert!(result.contains("echo bar"));
3245 assert!(result.contains("end"));
3246 }
3247
3248 #[test]
3251 fn stderr_redirect() {
3252 assert_eq!(t("cmd 2>/dev/null"), "cmd 2>/dev/null");
3253 }
3254
3255 #[test]
3256 fn stderr_to_stdout() {
3257 assert_eq!(t("cmd 2>&1"), "cmd 2>&1");
3258 }
3259
3260 #[test]
3263 fn brace_group() {
3264 let result = t("{ echo a; echo b; }");
3265 assert!(result.contains("begin"));
3266 assert!(result.contains("echo a"));
3267 assert!(result.contains("echo b"));
3268 assert!(result.contains("end"));
3269 }
3270
3271 #[test]
3278 fn nested_command_substitution() {
3279 let result = t("echo $(basename $(pwd))");
3280 assert!(result.contains("(basename (pwd))"));
3281 }
3282
3283 #[test]
3284 fn command_subst_in_args() {
3285 let result = t("$(which python3) --version");
3287 assert!(result.contains("(which python3)"));
3288 }
3289
3290 #[test]
3293 fn semicolon_separated() {
3294 let result = t("echo a; echo b; echo c");
3295 assert!(result.contains("echo a"));
3296 assert!(result.contains("echo b"));
3297 assert!(result.contains("echo c"));
3298 }
3299
3300 #[test]
3303 fn param_default_value() {
3304 let result = t("echo ${HOME:-/tmp}");
3306 assert!(result.contains("set -q HOME"));
3307 assert!(result.contains("echo $HOME"));
3308 assert!(result.contains("/tmp"));
3309 }
3310
3311 #[test]
3312 fn param_assign_default() {
3313 let result = t("echo ${FOO:=hello}");
3315 assert!(result.contains("set -q FOO"));
3316 assert!(result.contains("set FOO"));
3317 assert!(result.contains("hello"));
3318 }
3319
3320 #[test]
3321 fn param_error_if_unset() {
3322 let result = t("echo ${REQUIRED:?must be set}");
3324 assert!(result.contains("set -q REQUIRED"));
3325 assert!(result.contains("return 1"));
3326 }
3327
3328 #[test]
3329 fn param_alternative_value() {
3330 let result = t("echo ${DEBUG:+--verbose}");
3332 assert!(result.contains("set -q DEBUG"));
3333 assert!(result.contains("--verbose"));
3334 }
3335
3336 #[test]
3337 fn param_length() {
3338 let result = t("echo ${#PATH}");
3340 assert!(result.contains("string length"));
3341 assert!(result.contains("$PATH"));
3342 }
3343
3344 #[test]
3345 fn param_strip_suffix() {
3346 let result = t("echo ${file%.*}");
3348 assert!(result.contains("string replace"));
3349 assert!(result.contains("$file"));
3350 }
3351
3352 #[test]
3353 fn param_strip_prefix() {
3354 let result = t("echo ${path#*/}");
3356 assert!(result.contains("string replace"));
3357 assert!(result.contains("$path"));
3358 }
3359
3360 #[test]
3363 fn arithmetic_multiplication() {
3364 let result = t("echo $((3 * 4 + 1))");
3365 assert!(result.contains("math"));
3366 }
3367
3368 #[test]
3369 fn arithmetic_modulo() {
3370 let result = t("echo $((x % 2))");
3371 assert!(result.contains("math"));
3372 assert!(result.contains('%'));
3373 }
3374
3375 #[test]
3376 fn arithmetic_comparison() {
3377 let result = t("echo $((a > b))");
3379 assert!(result.contains("test"), "got: {}", result);
3380 assert!(result.contains("-gt"), "got: {}", result);
3381 }
3382
3383 #[test]
3384 fn arithmetic_in_double_quotes() {
3385 let result = t(r#"echo "result is $((x * 2))""#);
3389 assert!(result.contains("math"), "got: {}", result);
3390 assert!(
3392 result.contains(r#""result is ""#),
3393 "outer quotes should close before math, got: {}",
3394 result
3395 );
3396 }
3397
3398 #[test]
3401 fn nested_for_if() {
3402 let result = t("for f in $(ls); do if test -f $f; then echo $f is a file; fi; done");
3403 assert!(result.contains("for f in (ls | string split -n ' ')"));
3404 assert!(result.contains("if test -f $f"));
3405 assert!(result.contains("end\nend"));
3406 }
3407
3408 #[test]
3409 fn if_elif_else() {
3410 let result = t(
3411 "if test $x -eq 1; then echo one; elif test $x -eq 2; then echo two; else echo other; fi",
3412 );
3413 assert!(result.contains("if test $x -eq 1"));
3414 assert!(result.contains("else if test $x -eq 2"));
3415 assert!(result.contains("echo one"));
3416 assert!(result.contains("echo two"));
3417 assert!(result.contains("else\necho other"));
3418 assert!(result.contains("end"));
3419 }
3420
3421 #[test]
3424 fn until_loop() {
3425 let result = t("until test -f /tmp/ready; do sleep 1; done");
3426 assert!(result.contains("while not"));
3427 assert!(result.contains("test -f /tmp/ready"));
3428 assert!(result.contains("sleep 1"));
3429 assert!(result.contains("end"));
3430 }
3431
3432 #[test]
3435 fn multi_env_prefix() {
3436 t_unsupported("CC=gcc CXX=g++ make");
3438 }
3439
3440 #[test]
3443 fn multi_assignment() {
3444 let result = t("A=1; B=2; C=3");
3445 assert!(result.contains("set A 1"));
3446 assert!(result.contains("set B 2"));
3447 assert!(result.contains("set C 3"));
3448 }
3449
3450 #[test]
3453 fn single_quoted_string() {
3454 assert_eq!(t("echo 'hello world'"), "echo 'hello world'");
3455 }
3456
3457 #[test]
3458 fn ansi_c_quoting_simple() {
3459 assert_eq!(t("echo $'hello'"), "echo \"hello\"");
3460 }
3461
3462 #[test]
3463 fn ansi_c_quoting_newline() {
3464 assert_eq!(t("echo $'line1\\nline2'"), "echo \"line1\"\\n\"line2\"");
3466 }
3467
3468 #[test]
3469 fn ansi_c_quoting_tab() {
3470 assert_eq!(t("echo $'a\\tb'"), "echo \"a\"\\t\"b\"");
3471 }
3472
3473 #[test]
3474 fn ansi_c_quoting_escaped_squote() {
3475 assert_eq!(t("echo $'it\\'s'"), "echo \"it's\"");
3476 }
3477
3478 #[test]
3479 fn ansi_c_quoting_escape_e() {
3480 assert_eq!(t("echo $'\\E[31m'"), "echo \\e\"[31m\"");
3481 }
3482
3483 #[test]
3484 fn ansi_c_quoting_dollar() {
3485 assert_eq!(t("echo $'costs $5'"), "echo \"costs \\$5\"");
3486 }
3487
3488 #[test]
3489 fn double_quoted_with_var() {
3490 let result = t("echo \"hello $USER\"");
3491 assert!(result.contains("\"hello $USER\""));
3492 }
3493
3494 #[test]
3495 fn double_quoted_with_subst() {
3496 let result = t("echo \"today is $(date)\"");
3499 assert!(result.contains("\"today is \""), "got: {}", result);
3500 assert!(result.contains("(date)"), "got: {}", result);
3501 }
3502
3503 #[test]
3506 fn find_and_exec() {
3507 let result = t("find . -name '*.py' -exec grep -l TODO {} +");
3509 assert!(result.contains("find"));
3510 assert!(result.contains("'*.py'"));
3511 }
3512
3513 #[test]
3514 fn while_read_loop() {
3515 let result = t("while read line; do echo $line; done");
3517 assert!(result.contains("while read line"));
3518 assert!(result.contains("echo $line"));
3519 assert!(result.contains("end"));
3520 }
3521
3522 #[test]
3523 fn chained_and_or_complex() {
3524 let result = t("test -d /opt && echo exists || mkdir -p /opt && echo created");
3525 assert!(result.contains("test -d /opt"));
3526 assert!(result.contains("; and echo exists"));
3527 assert!(result.contains("; or mkdir -p /opt"));
3528 assert!(result.contains("; and echo created"));
3529 }
3530
3531 #[test]
3534 fn subshell() {
3535 let result = t("(cd /tmp && ls)");
3536 assert!(result.contains("begin\n"));
3537 assert!(result.contains("cd /tmp; and ls"));
3538 assert!(result.contains("set -l __reef_pwd (pwd)"));
3539 assert!(result.contains("cd $__reef_pwd 2>/dev/null"));
3540 assert!(result.contains("\nend"));
3541 }
3542
3543 #[test]
3544 fn subshell_pipeline() {
3545 let result = t("(echo hello; echo world)");
3546 assert!(result.contains("begin\n"));
3547 assert!(result.contains("echo hello\necho world"));
3548 assert!(result.contains("\nend"));
3549 }
3550
3551 #[test]
3554 fn function_def() {
3555 let result = t("greet() { echo hello $1; }");
3556 assert!(result.contains("function greet"));
3557 assert!(result.contains("echo hello $argv[1]"));
3558 assert!(result.contains("end"));
3559 }
3560
3561 #[test]
3564 fn pipeline_with_redirect() {
3565 let result = t("cat file 2>/dev/null | sort | uniq -c | sort -rn");
3566 assert!(result.contains("cat file 2>/dev/null"));
3567 assert!(result.contains("| sort |"));
3568 assert!(result.contains("| uniq -c |"));
3569 assert!(result.contains("sort -rn"));
3570 }
3571
3572 #[test]
3575 fn backtick_substitution() {
3576 let result = t("echo `whoami`");
3578 assert!(result.contains("(whoami)"));
3579 }
3580
3581 #[test]
3584 fn export_multiple() {
3585 let result = t("export A=1; export B=2");
3586 assert!(result.contains("set -gx A 1"));
3587 assert!(result.contains("set -gx B 2"));
3588 }
3589
3590 #[test]
3593 fn declare_export() {
3594 assert_eq!(t("declare -x FOO=bar"), "set -gx FOO bar");
3595 }
3596
3597 #[test]
3600 fn case_with_wildcards() {
3601 let result = t("case $1 in *.txt) echo text;; *.py) echo python;; *) echo unknown;; esac");
3602 assert!(result.contains("switch"));
3603 assert!(result.contains("case '*.txt'"));
3605 assert!(result.contains("case '*.py'"));
3606 assert!(result.contains("case '*'"));
3607 }
3608
3609 #[test]
3612 fn for_with_pipeline_body() {
3613 let result = t("for f in $(find . -name '*.log'); do cat $f | wc -l; done");
3614 assert!(result.contains("for f in"));
3615 assert!(result.contains("cat $f | wc -l"));
3616 assert!(result.contains("end"));
3617 }
3618
3619 #[test]
3622 fn append_redirect() {
3623 assert_eq!(t("echo hello >>log.txt"), "echo hello >>log.txt");
3624 }
3625
3626 #[test]
3629 fn input_redirect() {
3630 assert_eq!(t("sort <input.txt"), "sort <input.txt");
3631 }
3632
3633 #[test]
3636 fn redirect_with_var() {
3637 let result = t("echo hello >$LOGFILE");
3638 assert!(result.contains("echo hello >$LOGFILE"));
3639 }
3640
3641 #[test]
3644 fn comment_only() {
3645 let result = t("# this is a comment");
3647 assert_eq!(result, "");
3648 }
3649
3650 #[test]
3653 fn dollar_dollar() {
3654 assert_eq!(t("echo $$"), "echo $fish_pid");
3655 }
3656
3657 #[test]
3658 fn dollar_bang() {
3659 let result = t("echo $!");
3660 assert!(result.contains("$last_pid"));
3661 }
3662
3663 #[test]
3664 fn dollar_random() {
3665 assert_eq!(t("echo $RANDOM"), "echo (random)");
3666 }
3667
3668 #[test]
3669 fn dollar_pound() {
3670 let result = t("echo $#");
3671 assert!(result.contains("count $argv"));
3672 }
3673
3674 #[test]
3677 fn tilde_expansion() {
3678 let result = t("cd ~/projects");
3679 assert!(result.contains('~'));
3680 assert!(result.contains("projects"));
3681 }
3682
3683 #[test]
3686 fn escaped_dollar() {
3687 let result = t("echo \\$HOME");
3688 assert!(result.contains("\\$"));
3689 }
3690
3691 #[test]
3694 fn complex_and_or_pipeline() {
3695 let result = t("cat file | grep foo && echo found || echo not found");
3696 assert!(result.contains("cat file | grep foo"));
3697 assert!(result.contains("; and echo found"));
3698 assert!(result.contains("; or echo not found"));
3699 }
3700
3701 #[test]
3704 fn for_without_in() {
3705 let result = t("for arg; do echo $arg; done");
3707 assert!(result.contains("for arg in $argv"));
3708 assert!(result.contains("echo $arg"));
3709 assert!(result.contains("end"));
3710 }
3711
3712 #[test]
3715 fn nested_arithmetic() {
3716 let result = t("echo $((2 * (3 + 4)))");
3717 assert!(result.contains("math"));
3718 }
3719
3720 #[test]
3723 fn double_bracket_test() {
3724 let result = t("[[ -n $HOME ]]");
3726 assert!(result.contains("test -n $HOME"), "got: {}", result);
3727 assert!(!result.contains("[["));
3728 assert!(!result.contains("]]"));
3729 }
3730
3731 #[test]
3732 fn double_bracket_equality() {
3733 let result = t("[[ $a == $b ]]");
3735 assert!(result.contains("string match -q"), "got: {}", result);
3736 }
3737
3738 #[test]
3739 fn double_bracket_wildcard_pattern() {
3740 let result = t(r#"if [[ "world" == w* ]]; then echo yes; fi"#);
3742 assert!(result.contains("string match -q -- 'w*'"), "got: {}", result);
3743 assert!(result.contains("echo yes"), "got: {}", result);
3744 }
3745
3746 #[test]
3747 fn double_bracket_negated_pattern() {
3748 let result = t("[[ $x != *.txt ]]");
3750 assert!(result.contains("not string match -q"), "got: {}", result);
3751 }
3752
3753 #[test]
3754 fn double_bracket_and() {
3755 let result = t("if [[ -f /etc/hostname && -r /etc/hostname ]]; then echo ok; fi");
3757 assert!(result.contains("test -f /etc/hostname"), "got: {}", result);
3758 assert!(
3759 result.contains("; and test -r /etc/hostname"),
3760 "got: {}",
3761 result
3762 );
3763 }
3764
3765 #[test]
3766 fn double_bracket_or() {
3767 let result = t("[[ -z \"$x\" || -z \"$y\" ]]");
3768 assert!(result.contains("test -z \"$x\""), "got: {}", result);
3769 assert!(result.contains("; or test -z \"$y\""), "got: {}", result);
3770 }
3771
3772 #[test]
3773 fn double_bracket_regex() {
3774 let result = t(r#"[[ "$str" =~ ^[a-z]+$ ]]"#);
3775 assert!(result.contains("string match -r"), "got: {}", result);
3776 assert!(result.contains("__bash_rematch"), "got: {}", result);
3777 assert!(result.contains("'^[a-z]+$' \"$str\""), "got: {}", result);
3778 }
3779
3780 #[test]
3781 fn brace_range_simple() {
3782 let result = t("echo {1..5}");
3783 assert!(result.contains("echo (seq 1 5)"), "got: {}", result);
3784 }
3785
3786 #[test]
3787 fn brace_range_with_step() {
3788 let result = t("for i in {1..10..2}; do echo $i; done");
3789 assert!(result.contains("seq 1 2 10"), "got: {}", result);
3790 }
3791
3792 #[test]
3793 fn ternary_arithmetic() {
3794 let result = t("echo $((x > 5 ? 1 : 0))");
3795 assert!(result.contains("if test $x -gt 5"), "got: {}", result);
3796 assert!(result.contains("echo 1"), "got: {}", result);
3797 assert!(result.contains("echo 0"), "got: {}", result);
3798 }
3799
3800 #[test]
3801 fn herestring_with_preceding_statement() {
3802 let result = t(r#"name="world"; grep -o "world" <<< "hello $name""#);
3803 assert!(result.contains("set name \"world\""), "got: {}", result);
3804 assert!(
3805 result.contains("echo \"hello $name\" | grep"),
3806 "got: {}",
3807 result
3808 );
3809 }
3810
3811 #[test]
3814 fn curl_pipe_bash() {
3815 let result = t("curl -fsSL https://example.com/install.sh | bash");
3817 assert!(result.contains("curl"));
3818 assert!(result.contains("| bash"));
3819 }
3820
3821 #[test]
3822 fn git_clone_and_cd() {
3823 let result = t("git clone https://github.com/user/repo.git && cd repo");
3824 assert!(result.contains("git clone"));
3825 assert!(result.contains("; and cd repo"));
3826 }
3827
3828 #[test]
3831 fn deeply_nested_loops() {
3832 let result = t("for i in 1 2 3; do for j in a b c; do echo $i$j; done; done");
3833 assert!(result.contains("for i in 1 2 3"));
3834 assert!(result.contains("for j in a b c"));
3835 assert!(result.contains("echo $i$j"));
3836 let end_count = result.matches("end").count();
3838 assert!(
3839 end_count >= 2,
3840 "Expected at least 2 'end' keywords, got {}",
3841 end_count
3842 );
3843 }
3844
3845 #[test]
3848 fn if_with_and_condition() {
3849 let result = t("if test -f foo && test -r foo; then cat foo; fi");
3850 assert!(result.contains("if"));
3851 assert!(result.contains("test -f foo"));
3852 assert!(result.contains("test -r foo"));
3853 assert!(result.contains("cat foo"));
3854 assert!(result.contains("end"));
3855 }
3856
3857 #[test]
3860 fn single_quoted_special_chars() {
3861 let result = t("grep -E '^[0-9]+$' file.txt");
3862 assert!(result.contains("grep"));
3863 assert!(result.contains("'^[0-9]+$'"));
3864 }
3865
3866 #[test]
3869 fn export_no_value() {
3870 let result = t("export HOME");
3872 assert!(result.contains("set -gx HOME $HOME"));
3873 }
3874
3875 #[test]
3878 fn herestring_quoted() {
3879 let result = t(r#"while read line; do echo ">> $line"; done <<< "hello world""#);
3880 assert!(result.contains("echo \"hello world\" |"), "got: {}", result);
3881 assert!(result.contains("while read line"));
3882 }
3883
3884 #[test]
3885 fn herestring_bare() {
3886 let result = t("cat <<< hello");
3887 assert!(result.contains("echo hello | cat"), "got: {}", result);
3888 }
3889
3890 #[test]
3891 fn herestring_variable() {
3892 let result = t("grep foo <<< $input");
3893 assert!(result.contains("echo $input | grep foo"), "got: {}", result);
3894 }
3895
3896 #[test]
3899 fn standalone_arith_post_increment() {
3900 let result = t("(( i++ ))");
3901 assert!(result.contains("set i"), "got: {}", result);
3902 assert!(result.contains("math"), "got: {}", result);
3903 assert!(result.contains("+ 1"), "got: {}", result);
3904 }
3905
3906 #[test]
3907 fn standalone_arith_pre_increment() {
3908 let result = t("(( ++i ))");
3909 assert!(result.contains("set i"), "got: {}", result);
3910 assert!(result.contains("+ 1"), "got: {}", result);
3911 }
3912
3913 #[test]
3914 fn standalone_arith_post_decrement() {
3915 let result = t("(( i-- ))");
3916 assert!(result.contains("set i"), "got: {}", result);
3917 assert!(result.contains("- 1"), "got: {}", result);
3918 }
3919
3920 #[test]
3921 fn standalone_arith_pre_decrement() {
3922 let result = t("(( --i ))");
3923 assert!(result.contains("set i"), "got: {}", result);
3924 assert!(result.contains("- 1"), "got: {}", result);
3925 }
3926
3927 #[test]
3928 fn standalone_arith_plus_equals() {
3929 let result = t("(( count += 5 ))");
3930 assert!(result.contains("set count"), "got: {}", result);
3931 assert!(result.contains("math"), "got: {}", result);
3932 assert!(result.contains("+ 5"), "got: {}", result);
3933 }
3934
3935 #[test]
3936 fn standalone_arith_minus_equals() {
3937 let result = t("(( x -= 3 ))");
3938 assert!(result.contains("set x"), "got: {}", result);
3939 assert!(result.contains("- 3"), "got: {}", result);
3940 }
3941
3942 #[test]
3943 fn standalone_arith_times_equals() {
3944 let result = t("(( x *= 2 ))");
3945 assert!(result.contains("set x"), "got: {}", result);
3946 assert!(result.contains("* 2"), "got: {}", result);
3947 }
3948
3949 #[test]
3950 fn standalone_arith_div_equals() {
3951 let result = t("(( x /= 4 ))");
3952 assert!(result.contains("set x"), "got: {}", result);
3953 assert!(result.contains("/ 4"), "got: {}", result);
3954 }
3955
3956 #[test]
3957 fn standalone_arith_mod_equals() {
3958 let result = t("(( x %= 3 ))");
3959 assert!(result.contains("set x"), "got: {}", result);
3960 assert!(result.contains("% 3"), "got: {}", result);
3961 }
3962
3963 #[test]
3964 fn standalone_arith_simple_assign() {
3965 let result = t("(( x = 42 ))");
3966 assert!(result.contains("set x"), "got: {}", result);
3967 assert!(result.contains("42"), "got: {}", result);
3968 }
3969
3970 #[test]
3971 fn standalone_arith_assign_expr() {
3972 let result = t("(( x = y + 1 ))");
3973 assert!(result.contains("set x"), "got: {}", result);
3974 assert!(result.contains("math"), "got: {}", result);
3975 }
3976
3977 #[test]
3978 fn standalone_arith_in_loop() {
3979 let result = t("while test $i -lt 10; do echo $i; (( i++ )); done");
3981 assert!(result.contains("while test $i -lt 10"), "got: {}", result);
3982 assert!(result.contains("set i"), "got: {}", result);
3983 assert!(result.contains("+ 1"), "got: {}", result);
3984 assert!(result.contains("end"), "got: {}", result);
3985 }
3986
3987 #[test]
3988 fn standalone_arith_comparison() {
3989 assert_eq!(t("(( x > 5 ))"), "test $x -gt 5");
3990 }
3991
3992 #[test]
3993 fn standalone_arith_comparison_eq() {
3994 assert_eq!(t("(( x == 0 ))"), "test $x -eq 0");
3995 }
3996
3997 #[test]
3998 fn standalone_arith_logical_and() {
3999 assert_eq!(
4000 t("(( x > 0 && y < 10 ))"),
4001 "test $x -gt 0; and test $y -lt 10"
4002 );
4003 }
4004
4005 #[test]
4006 fn cstyle_for_loop() {
4007 let result = t("for (( i=0; i<10; i++ )); do echo $i; done");
4008 assert!(result.contains("set i (math \"0\")"), "got: {}", result);
4009 assert!(result.contains("while test $i -lt 10"), "got: {}", result);
4010 assert!(result.contains("echo $i"), "got: {}", result);
4011 assert!(
4012 result.contains("set i (math \"$i + 1\")"),
4013 "got: {}",
4014 result
4015 );
4016 assert!(result.contains("end"), "got: {}", result);
4017 }
4018
4019 #[test]
4020 fn standalone_arith_in_quotes_untouched() {
4021 let result = t("echo '(( i++ ))'");
4023 assert!(result.contains("(( i++ ))"), "got: {}", result);
4024 }
4025
4026 #[test]
4029 fn arith_subtraction() {
4030 let result = t("echo $((10 - 3))");
4031 assert_eq!(result, r#"echo (math "10 - 3")"#);
4032 }
4033
4034 #[test]
4035 fn arith_division() {
4036 let result = t("echo $((20 / 4))");
4037 assert_eq!(result, r#"echo (math "floor(20 / 4)")"#);
4038 }
4039
4040 #[test]
4041 fn arith_power() {
4042 let result = t("echo $((2 ** 10))");
4043 assert_eq!(result, r#"echo (math "2 ^ 10")"#);
4044 }
4045
4046 #[test]
4047 fn arith_nested_parens() {
4048 let result = t("echo $(( (2 + 3) * (4 - 1) ))");
4049 assert!(result.contains("math"), "got: {}", result);
4050 assert!(result.contains("(2 + 3) * (4 - 1)"), "got: {}", result);
4051 }
4052
4053 #[test]
4054 fn arith_unary_neg() {
4055 let result = t("echo $((-x + 5))");
4056 assert!(result.contains("math"), "got: {}", result);
4057 assert!(result.contains("-$x"), "got: {}", result);
4058 }
4059
4060 #[test]
4061 fn arith_variables_only() {
4062 let result = t("echo $((a + b * c))");
4063 assert!(result.contains("math"), "got: {}", result);
4064 assert!(result.contains("$a + ($b * $c)"), "got: {}", result);
4065 }
4066
4067 #[test]
4068 fn arith_comparison_eq() {
4069 let result = t("echo $((x == y))");
4070 assert!(result.contains("test $x -eq $y"), "got: {}", result);
4071 }
4072
4073 #[test]
4074 fn arith_comparison_ne() {
4075 let result = t("echo $((x != y))");
4076 assert!(result.contains("test $x -ne $y"), "got: {}", result);
4077 }
4078
4079 #[test]
4080 fn arith_comparison_le() {
4081 let result = t("echo $((a <= b))");
4082 assert!(result.contains("test $a -le $b"), "got: {}", result);
4083 }
4084
4085 #[test]
4086 fn arith_comparison_ge() {
4087 let result = t("echo $((a >= b))");
4088 assert!(result.contains("test $a -ge $b"), "got: {}", result);
4089 }
4090
4091 #[test]
4092 fn arith_comparison_lt() {
4093 let result = t("echo $((a < b))");
4094 assert!(result.contains("test $a -lt $b"), "got: {}", result);
4095 }
4096
4097 #[test]
4098 fn arith_logic_and() {
4099 let result = t("echo $((a > 0 && b > 0))");
4100 assert!(result.contains("test $a -gt 0"), "got: {}", result);
4101 assert!(result.contains("; and "), "got: {}", result);
4102 assert!(result.contains("test $b -gt 0"), "got: {}", result);
4103 }
4104
4105 #[test]
4106 fn arith_logic_or() {
4107 let result = t("echo $((a == 0 || b == 0))");
4108 assert!(result.contains("test $a -eq 0"), "got: {}", result);
4109 assert!(result.contains("; or "), "got: {}", result);
4110 }
4111
4112 #[test]
4113 fn arith_logic_not() {
4114 let result = t("echo $((!x))");
4115 assert!(result.contains("not "), "got: {}", result);
4116 }
4117
4118 #[test]
4119 fn arith_ternary_with_math() {
4120 let result = t("echo $((x > 0 ? x * 2 : 0))");
4121 assert!(result.contains("if test $x -gt 0"), "got: {}", result);
4122 assert!(result.contains("math"), "got: {}", result);
4123 }
4124
4125 #[test]
4126 fn arith_in_assignment() {
4127 let result = t("z=$((x + y))");
4128 assert!(result.contains("set z"), "got: {}", result);
4129 assert!(result.contains("math"), "got: {}", result);
4130 }
4131
4132 #[test]
4133 fn arith_in_condition() {
4134 let result = t("if [ $((x % 2)) -eq 0 ]; then echo even; fi");
4135 assert!(result.contains("math"), "got: {}", result);
4136 assert!(result.contains("echo even"), "got: {}", result);
4137 }
4138
4139 #[test]
4140 fn arith_multiple_in_line() {
4141 let result = t("echo $((a + 1)) $((b + 2))");
4142 assert!(result.contains(r#"(math "$a + 1")"#), "got: {}", result);
4143 assert!(result.contains(r#"(math "$b + 2")"#), "got: {}", result);
4144 }
4145
4146 #[test]
4147 fn arith_deeply_nested() {
4148 let result = t("echo $(( ((2 + 3)) * ((4 + 5)) ))");
4149 assert!(result.contains("math"), "got: {}", result);
4150 }
4151
4152 #[test]
4153 fn arith_empty() {
4154 let result = t("echo $(())");
4156 assert!(result.contains("echo"), "got: {}", result);
4157 }
4158
4159 #[test]
4160 fn arith_complex_expression() {
4161 let result = t("echo $(( (x + y) / 2 - z * 3 ))");
4162 assert!(result.contains("math"), "got: {}", result);
4163 assert!(result.contains("/ 2"), "got: {}", result);
4164 }
4165
4166 #[test]
4167 fn arith_in_export() {
4168 let result = t("export N=$((x + 1))");
4169 assert!(result.contains("set -gx N"), "got: {}", result);
4170 assert!(result.contains("math"), "got: {}", result);
4171 }
4172
4173 #[test]
4174 fn arith_in_local() {
4175 let result = t("local result=$((a * b))");
4176 assert!(result.contains("set -l result"), "got: {}", result);
4177 assert!(result.contains("math"), "got: {}", result);
4178 }
4179
4180 #[test]
4183 fn standalone_arith_assign_compound() {
4184 let result = t("(( total = x + y * 2 ))");
4185 assert!(result.contains("set total"), "got: {}", result);
4186 assert!(result.contains("math"), "got: {}", result);
4187 assert!(result.contains("$x + ($y * 2)"), "got: {}", result);
4188 }
4189
4190 #[test]
4191 fn standalone_arith_nested_assign() {
4192 let result = t("(( x = (a + b) * c ))");
4193 assert!(result.contains("set x"), "got: {}", result);
4194 assert!(result.contains("math"), "got: {}", result);
4195 }
4196
4197 #[test]
4198 fn standalone_arith_multiple_in_sequence() {
4199 let result = t("(( x++ )); (( y-- ))");
4200 assert!(result.contains("set x"), "got: {}", result);
4201 assert!(result.contains("set y"), "got: {}", result);
4202 assert!(result.contains("+ 1"), "got: {}", result);
4203 assert!(result.contains("- 1"), "got: {}", result);
4204 }
4205
4206 #[test]
4209 fn upper_all() {
4210 let result = t("echo ${var^^}");
4211 assert_eq!(result, "echo (string upper -- \"$var\")");
4212 }
4213
4214 #[test]
4215 fn lower_all() {
4216 let result = t("echo ${var,,}");
4217 assert_eq!(result, "echo (string lower -- \"$var\")");
4218 }
4219
4220 #[test]
4221 fn upper_first() {
4222 let result = t("echo ${var^}");
4223 assert!(result.contains("string sub -l 1"));
4224 assert!(result.contains("string upper"));
4225 assert!(result.contains("string sub -s 2"));
4226 }
4227
4228 #[test]
4229 fn lower_first() {
4230 let result = t("echo ${var,}");
4231 assert!(result.contains("string sub -l 1"));
4232 assert!(result.contains("string lower"));
4233 assert!(result.contains("string sub -s 2"));
4234 }
4235
4236 #[test]
4239 fn replace_first() {
4240 let result = t("echo ${var/foo/bar}");
4241 assert!(result.contains("string replace"), "got: {}", result);
4242 assert!(result.contains("'foo'"), "got: {}", result);
4243 assert!(result.contains("'bar'"), "got: {}", result);
4244 assert!(result.contains("$var"), "got: {}", result);
4245 }
4246
4247 #[test]
4248 fn replace_all() {
4249 let result = t("echo ${var//foo/bar}");
4250 assert!(result.contains("string replace"), "got: {}", result);
4251 assert!(result.contains("-a"), "got: {}", result);
4252 }
4253
4254 #[test]
4255 fn replace_prefix() {
4256 let result = t("echo ${var/#foo/bar}");
4257 assert!(result.contains("string replace"), "got: {}", result);
4258 assert!(result.contains("-r"), "got: {}", result);
4259 assert!(result.contains("'^foo'"), "got: {}", result);
4260 }
4261
4262 #[test]
4263 fn replace_suffix() {
4264 let result = t("echo ${var/%foo/bar}");
4265 assert!(result.contains("string replace"), "got: {}", result);
4266 assert!(result.contains("-r"), "got: {}", result);
4267 assert!(result.contains("'foo$'"), "got: {}", result);
4268 }
4269
4270 #[test]
4271 fn replace_delete() {
4272 let result = t("echo ${var/foo}");
4273 assert!(result.contains("string replace"), "got: {}", result);
4274 assert!(result.contains("-- 'foo' '' \"$var\""), "got: {}", result);
4275 }
4276
4277 #[test]
4280 fn substring_offset_only() {
4281 let result = t("echo ${var:2}");
4282 assert!(result.contains("string sub"), "got: {}", result);
4283 assert!(result.contains("-s (math \"2 + 1\")"), "got: {}", result);
4284 assert!(result.contains("$var"), "got: {}", result);
4285 }
4286
4287 #[test]
4288 fn substring_offset_and_length() {
4289 let result = t("echo ${var:2:5}");
4290 assert!(result.contains("string sub"), "got: {}", result);
4291 assert!(result.contains("-s (math \"2 + 1\")"), "got: {}", result);
4292 assert!(result.contains("-l (math \"5\")"), "got: {}", result);
4293 }
4294
4295 #[test]
4298 fn process_substitution_in() {
4299 let result = t("diff <(sort a) <(sort b)");
4300 assert!(result.contains("(sort a | psub)"), "got: {}", result);
4301 assert!(result.contains("(sort b | psub)"), "got: {}", result);
4302 }
4303
4304 #[test]
4305 fn process_substitution_out_unsupported() {
4306 let result = translate_bash_to_fish("tee >(grep foo)");
4307 assert!(result.is_err());
4308 }
4309
4310 #[test]
4313 fn cstyle_for_no_init() {
4314 let result = t("for (( ; i<5; i++ )); do echo $i; done");
4315 assert!(result.contains("while test $i -lt 5"), "got: {}", result);
4316 assert!(
4317 result.contains("set i (math \"$i + 1\")"),
4318 "got: {}",
4319 result
4320 );
4321 }
4322
4323 #[test]
4324 fn cstyle_for_no_step() {
4325 let result = t("for (( i=0; i<5; )); do echo $i; done");
4326 assert!(result.contains("set i (math \"0\")"), "got: {}", result);
4327 assert!(result.contains("while test $i -lt 5"), "got: {}", result);
4328 }
4329
4330 #[test]
4333 fn heredoc_quoted() {
4334 let result = t("cat <<'EOF'\nhello world\nEOF");
4335 assert!(result.contains("printf"), "got: {}", result);
4336 assert!(result.contains("hello world"), "got: {}", result);
4337 assert!(result.contains("| cat"), "got: {}", result);
4338 }
4339
4340 #[test]
4341 fn heredoc_double_quoted() {
4342 let result = t("cat <<\"EOF\"\nhello world\nEOF");
4343 assert!(result.contains("printf"), "got: {}", result);
4344 assert!(result.contains("| cat"), "got: {}", result);
4345 }
4346
4347 #[test]
4348 fn heredoc_unquoted() {
4349 let result = t("cat <<EOF\nhello $NAME\nEOF");
4350 assert!(result.contains("printf"), "got: {}", result);
4351 assert!(result.contains("$NAME"), "got: {}", result);
4352 assert!(result.contains("| cat"), "got: {}", result);
4353 }
4354
4355 #[test]
4358 fn case_fallthrough_error() {
4359 let result = translate_bash_to_fish("case $x in a) echo a;& b) echo b;; esac");
4360 assert!(result.is_err());
4361 }
4362
4363 #[test]
4364 fn case_continue_error() {
4365 let result = translate_bash_to_fish("case $x in a) echo a;;& b) echo b;; esac");
4366 assert!(result.is_err());
4367 }
4368
4369 #[test]
4372 fn array_assign() {
4373 let result = t("arr=(one two three)");
4374 assert_eq!(result, "set arr one two three");
4375 }
4376
4377 #[test]
4378 fn array_element_access() {
4379 let result = t("echo ${arr[1]}");
4380 assert!(result.contains("$arr[2]"), "got: {}", result);
4381 }
4382
4383 #[test]
4384 fn array_all() {
4385 let result = t("echo ${arr[@]}");
4386 assert!(result.contains("$arr"), "got: {}", result);
4387 }
4388
4389 #[test]
4390 fn array_length() {
4391 let result = t("echo ${#arr[@]}");
4392 assert!(result.contains("(count $arr)"), "got: {}", result);
4393 }
4394
4395 #[test]
4396 fn array_append() {
4397 let result = t("arr+=(three)");
4398 assert_eq!(result, "set -a arr three");
4399 }
4400
4401 #[test]
4402 fn array_slice() {
4403 let result = t("echo ${arr[@]:1:3}");
4404 assert!(result.contains("$arr["), "got: {}", result);
4405 }
4406
4407 #[test]
4410 fn trap_exit() {
4411 assert_eq!(
4412 t("trap 'echo bye' EXIT"),
4413 "function __reef_trap_EXIT --on-event fish_exit\necho bye\nend"
4414 );
4415 }
4416
4417 #[test]
4418 fn trap_signal() {
4419 assert_eq!(
4420 t("trap 'cleanup' INT"),
4421 "function __reef_trap_INT --on-signal INT\ncleanup\nend"
4422 );
4423 }
4424
4425 #[test]
4426 fn trap_sigprefix() {
4427 assert_eq!(
4428 t("trap 'cleanup' SIGTERM"),
4429 "function __reef_trap_TERM --on-signal TERM\ncleanup\nend"
4430 );
4431 }
4432
4433 #[test]
4434 fn trap_reset() {
4435 assert_eq!(t("trap - INT"), "functions -e __reef_trap_INT");
4436 }
4437
4438 #[test]
4439 fn trap_ignore() {
4440 assert_eq!(
4441 t("trap '' INT"),
4442 "function __reef_trap_INT --on-signal INT; end"
4443 );
4444 }
4445
4446 #[test]
4447 fn trap_multiple_signals() {
4448 let result = t("trap 'cleanup' INT TERM");
4449 assert!(result.contains("__reef_trap_INT --on-signal INT"));
4450 assert!(result.contains("__reef_trap_TERM --on-signal TERM"));
4451 }
4452
4453 #[test]
4456 fn declare_print() {
4457 assert_eq!(t("declare -p FOO"), "set --show FOO");
4458 }
4459
4460 #[test]
4461 fn declare_print_all() {
4462 assert_eq!(t("declare -p"), "set --show");
4463 }
4464
4465 #[test]
4466 fn declare_print_multiple() {
4467 let result = t("declare -p FOO BAR");
4468 assert!(result.contains("set --show FOO"), "got: {}", result);
4469 assert!(result.contains("set --show BAR"), "got: {}", result);
4470 }
4471
4472 #[test]
4475 fn prefix_list() {
4476 assert_eq!(
4477 t("echo ${!BASH_*}"),
4478 "echo (set -n | string match 'BASH_*')"
4479 );
4480 }
4481
4482 #[test]
4483 fn prefix_list_at() {
4484 assert_eq!(t("echo ${!MY@}"), "echo (set -n | string match 'MY*')");
4485 }
4486
4487 #[test]
4490 fn bash_set_errexit() {
4491 let result = t("set -e");
4492 assert!(result.contains("# set -e"), "got: {}", result);
4493 assert!(result.contains("no fish equivalent"), "got: {}", result);
4494 }
4495
4496 #[test]
4497 fn bash_set_eux() {
4498 let result = t("set -eux");
4499 assert!(result.contains("# set -eux"), "got: {}", result);
4500 }
4501
4502 #[test]
4503 fn bash_set_positional() {
4504 assert_eq!(t("set -- a b c"), "set argv a b c");
4505 }
4506
4507 #[test]
4510 fn select_unsupported() {
4511 assert!(translate_bash_to_fish("select opt in a b c; do echo $opt; done").is_err());
4512 }
4513
4514 #[test]
4515 fn getopts_unsupported() {
4516 assert!(translate_bash_to_fish("getopts 'abc' opt").is_err());
4517 }
4518
4519 #[test]
4520 fn exec_fd_unsupported() {
4521 assert!(translate_bash_to_fish("exec 3>&1").is_err());
4522 }
4523
4524 #[test]
4525 fn eval_cmd_subst() {
4526 assert_eq!(
4527 t("eval \"$(pyenv init --path)\""),
4528 "pyenv init --path | source"
4529 );
4530 }
4531
4532 #[test]
4533 fn eval_dynamic_unsupported() {
4534 assert!(translate_bash_to_fish("eval $cmd").is_err());
4535 }
4536
4537 #[test]
4540 fn lineno_unsupported() {
4541 assert!(translate_bash_to_fish("echo $LINENO").is_err());
4542 }
4543
4544 #[test]
4545 fn funcname_unsupported() {
4546 assert!(translate_bash_to_fish("echo $FUNCNAME").is_err());
4547 }
4548
4549 #[test]
4550 fn seconds_unsupported() {
4551 assert!(translate_bash_to_fish("echo $SECONDS").is_err());
4552 }
4553
4554 #[test]
4557 fn transform_e_unsupported() {
4558 assert!(translate_bash_to_fish("echo ${var@E}").is_err());
4559 }
4560
4561 #[test]
4562 fn transform_a_unsupported() {
4563 assert!(translate_bash_to_fish("echo ${var@A}").is_err());
4564 }
4565
4566 #[test]
4569 fn negation_double_bracket_glob() {
4570 let result = t(r#"[[ ! "hello" == w* ]]"#);
4571 assert!(result.contains("not "), "should negate: got: {}", result);
4572 assert!(!result.contains(r"\!"), "should not escape !: got: {}", result);
4573 }
4574
4575 #[test]
4576 fn negation_double_bracket_string() {
4577 let result = t(r#"[[ ! "$x" == "yes" ]]"#);
4578 assert!(
4579 result.contains("not ") || result.contains("!="),
4580 "should negate: got: {}",
4581 result
4582 );
4583 }
4584
4585 #[test]
4586 fn negation_double_bracket_test_flag() {
4587 let result = t(r#"[[ ! -z "$var" ]]"#);
4588 assert!(result.contains("not test"), "should negate: got: {}", result);
4589 }
4590
4591 #[test]
4592 fn integer_division_truncates() {
4593 let result = t("echo $((10 / 3))");
4594 assert!(result.contains("floor(10 / 3)"), "got: {}", result);
4595 }
4596
4597 #[test]
4598 fn integer_division_exact() {
4599 let result = t("echo $((20 / 4))");
4600 assert!(result.contains("floor(20 / 4)"), "got: {}", result);
4601 }
4602
4603 #[test]
4604 fn path_colon_splitting() {
4605 let result = t("export PATH=/usr/local/bin:/usr/bin:$PATH");
4606 assert!(
4607 !result.contains(':'),
4608 "colons should be split: got: {}",
4609 result
4610 );
4611 assert!(result.contains("/usr/local/bin /usr/bin"), "got: {}", result);
4612 }
4613
4614 #[test]
4615 fn manpath_colon_splitting() {
4616 let result = t("export MANPATH=/usr/share/man:/usr/local/man");
4617 assert!(
4618 result.contains("/usr/share/man /usr/local/man"),
4619 "got: {}",
4620 result
4621 );
4622 }
4623
4624 #[test]
4625 fn non_path_var_keeps_colons() {
4626 assert_eq!(t("export FOO=a:b:c"), "set -gx FOO a:b:c");
4627 }
4628
4629 #[test]
4630 fn prefix_assignment_bails_to_t2() {
4631 assert!(translate_bash_to_fish("IFS=: read -ra parts").is_err());
4632 }
4633
4634 #[test]
4635 fn subshell_exit_bails_to_t2() {
4636 assert!(translate_bash_to_fish("(exit 1)").is_err());
4637 }
4638
4639 #[test]
4640 fn trap_exit_in_subshell_bails() {
4641 assert!(translate_bash_to_fish("( trap 'echo bye' EXIT; echo hi )").is_err());
4642 assert!(translate_bash_to_fish("trap 'echo bye' EXIT").is_ok());
4644 }
4645
4646 #[test]
4647 fn brace_range_with_subst_bails() {
4648 assert!(translate_bash_to_fish("echo {a..c}$(echo X)").is_err());
4650 assert!(translate_bash_to_fish("echo {a..c}$suffix").is_err());
4652 assert!(translate_bash_to_fish("echo {a..c}").is_ok());
4654 assert!(translate_bash_to_fish("echo {1..5}").is_ok());
4655 assert!(translate_bash_to_fish(r#"echo {a..c}"hello""#).is_ok());
4657 }
4658
4659 #[test]
4662 fn translate_if_dir_exists() {
4663 let result = t("if [ -d /tmp ]; then echo exists; else echo nope; fi");
4664 assert!(result.contains("[ -d /tmp ]"), "got: {}", result);
4665 assert!(result.contains("else"), "got: {}", result);
4666 assert!(result.contains("end"), "got: {}", result);
4667 }
4668
4669 #[test]
4670 fn translate_for_glob() {
4671 let result = t("for f in *.txt; do echo $f; done");
4672 assert!(result.contains("for f in *.txt"), "got: {}", result);
4673 assert!(result.contains("end"), "got: {}", result);
4674 }
4675
4676 #[test]
4677 fn translate_while_read() {
4678 let result = t("while read -r line; do echo $line; done < /tmp/input");
4679 assert!(result.contains("while read"), "got: {}", result);
4680 assert!(result.contains("end"), "got: {}", result);
4681 }
4682
4683 #[test]
4684 fn translate_command_in_string() {
4685 let result = t(r#"echo "Hello $USER, you are in $(pwd)""#);
4686 assert!(result.contains("$USER"), "got: {}", result);
4687 assert!(result.contains("(pwd)"), "got: {}", result);
4688 }
4689
4690 #[test]
4691 fn translate_test_and_or() {
4692 let result = t("test -f /etc/passwd && echo found || echo missing");
4693 assert!(result.contains("test -f /etc/passwd"), "got: {}", result);
4694 assert!(result.contains("; and echo found"), "got: {}", result);
4695 assert!(result.contains("; or echo missing"), "got: {}", result);
4696 }
4697
4698 #[test]
4699 fn translate_chained_commands() {
4700 let result = t("mkdir -p /tmp/test && cd /tmp/test && touch file.txt");
4701 assert!(result.contains("mkdir -p /tmp/test"), "got: {}", result);
4702 assert!(result.contains("cd /tmp/test"), "got: {}", result);
4703 }
4704
4705 #[test]
4706 fn translate_pipeline() {
4707 let result = t("cat file.txt | grep pattern | sort | uniq -c");
4708 assert!(result.contains("cat file.txt | grep pattern | sort | uniq -c"), "got: {}", result);
4709 }
4710
4711 #[test]
4712 fn translate_home_expansion() {
4713 let result = t("echo ${HOME}/documents");
4714 assert!(result.contains("$HOME"), "got: {}", result);
4715 assert!(result.contains("/documents"), "got: {}", result);
4716 }
4717
4718 #[test]
4719 fn translate_command_v() {
4720 let result = t("command -v git > /dev/null 2>&1 && echo installed");
4721 assert!(result.contains("command -v git"), "got: {}", result);
4722 }
4723
4724 #[test]
4725 fn translate_regex_match() {
4726 let result = t(r#"[[ "$x" =~ ^[0-9]+$ ]]"#);
4727 assert!(result.contains("string match -r"), "got: {}", result);
4728 assert!(result.contains("^[0-9]+$"), "got: {}", result);
4729 }
4730
4731 #[test]
4734 fn cstyle_for_decrementing() {
4735 let result = t("for ((i=10; i>0; i--)); do echo $i; done");
4736 assert!(result.contains("set i"), "got: {}", result);
4737 assert!(result.contains("while test"), "got: {}", result);
4738 assert!(result.contains("end"), "got: {}", result);
4739 }
4740
4741 #[test]
4742 fn cstyle_for_step_by_two() {
4743 let result = t("for ((i=0; i<10; i+=2)); do echo $i; done");
4744 assert!(result.contains("set i"), "got: {}", result);
4745 assert!(result.contains("$i + 2"), "got: {}", result);
4746 }
4747
4748 #[test]
4749 fn cstyle_for_infinite() {
4750 let result = t("for ((;;)); do echo loop; break; done");
4751 assert!(result.contains("while true"), "got: {}", result);
4752 assert!(result.contains("break"), "got: {}", result);
4753 }
4754
4755 #[test]
4758 fn case_char_classes() {
4759 let result = t(r#"case "$x" in [0-9]*) echo num;; [a-z]*) echo alpha;; esac"#);
4760 assert!(result.contains("switch"), "got: {}", result);
4761 assert!(result.contains("'[0-9]*'"), "got: {}", result);
4762 }
4763
4764 #[test]
4765 fn case_multiple_patterns() {
4766 let result = t(
4767 r#"case "$1" in -h|--help) echo help;; -v|--verbose) echo verbose;; esac"#,
4768 );
4769 assert!(result.contains("switch"), "got: {}", result);
4770 assert!(result.contains("--help"), "got: {}", result);
4771 assert!(result.contains("-h"), "got: {}", result);
4772 }
4773
4774 #[test]
4777 fn replace_with_empty_replacement() {
4778 let result = t("echo ${var/foo}");
4779 assert!(result.contains("string replace"), "got: {}", result);
4780 assert!(result.contains("foo"), "got: {}", result);
4781 }
4782
4783 #[test]
4784 fn substring_negative_not_supported() {
4785 let _ = translate_bash_to_fish("echo ${var: -3}");
4787 }
4788
4789 #[test]
4792 fn heredoc_multiline_body() {
4793 let result = t("cat <<'EOF'\nline1\nline2\nline3\nEOF");
4794 assert!(result.contains("printf"), "got: {}", result);
4795 assert!(result.contains("line1"), "got: {}", result);
4796 assert!(result.contains("line3"), "got: {}", result);
4797 assert!(result.contains("| cat"), "got: {}", result);
4798 }
4799
4800 #[test]
4801 fn heredoc_with_grep() {
4802 let result = t("grep pattern <<'END'\nfoo\nbar\nbaz\nEND");
4803 assert!(result.contains("printf"), "got: {}", result);
4804 assert!(result.contains("| grep pattern"), "got: {}", result);
4805 }
4806
4807 #[test]
4810 fn process_sub_diff() {
4811 let result = t("diff <(sort file1) <(sort file2)");
4812 assert!(result.contains("psub"), "got: {}", result);
4813 assert!(result.contains("sort file1"), "got: {}", result);
4814 assert!(result.contains("sort file2"), "got: {}", result);
4815 }
4816
4817 #[test]
4820 fn arith_modulo_integer() {
4821 let result = t("echo $((10 % 3))");
4822 assert!(result.contains("10 % 3"), "got: {}", result);
4823 }
4824
4825 #[test]
4826 fn arith_nested_operations() {
4827 let result = t("echo $(( (a + b) * (c - d) ))");
4828 assert!(result.contains("$a + $b"), "got: {}", result);
4829 assert!(result.contains("$c - $d"), "got: {}", result);
4830 }
4831
4832 #[test]
4833 fn arith_postincrement_standalone() {
4834 let result = t("(( i++ ))");
4835 assert!(result.contains("set i (math"), "got: {}", result);
4836 }
4837
4838 #[test]
4839 fn arith_compound_assign_standalone() {
4840 let result = t("(( x += 5 ))");
4841 assert!(result.contains("set x (math"), "got: {}", result);
4842 }
4843
4844 #[test]
4847 fn double_bracket_not_equal() {
4848 let result = t(r#"[[ "$x" != "hello" ]]"#);
4849 assert!(result.contains("string match") || result.contains("!="), "got: {}", result);
4850 }
4851
4852 #[test]
4853 fn double_bracket_less_than() {
4854 let _ = translate_bash_to_fish(r#"[[ "$a" < "$b" ]]"#);
4857 }
4858
4859 #[test]
4860 fn double_bracket_n_flag() {
4861 let result = t(r#"[[ -n "$var" ]]"#);
4862 assert!(result.contains("test -n"), "got: {}", result);
4863 }
4864
4865 #[test]
4866 fn double_bracket_z_flag() {
4867 let result = t(r#"[[ -z "$var" ]]"#);
4868 assert!(result.contains("test -z"), "got: {}", result);
4869 }
4870
4871 #[test]
4874 fn redirect_dev_null() {
4875 let result = t("command > /dev/null 2>&1");
4876 assert!(result.contains(">/dev/null") || result.contains("> /dev/null"), "got: {}", result);
4877 }
4878
4879 #[test]
4880 fn redirect_stderr_to_file() {
4881 let result = t("command 2> errors.log");
4882 assert!(result.contains("errors.log"), "got: {}", result);
4883 }
4884
4885 #[test]
4888 fn nested_if_with_arithmetic() {
4889 let result = t("if [ $((x + 1)) -gt 5 ]; then echo big; fi");
4890 assert!(result.contains("if "), "got: {}", result);
4891 assert!(result.contains("-gt 5"), "got: {}", result);
4892 assert!(result.contains("end"), "got: {}", result);
4893 }
4894
4895 #[test]
4896 fn function_with_local_vars() {
4897 let result = t("myfunc() { local x=1; echo $x; }");
4898 assert!(result.contains("function myfunc"), "got: {}", result);
4899 assert!(result.contains("set -l x 1"), "got: {}", result);
4900 }
4901
4902 #[test]
4903 fn for_loop_with_command_substitution() {
4904 let result = t("for f in $(ls *.txt); do echo $f; done");
4905 assert!(result.contains("for f in"), "got: {}", result);
4906 assert!(result.contains("ls *.txt"), "got: {}", result);
4907 assert!(result.contains("end"), "got: {}", result);
4908 }
4909
4910 #[test]
4911 fn shopt_bails_to_t2() {
4912 assert!(translate_bash_to_fish("shopt -s nullglob").is_err());
4913 }
4914
4915 #[test]
4916 fn declare_export_flag() {
4917 assert!(t("declare -x FOO=bar").contains("set -gx FOO bar"));
4918 }
4919
4920 #[test]
4923 fn eval_pyenv_init() {
4924 let result = t(r#"eval "$(pyenv init -)""#);
4925 assert!(result.contains("pyenv init -"), "got: {}", result);
4926 assert!(result.contains("source"), "got: {}", result);
4927 }
4928
4929 #[test]
4930 fn eval_ssh_agent() {
4931 let result = t(r#"eval "$(ssh-agent -s)""#);
4932 assert!(result.contains("ssh-agent -s"), "got: {}", result);
4933 assert!(result.contains("source"), "got: {}", result);
4934 }
4935
4936 #[test]
4939 fn herestring_with_variable() {
4940 let result = t("read x <<< $HOME");
4941 assert!(result.contains("echo $HOME"), "got: {}", result);
4942 assert!(result.contains("| read x"), "got: {}", result);
4943 }
4944
4945 #[test]
4946 fn herestring_with_double_quoted() {
4947 let result = t(r#"read x <<< "hello world""#);
4948 assert!(result.contains("hello world"), "got: {}", result);
4949 assert!(result.contains("| read x"), "got: {}", result);
4950 }
4951
4952 #[test]
4955 fn translate_empty_command() {
4956 assert!(translate_bash_to_fish("").is_ok());
4957 }
4958
4959 #[test]
4960 fn translate_comment_stripped() {
4961 assert!(t("# this is a comment").is_empty());
4963 }
4964
4965 #[test]
4966 fn translate_multiple_semicolons() {
4967 let result = t("echo a; echo b; echo c");
4968 assert_eq!(result, "echo a\necho b\necho c");
4969 }
4970
4971 #[test]
4974 fn arith_bitand() {
4975 let result = t("echo $((x & 0xFF))");
4976 assert!(result.contains("bitand("), "got: {}", result);
4977 }
4978
4979 #[test]
4980 fn arith_bitor() {
4981 let result = t("echo $((a | b))");
4982 assert!(result.contains("bitor("), "got: {}", result);
4983 }
4984
4985 #[test]
4986 fn arith_bitxor() {
4987 let result = t("echo $((a ^ b))");
4988 assert!(result.contains("bitxor("), "got: {}", result);
4989 }
4990
4991 #[test]
4992 fn arith_bitnot() {
4993 let result = t("echo $((~x))");
4994 assert!(result.contains("bitxor("), "got: {}", result);
4995 assert!(result.contains("-1"), "got: {}", result);
4996 }
4997
4998 #[test]
4999 fn arith_shift_left() {
5000 let result = t("echo $((1 << 4))");
5001 assert!(result.contains("* 2 ^"), "got: {}", result);
5002 }
5003
5004 #[test]
5005 fn arith_shift_right() {
5006 let result = t("echo $((x >> 2))");
5007 assert!(result.contains("floor("), "got: {}", result);
5008 assert!(result.contains("/ 2 ^"), "got: {}", result);
5009 }
5010
5011 #[test]
5014 fn indirect_expansion() {
5015 let result = t(r#"echo "${!ref}""#);
5016 assert!(result.contains("$$ref"), "got: {}", result);
5017 }
5018
5019 #[test]
5022 fn transform_quote() {
5023 let result = t(r#"echo "${var@Q}""#);
5024 assert!(result.contains("string escape -- $var"), "got: {}", result);
5025 }
5026
5027 #[test]
5028 fn transform_upper() {
5029 let result = t(r#"echo "${var@U}""#);
5030 assert!(result.contains("string upper -- $var"), "got: {}", result);
5031 }
5032
5033 #[test]
5034 fn transform_lower() {
5035 let result = t(r#"echo "${var@L}""#);
5036 assert!(result.contains("string lower -- $var"), "got: {}", result);
5037 }
5038
5039 #[test]
5040 fn transform_capitalize() {
5041 let result = t(r#"echo "${var@u}""#);
5042 assert!(result.contains("string sub -l 1"), "got: {}", result);
5043 assert!(result.contains("string upper"), "got: {}", result);
5044 }
5045
5046 #[test]
5047 fn transform_p_unsupported() {
5048 assert!(translate_bash_to_fish(r#"echo "${var@P}""#).is_err());
5049 }
5050
5051 #[test]
5052 fn transform_k_unsupported() {
5053 assert!(translate_bash_to_fish(r#"echo "${var@K}""#).is_err());
5054 }
5055
5056 #[test]
5059 fn pip_install() {
5060 let result = t("pip install -r requirements.txt");
5061 assert_eq!(result, "pip install -r requirements.txt");
5062 }
5063
5064 #[test]
5065 fn docker_run() {
5066 let result = t("docker run -it --rm -v /tmp:/data ubuntu bash");
5067 assert!(result.contains("docker run"), "got: {}", result);
5068 }
5069
5070 #[test]
5071 fn npm_run_dev() {
5072 let result = t("npm run dev");
5073 assert_eq!(result, "npm run dev");
5074 }
5075
5076 #[test]
5077 fn make_j() {
5078 let result = t("make -j4");
5079 assert_eq!(result, "make -j4");
5080 }
5081
5082 #[test]
5083 fn cargo_test_filter() {
5084 let result = t("cargo test -- --test-threads=1");
5085 assert_eq!(result, "cargo test -- --test-threads=1");
5086 }
5087
5088 #[test]
5089 fn git_log_oneline() {
5090 let result = t("git log --oneline -10");
5091 assert_eq!(result, "git log --oneline -10");
5092 }
5093
5094 #[test]
5095 fn tar_extract() {
5096 let result = t("tar xzf archive.tar.gz -C /tmp");
5097 assert_eq!(result, "tar xzf archive.tar.gz -C /tmp");
5098 }
5099
5100 #[test]
5101 fn chmod_recursive() {
5102 let result = t("chmod -R 755 /var/www");
5103 assert_eq!(result, "chmod -R 755 /var/www");
5104 }
5105
5106 #[test]
5107 fn grep_recursive() {
5108 let result = t("grep -rn TODO src/");
5109 assert_eq!(result, "grep -rn TODO src/");
5110 }
5111
5112 #[test]
5113 fn xargs_rm() {
5114 let result = t("find . -name '*.bak' -print0 | xargs -0 rm -f");
5115 assert!(result.contains("find ."), "got: {}", result);
5116 assert!(result.contains("| xargs"), "got: {}", result);
5117 }
5118
5119 #[test]
5120 fn ssh_command() {
5121 let result = t("ssh user@host 'uptime'");
5122 assert!(result.contains("ssh user@host"), "got: {}", result);
5123 }
5124
5125 #[test]
5126 fn rsync_command() {
5127 let result = t("rsync -avz --delete src/ dest/");
5128 assert_eq!(result, "rsync -avz --delete src/ dest/");
5129 }
5130
5131 #[test]
5132 fn curl_json() {
5133 let result = t("curl -s -H 'Content-Type: application/json' https://api.example.com/data");
5134 assert!(result.contains("curl -s"), "got: {}", result);
5135 }
5136
5137 #[test]
5138 fn systemctl_status() {
5139 let result = t("systemctl status nginx");
5140 assert_eq!(result, "systemctl status nginx");
5141 }
5142
5143 #[test]
5144 fn kill_process() {
5145 let result = t("kill -9 1234");
5146 assert_eq!(result, "kill -9 1234");
5147 }
5148
5149 #[test]
5150 fn ps_grep_pipeline() {
5151 let result = t("ps aux | grep nginx | grep -v grep");
5152 assert_eq!(result, "ps aux | grep nginx | grep -v grep");
5153 }
5154
5155 #[test]
5156 fn du_sort() {
5157 let result = t("du -sh * | sort -hr | head -10");
5158 assert!(result.contains("du -sh"), "got: {}", result);
5159 assert!(result.contains("| sort -hr"), "got: {}", result);
5160 }
5161
5162 #[test]
5163 fn source_env_file() {
5164 let result = t("source ~/.bashrc");
5166 assert!(result.contains("source"), "got: {}", result);
5167 }
5168
5169 #[test]
5170 fn dot_source_profile() {
5171 let result = t(". ~/.profile");
5173 assert!(result.contains('.'), "got: {}", result);
5174 }
5175
5176 #[test]
5179 fn nested_param_in_cmd_subst() {
5180 let result = t(r#"echo "$(basename "${file}")""#);
5181 assert!(result.contains("basename"), "got: {}", result);
5182 }
5183
5184 #[test]
5185 fn cmd_subst_in_assignment() {
5186 let result = t("result=$(grep -c error log.txt)");
5187 assert!(result.contains("set result"), "got: {}", result);
5188 assert!(result.contains("grep -c error"), "got: {}", result);
5189 }
5190
5191 #[test]
5192 fn arith_in_array_index() {
5193 let result = t("echo ${arr[$((i+1))]}");
5194 assert!(result.contains("$arr"), "got: {}", result);
5195 }
5196
5197 #[test]
5198 fn nested_cmd_subst_three_deep() {
5199 let result = t("echo $(dirname $(dirname $(which python)))");
5200 assert!(result.contains("dirname"), "got: {}", result);
5201 assert!(result.contains("which python"), "got: {}", result);
5202 }
5203
5204 #[test]
5207 fn mixed_quotes_in_command() {
5208 let result = t(r#"echo "It's a test""#);
5209 assert!(result.contains("It"), "got: {}", result);
5210 }
5211
5212 #[test]
5213 fn double_quotes_preserve_variable() {
5214 let result = t(r#"echo "Hello $USER, you are in $PWD""#);
5215 assert!(result.contains("$USER"), "got: {}", result);
5216 assert!(result.contains("$PWD"), "got: {}", result);
5217 }
5218
5219 #[test]
5220 fn empty_string_arg() {
5221 let result = t(r#"echo "" foo"#);
5222 assert!(result.contains(r#""""#), "got: {}", result);
5223 }
5224
5225 #[test]
5228 fn for_in_brace_range() {
5229 let result = t("for i in {1..5}; do echo $i; done");
5230 assert!(result.contains("for i in (seq 1 5)"), "got: {}", result);
5231 }
5232
5233 #[test]
5234 fn for_in_brace_range_with_step() {
5235 let result = t("for i in {0..10..2}; do echo $i; done");
5236 assert!(result.contains("seq 0 2 10"), "got: {}", result);
5237 }
5238
5239 #[test]
5240 fn for_loop_multiple_commands() {
5241 let result = t("for f in *.txt; do echo $f; wc -l $f; done");
5242 assert!(result.contains("for f in *.txt"), "got: {}", result);
5243 assert!(result.contains("echo $f"), "got: {}", result);
5244 assert!(result.contains("wc -l $f"), "got: {}", result);
5245 }
5246
5247 #[test]
5250 fn while_true_loop() {
5251 let result = t("while true; do echo loop; sleep 1; done");
5252 assert!(result.contains("while true"), "got: {}", result);
5253 assert!(result.contains("sleep 1"), "got: {}", result);
5254 }
5255
5256 #[test]
5257 fn while_command_condition() {
5258 let result = t("while pgrep -x nginx > /dev/null; do sleep 5; done");
5259 assert!(result.contains("while pgrep"), "got: {}", result);
5260 }
5261
5262 #[test]
5265 fn if_command_condition() {
5266 let result = t("if grep -q error /var/log/syslog; then echo found; fi");
5267 assert!(result.contains("if grep -q error"), "got: {}", result);
5268 assert!(result.contains("echo found"), "got: {}", result);
5269 }
5270
5271 #[test]
5272 fn if_negated_condition() {
5273 let result = t("if ! command -v git > /dev/null; then echo missing; fi");
5274 assert!(result.contains("if not"), "got: {}", result);
5275 assert!(result.contains("command -v git"), "got: {}", result);
5276 }
5277
5278 #[test]
5279 fn if_test_file_ops() {
5280 let result = t("if [ -f /etc/passwd ] && [ -r /etc/passwd ]; then echo ok; fi");
5281 assert!(result.contains("-f /etc/passwd"), "got: {}", result);
5282 assert!(result.contains("-r /etc/passwd"), "got: {}", result);
5283 }
5284
5285 #[test]
5286 fn if_elif_chain() {
5287 let result = t("if [ $x -eq 1 ]; then echo one; elif [ $x -eq 2 ]; then echo two; elif [ $x -eq 3 ]; then echo three; else echo other; fi");
5288 assert!(result.contains("else if"), "got: {}", result);
5289 assert!(result.contains("echo three"), "got: {}", result);
5290 assert!(result.contains("echo other"), "got: {}", result);
5291 }
5292
5293 #[test]
5296 fn case_with_default_only() {
5297 let result = t(r#"case "$x" in *) echo default ;; esac"#);
5298 assert!(result.contains("switch"), "got: {}", result);
5299 assert!(result.contains("case '*'"), "got: {}", result);
5300 }
5301
5302 #[test]
5303 fn case_empty_body() {
5304 let result = t(r#"case "$x" in a) ;; b) echo b ;; esac"#);
5306 assert!(result.contains("switch"), "got: {}", result);
5307 assert!(result.contains("echo b"), "got: {}", result);
5308 }
5309
5310 #[test]
5313 fn function_with_return() {
5314 let result = t("myfunc() { echo hello; return 0; }");
5315 assert!(result.contains("function myfunc"), "got: {}", result);
5316 assert!(result.contains("return 0"), "got: {}", result);
5317 }
5318
5319 #[test]
5320 fn function_keyword_syntax() {
5321 let result = t("function myfunc { echo hello; }");
5322 assert!(result.contains("function myfunc"), "got: {}", result);
5323 }
5324
5325 #[test]
5328 fn export_with_special_chars_value() {
5329 let result = t(r#"export GREETING="Hello World""#);
5330 assert!(result.contains("set -gx GREETING"), "got: {}", result);
5331 assert!(result.contains("Hello World"), "got: {}", result);
5332 }
5333
5334 #[test]
5335 fn export_append_path() {
5336 let result = t(r#"export PATH="$HOME/bin:$PATH""#);
5337 assert!(result.contains("set -gx PATH"), "got: {}", result);
5338 }
5339
5340 #[test]
5343 fn declare_local() {
5344 let result = t("declare foo=bar");
5345 assert!(result.contains("set") && result.contains("foo") && result.contains("bar"), "got: {}", result);
5346 }
5347
5348 #[test]
5349 fn declare_integer() {
5350 let result = translate_bash_to_fish("declare -i num=42");
5351 let _ = result;
5353 }
5354
5355 #[test]
5358 fn read_single_var() {
5359 let result = t("read name");
5360 assert!(result.contains("read name"), "got: {}", result);
5361 }
5362
5363 #[test]
5364 fn read_prompt() {
5365 let result = t(r#"read -p "Enter name: " name"#);
5366 assert!(result.contains("read"), "got: {}", result);
5367 }
5368
5369 #[test]
5372 fn test_string_equality() {
5373 let result = t(r#"[ "$a" = "hello" ]"#);
5374 assert!(result.contains("test") || result.contains('['), "got: {}", result);
5375 }
5376
5377 #[test]
5378 fn test_numeric_comparison() {
5379 let result = t("[ $count -gt 10 ]");
5380 assert!(result.contains("10"), "got: {}", result);
5381 }
5382
5383 #[test]
5384 fn double_bracket_regex_with_capture() {
5385 let result = t(r#"[[ "$line" =~ ^([0-9]+) ]]"#);
5386 assert!(result.contains("string match -r"), "got: {}", result);
5387 }
5388
5389 #[test]
5390 fn double_bracket_compound() {
5391 let result = t(r#"[[ -n "$a" && -z "$b" ]]"#);
5392 assert!(result.contains("-n"), "got: {}", result);
5393 assert!(result.contains("-z"), "got: {}", result);
5394 }
5395
5396 #[test]
5399 fn redirect_both_to_file() {
5400 let result = t("command > out.txt 2>&1");
5401 assert!(result.contains("out.txt"), "got: {}", result);
5402 }
5403
5404 #[test]
5405 fn redirect_input_and_output() {
5406 let result = t("sort < input.txt > output.txt");
5407 assert!(result.contains("sort"), "got: {}", result);
5408 assert!(result.contains("input.txt"), "got: {}", result);
5409 }
5410
5411 #[test]
5412 fn redirect_append_stderr() {
5413 let result = t("command >> log.txt 2>&1");
5414 assert!(result.contains("log.txt"), "got: {}", result);
5415 }
5416
5417 #[test]
5420 fn trap_on_err() {
5421 let result = translate_bash_to_fish("trap 'echo error' ERR");
5422 let _ = result;
5424 }
5425
5426 #[test]
5427 fn trap_cleanup_function() {
5428 let result = t("trap cleanup EXIT");
5429 assert!(result.contains("cleanup"), "got: {}", result);
5430 assert!(result.contains("fish_exit"), "got: {}", result);
5431 }
5432
5433 #[test]
5436 fn arith_comma_operator() {
5437 let result = translate_bash_to_fish("((a=1, b=2))");
5439 let _ = result;
5441 }
5442
5443 #[test]
5444 fn arith_pre_decrement_in_subst() {
5445 assert!(translate_bash_to_fish("echo $((--x))").is_err());
5447 }
5448
5449 #[test]
5450 fn arith_hex_literal() {
5451 let result = t("echo $((0xFF))");
5452 assert!(result.contains("math"), "got: {}", result);
5453 }
5454
5455 #[test]
5458 fn brace_group_with_redirect() {
5459 let result = t("{ echo a; echo b; } > output.txt");
5460 assert!(result.contains("echo a"), "got: {}", result);
5461 assert!(result.contains("echo b"), "got: {}", result);
5462 }
5463
5464 #[test]
5465 fn subshell_with_env() {
5466 let result = translate_bash_to_fish("(cd /tmp && ls)");
5468 let _ = result;
5470 }
5471
5472 #[test]
5475 fn nvm_init_pattern() {
5476 let result = translate_bash_to_fish(r#"export NVM_DIR="$HOME/.nvm""#);
5477 assert!(result.is_ok());
5478 }
5479
5480 #[test]
5481 fn conditional_mkdir() {
5482 let result = t("[ -d /tmp/mydir ] || mkdir -p /tmp/mydir");
5483 assert!(result.contains("/tmp/mydir"), "got: {}", result);
5484 assert!(result.contains("mkdir"), "got: {}", result);
5485 }
5486
5487 #[test]
5488 fn count_files() {
5489 let result = t("ls -1 | wc -l");
5490 assert_eq!(result, "ls -1 | wc -l");
5491 }
5492
5493 #[test]
5494 fn check_exit_code() {
5495 let result = t("if [ $? -ne 0 ]; then echo failed; fi");
5496 assert!(result.contains("$status"), "got: {}", result);
5497 }
5498
5499 #[test]
5500 fn string_contains_check() {
5501 let result = t(r#"[[ "$string" == *"substring"* ]]"#);
5502 assert!(result.contains("string match"), "got: {}", result);
5503 }
5504
5505 #[test]
5506 fn default_value_in_assignment() {
5507 let result = t(r#"name="${1:-World}""#);
5508 assert!(result.contains("World"), "got: {}", result);
5509 }
5510
5511 #[test]
5512 fn multiline_if() {
5513 let result = t("if [ -f ~/.bashrc ]; then\n echo found\nfi");
5514 assert!(result.contains("if"), "got: {}", result);
5515 assert!(result.contains("echo found"), "got: {}", result);
5516 }
5517
5518 #[test]
5519 fn variable_in_path() {
5520 let result = t(r#"ls "$HOME/Documents""#);
5521 assert!(result.contains("$HOME"), "got: {}", result);
5522 }
5523
5524 #[test]
5525 fn command_chaining() {
5526 let result = t("mkdir -p build && cd build && cmake ..");
5527 assert!(result.contains("mkdir -p build"), "got: {}", result);
5528 assert!(result.contains("cd build"), "got: {}", result);
5529 }
5530
5531 #[test]
5532 fn process_sub_with_while() {
5533 let result = t("while read line; do echo $line; done < <(ls -1)");
5534 assert!(result.contains("psub"), "got: {}", result);
5535 }
5536
5537 #[test]
5538 fn heredoc_cat_pattern() {
5539 let result = t("cat <<'EOF'\nhello world\nEOF");
5540 assert!(result.contains("hello world"), "got: {}", result);
5541 }
5542
5543 #[test]
5544 fn heredoc_to_file() {
5545 let result = t("cat <<'EOF' > /tmp/file\ncontent\nEOF");
5546 assert!(result.contains("content"), "got: {}", result);
5547 }
5548
5549 #[test]
5552 fn param_strip_extension() {
5553 let result = t(r#"echo "${filename%.*}""#);
5554 assert!(result.contains("string replace -r"), "got: {}", result);
5555 }
5556
5557 #[test]
5558 fn param_strip_path() {
5559 let result = t(r#"echo "${filepath##*/}""#);
5560 assert!(result.contains("string replace -r"), "got: {}", result);
5561 }
5562
5563 #[test]
5564 fn param_get_extension() {
5565 let result = t(r#"echo "${filename##*.}""#);
5566 assert!(result.contains("string replace -r"), "got: {}", result);
5567 }
5568
5569 #[test]
5570 fn param_get_directory() {
5571 let result = t(r#"echo "${filepath%/*}""#);
5572 assert!(result.contains("string replace -r"), "got: {}", result);
5573 }
5574
5575 #[test]
5576 fn param_default_empty_var() {
5577 let result = t(r#"echo "${unset_var:-default_value}""#);
5578 assert!(result.contains("default_value"), "got: {}", result);
5579 }
5580
5581 #[test]
5582 fn param_error_with_message() {
5583 let result = t(r#"echo "${required:?must be set}""#);
5584 assert!(result.contains("must be set"), "got: {}", result);
5585 }
5586
5587 #[test]
5588 fn substring_from_end() {
5589 let result = t(r#"echo "${str:0:3}""#);
5590 assert!(result.contains("string sub"), "got: {}", result);
5591 }
5592
5593 #[test]
5596 fn array_iteration() {
5597 let result = t(r#"for item in "${arr[@]}"; do echo "$item"; done"#);
5598 assert!(result.contains("for item in"), "got: {}", result);
5599 assert!(result.contains("$arr"), "got: {}", result);
5600 }
5601
5602 #[test]
5603 fn array_length_check() {
5604 let result = t(r#"echo "${#arr[@]}""#);
5605 assert!(result.contains("count $arr"), "got: {}", result);
5606 }
5607
5608 #[test]
5609 fn array_with_spaces() {
5610 let result = t(r#"arr=("hello world" "foo bar")"#);
5611 assert!(result.contains("set arr"), "got: {}", result);
5612 }
5613
5614 #[test]
5617 fn background_with_redirect() {
5618 let result = t("long_running_task > /dev/null 2>&1 &");
5619 assert!(result.contains('&'), "got: {}", result);
5620 }
5621
5622 #[test]
5623 fn sequential_background() {
5624 let result = t("cmd1 & cmd2 &");
5625 assert!(result.contains('&'), "got: {}", result);
5626 }
5627
5628 #[test]
5631 fn unset_multiple() {
5632 let result = t("unset FOO BAR BAZ");
5633 assert!(result.contains("set -e FOO"), "got: {}", result);
5634 assert!(result.contains("set -e BAR"), "got: {}", result);
5635 assert!(result.contains("set -e BAZ"), "got: {}", result);
5636 }
5637
5638 #[test]
5639 fn unset_function() {
5640 let result = translate_bash_to_fish("unset -f myfunc");
5642 let _ = result;
5643 }
5644
5645 #[test]
5648 fn true_false_commands() {
5649 assert_eq!(t("true"), "true");
5650 assert_eq!(t("false"), "false");
5651 }
5652
5653 #[test]
5654 fn colon_noop() {
5655 let result = t(":");
5656 assert!(result.contains(':') || result.contains("true") || result.is_empty(), "got: {}", result);
5657 }
5658
5659 #[test]
5660 fn echo_with_flags() {
5661 let result = t("echo -n hello");
5662 assert!(result.contains("echo -n hello"), "got: {}", result);
5663 }
5664
5665 #[test]
5666 fn echo_with_escape() {
5667 let result = t("echo -e 'hello\\nworld'");
5668 assert!(result.contains("echo"), "got: {}", result);
5669 }
5670
5671 #[test]
5672 fn printf_format() {
5673 let result = t(r#"printf "%s\n" hello"#);
5674 assert!(result.contains("printf"), "got: {}", result);
5675 }
5676
5677 #[test]
5678 fn test_with_not() {
5679 let result = t("[ ! -f /tmp/lock ]");
5680 assert!(result.contains('!') || result.contains("not"), "got: {}", result);
5681 }
5682
5683 #[test]
5684 fn pipeline_three_stages() {
5685 let result = t("cat file | sort | uniq -c");
5686 assert!(result.contains("| sort |"), "got: {}", result);
5687 }
5688
5689 #[test]
5690 fn subshell_captures_output() {
5691 let result = t("result=$(cd /tmp && pwd)");
5692 assert!(result.contains("set result"), "got: {}", result);
5693 }
5694
5695 #[test]
5696 fn multiple_var_assignment() {
5697 let result = t("a=1; b=2; c=3");
5698 assert!(result.contains("set a 1"), "got: {}", result);
5699 assert!(result.contains("set b 2"), "got: {}", result);
5700 assert!(result.contains("set c 3"), "got: {}", result);
5701 }
5702
5703 #[test]
5704 fn replace_all_slashes() {
5705 let result = t(r#"echo "${path//\//\\.}""#);
5706 assert!(result.contains("string replace"), "got: {}", result);
5707 }
5708}