1use crate::parser::{
13 CaseTerminator, CompoundCommand, CondExpr, ListOp, Redirect, RedirectOp, ShellCommand,
14 ShellWord, SimpleCommand,
15};
16
17pub static COND_BINARY_OPS: &[&str] = &[
19 "=", "==", "!=", "<", ">", "-nt", "-ot", "-ef", "-eq", "-ne", "-lt", "-gt", "-le", "-ge", "=~",
20];
21
22pub fn is_cond_binary_op(s: &str) -> bool {
24 COND_BINARY_OPS.contains(&s)
25}
26
27#[derive(Debug, Clone)]
29pub struct TextConfig {
30 pub expand_tabs: i32,
32 pub newlines: bool,
34 pub is_job: bool,
36 pub max_size: Option<usize>,
38}
39
40impl Default for TextConfig {
41 fn default() -> Self {
42 TextConfig {
43 expand_tabs: 0,
44 newlines: true,
45 is_job: false,
46 max_size: None,
47 }
48 }
49}
50
51impl TextConfig {
52 pub fn job_text() -> Self {
53 TextConfig {
54 expand_tabs: 0,
55 newlines: false,
56 is_job: true,
57 max_size: Some(80),
58 }
59 }
60
61 pub fn single_line() -> Self {
62 TextConfig {
63 expand_tabs: -1,
64 newlines: false,
65 is_job: false,
66 max_size: None,
67 }
68 }
69}
70
71pub struct TextFormatter {
73 config: TextConfig,
74 buffer: String,
75 indent: usize,
76 pending: Option<String>,
77}
78
79impl TextFormatter {
80 pub fn new(config: TextConfig) -> Self {
81 TextFormatter {
82 config,
83 buffer: String::with_capacity(256),
84 indent: 0,
85 pending: None,
86 }
87 }
88
89 pub fn with_indent(mut self, indent: usize) -> Self {
90 self.indent = indent;
91 self
92 }
93
94 pub fn format(mut self, cmd: &ShellCommand) -> String {
96 self.format_command(cmd);
97 self.flush_pending();
98 self.buffer
99 }
100
101 pub fn format_list(mut self, cmds: &[ShellCommand]) -> String {
103 for (i, cmd) in cmds.iter().enumerate() {
104 if i > 0 {
105 self.add_separator();
106 }
107 self.format_command(cmd);
108 }
109 self.flush_pending();
110 self.buffer
111 }
112
113 fn add_char(&mut self, c: char) {
114 if let Some(max) = self.config.max_size {
115 if self.buffer.len() >= max {
116 return;
117 }
118 }
119 self.buffer.push(c);
120 }
121
122 fn add_str(&mut self, s: &str) {
123 if let Some(max) = self.config.max_size {
124 if self.buffer.len() >= max {
125 return;
126 }
127 let remaining = max - self.buffer.len();
128 if s.len() > remaining {
129 self.buffer.push_str(&s[..remaining]);
130 return;
131 }
132 }
133
134 if self.config.newlines {
135 self.buffer.push_str(s);
136 } else {
137 for c in s.chars() {
138 self.add_char(if c == '\n' { ' ' } else { c });
139 }
140 }
141 }
142
143 fn flush_pending(&mut self) {
144 if let Some(pending) = self.pending.take() {
145 self.add_char('\n');
146 self.add_str(&pending);
147 }
148 }
149
150 fn add_newline(&mut self, no_semicolon: bool) {
151 if self.config.newlines {
152 self.flush_pending();
153 self.add_char('\n');
154 self.add_indent();
155 } else if no_semicolon {
156 self.add_char(' ');
157 } else {
158 self.add_str("; ");
159 }
160 }
161
162 fn add_indent(&mut self) {
163 if self.config.expand_tabs < 0 {
164 return;
165 }
166 for _ in 0..self.indent {
167 if self.config.expand_tabs > 0 {
168 for _ in 0..self.config.expand_tabs {
169 self.add_char(' ');
170 }
171 } else {
172 self.add_char('\t');
173 }
174 }
175 }
176
177 fn add_separator(&mut self) {
178 if self.config.newlines {
179 self.add_newline(false);
180 } else {
181 self.add_str("; ");
182 }
183 }
184
185 fn inc_indent(&mut self) {
186 self.indent += 1;
187 }
188
189 fn dec_indent(&mut self) {
190 if self.indent > 0 {
191 self.indent -= 1;
192 }
193 }
194
195 fn format_command(&mut self, cmd: &ShellCommand) {
196 match cmd {
197 ShellCommand::Simple(simple) => self.format_simple(simple),
198 ShellCommand::Pipeline(cmds, negated) => self.format_pipeline(cmds, *negated),
199 ShellCommand::List(list) => self.format_list_cmd(list),
200 ShellCommand::Compound(compound) => self.format_compound(compound),
201 ShellCommand::FunctionDef(name, body) => self.format_function(name, body),
202 }
203 }
204
205 fn format_simple(&mut self, cmd: &SimpleCommand) {
206 for (name, value, is_append) in &cmd.assignments {
208 self.add_str(name);
209 if *is_append {
210 self.add_char('+');
211 }
212 self.add_char('=');
213 self.format_word(value);
214 self.add_char(' ');
215 }
216
217 let mut first = true;
219 for word in &cmd.words {
220 if !first {
221 self.add_char(' ');
222 }
223 self.format_word(word);
224 first = false;
225 }
226
227 self.format_redirects(&cmd.redirects);
229 }
230
231 fn format_word(&mut self, word: &ShellWord) {
232 match word {
233 ShellWord::Literal(s) => self.add_str(s),
234 ShellWord::SingleQuoted(s) => {
235 self.add_char('\'');
236 self.add_str(s);
237 self.add_char('\'');
238 }
239 ShellWord::DoubleQuoted(parts) => {
240 self.add_char('"');
241 for part in parts {
242 self.format_word(part);
243 }
244 self.add_char('"');
245 }
246 ShellWord::Variable(name) => {
247 self.add_char('$');
248 self.add_str(name);
249 }
250 ShellWord::VariableBraced(name, modifier) => {
251 self.add_str("${");
252 self.add_str(name);
253 if modifier.is_some() {
254 self.add_str("..."); }
256 self.add_char('}');
257 }
258 ShellWord::ArrayVar(name, _idx) => {
259 self.add_str("${");
260 self.add_str(name);
261 self.add_str("[...]}");
262 }
263 ShellWord::CommandSub(cmd) => {
264 self.add_str("$(");
265 self.format_command(cmd);
266 self.add_char(')');
267 }
268 ShellWord::ProcessSubIn(cmd) => {
269 self.add_str("<(");
270 self.format_command(cmd);
271 self.add_char(')');
272 }
273 ShellWord::ProcessSubOut(cmd) => {
274 self.add_str(">(");
275 self.format_command(cmd);
276 self.add_char(')');
277 }
278 ShellWord::ArithSub(expr) => {
279 self.add_str("$((");
280 self.add_str(expr);
281 self.add_str("))");
282 }
283 ShellWord::ArrayLiteral(words) => {
284 self.add_char('(');
285 for (i, w) in words.iter().enumerate() {
286 if i > 0 {
287 self.add_char(' ');
288 }
289 self.format_word(w);
290 }
291 self.add_char(')');
292 }
293 ShellWord::Glob(pattern) => self.add_str(pattern),
294 ShellWord::Tilde(user) => {
295 self.add_char('~');
296 if let Some(u) = user {
297 self.add_str(u);
298 }
299 }
300 ShellWord::Concat(parts) => {
301 for part in parts {
302 self.format_word(part);
303 }
304 }
305 }
306 }
307
308 fn format_pipeline(&mut self, cmds: &[ShellCommand], negated: bool) {
309 if negated {
310 self.add_str("! ");
311 }
312 for (i, cmd) in cmds.iter().enumerate() {
313 if i > 0 {
314 self.add_str(" | ");
315 }
316 self.format_command(cmd);
317 }
318 }
319
320 fn format_list_cmd(&mut self, list: &[(ShellCommand, ListOp)]) {
321 for (i, (cmd, op)) in list.iter().enumerate() {
322 if i > 0 {
323 match list.get(i - 1).map(|(_, o)| o) {
324 Some(ListOp::And) => self.add_str(" && "),
325 Some(ListOp::Or) => self.add_str(" || "),
326 Some(ListOp::Amp) => self.add_str(" & "),
327 Some(ListOp::Semi) | Some(ListOp::Newline) => {
328 if self.config.newlines {
329 self.add_newline(false);
330 } else {
331 self.add_str("; ");
332 }
333 }
334 None => {}
335 }
336 }
337 self.format_command(cmd);
338
339 if i == list.len() - 1 {
341 match op {
342 ListOp::Amp => self.add_str(" &"),
343 _ => {}
344 }
345 }
346 }
347 }
348
349 fn format_compound(&mut self, compound: &CompoundCommand) {
350 match compound {
351 CompoundCommand::BraceGroup(cmds) => self.format_brace_group(cmds),
352 CompoundCommand::Subshell(cmds) => self.format_subshell(cmds),
353 CompoundCommand::If {
354 conditions,
355 else_part,
356 } => {
357 self.format_if(conditions, else_part);
358 }
359 CompoundCommand::For { var, words, body } => {
360 self.format_for(var, words, body);
361 }
362 CompoundCommand::ForArith {
363 init,
364 cond,
365 step,
366 body,
367 } => {
368 self.format_for_arith(init, cond, step, body);
369 }
370 CompoundCommand::While { condition, body } => {
371 self.format_while(condition, body);
372 }
373 CompoundCommand::Until { condition, body } => {
374 self.format_until(condition, body);
375 }
376 CompoundCommand::Case { word, cases } => {
377 self.format_case(word, cases);
378 }
379 CompoundCommand::Select { var, words, body } => {
380 self.format_select(var, words, body);
381 }
382 CompoundCommand::Repeat { count, body } => {
383 self.add_str("repeat ");
384 self.add_str(count);
385 self.add_newline(false);
386 self.add_str("do");
387 self.inc_indent();
388 self.add_newline(false);
389 for cmd in body {
390 self.format_command(cmd);
391 self.add_newline(false);
392 }
393 self.dec_indent();
394 self.add_str("done");
395 }
396 CompoundCommand::Try {
397 try_body,
398 always_body,
399 } => {
400 self.add_char('{');
401 self.inc_indent();
402 self.add_newline(false);
403 for cmd in try_body {
404 self.format_command(cmd);
405 self.add_newline(false);
406 }
407 self.dec_indent();
408 self.add_str("} always {");
409 self.inc_indent();
410 self.add_newline(false);
411 for cmd in always_body {
412 self.format_command(cmd);
413 self.add_newline(false);
414 }
415 self.dec_indent();
416 self.add_char('}');
417 }
418 CompoundCommand::Coproc { name, body } => {
419 self.add_str("coproc ");
420 if let Some(n) = name {
421 self.add_str(n);
422 self.add_char(' ');
423 }
424 self.format_command(body);
425 }
426 CompoundCommand::Cond(expr) => {
427 self.add_str("[[ ");
428 self.format_cond_expr(expr);
429 self.add_str(" ]]");
430 }
431 CompoundCommand::Arith(expr) => {
432 self.add_str("((");
433 self.add_str(expr);
434 self.add_str("))");
435 }
436 CompoundCommand::WithRedirects(cmd, redirects) => {
437 self.format_command(cmd);
438 self.format_redirects(redirects);
439 }
440 }
441 }
442
443 fn format_cond_expr(&mut self, expr: &CondExpr) {
444 match expr {
445 CondExpr::Not(inner) => {
446 self.add_str("! ");
447 self.format_cond_expr(inner);
448 }
449 CondExpr::And(left, right) => {
450 self.format_cond_expr(left);
451 self.add_str(" && ");
452 self.format_cond_expr(right);
453 }
454 CondExpr::Or(left, right) => {
455 self.format_cond_expr(left);
456 self.add_str(" || ");
457 self.format_cond_expr(right);
458 }
459 CondExpr::FileExists(w) => {
461 self.add_str("-e ");
462 self.format_word(w);
463 }
464 CondExpr::FileRegular(w) => {
465 self.add_str("-f ");
466 self.format_word(w);
467 }
468 CondExpr::FileDirectory(w) => {
469 self.add_str("-d ");
470 self.format_word(w);
471 }
472 CondExpr::FileSymlink(w) => {
473 self.add_str("-L ");
474 self.format_word(w);
475 }
476 CondExpr::FileReadable(w) => {
477 self.add_str("-r ");
478 self.format_word(w);
479 }
480 CondExpr::FileWritable(w) => {
481 self.add_str("-w ");
482 self.format_word(w);
483 }
484 CondExpr::FileExecutable(w) => {
485 self.add_str("-x ");
486 self.format_word(w);
487 }
488 CondExpr::FileNonEmpty(w) => {
489 self.add_str("-s ");
490 self.format_word(w);
491 }
492 CondExpr::StringEmpty(w) => {
494 self.add_str("-z ");
495 self.format_word(w);
496 }
497 CondExpr::StringNonEmpty(w) => {
498 self.add_str("-n ");
499 self.format_word(w);
500 }
501 CondExpr::StringEqual(l, r) => {
502 self.format_word(l);
503 self.add_str(" == ");
504 self.format_word(r);
505 }
506 CondExpr::StringNotEqual(l, r) => {
507 self.format_word(l);
508 self.add_str(" != ");
509 self.format_word(r);
510 }
511 CondExpr::StringMatch(l, r) => {
512 self.format_word(l);
513 self.add_str(" =~ ");
514 self.format_word(r);
515 }
516 CondExpr::StringLess(l, r) => {
517 self.format_word(l);
518 self.add_str(" < ");
519 self.format_word(r);
520 }
521 CondExpr::StringGreater(l, r) => {
522 self.format_word(l);
523 self.add_str(" > ");
524 self.format_word(r);
525 }
526 CondExpr::NumEqual(l, r) => {
528 self.format_word(l);
529 self.add_str(" -eq ");
530 self.format_word(r);
531 }
532 CondExpr::NumNotEqual(l, r) => {
533 self.format_word(l);
534 self.add_str(" -ne ");
535 self.format_word(r);
536 }
537 CondExpr::NumLess(l, r) => {
538 self.format_word(l);
539 self.add_str(" -lt ");
540 self.format_word(r);
541 }
542 CondExpr::NumLessEqual(l, r) => {
543 self.format_word(l);
544 self.add_str(" -le ");
545 self.format_word(r);
546 }
547 CondExpr::NumGreater(l, r) => {
548 self.format_word(l);
549 self.add_str(" -gt ");
550 self.format_word(r);
551 }
552 CondExpr::NumGreaterEqual(l, r) => {
553 self.format_word(l);
554 self.add_str(" -ge ");
555 self.format_word(r);
556 }
557 }
558 }
559
560 fn format_for(&mut self, var: &str, words: &Option<Vec<ShellWord>>, body: &[ShellCommand]) {
561 self.add_str("for ");
562 self.add_str(var);
563
564 if let Some(word_list) = words {
565 self.add_str(" in ");
566 for (i, w) in word_list.iter().enumerate() {
567 if i > 0 {
568 self.add_char(' ');
569 }
570 self.format_word(w);
571 }
572 }
573
574 self.add_newline(false);
575 self.add_str("do");
576 self.inc_indent();
577 self.add_newline(false);
578
579 for cmd in body {
580 self.format_command(cmd);
581 self.add_newline(false);
582 }
583
584 self.dec_indent();
585 self.add_newline(false);
586 self.add_str("done");
587 }
588
589 fn format_for_arith(&mut self, init: &str, cond: &str, step: &str, body: &[ShellCommand]) {
590 self.add_str("for ((");
591 self.add_str(init);
592 self.add_str("; ");
593 self.add_str(cond);
594 self.add_str("; ");
595 self.add_str(step);
596 self.add_str(")) do");
597 self.inc_indent();
598 self.add_newline(false);
599
600 for cmd in body {
601 self.format_command(cmd);
602 self.add_newline(false);
603 }
604
605 self.dec_indent();
606 self.add_newline(false);
607 self.add_str("done");
608 }
609
610 fn format_while(&mut self, condition: &[ShellCommand], body: &[ShellCommand]) {
611 self.add_str("while ");
612 self.inc_indent();
613
614 for cmd in condition {
615 self.format_command(cmd);
616 }
617
618 self.dec_indent();
619 self.add_newline(false);
620 self.add_str("do");
621 self.inc_indent();
622 self.add_newline(false);
623
624 for cmd in body {
625 self.format_command(cmd);
626 self.add_newline(false);
627 }
628
629 self.dec_indent();
630 self.add_newline(false);
631 self.add_str("done");
632 }
633
634 fn format_until(&mut self, condition: &[ShellCommand], body: &[ShellCommand]) {
635 self.add_str("until ");
636 self.inc_indent();
637
638 for cmd in condition {
639 self.format_command(cmd);
640 }
641
642 self.dec_indent();
643 self.add_newline(false);
644 self.add_str("do");
645 self.inc_indent();
646 self.add_newline(false);
647
648 for cmd in body {
649 self.format_command(cmd);
650 self.add_newline(false);
651 }
652
653 self.dec_indent();
654 self.add_newline(false);
655 self.add_str("done");
656 }
657
658 fn format_case(
659 &mut self,
660 word: &ShellWord,
661 cases: &[(Vec<ShellWord>, Vec<ShellCommand>, CaseTerminator)],
662 ) {
663 self.add_str("case ");
664 self.format_word(word);
665 self.add_str(" in");
666
667 if cases.is_empty() {
668 if self.config.newlines {
669 self.add_newline(false);
670 } else {
671 self.add_char(' ');
672 }
673 self.add_str("esac");
674 return;
675 }
676
677 self.inc_indent();
678
679 for (patterns, body, terminator) in cases {
680 if self.config.newlines {
681 self.add_newline(false);
682 } else {
683 self.add_char(' ');
684 }
685
686 self.add_str("(");
687 for (i, pat) in patterns.iter().enumerate() {
688 if i > 0 {
689 self.add_str(" | ");
690 }
691 self.format_word(pat);
692 }
693 self.add_str(") ");
694
695 self.inc_indent();
696 for cmd in body {
697 self.format_command(cmd);
698 }
699 self.dec_indent();
700
701 match terminator {
702 CaseTerminator::Break => self.add_str(" ;;"),
703 CaseTerminator::Fallthrough => self.add_str(" ;&"),
704 CaseTerminator::Continue => self.add_str(" ;|"),
705 }
706 }
707
708 self.dec_indent();
709 if self.config.newlines {
710 self.add_newline(false);
711 } else {
712 self.add_char(' ');
713 }
714 self.add_str("esac");
715 }
716
717 fn format_if(
718 &mut self,
719 conditions: &[(Vec<ShellCommand>, Vec<ShellCommand>)],
720 else_part: &Option<Vec<ShellCommand>>,
721 ) {
722 for (i, (cond, body)) in conditions.iter().enumerate() {
723 if i == 0 {
724 self.add_str("if ");
725 } else {
726 self.dec_indent();
727 self.add_newline(false);
728 self.add_str("elif ");
729 }
730
731 self.inc_indent();
732 for cmd in cond {
733 self.format_command(cmd);
734 }
735 self.dec_indent();
736
737 self.add_newline(false);
738 self.add_str("then");
739 self.inc_indent();
740 self.add_newline(false);
741
742 for cmd in body {
743 self.format_command(cmd);
744 self.add_newline(false);
745 }
746 }
747
748 if let Some(else_body) = else_part {
749 self.dec_indent();
750 self.add_newline(false);
751 self.add_str("else");
752 self.inc_indent();
753 self.add_newline(false);
754
755 for cmd in else_body {
756 self.format_command(cmd);
757 self.add_newline(false);
758 }
759 }
760
761 self.dec_indent();
762 self.add_newline(false);
763 self.add_str("fi");
764 }
765
766 fn format_select(&mut self, var: &str, words: &Option<Vec<ShellWord>>, body: &[ShellCommand]) {
767 self.add_str("select ");
768 self.add_str(var);
769
770 if let Some(word_list) = words {
771 self.add_str(" in ");
772 for (i, w) in word_list.iter().enumerate() {
773 if i > 0 {
774 self.add_char(' ');
775 }
776 self.format_word(w);
777 }
778 }
779
780 self.add_newline(false);
781 self.add_str("do");
782 self.add_newline(false);
783 self.inc_indent();
784
785 for cmd in body {
786 self.format_command(cmd);
787 self.add_newline(false);
788 }
789
790 self.dec_indent();
791 self.add_newline(false);
792 self.add_str("done");
793 }
794
795 fn format_function(&mut self, name: &str, body: &ShellCommand) {
796 self.add_str(name);
797 self.add_str("() ");
798
799 if self.config.is_job {
800 self.add_str("{ ... }");
801 return;
802 }
803
804 self.add_str("{");
805 self.inc_indent();
806 self.add_newline(true);
807
808 self.format_command(body);
809
810 self.dec_indent();
811 self.add_newline(false);
812 self.add_str("}");
813 }
814
815 fn format_subshell(&mut self, cmds: &[ShellCommand]) {
816 self.add_str("(");
817 self.inc_indent();
818 self.add_newline(true);
819
820 for cmd in cmds {
821 self.format_command(cmd);
822 self.add_newline(false);
823 }
824
825 self.dec_indent();
826 self.add_newline(false);
827 self.add_str(")");
828 }
829
830 fn format_brace_group(&mut self, cmds: &[ShellCommand]) {
831 self.add_str("{");
832 self.inc_indent();
833 self.add_newline(true);
834
835 for cmd in cmds {
836 self.format_command(cmd);
837 self.add_newline(false);
838 }
839
840 self.dec_indent();
841 self.add_newline(false);
842 self.add_str("}");
843 }
844
845 fn format_redirects(&mut self, redirects: &[Redirect]) {
846 if redirects.is_empty() {
847 return;
848 }
849
850 self.add_char(' ');
851
852 for redir in redirects {
853 self.format_redirect(redir);
854 self.add_char(' ');
855 }
856
857 if self.buffer.ends_with(' ') {
859 self.buffer.pop();
860 }
861 }
862
863 fn format_redirect(&mut self, redir: &Redirect) {
864 if let Some(ref var) = redir.fd_var {
866 self.add_char('{');
867 self.add_str(var);
868 self.add_char('}');
869 } else if let Some(fd) = redir.fd {
870 let default_fd = match redir.op {
871 RedirectOp::Read
872 | RedirectOp::ReadWrite
873 | RedirectOp::HereDoc
874 | RedirectOp::HereString
875 | RedirectOp::DupRead => 0,
876 _ => 1,
877 };
878 if fd != default_fd {
879 self.add_str(&fd.to_string());
880 }
881 }
882
883 let op = match redir.op {
885 RedirectOp::Write => ">",
886 RedirectOp::Clobber => ">|",
887 RedirectOp::Append => ">>",
888 RedirectOp::WriteBoth => "&>",
889 RedirectOp::AppendBoth => "&>>",
890 RedirectOp::ReadWrite => "<>",
891 RedirectOp::Read => "<",
892 RedirectOp::HereDoc => "<<",
893 RedirectOp::HereString => "<<<",
894 RedirectOp::DupRead => "<&",
895 RedirectOp::DupWrite => ">&",
896 };
897 self.add_str(op);
898
899 if !matches!(redir.op, RedirectOp::DupRead | RedirectOp::DupWrite) {
901 self.add_char(' ');
902 }
903 self.format_word(&redir.target);
904 }
905}
906
907pub fn getpermtext(cmd: &ShellCommand) -> String {
909 TextFormatter::new(TextConfig::default()).format(cmd)
910}
911
912pub fn getpermtext_indent(cmd: &ShellCommand, indent: usize) -> String {
914 TextFormatter::new(TextConfig::default())
915 .with_indent(indent)
916 .format(cmd)
917}
918
919pub fn getjobtext(cmd: &ShellCommand) -> String {
921 TextFormatter::new(TextConfig::job_text()).format(cmd)
922}
923
924pub fn getsingleline(cmd: &ShellCommand) -> String {
926 TextFormatter::new(TextConfig::single_line()).format(cmd)
927}
928
929pub fn format_commands(cmds: &[ShellCommand], config: TextConfig) -> String {
931 TextFormatter::new(config).format_list(cmds)
932}
933
934#[cfg(test)]
935mod tests {
936 use super::*;
937
938 fn simple_cmd(words: &[&str]) -> ShellCommand {
939 ShellCommand::Simple(SimpleCommand {
940 words: words
941 .iter()
942 .map(|s| ShellWord::Literal(s.to_string()))
943 .collect(),
944 assignments: vec![],
945 redirects: vec![],
946 })
947 }
948
949 #[test]
950 fn test_simple_command() {
951 let cmd = simple_cmd(&["echo", "hello"]);
952 assert_eq!(getpermtext(&cmd), "echo hello");
953 }
954
955 #[test]
956 fn test_pipeline() {
957 let pipeline = ShellCommand::Pipeline(
958 vec![
959 simple_cmd(&["cat", "file"]),
960 simple_cmd(&["grep", "pattern"]),
961 ],
962 false,
963 );
964 assert_eq!(getpermtext(&pipeline), "cat file | grep pattern");
965 }
966
967 #[test]
968 fn test_negated_pipeline() {
969 let pipeline = ShellCommand::Pipeline(vec![simple_cmd(&["test", "-f", "file"])], true);
970 assert_eq!(getpermtext(&pipeline), "! test -f file");
971 }
972
973 #[test]
974 fn test_and_list() {
975 let list = ShellCommand::List(vec![
976 (simple_cmd(&["test", "-f", "file"]), ListOp::And),
977 (simple_cmd(&["cat", "file"]), ListOp::Semi),
978 ]);
979 let text = getpermtext(&list);
980 assert!(text.contains("&&"));
981 }
982
983 #[test]
984 fn test_or_list() {
985 let list = ShellCommand::List(vec![
986 (simple_cmd(&["test", "-f", "file"]), ListOp::Or),
987 (simple_cmd(&["echo", "not found"]), ListOp::Semi),
988 ]);
989 let text = getpermtext(&list);
990 assert!(text.contains("||"));
991 }
992
993 #[test]
994 fn test_subshell() {
995 let cmd =
996 ShellCommand::Compound(CompoundCommand::Subshell(vec![simple_cmd(&["echo", "hi"])]));
997 let text = getpermtext(&cmd);
998 assert!(text.contains("("));
999 assert!(text.contains(")"));
1000 assert!(text.contains("echo hi"));
1001 }
1002
1003 #[test]
1004 fn test_brace_group() {
1005 let cmd = ShellCommand::Compound(CompoundCommand::BraceGroup(vec![simple_cmd(&[
1006 "echo", "hi",
1007 ])]));
1008 let text = getpermtext(&cmd);
1009 assert!(text.contains("{"));
1010 assert!(text.contains("}"));
1011 }
1012
1013 #[test]
1014 fn test_job_text() {
1015 let cmd = simple_cmd(&["very", "long", "command", "with", "many", "arguments"]);
1016 let job_text = getjobtext(&cmd);
1017 assert!(job_text.len() <= 80);
1018 }
1019
1020 #[test]
1021 fn test_single_line() {
1022 let cmd = ShellCommand::Compound(CompoundCommand::BraceGroup(vec![
1023 simple_cmd(&["echo", "a"]),
1024 simple_cmd(&["echo", "b"]),
1025 ]));
1026 let text = getsingleline(&cmd);
1027 assert!(!text.contains('\n'));
1028 assert!(text.contains(';'));
1029 }
1030
1031 #[test]
1032 fn test_is_cond_binary_op() {
1033 assert!(is_cond_binary_op("="));
1034 assert!(is_cond_binary_op("-eq"));
1035 assert!(is_cond_binary_op("-nt"));
1036 assert!(!is_cond_binary_op("-f"));
1037 assert!(!is_cond_binary_op("foo"));
1038 }
1039
1040 #[test]
1041 fn test_redirect_output() {
1042 let cmd = ShellCommand::Simple(SimpleCommand {
1043 words: vec![
1044 ShellWord::Literal("echo".to_string()),
1045 ShellWord::Literal("hello".to_string()),
1046 ],
1047 assignments: vec![],
1048 redirects: vec![Redirect {
1049 fd: Some(1),
1050 op: RedirectOp::Write,
1051 target: ShellWord::Literal("file.txt".to_string()),
1052 heredoc_content: None,
1053 fd_var: None,
1054 }],
1055 });
1056 let text = getpermtext(&cmd);
1057 assert!(text.contains("> file.txt"));
1058 }
1059}