mirror of
				https://github.com/RGBCube/serenity
				synced 2025-10-31 16:22:43 +00:00 
			
		
		
		
	Shell: Add support for 'immediate' expressions as variable substitutions
This commit adds a few basic variable substitution operations:
- length
    Find the length of a string or a list
- length_across
    Find the lengths of things inside a list
- remove_{suffix,prefix}
    Remove a suffix or a prefix from all the passed values
- regex_replace
    Replace all matches of a given regex with a given template
- split
    Split the given string with the given delimiter (or to its
    code points if the delimiter is empty)
- concat_lists
    concatenates any given lists into one
Closes #4316 (the ancient version of this same feature)
			
			
This commit is contained in:
		
							parent
							
								
									a303b69caa
								
							
						
					
					
						commit
						a45b2ea6fb
					
				
					 16 changed files with 911 additions and 37 deletions
				
			
		
							
								
								
									
										431
									
								
								Userland/Shell/ImmediateFunctions.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										431
									
								
								Userland/Shell/ImmediateFunctions.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,431 @@ | |||
| /*
 | ||||
|  * Copyright (c) 2021, The SerenityOS developers. | ||||
|  * All rights reserved. | ||||
|  * | ||||
|  * Redistribution and use in source and binary forms, with or without | ||||
|  * modification, are permitted provided that the following conditions are met: | ||||
|  * | ||||
|  * 1. Redistributions of source code must retain the above copyright notice, this | ||||
|  *    list of conditions and the following disclaimer. | ||||
|  * | ||||
|  * 2. Redistributions in binary form must reproduce the above copyright notice, | ||||
|  *    this list of conditions and the following disclaimer in the documentation | ||||
|  *    and/or other materials provided with the distribution. | ||||
|  * | ||||
|  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||
|  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||
|  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||
|  * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||||
|  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||||
|  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||||
|  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||||
|  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||||
|  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||||
|  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||
|  */ | ||||
| 
 | ||||
| #include "Formatter.h" | ||||
| #include "Shell.h" | ||||
| #include <LibRegex/Regex.h> | ||||
| 
 | ||||
| namespace Shell { | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_length_impl(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments, bool across) | ||||
| { | ||||
|     auto name = across ? "length_across" : "length"; | ||||
|     if (arguments.size() < 1 || arguments.size() > 2) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, String::formatted("Expected one or two arguments to `{}'", name), invoking_node.position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     enum { | ||||
|         Infer, | ||||
|         String, | ||||
|         List, | ||||
|     } mode { Infer }; | ||||
| 
 | ||||
|     bool is_inferred = false; | ||||
| 
 | ||||
|     const AST::Node* expr_node; | ||||
|     if (arguments.size() == 2) { | ||||
|         // length string <expr>
 | ||||
|         // length list <expr>
 | ||||
| 
 | ||||
|         auto& mode_arg = arguments.first(); | ||||
|         if (!mode_arg.is_bareword()) { | ||||
|             raise_error(ShellError::EvaluatedSyntaxError, String::formatted("Expected a bareword (either 'string' or 'list') in the two-argument form of the `{}' immediate", name), mode_arg.position()); | ||||
|             return nullptr; | ||||
|         } | ||||
| 
 | ||||
|         const auto& mode_name = static_cast<const AST::BarewordLiteral&>(mode_arg).text(); | ||||
|         if (mode_name == "list") { | ||||
|             mode = List; | ||||
|         } else if (mode_name == "string") { | ||||
|             mode = String; | ||||
|         } else if (mode_name == "infer") { | ||||
|             mode = Infer; | ||||
|         } else { | ||||
|             raise_error(ShellError::EvaluatedSyntaxError, String::formatted("Expected either 'string' or 'list' (and not {}) in the two-argument form of the `{}' immediate", mode_name, name), mode_arg.position()); | ||||
|             return nullptr; | ||||
|         } | ||||
| 
 | ||||
|         expr_node = &arguments[1]; | ||||
|     } else { | ||||
|         expr_node = &arguments[0]; | ||||
|     } | ||||
| 
 | ||||
|     if (mode == Infer) { | ||||
|         is_inferred = true; | ||||
|         if (expr_node->is_list()) | ||||
|             mode = List; | ||||
|         else if (expr_node->is_simple_variable()) // "Look inside" variables
 | ||||
|             mode = const_cast<AST::Node*>(expr_node)->run(this)->resolve_without_cast(this)->is_list_without_resolution() ? List : String; | ||||
|         else if (is<AST::ImmediateExpression>(expr_node)) | ||||
|             mode = List; | ||||
|         else | ||||
|             mode = String; | ||||
|     } | ||||
| 
 | ||||
|     auto value_with_number = [&](auto number) -> NonnullRefPtr<AST::Node> { | ||||
|         return AST::create<AST::BarewordLiteral>(invoking_node.position(), String::number(number)); | ||||
|     }; | ||||
| 
 | ||||
|     auto do_across = [&](StringView mode_name, auto& values) { | ||||
|         if (is_inferred) | ||||
|             mode_name = "infer"; | ||||
|         // Translate to a list of applications of `length <mode_name>`
 | ||||
|         Vector<NonnullRefPtr<AST::Node>> resulting_nodes; | ||||
|         resulting_nodes.ensure_capacity(values.size()); | ||||
|         for (auto& entry : values) { | ||||
|             // ImmediateExpression(length <mode_name> <entry>)
 | ||||
|             resulting_nodes.unchecked_append(AST::create<AST::ImmediateExpression>( | ||||
|                 expr_node->position(), | ||||
|                 AST::NameWithPosition { "length", invoking_node.function_position() }, | ||||
|                 NonnullRefPtrVector<AST::Node> { Vector<NonnullRefPtr<AST::Node>> { | ||||
|                     static_cast<NonnullRefPtr<AST::Node>>(AST::create<AST::BarewordLiteral>(expr_node->position(), mode_name)), | ||||
|                     AST::create<AST::SyntheticNode>(expr_node->position(), NonnullRefPtr<AST::Value>(entry)), | ||||
|                 } }, | ||||
|                 expr_node->position())); | ||||
|         } | ||||
| 
 | ||||
|         return AST::create<AST::ListConcatenate>(invoking_node.position(), move(resulting_nodes)); | ||||
|     }; | ||||
| 
 | ||||
|     switch (mode) { | ||||
|     default: | ||||
|     case Infer: | ||||
|         VERIFY_NOT_REACHED(); | ||||
|     case List: { | ||||
|         auto value = (const_cast<AST::Node*>(expr_node))->run(this); | ||||
|         if (!value) | ||||
|             return value_with_number(0); | ||||
| 
 | ||||
|         value = value->resolve_without_cast(this); | ||||
| 
 | ||||
|         if (auto list = dynamic_cast<AST::ListValue*>(value.ptr())) { | ||||
|             if (across) | ||||
|                 return do_across("list", list->values()); | ||||
| 
 | ||||
|             return value_with_number(list->values().size()); | ||||
|         } | ||||
| 
 | ||||
|         auto list = value->resolve_as_list(this); | ||||
|         if (!across) | ||||
|             return value_with_number(list.size()); | ||||
| 
 | ||||
|         dbgln("List has {} entries", list.size()); | ||||
|         auto values = AST::create<AST::ListValue>(move(list)); | ||||
|         return do_across("list", values->values()); | ||||
|     } | ||||
|     case String: { | ||||
|         // 'across' will only accept lists, and '!across' will only accept non-lists here.
 | ||||
|         if (expr_node->is_list()) { | ||||
|             if (!across) { | ||||
|             raise_no_list_allowed:; | ||||
|                 Formatter formatter { *expr_node }; | ||||
| 
 | ||||
|                 if (is_inferred) { | ||||
|                     raise_error(ShellError::EvaluatedSyntaxError, | ||||
|                         String::formatted("Could not infer expression type, please explicitly use `{0} string' or `{0} list'", name), | ||||
|                         invoking_node.position()); | ||||
|                     return nullptr; | ||||
|                 } | ||||
| 
 | ||||
|                 auto source = formatter.format(); | ||||
|                 raise_error(ShellError::EvaluatedSyntaxError, | ||||
|                     source.is_empty() | ||||
|                         ? "Invalid application of `length' to a list" | ||||
|                         : String::formatted("Invalid application of `length' to a list\nperhaps you meant `{1}length \"{0}\"{2}' or `{1}length_across {0}{2}'?", source, "\x1b[32m", "\x1b[0m"), | ||||
|                     expr_node->position()); | ||||
|                 return nullptr; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         auto value = (const_cast<AST::Node*>(expr_node))->run(this); | ||||
|         if (!value) | ||||
|             return value_with_number(0); | ||||
| 
 | ||||
|         value = value->resolve_without_cast(*this); | ||||
| 
 | ||||
|         if (auto list = dynamic_cast<AST::ListValue*>(value.ptr())) { | ||||
|             if (!across) | ||||
|                 goto raise_no_list_allowed; | ||||
| 
 | ||||
|             return do_across("string", list->values()); | ||||
|         } | ||||
| 
 | ||||
|         if (across && !value->is_list()) { | ||||
|             Formatter formatter { *expr_node }; | ||||
| 
 | ||||
|             auto source = formatter.format(); | ||||
|             raise_error(ShellError::EvaluatedSyntaxError, | ||||
|                 String::formatted("Invalid application of `length_across' to a non-list\nperhaps you meant `{1}length {0}{2}'?", source, "\x1b[32m", "\x1b[0m"), | ||||
|                 expr_node->position()); | ||||
|             return nullptr; | ||||
|         } | ||||
| 
 | ||||
|         // Evaluate the nodes and substitute with the lengths.
 | ||||
|         auto list = value->resolve_as_list(this); | ||||
| 
 | ||||
|         if (!expr_node->is_list()) { | ||||
|             if (list.size() == 1) { | ||||
|                 if (across) | ||||
|                     goto raise_no_list_allowed; | ||||
| 
 | ||||
|                 // This is the normal case, the expression is a normal non-list expression.
 | ||||
|                 return value_with_number(list.first().length()); | ||||
|             } | ||||
| 
 | ||||
|             // This can be hit by asking for the length of a command list (e.g. `(>/dev/null)`)
 | ||||
|             // raise an error about misuse of command lists for now.
 | ||||
|             // FIXME: What's the length of `(>/dev/null)` supposed to be?
 | ||||
|             raise_error(ShellError::EvaluatedSyntaxError, "Length of meta value (or command list) requested, this is currently not supported.", expr_node->position()); | ||||
|             return nullptr; | ||||
|         } | ||||
| 
 | ||||
|         auto values = AST::create<AST::ListValue>(move(list)); | ||||
|         return do_across("string", values->values()); | ||||
|     } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_length(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
|     return immediate_length_impl(invoking_node, arguments, false); | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_length_across(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
|     return immediate_length_impl(invoking_node, arguments, true); | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_regex_replace(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
|     if (arguments.size() != 3) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 3 arguments to regex_replace", invoking_node.position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     auto pattern = const_cast<AST::Node&>(arguments[0]).run(this); | ||||
|     auto replacement = const_cast<AST::Node&>(arguments[1]).run(this); | ||||
|     auto value = const_cast<AST::Node&>(arguments[2]).run(this)->resolve_without_cast(this); | ||||
| 
 | ||||
|     if (!pattern->is_string()) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected the regex_replace pattern to be a string", arguments[0].position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     if (!replacement->is_string()) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected the regex_replace replacement string to be a string", arguments[1].position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     if (!value->is_string()) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected the regex_replace target value to be a string", arguments[2].position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     Regex<PosixExtendedParser> re { pattern->resolve_as_list(this).first() }; | ||||
|     auto result = re.replace(value->resolve_as_list(this)[0], replacement->resolve_as_list(this)[0], PosixFlags::Global | PosixFlags::Multiline | PosixFlags::Unicode); | ||||
| 
 | ||||
|     return AST::create<AST::StringLiteral>(invoking_node.position(), move(result)); | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_remove_suffix(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
|     if (arguments.size() != 2) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to remove_suffix", invoking_node.position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     auto suffix = const_cast<AST::Node&>(arguments[0]).run(this); | ||||
|     auto value = const_cast<AST::Node&>(arguments[1]).run(this)->resolve_without_cast(this); | ||||
| 
 | ||||
|     if (!suffix->is_string()) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected the remove_suffix suffix string to be a string", arguments[0].position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     auto suffix_str = suffix->resolve_as_list(this)[0]; | ||||
|     auto values = value->resolve_as_list(this); | ||||
| 
 | ||||
|     Vector<NonnullRefPtr<AST::Node>> nodes; | ||||
| 
 | ||||
|     for (auto& value_str : values) { | ||||
|         StringView removed { value_str }; | ||||
| 
 | ||||
|         if (value_str.ends_with(suffix_str)) | ||||
|             removed = removed.substring_view(0, value_str.length() - suffix_str.length()); | ||||
|         nodes.append(AST::create<AST::StringLiteral>(invoking_node.position(), removed)); | ||||
|     } | ||||
| 
 | ||||
|     return AST::create<AST::ListConcatenate>(invoking_node.position(), move(nodes)); | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_remove_prefix(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
|     if (arguments.size() != 2) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to remove_prefix", invoking_node.position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     auto prefix = const_cast<AST::Node&>(arguments[0]).run(this); | ||||
|     auto value = const_cast<AST::Node&>(arguments[1]).run(this)->resolve_without_cast(this); | ||||
| 
 | ||||
|     if (!prefix->is_string()) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected the remove_prefix prefix string to be a string", arguments[0].position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     auto prefix_str = prefix->resolve_as_list(this)[0]; | ||||
|     auto values = value->resolve_as_list(this); | ||||
| 
 | ||||
|     Vector<NonnullRefPtr<AST::Node>> nodes; | ||||
| 
 | ||||
|     for (auto& value_str : values) { | ||||
|         StringView removed { value_str }; | ||||
| 
 | ||||
|         if (value_str.starts_with(prefix_str)) | ||||
|             removed = removed.substring_view(prefix_str.length()); | ||||
|         nodes.append(AST::create<AST::StringLiteral>(invoking_node.position(), removed)); | ||||
|     } | ||||
| 
 | ||||
|     return AST::create<AST::ListConcatenate>(invoking_node.position(), move(nodes)); | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_split(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
|     if (arguments.size() != 2) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected exactly 2 arguments to split", invoking_node.position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     auto delimiter = const_cast<AST::Node&>(arguments[0]).run(this); | ||||
|     auto value = const_cast<AST::Node&>(arguments[1]).run(this)->resolve_without_cast(this); | ||||
| 
 | ||||
|     if (!delimiter->is_string()) { | ||||
|         raise_error(ShellError::EvaluatedSyntaxError, "Expected the split delimiter string to be a string", arguments[0].position()); | ||||
|         return nullptr; | ||||
|     } | ||||
| 
 | ||||
|     auto delimiter_str = delimiter->resolve_as_list(this)[0]; | ||||
| 
 | ||||
|     auto transform = [&](const auto& values) { | ||||
|         // Translate to a list of applications of `split <delimiter>`
 | ||||
|         Vector<NonnullRefPtr<AST::Node>> resulting_nodes; | ||||
|         resulting_nodes.ensure_capacity(values.size()); | ||||
|         for (auto& entry : values) { | ||||
|             // ImmediateExpression(split <delimiter> <entry>)
 | ||||
|             resulting_nodes.unchecked_append(AST::create<AST::ImmediateExpression>( | ||||
|                 arguments[1].position(), | ||||
|                 invoking_node.function(), | ||||
|                 NonnullRefPtrVector<AST::Node> { Vector<NonnullRefPtr<AST::Node>> { | ||||
|                     arguments[0], | ||||
|                     AST::create<AST::SyntheticNode>(arguments[1].position(), NonnullRefPtr<AST::Value>(entry)), | ||||
|                 } }, | ||||
|                 arguments[1].position())); | ||||
|         } | ||||
| 
 | ||||
|         return AST::create<AST::ListConcatenate>(invoking_node.position(), move(resulting_nodes)); | ||||
|     }; | ||||
| 
 | ||||
|     if (auto list = dynamic_cast<AST::ListValue*>(value.ptr())) { | ||||
|         return transform(list->values()); | ||||
|     } | ||||
| 
 | ||||
|     // Otherwise, just resolve to a list and transform that.
 | ||||
|     auto list = value->resolve_as_list(this); | ||||
|     if (!value->is_list()) { | ||||
|         if (list.is_empty()) | ||||
|             return AST::create<AST::ListConcatenate>(invoking_node.position(), NonnullRefPtrVector<AST::Node> {}); | ||||
| 
 | ||||
|         auto& value = list.first(); | ||||
|         Vector<String> split_strings; | ||||
|         if (delimiter_str.is_empty()) { | ||||
|             StringBuilder builder; | ||||
|             for (auto code_point : Utf8View { value }) { | ||||
|                 builder.append_code_point(code_point); | ||||
|                 split_strings.append(builder.build()); | ||||
|                 builder.clear(); | ||||
|             } | ||||
|         } else { | ||||
|             auto split = StringView { value }.split_view(delimiter_str, options.inline_exec_keep_empty_segments); | ||||
|             split_strings.ensure_capacity(split.size()); | ||||
|             for (auto& entry : split) | ||||
|                 split_strings.append(entry); | ||||
|         } | ||||
|         return AST::create<AST::SyntheticNode>(invoking_node.position(), AST::create<AST::ListValue>(move(split_strings))); | ||||
|     } | ||||
| 
 | ||||
|     return transform(AST::create<AST::ListValue>(list)->values()); | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::immediate_concat_lists(AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
|     NonnullRefPtrVector<AST::Node> result; | ||||
| 
 | ||||
|     for (auto& argument : arguments) { | ||||
|         if (auto* list = dynamic_cast<const AST::ListConcatenate*>(&argument)) { | ||||
|             result.append(list->list()); | ||||
|         } else { | ||||
|             auto list_of_values = const_cast<AST::Node&>(argument).run(this)->resolve_without_cast(this); | ||||
|             if (auto* list = dynamic_cast<AST::ListValue*>(list_of_values.ptr())) { | ||||
|                 for (auto& entry : static_cast<Vector<NonnullRefPtr<AST::Value>>&>(list->values())) | ||||
|                     result.append(AST::create<AST::SyntheticNode>(argument.position(), entry)); | ||||
|             } else { | ||||
|                 auto values = list_of_values->resolve_as_list(this); | ||||
|                 for (auto& entry : values) | ||||
|                     result.append(AST::create<AST::StringLiteral>(argument.position(), entry)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return AST::create<AST::ListConcatenate>(invoking_node.position(), move(result)); | ||||
| } | ||||
| 
 | ||||
| RefPtr<AST::Node> Shell::run_immediate_function(StringView str, AST::ImmediateExpression& invoking_node, const NonnullRefPtrVector<AST::Node>& arguments) | ||||
| { | ||||
| #define __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(name) \ | ||||
|     if (str == #name)                              \ | ||||
|         return immediate_##name(invoking_node, arguments); | ||||
| 
 | ||||
|     ENUMERATE_SHELL_IMMEDIATE_FUNCTIONS() | ||||
| 
 | ||||
| #undef __ENUMERATE_SHELL_IMMEDIATE_FUNCTION | ||||
|     raise_error(ShellError::EvaluatedSyntaxError, String::formatted("Unknown immediate function {}", str), invoking_node.position()); | ||||
|     return nullptr; | ||||
| } | ||||
| 
 | ||||
| bool Shell::has_immediate_function(const StringView& str) | ||||
| { | ||||
| #define __ENUMERATE_SHELL_IMMEDIATE_FUNCTION(name) \ | ||||
|     if (str == #name)                              \ | ||||
|         return true; | ||||
| 
 | ||||
|     ENUMERATE_SHELL_IMMEDIATE_FUNCTIONS() | ||||
| 
 | ||||
| #undef __ENUMERATE_SHELL_IMMEDIATE_FUNCTION | ||||
| 
 | ||||
|     return false; | ||||
| } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 AnotherTest
						AnotherTest