Serenity Operating System
1/*
2 * Copyright (c) 2020, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include "Formatter.h"
8#include "AST.h"
9#include "Parser.h"
10#include "PosixParser.h"
11#include <AK/Hex.h>
12#include <AK/ScopedValueRollback.h>
13#include <AK/TemporaryChange.h>
14
15namespace Shell {
16
17DeprecatedString Formatter::format()
18{
19 auto node = m_root_node ?: (m_parse_as_posix ? Posix::Parser(m_source).parse() : Parser(m_source).parse());
20 if (m_cursor >= 0)
21 m_output_cursor = m_cursor;
22
23 if (!node)
24 return DeprecatedString();
25
26 if (node->is_syntax_error())
27 return m_source;
28
29 if (m_cursor >= 0) {
30 auto hit_test = node->hit_test_position(m_cursor);
31 if (hit_test.matching_node)
32 m_hit_node = hit_test.matching_node.ptr();
33 else
34 m_hit_node = nullptr;
35 }
36
37 m_parent_node = nullptr;
38
39 node->visit(*this);
40
41 VERIFY(m_builders.size() == 1);
42
43 auto string = current_builder().string_view();
44
45 if (!string.ends_with(' '))
46 current_builder().append(m_trivia);
47
48 return current_builder().to_deprecated_string();
49}
50
51void Formatter::with_added_indent(int indent, Function<void()> callback)
52{
53 TemporaryChange indent_change { m_current_indent, m_current_indent + indent };
54 callback();
55}
56
57void Formatter::in_new_block(Function<void()> callback)
58{
59 current_builder().append('{');
60
61 with_added_indent(1, [&] {
62 insert_separator();
63 callback();
64 });
65
66 insert_separator();
67 current_builder().append('}');
68}
69
70DeprecatedString Formatter::in_new_builder(Function<void()> callback, StringBuilder new_builder)
71{
72 m_builders.append(move(new_builder));
73 callback();
74 return m_builders.take_last().to_deprecated_string();
75}
76
77void Formatter::test_and_update_output_cursor(const AST::Node* node)
78{
79 if (!node)
80 return;
81
82 if (node != m_hit_node)
83 return;
84
85 m_output_cursor = current_builder().length() + m_cursor - node->position().start_offset;
86}
87
88void Formatter::visited(const AST::Node* node)
89{
90 m_last_visited_node = node;
91}
92
93void Formatter::will_visit(const AST::Node* node)
94{
95 if (!m_last_visited_node)
96 return;
97
98 if (!node)
99 return;
100
101 auto direct_sequence_child = !m_parent_node || m_parent_node->kind() == AST::Node::Kind::Sequence;
102
103 if (direct_sequence_child && node->kind() != AST::Node::Kind::Sequence && node->kind() != AST::Node::Kind::Execute) {
104 // Collapse more than one empty line to a single one.
105 if (node->position().start_line.line_number - m_last_visited_node->position().end_line.line_number > 1)
106 insert_separator();
107 }
108}
109
110void Formatter::insert_separator(bool escaped)
111{
112 if (escaped)
113 current_builder().append('\\');
114 current_builder().append('\n');
115 if (!escaped && !m_heredocs_to_append_after_sequence.is_empty()) {
116 for (auto& entry : m_heredocs_to_append_after_sequence) {
117 current_builder().append(entry);
118 }
119 m_heredocs_to_append_after_sequence.clear();
120 }
121 insert_indent();
122}
123
124void Formatter::insert_indent()
125{
126 for (size_t i = 0; i < m_current_indent; ++i)
127 current_builder().append(" "sv);
128}
129
130void Formatter::visit(const AST::PathRedirectionNode* node)
131{
132 will_visit(node);
133 test_and_update_output_cursor(node);
134 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
135 NodeVisitor::visit(node);
136 visited(node);
137}
138
139void Formatter::visit(const AST::And* node)
140{
141 will_visit(node);
142 test_and_update_output_cursor(node);
143 auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::And;
144 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
145
146 with_added_indent(should_indent ? 1 : 0, [&] {
147 node->left()->visit(*this);
148
149 current_builder().append(' ');
150 insert_separator(true);
151 current_builder().append("&& "sv);
152
153 node->right()->visit(*this);
154 });
155 visited(node);
156}
157
158void Formatter::visit(const AST::ListConcatenate* node)
159{
160 will_visit(node);
161 test_and_update_output_cursor(node);
162 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
163
164 auto first = true;
165 for (auto& subnode : node->list()) {
166 if (!first)
167 current_builder().append(' ');
168 first = false;
169 subnode->visit(*this);
170 }
171 visited(node);
172}
173
174void Formatter::visit(const AST::Background* node)
175{
176 will_visit(node);
177 test_and_update_output_cursor(node);
178
179 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
180 NodeVisitor::visit(node);
181 current_builder().append(" &"sv);
182 visited(node);
183}
184
185void Formatter::visit(const AST::BarewordLiteral* node)
186{
187 will_visit(node);
188 test_and_update_output_cursor(node);
189 current_builder().append(node->text());
190 visited(node);
191}
192
193void Formatter::visit(const AST::BraceExpansion* node)
194{
195 will_visit(node);
196 test_and_update_output_cursor(node);
197 if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
198 current_builder().append('{');
199
200 {
201 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
202 bool first = true;
203 for (auto& entry : node->entries()) {
204 if (!first)
205 current_builder().append(',');
206 first = false;
207 entry->visit(*this);
208 }
209 }
210
211 if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
212 current_builder().append('}');
213 visited(node);
214}
215
216void Formatter::visit(const AST::CastToCommand* node)
217{
218 will_visit(node);
219 test_and_update_output_cursor(node);
220
221 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
222 NodeVisitor::visit(node);
223
224 visited(node);
225}
226
227void Formatter::visit(const AST::CastToList* node)
228{
229 will_visit(node);
230 test_and_update_output_cursor(node);
231 current_builder().append('(');
232
233 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
234 NodeVisitor::visit(node);
235
236 current_builder().append(')');
237 visited(node);
238}
239
240void Formatter::visit(const AST::CloseFdRedirection* node)
241{
242 will_visit(node);
243 test_and_update_output_cursor(node);
244 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
245
246 current_builder().appendff("{}>&-", node->fd());
247 visited(node);
248}
249
250void Formatter::visit(const AST::CommandLiteral*)
251{
252 VERIFY_NOT_REACHED();
253}
254
255void Formatter::visit(const AST::Comment* node)
256{
257 will_visit(node);
258 test_and_update_output_cursor(node);
259 current_builder().append("#"sv);
260 current_builder().append(node->text());
261 visited(node);
262}
263
264void Formatter::visit(const AST::ContinuationControl* node)
265{
266 will_visit(node);
267 test_and_update_output_cursor(node);
268 if (node->continuation_kind() == AST::ContinuationControl::Break)
269 current_builder().append("break"sv);
270 else if (node->continuation_kind() == AST::ContinuationControl::Continue)
271 current_builder().append("continue"sv);
272 else
273 VERIFY_NOT_REACHED();
274 visited(node);
275}
276
277void Formatter::visit(const AST::DynamicEvaluate* node)
278{
279 will_visit(node);
280 test_and_update_output_cursor(node);
281 current_builder().append('$');
282 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
283 NodeVisitor::visit(node);
284 visited(node);
285}
286
287void Formatter::visit(const AST::DoubleQuotedString* node)
288{
289 will_visit(node);
290 test_and_update_output_cursor(node);
291 auto not_in_heredoc = m_parent_node->kind() != AST::Node::Kind::Heredoc;
292 if (not_in_heredoc)
293 current_builder().append("\""sv);
294
295 TemporaryChange quotes { m_options.in_double_quotes, true };
296 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
297
298 NodeVisitor::visit(node);
299
300 if (not_in_heredoc)
301 current_builder().append("\""sv);
302 visited(node);
303}
304
305void Formatter::visit(const AST::Fd2FdRedirection* node)
306{
307 will_visit(node);
308 test_and_update_output_cursor(node);
309 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
310
311 current_builder().appendff("{}>&{}", node->source_fd(), node->dest_fd());
312 if (m_hit_node == node)
313 ++m_output_cursor;
314 visited(node);
315}
316
317void Formatter::visit(const AST::FunctionDeclaration* node)
318{
319 will_visit(node);
320 test_and_update_output_cursor(node);
321 current_builder().append(node->name().name);
322 current_builder().append('(');
323 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
324
325 auto first = true;
326 for (auto& arg : node->arguments()) {
327 if (!first)
328 current_builder().append(' ');
329 first = false;
330 current_builder().append(arg.name);
331 }
332
333 current_builder().append(") "sv);
334
335 in_new_block([&] {
336 if (node->block())
337 node->block()->visit(*this);
338 });
339 visited(node);
340}
341
342void Formatter::visit(const AST::ForLoop* node)
343{
344 will_visit(node);
345 test_and_update_output_cursor(node);
346 auto is_loop = node->iterated_expression().is_null();
347 current_builder().append(is_loop ? "loop"sv : "for "sv);
348 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
349
350 if (!is_loop) {
351 if (node->index_variable().has_value()) {
352 current_builder().append("index "sv);
353 current_builder().append(node->index_variable()->name);
354 current_builder().append(" "sv);
355 }
356 if (node->variable().has_value() && node->variable()->name != "it") {
357 current_builder().append(node->variable()->name);
358 current_builder().append(" in "sv);
359 }
360
361 node->iterated_expression()->visit(*this);
362 }
363
364 current_builder().append(' ');
365 in_new_block([&] {
366 if (node->block())
367 node->block()->visit(*this);
368 });
369 visited(node);
370}
371
372void Formatter::visit(const AST::Glob* node)
373{
374 will_visit(node);
375 test_and_update_output_cursor(node);
376 current_builder().append(node->text());
377 visited(node);
378}
379
380void Formatter::visit(const AST::Heredoc* node)
381{
382 will_visit(node);
383 test_and_update_output_cursor(node);
384
385 current_builder().append("<<"sv);
386 if (node->deindent())
387 current_builder().append('~');
388 else
389 current_builder().append('-');
390
391 if (node->allow_interpolation())
392 current_builder().appendff("{}", node->end());
393 else
394 current_builder().appendff("'{}'", node->end());
395
396 auto content = in_new_builder([&] {
397 if (!node->contents())
398 return;
399
400 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
401 TemporaryChange heredoc { m_options.in_heredoc, true };
402
403 auto& contents = *node->contents();
404 contents.visit(*this);
405 current_builder().appendff("\n{}\n", node->end());
406 });
407
408 m_heredocs_to_append_after_sequence.append(move(content));
409
410 visited(node);
411}
412
413void Formatter::visit(const AST::HistoryEvent* node)
414{
415 will_visit(node);
416 test_and_update_output_cursor(node);
417
418 current_builder().append('!');
419 switch (node->selector().event.kind) {
420 case AST::HistorySelector::EventKind::ContainingStringLookup:
421 current_builder().append('?');
422 current_builder().append(node->selector().event.text);
423 break;
424 case AST::HistorySelector::EventKind::StartingStringLookup:
425 current_builder().append(node->selector().event.text);
426 break;
427 case AST::HistorySelector::EventKind::IndexFromStart:
428 current_builder().append(node->selector().event.text);
429 break;
430 case AST::HistorySelector::EventKind::IndexFromEnd:
431 if (node->selector().event.index == 0)
432 current_builder().append('!');
433 else
434 current_builder().append(node->selector().event.text);
435 break;
436 }
437
438 auto& range = node->selector().word_selector_range;
439 if (!range.end.has_value()
440 || range.end.value().kind != AST::HistorySelector::WordSelectorKind::Last
441 || range.start.kind != AST::HistorySelector::WordSelectorKind::Index || range.start.selector != 0) {
442
443 auto append_word = [this](auto& selector) {
444 switch (selector.kind) {
445 case AST::HistorySelector::WordSelectorKind::Index:
446 if (selector.selector == 0)
447 current_builder().append('^');
448 else
449 current_builder().appendff("{}", selector.selector);
450 break;
451 case AST::HistorySelector::WordSelectorKind::Last:
452 current_builder().append('$');
453 break;
454 }
455 };
456
457 current_builder().append(':');
458 append_word(range.start);
459
460 if (range.end.has_value()) {
461 current_builder().append('-');
462 append_word(range.end.value());
463 }
464 }
465
466 visited(node);
467}
468
469void Formatter::visit(const AST::Execute* node)
470{
471 will_visit(node);
472 test_and_update_output_cursor(node);
473 auto& builder = current_builder();
474 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
475 ScopedValueRollback options_rollback { m_options };
476
477 if (node->does_capture_stdout())
478 builder.append("$("sv);
479
480 NodeVisitor::visit(node);
481
482 if (node->does_capture_stdout())
483 builder.append(")"sv);
484
485 visited(node);
486}
487
488void Formatter::visit(const AST::IfCond* node)
489{
490 will_visit(node);
491 test_and_update_output_cursor(node);
492
493 current_builder().append("if "sv);
494 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
495
496 node->condition()->visit(*this);
497
498 current_builder().append(' ');
499
500 in_new_block([&] {
501 if (node->true_branch())
502 node->true_branch()->visit(*this);
503 });
504
505 if (node->false_branch()) {
506 current_builder().append(" else "sv);
507 if (node->false_branch()->kind() != AST::Node::Kind::IfCond) {
508 in_new_block([&] {
509 node->false_branch()->visit(*this);
510 });
511 } else {
512 node->false_branch()->visit(*this);
513 }
514 } else if (node->else_position().has_value()) {
515 current_builder().append(" else "sv);
516 }
517 visited(node);
518}
519
520void Formatter::visit(const AST::ImmediateExpression* node)
521{
522 will_visit(node);
523 test_and_update_output_cursor(node);
524
525 current_builder().append("${"sv);
526 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
527
528 current_builder().append(node->function_name());
529
530 for (auto& node : node->arguments()) {
531 current_builder().append(' ');
532 node->visit(*this);
533 }
534
535 if (node->has_closing_brace())
536 current_builder().append('}');
537
538 visited(node);
539}
540
541void Formatter::visit(const AST::Join* node)
542{
543 will_visit(node);
544 test_and_update_output_cursor(node);
545 auto should_parenthesise = m_options.explicit_parentheses;
546
547 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
548 TemporaryChange parens { m_options.explicit_parentheses, false };
549
550 if (should_parenthesise)
551 current_builder().append('(');
552
553 node->left()->visit(*this);
554
555 current_builder().append(' ');
556
557 node->right()->visit(*this);
558
559 if (should_parenthesise)
560 current_builder().append(')');
561 visited(node);
562}
563
564void Formatter::visit(const AST::MatchExpr* node)
565{
566 will_visit(node);
567 test_and_update_output_cursor(node);
568 current_builder().append("match "sv);
569
570 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
571
572 node->matched_expr()->visit(*this);
573
574 if (!node->expr_name().is_empty()) {
575 current_builder().append(" as "sv);
576 current_builder().append(node->expr_name());
577 }
578
579 current_builder().append(' ');
580 in_new_block([&] {
581 auto first_entry = true;
582 for (auto& entry : node->entries()) {
583 if (!first_entry)
584 insert_separator();
585 first_entry = false;
586 auto first = true;
587 entry.options.visit(
588 [&](Vector<NonnullRefPtr<AST::Node>> const& patterns) {
589 for (auto& option : patterns) {
590 if (!first)
591 current_builder().append(" | "sv);
592 first = false;
593 option->visit(*this);
594 }
595 },
596 [&](Vector<Regex<ECMA262>> const& patterns) {
597 for (auto& option : patterns) {
598 if (!first)
599 current_builder().append(" | "sv);
600 first = false;
601 auto node = make_ref_counted<AST::BarewordLiteral>(AST::Position {}, String::from_utf8(option.pattern_value).release_value_but_fixme_should_propagate_errors());
602 node->visit(*this);
603 }
604 });
605
606 current_builder().append(' ');
607 if (entry.match_names.has_value() && !entry.match_names.value().is_empty()) {
608 current_builder().append("as ("sv);
609 auto first = true;
610 for (auto& name : entry.match_names.value()) {
611 if (!first)
612 current_builder().append(' ');
613 first = false;
614 current_builder().append(name);
615 }
616 current_builder().append(") "sv);
617 }
618 in_new_block([&] {
619 if (entry.body)
620 entry.body->visit(*this);
621 });
622 }
623 });
624 visited(node);
625}
626
627void Formatter::visit(const AST::Or* node)
628{
629 will_visit(node);
630 test_and_update_output_cursor(node);
631 auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::Or;
632 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
633
634 with_added_indent(should_indent ? 1 : 0, [&] {
635 node->left()->visit(*this);
636
637 current_builder().append(" "sv);
638 insert_separator(true);
639 current_builder().append("|| "sv);
640
641 node->right()->visit(*this);
642 });
643 visited(node);
644}
645
646void Formatter::visit(const AST::Pipe* node)
647{
648 will_visit(node);
649 test_and_update_output_cursor(node);
650 auto should_indent = m_parent_node && m_parent_node->kind() != AST::Node::Kind::Pipe;
651 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
652
653 node->left()->visit(*this);
654 current_builder().append(" "sv);
655
656 with_added_indent(should_indent ? 1 : 0, [&] {
657 insert_separator(true);
658 current_builder().append("| "sv);
659
660 node->right()->visit(*this);
661 });
662 visited(node);
663}
664
665void Formatter::visit(const AST::Range* node)
666{
667 will_visit(node);
668 test_and_update_output_cursor(node);
669 if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
670 current_builder().append('{');
671
672 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
673 node->start()->visit(*this);
674 current_builder().append(".."sv);
675 node->end()->visit(*this);
676
677 if (!m_parent_node || m_parent_node->kind() != AST::Node::Kind::Slice)
678 current_builder().append('}');
679 visited(node);
680}
681
682void Formatter::visit(const AST::ReadRedirection* node)
683{
684 will_visit(node);
685 test_and_update_output_cursor(node);
686 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
687
688 if (node->fd() != 0)
689 current_builder().appendff(" {}<", node->fd());
690 else
691 current_builder().append(" <"sv);
692 NodeVisitor::visit(node);
693 visited(node);
694}
695
696void Formatter::visit(const AST::ReadWriteRedirection* node)
697{
698 will_visit(node);
699 test_and_update_output_cursor(node);
700 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
701
702 if (node->fd() != 0)
703 current_builder().appendff(" {}<>", node->fd());
704 else
705 current_builder().append(" <>"sv);
706 NodeVisitor::visit(node);
707 visited(node);
708}
709
710void Formatter::visit(const AST::Sequence* node)
711{
712 will_visit(node);
713 test_and_update_output_cursor(node);
714
715 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
716
717 bool first = true;
718 for (auto& entry : node->entries()) {
719 if (first)
720 first = false;
721 else
722 insert_separator();
723
724 entry->visit(*this);
725 }
726
727 visited(node);
728}
729
730void Formatter::visit(const AST::Subshell* node)
731{
732 will_visit(node);
733 test_and_update_output_cursor(node);
734 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
735
736 in_new_block([&] {
737 NodeVisitor::visit(node);
738 });
739 visited(node);
740}
741
742void Formatter::visit(const AST::Slice* node)
743{
744 will_visit(node);
745 test_and_update_output_cursor(node);
746 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
747
748 current_builder().append('[');
749 node->selector()->visit(*this);
750 current_builder().append(']');
751
752 visited(node);
753}
754
755void Formatter::visit(const AST::SimpleVariable* node)
756{
757 will_visit(node);
758 test_and_update_output_cursor(node);
759 current_builder().append('$');
760 current_builder().append(node->name());
761 if (const AST::Node* slice = node->slice())
762 slice->visit(*this);
763 visited(node);
764}
765
766void Formatter::visit(const AST::SpecialVariable* node)
767{
768 will_visit(node);
769 test_and_update_output_cursor(node);
770 current_builder().append('$');
771 current_builder().append(node->name());
772 if (const AST::Node* slice = node->slice())
773 slice->visit(*this);
774 visited(node);
775}
776
777void Formatter::visit(const AST::Juxtaposition* node)
778{
779 will_visit(node);
780 test_and_update_output_cursor(node);
781 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
782 NodeVisitor::visit(node);
783 visited(node);
784}
785
786void Formatter::visit(const AST::StringLiteral* node)
787{
788 will_visit(node);
789 test_and_update_output_cursor(node);
790 if (!m_options.in_double_quotes && !m_options.in_heredoc)
791 current_builder().append("'"sv);
792
793 if (m_options.in_double_quotes && !m_options.in_heredoc) {
794 for (auto ch : node->text().bytes_as_string_view()) {
795 switch (ch) {
796 case '"':
797 case '\\':
798 case '$':
799 current_builder().append('\\');
800 break;
801 case '\n':
802 current_builder().append("\\n"sv);
803 continue;
804 case '\r':
805 current_builder().append("\\r"sv);
806 continue;
807 case '\t':
808 current_builder().append("\\t"sv);
809 continue;
810 case '\v':
811 current_builder().append("\\v"sv);
812 continue;
813 case '\f':
814 current_builder().append("\\f"sv);
815 continue;
816 case '\a':
817 current_builder().append("\\a"sv);
818 continue;
819 case '\e':
820 current_builder().append("\\e"sv);
821 continue;
822 default:
823 break;
824 }
825 current_builder().append(ch);
826 }
827 } else {
828 current_builder().append(node->text());
829 }
830
831 if (!m_options.in_double_quotes && !m_options.in_heredoc)
832 current_builder().append("'"sv);
833 visited(node);
834}
835
836void Formatter::visit(const AST::StringPartCompose* node)
837{
838 will_visit(node);
839 test_and_update_output_cursor(node);
840 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
841 NodeVisitor::visit(node);
842 visited(node);
843}
844
845void Formatter::visit(const AST::SyntaxError* node)
846{
847 will_visit(node);
848 test_and_update_output_cursor(node);
849 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
850 NodeVisitor::visit(node);
851 visited(node);
852}
853
854void Formatter::visit(const AST::Tilde* node)
855{
856 will_visit(node);
857 test_and_update_output_cursor(node);
858 current_builder().append(node->text());
859 visited(node);
860}
861
862void Formatter::visit(const AST::VariableDeclarations* node)
863{
864 will_visit(node);
865 test_and_update_output_cursor(node);
866 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
867
868 auto first = true;
869 for (auto& entry : node->variables()) {
870 if (!first)
871 current_builder().append(' ');
872 first = false;
873 entry.name->visit(*this);
874 current_builder().append('=');
875
876 if (entry.value->is_command())
877 current_builder().append('(');
878
879 entry.value->visit(*this);
880
881 if (entry.value->is_command())
882 current_builder().append(')');
883 }
884 visited(node);
885}
886
887void Formatter::visit(const AST::WriteAppendRedirection* node)
888{
889 will_visit(node);
890 test_and_update_output_cursor(node);
891 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
892
893 if (node->fd() != 1)
894 current_builder().appendff(" {}>>", node->fd());
895 else
896 current_builder().append(" >>"sv);
897 NodeVisitor::visit(node);
898 visited(node);
899}
900
901void Formatter::visit(const AST::WriteRedirection* node)
902{
903 will_visit(node);
904 test_and_update_output_cursor(node);
905 TemporaryChange<const AST::Node*> parent { m_parent_node, node };
906
907 if (node->fd() != 1)
908 current_builder().appendff(" {}>", node->fd());
909 else
910 current_builder().append(" >"sv);
911 NodeVisitor::visit(node);
912 visited(node);
913}
914
915}