mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 16:02:45 +00:00 
			
		
		
		
	Shell: Allow parts of globs to be named in match expressions
This patchset allows a match expression to have a list of names for its
glob parts, which are assigned to the matched values in the body of the
match.
For example,
```sh
stuff=foobarblahblah/target_{1..30}
for $stuff {
    match $it {
        */* as (dir sub) {
            echo "doing things with $sub in $dir"
            make -C $dir $sub # or whatever...
        }
    }
}
```
With this, match expressions are now significantly more powerful!
			
			
This commit is contained in:
		
							parent
							
								
									0801b1fada
								
							
						
					
					
						commit
						1a4ac3531f
					
				
					 6 changed files with 115 additions and 10 deletions
				
			
		|  | @ -1520,7 +1520,23 @@ void MatchExpr::dump(int level) const | |||
|     print_indented(String::format("(named: %s)", m_expr_name.characters()), level + 1); | ||||
|     print_indented("(entries)", level + 1); | ||||
|     for (auto& entry : m_entries) { | ||||
|         print_indented("(match)", level + 2); | ||||
|         StringBuilder builder; | ||||
|         builder.append("(match"); | ||||
|         if (entry.match_names.has_value()) { | ||||
|             builder.append(" to names ("); | ||||
|             bool first = true; | ||||
|             for (auto& name : entry.match_names.value()) { | ||||
|                 if (!first) | ||||
|                     builder.append(' '); | ||||
|                 first = false; | ||||
|                 builder.append(name); | ||||
|             } | ||||
|             builder.append("))"); | ||||
| 
 | ||||
|         } else { | ||||
|             builder.append(')'); | ||||
|         } | ||||
|         print_indented(builder.string_view(), level + 2); | ||||
|         for (auto& node : entry.options) | ||||
|             node.dump(level + 3); | ||||
|         print_indented("(execute)", level + 2); | ||||
|  | @ -1536,13 +1552,16 @@ RefPtr<Value> MatchExpr::run(RefPtr<Shell> shell) | |||
|     auto value = m_matched_expr->run(shell)->resolve_without_cast(shell); | ||||
|     auto list = value->resolve_as_list(shell); | ||||
| 
 | ||||
|     auto list_matches = [&](auto&& pattern) { | ||||
|     auto list_matches = [&](auto&& pattern, auto& spans) { | ||||
|         if (pattern.size() != list.size()) | ||||
|             return false; | ||||
| 
 | ||||
|         for (size_t i = 0; i < pattern.size(); ++i) { | ||||
|             if (!list[i].matches(pattern[i])) | ||||
|             Vector<AK::MaskSpan> mask_spans; | ||||
|             if (!list[i].matches(pattern[i], mask_spans)) | ||||
|                 return false; | ||||
|             for (auto& span : mask_spans) | ||||
|                 spans.append(list[i].substring(span.start, span.length)); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|  | @ -1554,7 +1573,7 @@ RefPtr<Value> MatchExpr::run(RefPtr<Shell> shell) | |||
|             pattern.append(static_cast<const Glob*>(&option)->text()); | ||||
|         } else if (option.is_bareword()) { | ||||
|             pattern.append(static_cast<const BarewordLiteral*>(&option)->text()); | ||||
|         } else if (option.is_list()) { | ||||
|         } else { | ||||
|             auto list = option.run(shell); | ||||
|             option.for_each_entry(shell, [&](auto&& value) { | ||||
|                 pattern.append(value->resolve_as_list(nullptr)); // Note: 'nullptr' incurs special behaviour,
 | ||||
|  | @ -1572,11 +1591,21 @@ RefPtr<Value> MatchExpr::run(RefPtr<Shell> shell) | |||
| 
 | ||||
|     for (auto& entry : m_entries) { | ||||
|         for (auto& option : entry.options) { | ||||
|             if (list_matches(resolve_pattern(option))) { | ||||
|                 if (entry.body) | ||||
|             Vector<String> spans; | ||||
|             if (list_matches(resolve_pattern(option), spans)) { | ||||
|                 if (entry.body) { | ||||
|                     if (entry.match_names.has_value()) { | ||||
|                         size_t i = 0; | ||||
|                         for (auto& name : entry.match_names.value()) { | ||||
|                             if (spans.size() > i) | ||||
|                                 shell->set_local_variable(name, create<AST::StringValue>(spans[i])); | ||||
|                             ++i; | ||||
|                         } | ||||
|                     } | ||||
|                     return entry.body->run(shell); | ||||
|                 else | ||||
|                 } else { | ||||
|                     return create<AST::ListValue>({}); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | @ -1606,6 +1635,9 @@ void MatchExpr::highlight_in_editor(Line::Editor& editor, Shell& shell, Highligh | |||
| 
 | ||||
|         for (auto& position : entry.pipe_positions) | ||||
|             editor.stylize({ position.start_offset, position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); | ||||
| 
 | ||||
|         if (entry.match_as_position.has_value()) | ||||
|             editor.stylize({ entry.match_as_position.value().start_offset, entry.match_as_position.value().end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow) }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -912,6 +912,8 @@ private: | |||
| 
 | ||||
| struct MatchEntry { | ||||
|     NonnullRefPtrVector<Node> options; | ||||
|     Optional<Vector<String>> match_names; | ||||
|     Optional<Position> match_as_position; | ||||
|     Vector<Position> pipe_positions; | ||||
|     RefPtr<Node> body; | ||||
| }; | ||||
|  |  | |||
|  | @ -452,6 +452,17 @@ void Formatter::visit(const AST::MatchExpr* node) | |||
|             } | ||||
| 
 | ||||
|             current_builder().append(' '); | ||||
|             if (entry.match_names.has_value() && !entry.match_names.value().is_empty()) { | ||||
|                 current_builder().append("as ("); | ||||
|                 auto first = true; | ||||
|                 for (auto& name : entry.match_names.value()) { | ||||
|                     if (!first) | ||||
|                         current_builder().append(' '); | ||||
|                     first = false; | ||||
|                     current_builder().append(name); | ||||
|                 } | ||||
|                 current_builder().append(") "); | ||||
|             } | ||||
|             in_new_block([&] { | ||||
|                 if (entry.body) | ||||
|                     entry.body->visit(*this); | ||||
|  |  | |||
|  | @ -752,10 +752,12 @@ AST::MatchEntry Parser::parse_match_entry() | |||
| 
 | ||||
|     NonnullRefPtrVector<AST::Node> patterns; | ||||
|     Vector<AST::Position> pipe_positions; | ||||
|     Optional<Vector<String>> match_names; | ||||
|     Optional<AST::Position> match_as_position; | ||||
| 
 | ||||
|     auto pattern = parse_match_pattern(); | ||||
|     if (!pattern) | ||||
|         return { {}, {}, create<AST::SyntaxError>("Expected a pattern in 'match' body") }; | ||||
|         return { {}, {}, {}, {}, create<AST::SyntaxError>("Expected a pattern in 'match' body") }; | ||||
| 
 | ||||
|     patterns.append(pattern.release_nonnull()); | ||||
| 
 | ||||
|  | @ -782,6 +784,32 @@ AST::MatchEntry Parser::parse_match_entry() | |||
| 
 | ||||
|     consume_while(is_any_of(" \t\n")); | ||||
| 
 | ||||
|     auto as_start_position = m_offset; | ||||
|     auto as_start_line = line(); | ||||
|     if (expect("as")) { | ||||
|         match_as_position = AST::Position { as_start_position, m_offset, as_start_line, line() }; | ||||
|         consume_while(is_any_of(" \t\n")); | ||||
|         if (!expect('(')) { | ||||
|             if (!error) | ||||
|                 error = create<AST::SyntaxError>("Expected an explicit list of identifiers after a pattern 'as'"); | ||||
|         } else { | ||||
|             match_names = Vector<String>(); | ||||
|             for (;;) { | ||||
|                 consume_while(is_whitespace); | ||||
|                 auto name = consume_while(is_word_character); | ||||
|                 if (name.is_empty()) | ||||
|                     break; | ||||
|                 match_names.value().append(move(name)); | ||||
|             } | ||||
| 
 | ||||
|             if (!expect(')')) { | ||||
|                 if (!error) | ||||
|                     error = create<AST::SyntaxError>("Expected a close paren ')' to end the identifier list of pattern 'as'"); | ||||
|             } | ||||
|         } | ||||
|         consume_while(is_any_of(" \t\n")); | ||||
|     } | ||||
| 
 | ||||
|     if (!expect('{')) { | ||||
|         if (!error) | ||||
|             error = create<AST::SyntaxError>("Expected an open brace '{' to start a match entry body"); | ||||
|  | @ -799,7 +827,7 @@ AST::MatchEntry Parser::parse_match_entry() | |||
|     else if (error) | ||||
|         body = error; | ||||
| 
 | ||||
|     return { move(patterns), move(pipe_positions), move(body) }; | ||||
|     return { move(patterns), move(match_names), move(match_as_position), move(pipe_positions), move(body) }; | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Parser::parse_match_pattern() | ||||
|  |  | |||
|  | @ -185,7 +185,9 @@ subshell :: '{' toplevel '}' | |||
| 
 | ||||
| match_expr :: 'match' ws+ expression ws* ('as' ws+ identifier)? '{' match_entry* '}' | ||||
| 
 | ||||
| match_entry :: match_pattern ws* '{' toplevel '}' | ||||
| match_entry :: match_pattern ws* (as identifier_list)? '{' toplevel '}' | ||||
| 
 | ||||
| identifier_list :: '(' (identifier ws*)* ')' | ||||
| 
 | ||||
| match_pattern :: expression (ws* '|' ws* expression)* | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,3 +51,33 @@ match "$(echo)" { | |||
| }; | ||||
| 
 | ||||
| test "$result" = yes || echo invalid result $result for string subst match && exit 1 | ||||
| 
 | ||||
| match (foo bar) { | ||||
|     (f? *) as (x y) { | ||||
|         result=fail | ||||
|     } | ||||
|     (f* b*) as (x y) { | ||||
|         if [ "$x" = oo -a "$y" = ar ] { | ||||
|             result=yes | ||||
|         } else { | ||||
|             result=fail | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| test "$result" = yes || echo invalid result $result for subst match with name && exit 1 | ||||
| 
 | ||||
| match (foo bar baz) { | ||||
|     (f? * *z) as (x y z) { | ||||
|         result=fail | ||||
|     } | ||||
|     (f* b* *z) as (x y z) { | ||||
|         if [ "$x" = oo -a "$y" = ar -a "$z" = ba ] { | ||||
|             result=yes | ||||
|         } else { | ||||
|             result=fail | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| test "$result" = yes || echo invalid result $result for subst match with name 2 && exit 1 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 AnotherTest
						AnotherTest