From 56457d7110c85df4a535858d4e2eb32936babb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Thu, 26 Jan 2023 14:16:08 +0100 Subject: [PATCH 01/20] new parser --- .../gdshell/scripts/gdshell_command_parser.gd | 594 ++++++++---------- 1 file changed, 269 insertions(+), 325 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd index fb6732b..495ae42 100644 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ b/addons/gdshell/scripts/gdshell_command_parser.gd @@ -3,393 +3,337 @@ class_name GDShellCommandParser extends RefCounted -enum TokenType { - ERROR, - TOKEN_SEQUENCE, - TEXT, - UNTERMINATED_TEXT, - SEPARATOR, - PIPE, - AND, - OR, - NOT, - BACKGROUND, - SEQUENCE, -} - -enum ParserBlockType { - COMMAND, - BACKGROUND, - NOT, - PIPE, - AND, - OR, -} - -enum ParserResultStatus { - OK, - UNTERMINATED, - ERROR, -} - - -# status - ParserResultStatus: -# OK - result is an Array of Dictionaries (ParserBlockTypes) -# ERROR - result is the error token Dictionary -# UNTERMINATED - result is an empty Array - new input is necessary - -static func parse(input: String, command_db: GDShellCommandDB) -> Dictionary: - var tokens: Array[Dictionary] = tokenize(input, command_db) +class ParserResult: + enum Status { + OK, + UNTERMINATED, + ERROR, + } - if tokens.is_empty(): - return {"status": ParserResultStatus.OK, "result": []} + var status: Status + var input: String + var result: Variant + var err_index: int + var err_string: String = "" - if tokens[-1]["type"] == TokenType.UNTERMINATED_TEXT: - return {"status": ParserResultStatus.UNTERMINATED, "result": []} + func _init(_status: Status, _input: String, _result: Array[Token], _err_index: int=-1, _err_string: String=""): + status = _status + input = _input + result = _result + err_index = _err_index + err_string = _err_string + + +class Token: + enum Type { + ERROR, + SPACE, + WORD, + WORD_UNTERMINATED, + OPERATOR_PIPE, + OPERATOR_AND, + OPERATOR_OR, + OPERATOR_NOT, + OPERATOR_BACKGROUND, + OPERATOR_SEQUENCE, + OPERATOR_EXPAND, + OPERATOR_OPENING_PARENTHESIS, + OPERATOR_CLOSING_PARENTHESIS, + } - var command_sequence: Array[Dictionary] = [] - var command_construction_temp: Array[String] = [] + var type: Type + var content: String + var start_char_index: int + var consumed: int - for i in tokens.size(): - match tokens[i]["type"]: - TokenType.ERROR: - return {"status": ParserResultStatus.ERROR, "result": tokens[i]} - - TokenType.TEXT: - command_construction_temp.push_back(tokens[i]["content"]) - - TokenType.PIPE: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.PIPE}) - - TokenType.OR: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.OR}) - - TokenType.AND: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.AND}) - - TokenType.NOT: - var operator_validation: Dictionary = _validate_operator(tokens, i, false, true) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - command_sequence.push_back({"type": ParserBlockType.NOT}) - - TokenType.BACKGROUND: - var operator_validation: Dictionary = _validate_operator(tokens, i, true, false) - if operator_validation["status"] == ParserResultStatus.ERROR: - return operator_validation - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back({"type": ParserBlockType.BACKGROUND}) - command_sequence.push_back(command["data"]) - - TokenType.SEQUENCE: - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - - command_construction_temp = [] - command_sequence.push_back(command["data"]) - - _: - return { - "status": ParserResultStatus.ERROR, - "result": { - "error": { - "char": tokens[i]["start_char"], - "error": "Unknown token", - } - } - } + func _init(_type: Type, _content: String, _start_char_index: int, _consumed: int): + type = _type + content = _content + start_char_index = _start_char_index + consumed = _consumed + + +static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: + var tokens: Array[Token] = tokenize(input) + if tokens.is_empty(): + return ParserResult.new( + ParserResult.Status.OK, + input, + [], + ) + elif tokens[-1].type == Token.Type.ERROR: + return ParserResult.new( + ParserResult.Status.ERROR, + input, + tokens, + tokens[-1].start_char_index, + tokens[-1].content, + ) + elif tokens[-1].type == Token.Type.WORD_UNTERMINATED: + return ParserResult.new( + ParserResult.Status.UNTERMINATED, + input, + tokens, + tokens[-1].start_char_index, # This points at the start of the token - opening quote + tokens[-1].content, + ) + + # Filters out Token.Type.SPACE tokens because their absence simplifies next processes + tokens = tokens.filter(func(t: Token): return t.type != Token.Type.SPACE) - # Empty the `command_construction_temp` - var command: Dictionary = _construct_command(command_construction_temp, command_db) - if command["status"] == ParserResultStatus.ERROR: - return command - command_sequence.push_back(command["data"]) +# tokens = _expand_aliases(tokens, command_db) - command_sequence = command_sequence.filter(func(x: Dictionary): return not x.is_empty()) + # Validate all the operators + var err_operator_index: int = validate_operators(tokens) + if err_operator_index != -1: + printerr("Bad operator on index: ", err_operator_index) + return ParserResult.new( + ParserResult.Status.ERROR, + input, + tokens, + tokens[err_operator_index].start_char_index, + "bad operator usage", + ) + printerr("operators OK") + return ParserResult.new(ParserResult.Status.OK, input, tokens) + + +static func _expand_aliases(tokens: Array[Token], command_db: GDShellCommandDB, forbidden_aliases: Array[String]=[]) -> Array[Token]: + for i in tokens.size(): + if tokens[i].type != Token.Type.WORD: + # try to expand the next one + pass - return {"status": ParserResultStatus.OK, "result": command_sequence} + return [] -static func _construct_command(from: Array[String], command_db: GDShellCommandDB) -> Dictionary: - if from.size() == 0: - # Empty commands will be filtered out - return { - "status": ParserResultStatus.OK, - "data": {}, - } +static func validate_operators(tokens: Array[Token]) -> int: + var parenthesis_level: int = 0 + var open_parenthesis_index: int = -1 - var command_path: String = command_db.get_command_path(from[0]) - if command_path.is_empty(): - return { - "status": ParserResultStatus.ERROR, - "result": { - "error": { - "char": 0, - "error": "Unknown command: %s" % from[0]} - }, - } + for i in tokens.size(): + if tokens[i].type == Token.Type.WORD: + continue + if not _is_operator_valid(tokens, i): + return i + + # validate parenthesis pairs + if tokens[i].type == Token.Type.OPERATOR_OPENING_PARENTHESIS: + if parenthesis_level == 0: + open_parenthesis_index = i # tracks the parenthesis that would be closed last + parenthesis_level += 1 + + elif tokens[i].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS: + parenthesis_level -= 1 + if parenthesis_level < 0: + return i # more closing parenthesis than opening ones - return { - "status": ParserResultStatus.OK, - "data": { - "type": ParserBlockType.COMMAND, - "data": { - "command": command_path, - "params": { - "argv": from.duplicate(true), - "data": null, - } - } - } - } + return -1 if parenthesis_level == 0 else open_parenthesis_index -# `from` - token array ; `at` - index of the operator -# `lect`, `right` - determines if the oprator mush have left or RIGHT operands -static func _validate_operator(from: Array[Dictionary], at: int, left: bool, right: bool) -> Dictionary: - if left: - if not at > 0: - return { - "status": ParserResultStatus.ERROR, - "result": { - "error": { - "char": from[at]["start_char"], - "error": "Missing operand" - } - } - } - if from[at-1]["type"] != TokenType.TEXT: - if from[at-1]["type"] == TokenType.BACKGROUND and from[at]["type"] == TokenType.BACKGROUND: - return { - "status": ParserResultStatus.ERROR, - "result": { - "error": { - "char": from[at]["start_char"], - "error": "Missing operand" - } - } - } +static func _is_operator_valid(tokens: Array[Token], operator_token_index: int) -> bool: + if operator_token_index < 0 or operator_token_index >= tokens.size(): + push_error("[GDShell] 'operator_token_index' (index: %s) is out of range of 'tokens' (size: %s) - Handled as an invalid operator" % [operator_token_index, tokens.size()]) + return false - if right: - if not at < from.size()-1: - return { - "status": ParserResultStatus.ERROR, - "result": { - "error": { - "char": from[at]["start_char"] + from[at]["consumed"], - "error": "Missing operand" - } - } - } - if from[at+1]["type"] != TokenType.TEXT: - if from[at+1]["type"] != TokenType.NOT or (from[at+1]["type"] == TokenType.NOT and from[at]["type"] == TokenType.NOT): - return { - "status": ParserResultStatus.ERROR, - "result": { - "error": { - "char": from[at]["start_char"] + from[at]["consumed"], - "error": "Missing operand" - } - } - } - return {"status": ParserResultStatus.OK} - - -static func tokenize(input: String, command_db: GDShellCommandDB) -> Array[Dictionary]: - var tokens: Array[Dictionary] = [] + match tokens[operator_token_index].type: + # binary operators (operand operator operand) + Token.Type.OPERATOR_PIPE, Token.Type.OPERATOR_AND, Token.Type.OPERATOR_OR: + if operator_token_index-1 < 0 or operator_token_index+1 >= tokens.size(): + return false + # check the left operand + if (not (tokens[operator_token_index-1].type == Token.Type.WORD + or tokens[operator_token_index-1].type == Token.Type.OPERATOR_BACKGROUND)): + return false + # check the right operand + if (not (tokens[operator_token_index+1].type == Token.Type.WORD + or tokens[operator_token_index+1].type == Token.Type.OPERATOR_NOT)): + return false + + # left operators (operator operand) + Token.Type.OPERATOR_NOT, Token.Type.OPERATOR_EXPAND: + if operator_token_index+1 >= tokens.size(): + return false + # check the right operand + if tokens[operator_token_index+1].type != Token.Type.WORD: + return false + + # right operators (operand operator) + Token.Type.OPERATOR_BACKGROUND, Token.Type.OPERATOR_SEQUENCE: + if operator_token_index-1 < 0: + return false + # check the left operand + if tokens[operator_token_index-1].type != Token.Type.WORD: + return false + + # standalone operators + Token.Type.OPERATOR_OPENING_PARENTHESIS, Token.Type.OPERATOR_CLOSING_PARENTHESIS: + return true + + _: + push_error("[GDHell] Non-operator token: 'Token.Type.%s'" % str(Token.Type.find_key(tokens[operator_token_index].type))) + return false + + return true + + +static func tokenize(input: String) -> Array[Token]: + var tokens: Array[Token] = [] var current: int = 0 while current < input.length(): match input[current]: " ": - tokens.push_back(_tokenize_separator(input, current)) - "|": - tokens.push_back(_tokenize_pipe_or(input, current)) - "&": - tokens.push_back(_tokenize_background_and(input, current)) + tokens.push_back(_tokenize_space(input, current)) ";": - tokens.push_back(_tokenize_sequence(input, current)) - "\"", "'": - tokens.push_back(_tokenize_quoted_text(input, current)) + tokens.push_back(_tokenize_semicolon(input, current)) + "&": + tokens.push_back(_tokenize_and(input, current)) + "|": + tokens.push_back(_tokenize_pipe(input, current)) "!": - tokens.push_back(_tokenize_not(input, current)) + tokens.push_back(_tokenize_exclamation(input, current)) + "$": + tokens.push_back(_tokenize_dollar_sign(input, current)) + "(", ")": + tokens.push_back(_tokenize_parenthesis(input, current)) + "\"", "\'": + tokens.push_back(_tokenize_quoted_text(input, current)) _: tokens.push_back(_tokenize_text(input, current)) - if tokens[-1]["type"] == TokenType.ERROR: - return [tokens[-1]] + if tokens[-1].type == Token.Type.ERROR: + return tokens - current += tokens[-1]["consumed"] + current += tokens[-1].consumed - tokens = _remove_separator_tokens(tokens) - return _unalias_tokens(tokens, command_db) + return _merge_word_tokens(tokens) -static func _token(type: TokenType, start_char: int, consumed: int, content: String="", error: Dictionary={}) -> Dictionary: - return { - "type": type, - "start_char": start_char, - "consumed": consumed, - "content": content, - "error": error, - } +static func _tokenize_space(_input: String, current: int) -> Token: + return Token.new(Token.Type.SPACE, " ", current, 1) + + +static func _tokenize_semicolon(_input: String, current: int) -> Token: + return Token.new(Token.Type.OPERATOR_SEQUENCE, ";", current, 1) + + +static func _tokenize_and(input: String, current: int) -> Token: + if current < input.length()-1 and input[current+1] == "&": + return Token.new(Token.Type.OPERATOR_AND, "&&", current, 2) + return Token.new(Token.Type.OPERATOR_BACKGROUND, "&", current, 1) + + +static func _tokenize_pipe(input: String, current: int) -> Token: + if current < input.length()-1 and input[current+1] == "|": + return Token.new(Token.Type.OPERATOR_OR, "||", current, 2) + return Token.new(Token.Type.OPERATOR_PIPE, "|", current, 1) + +static func _tokenize_exclamation(_input: String, current: int) -> Token: + return Token.new(Token.Type.OPERATOR_NOT, "!", current, 1) -static func _tokenize_quoted_text(input: String, current: int) -> Dictionary: + +static func _tokenize_dollar_sign(_input: String, current: int) -> Token: + return Token.new(Token.Type.OPERATOR_EXPAND, "$", current, 1) + + +static func _tokenize_parenthesis(input: String, current: int) -> Token: + if input[current] == "(": + return Token.new(Token.Type.OPERATOR_OPENING_PARENTHESIS, "(", current, 1) + else: + return Token.new(Token.Type.OPERATOR_CLOSING_PARENTHESIS, ")", current, 1) + + +static func _tokenize_quoted_text(input: String, current: int) -> Token: var start_char = current var content: String = "" var quote_type: String = input[current] - current += 1 + current += 1 # skip the opening quote while true: if current >= input.length(): - return _token( - TokenType.UNTERMINATED_TEXT, + return Token.new( + Token.Type.WORD_UNTERMINATED, + "unterminated token", start_char, - content.length() + 1, # accounts for the starting quote - content.c_unescape() + content.length() + 1 # accounts for the starting quote ) if input[current] == quote_type: if input[max(0, current-1)] != "\\": - return _token( - TokenType.TEXT, + return Token.new( + Token.Type.WORD, + content.c_unescape(), start_char, - content.length() + 2, # accounts for the starting and ending quotes - content.c_unescape() + content.length() + 2 # accounts for the starting and ending quotes ) content += input[current] current += 1 - return {} + return null -static func _tokenize_text(input: String, current: int) -> Dictionary: +static func _tokenize_text(input: String, current: int) -> Token: var start_char = current var content: String = "" while true: - if current == input.length() or input[current] in [" ", "&"]: - return _token( - TokenType.TEXT, + if current == input.length() or input[current] in [" ", ";", "&", "\"", "\'", ")"]: + return Token.new( + Token.Type.WORD, + content.c_unescape(), start_char, - content.length(), - content.c_unescape() - ) + content.length() + ) - if input[current] in [";", "|", "\"", "'", "!"]: - return _token( - TokenType.ERROR, + if input[current] in ["|", "!", "$", "("]: + return Token.new( + Token.Type.ERROR, + "unexpected token", start_char, - content.length() + 1, - content, - {"char": current, "error": "Unexpected token"} + content.length() + 1 ) content += input[current] current += 1 - return {} - - -static func _tokenize_separator(_input: String, current: int) -> Dictionary: - return _token(TokenType.SEPARATOR, current, 1, " ") + return null -static func _tokenize_pipe_or(input: String, current: int) -> Dictionary: - if current < input.length()-1 and input[current+1] == "|": - return _token(TokenType.OR, current, 2, "||") - return _token(TokenType.PIPE, current, 1, "|") - - -static func _tokenize_background_and(input: String, current: int) -> Dictionary: - if current < input.length()-1 and input[current+1] == "&": - return _token(TokenType.AND, current, 2, "&&") - return _token(TokenType.BACKGROUND, current, 1, "&") - - -static func _tokenize_not(_input: String, current: int) -> Dictionary: - return _token(TokenType.NOT, current, 1, "!") - - -static func _tokenize_sequence(_input: String, current: int) -> Dictionary: - return _token(TokenType.SEQUENCE, current, 1, ";") - - -static func _unalias_tokens(tokens: Array[Dictionary], command_db: GDShellCommandDB) -> Array[Dictionary]: - if tokens.size() == 0: - return tokens - if tokens[-1]["type"] == TokenType.UNTERMINATED_TEXT: - return tokens - - tokens = _remove_separator_tokens(tokens) - - # Replace aliasable token by a token sequence representing the alias - for i in tokens.size(): - if tokens[i]["type"] != TokenType.TEXT: - continue - if i > 0 and (tokens[i-1]["type"] == TokenType.TEXT - or tokens[i-1]["type"] == TokenType.TOKEN_SEQUENCE): - continue - # Alias is found for the aliasable token - if tokens[i]["content"] in command_db._aliases.keys(): - tokens[i] = { - "type": TokenType.TOKEN_SEQUENCE, - "content": tokenize(command_db._aliases[tokens[i]["content"]], command_db) - } - - # Insert token sequences as tokens into the `tokens` array - while tokens.any(func(x): return x["type"] == TokenType.TOKEN_SEQUENCE): - for i in tokens.size(): - if tokens[i]["type"] == TokenType.TOKEN_SEQUENCE: - var token_sequence: Dictionary = tokens.pop_at(i) - @warning_ignore(unsafe_method_access) - for ii in token_sequence["content"].size(): - tokens.insert(i+ii, token_sequence["content"][ii]) - break # Break because the indexing has changed because of the inserting +# Merges WORD tokens if they are not separated by any other token +static func _merge_word_tokens(tokens: Array[Token]) -> Array[Token]: + var merged_tokens: Array[Token] = [] + var last_token_type: Token.Type = Token.Type.ERROR + var word_temp: Token = Token.new(Token.Type.WORD, "", 0, -1) + var current: int = 0 + while current < tokens.size(): + if tokens[current].type == Token.Type.WORD: + if last_token_type == Token.Type.WORD: # Merge words + # Append to word_temp + word_temp.content += tokens[current].content + word_temp.consumed += tokens[current].consumed + current += 1 + elif last_token_type != Token.Type.WORD: # Prepare for possible future merge + # Setup a new word_temp + word_temp.start_char_index = tokens[current].start_char_index + word_temp.content = tokens[current].content + word_temp.consumed = tokens[current].consumed + current += 1 + last_token_type = Token.Type.WORD + else: + if word_temp.consumed != -1: # If the word_temp is not empty + # Append and then clear word_temp + merged_tokens.append(word_temp) + word_temp = Token.new(Token.Type.WORD, "", 0, -1) + # Append the current non-word token + merged_tokens.append(tokens[current]) + last_token_type = tokens[current].type + current += 1 + # Append the word_temp if it is not empty + if word_temp.consumed != -1: + merged_tokens.append(word_temp) - return tokens - - -static func _remove_separator_tokens(tokens: Array[Dictionary]) -> Array[Dictionary]: - return tokens.filter(func(x): return x["type"] != TokenType.SEPARATOR) + return merged_tokens From f5e76bba3de1768bac76fc2b05fb4b513fa5c383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Thu, 30 Mar 2023 12:54:59 +0200 Subject: [PATCH 02/20] Simplify and demystify the parser a bit --- .../gdshell/scripts/gdshell_command_parser.gd | 229 ++++++++---------- 1 file changed, 95 insertions(+), 134 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd index 495ae42..2eb9140 100644 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ b/addons/gdshell/scripts/gdshell_command_parser.gd @@ -12,21 +12,20 @@ class ParserResult: var status: Status var input: String - var result: Variant - var err_index: int + var result: Array[Token] + var err_token_index: int var err_string: String = "" - func _init(_status: Status, _input: String, _result: Array[Token], _err_index: int=-1, _err_string: String=""): + func _init(_status: Status, _input: String, _result: Array[Token], _err_token_index: int = -1, _err_string: String = ""): status = _status input = _input result = _result - err_index = _err_index + err_token_index = _err_token_index err_string = _err_string class Token: enum Type { - ERROR, SPACE, WORD, WORD_UNTERMINATED, @@ -59,15 +58,7 @@ static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: return ParserResult.new( ParserResult.Status.OK, input, - [], - ) - elif tokens[-1].type == Token.Type.ERROR: - return ParserResult.new( - ParserResult.Status.ERROR, - input, tokens, - tokens[-1].start_char_index, - tokens[-1].content, ) elif tokens[-1].type == Token.Type.WORD_UNTERMINATED: return ParserResult.new( @@ -75,7 +66,8 @@ static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: input, tokens, tokens[-1].start_char_index, # This points at the start of the token - opening quote - tokens[-1].content, + "The input is not terminated with corrent quote so another appended input is required. + See GDShell Docs for help", ) # Filters out Token.Type.SPACE tokens because their absence simplifies next processes @@ -99,7 +91,7 @@ static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: return ParserResult.new(ParserResult.Status.OK, input, tokens) -static func _expand_aliases(tokens: Array[Token], command_db: GDShellCommandDB, forbidden_aliases: Array[String]=[]) -> Array[Token]: +static func _expand_aliases(tokens: Array[Token], command_db: GDShellCommandDB, forbidden_aliases: Array[String] = []) -> Array[Token]: for i in tokens.size(): if tokens[i].type != Token.Type.WORD: # try to expand the next one @@ -133,6 +125,11 @@ static func validate_operators(tokens: Array[Token]) -> int: static func _is_operator_valid(tokens: Array[Token], operator_token_index: int) -> bool: + + # TODO : make sure no word is outside parenthesis + + + if operator_token_index < 0 or operator_token_index >= tokens.size(): push_error("[GDShell] 'operator_token_index' (index: %s) is out of range of 'tokens' (size: %s) - Handled as an invalid operator" % [operator_token_index, tokens.size()]) return false @@ -144,11 +141,14 @@ static func _is_operator_valid(tokens: Array[Token], operator_token_index: int) return false # check the left operand if (not (tokens[operator_token_index-1].type == Token.Type.WORD - or tokens[operator_token_index-1].type == Token.Type.OPERATOR_BACKGROUND)): + or tokens[operator_token_index-1].type == Token.Type.OPERATOR_BACKGROUND + or tokens[operator_token_index-1].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS) + ): return false # check the right operand if (not (tokens[operator_token_index+1].type == Token.Type.WORD - or tokens[operator_token_index+1].type == Token.Type.OPERATOR_NOT)): + or tokens[operator_token_index+1].type == Token.Type.OPERATOR_NOT + or tokens[operator_token_index+1].type == Token.Type.OPERATOR_OPENING_PARENTHESIS)): return false # left operators (operator operand) @@ -180,160 +180,121 @@ static func _is_operator_valid(tokens: Array[Token], operator_token_index: int) static func tokenize(input: String) -> Array[Token]: var tokens: Array[Token] = [] - var current: int = 0 + var current_char: int = 0 - while current < input.length(): - match input[current]: + while current_char < input.length(): + match input[current_char]: " ": - tokens.push_back(_tokenize_space(input, current)) + tokens.push_back(_tokenize_space(input, current_char)) ";": - tokens.push_back(_tokenize_semicolon(input, current)) + tokens.push_back(_tokenize_semicolon(input, current_char)) "&": - tokens.push_back(_tokenize_and(input, current)) + tokens.push_back(_tokenize_and(input, current_char)) "|": - tokens.push_back(_tokenize_pipe(input, current)) + tokens.push_back(_tokenize_pipe(input, current_char)) "!": - tokens.push_back(_tokenize_exclamation(input, current)) + tokens.push_back(_tokenize_exclamation(input, current_char)) "$": - tokens.push_back(_tokenize_dollar_sign(input, current)) + tokens.push_back(_tokenize_dollar_sign(input, current_char)) "(", ")": - tokens.push_back(_tokenize_parenthesis(input, current)) + tokens.push_back(_tokenize_parenthesis(input, current_char)) "\"", "\'": - tokens.push_back(_tokenize_quoted_text(input, current)) + tokens.push_back(_tokenize_quote(input, current_char)) _: - tokens.push_back(_tokenize_text(input, current)) + tokens.push_back(_tokenize_text(input, current_char)) - if tokens[-1].type == Token.Type.ERROR: - return tokens - - current += tokens[-1].consumed + current_char += tokens[-1].consumed - return _merge_word_tokens(tokens) + return merge_word_tokens(tokens) -static func _tokenize_space(_input: String, current: int) -> Token: - return Token.new(Token.Type.SPACE, " ", current, 1) +static func _tokenize_space(_input: String, current_char: int) -> Token: + return Token.new(Token.Type.SPACE, " ", current_char, 1) -static func _tokenize_semicolon(_input: String, current: int) -> Token: - return Token.new(Token.Type.OPERATOR_SEQUENCE, ";", current, 1) +static func _tokenize_semicolon(_input: String, current_char: int) -> Token: + return Token.new(Token.Type.OPERATOR_SEQUENCE, ";", current_char, 1) -static func _tokenize_and(input: String, current: int) -> Token: - if current < input.length()-1 and input[current+1] == "&": - return Token.new(Token.Type.OPERATOR_AND, "&&", current, 2) - return Token.new(Token.Type.OPERATOR_BACKGROUND, "&", current, 1) +static func _tokenize_and(input: String, current_char: int) -> Token: + if current_char < input.length()-1 and input[current_char+1] == "&": + return Token.new(Token.Type.OPERATOR_AND, "&&", current_char, 2) + return Token.new(Token.Type.OPERATOR_BACKGROUND, "&", current_char, 1) -static func _tokenize_pipe(input: String, current: int) -> Token: - if current < input.length()-1 and input[current+1] == "|": - return Token.new(Token.Type.OPERATOR_OR, "||", current, 2) - return Token.new(Token.Type.OPERATOR_PIPE, "|", current, 1) +static func _tokenize_pipe(input: String, current_char: int) -> Token: + if current_char < input.length()-1 and input[current_char+1] == "|": + return Token.new(Token.Type.OPERATOR_OR, "||", current_char, 2) + return Token.new(Token.Type.OPERATOR_PIPE, "|", current_char, 1) -static func _tokenize_exclamation(_input: String, current: int) -> Token: - return Token.new(Token.Type.OPERATOR_NOT, "!", current, 1) +static func _tokenize_exclamation(_input: String, current_char: int) -> Token: + return Token.new(Token.Type.OPERATOR_NOT, "!", current_char, 1) -static func _tokenize_dollar_sign(_input: String, current: int) -> Token: - return Token.new(Token.Type.OPERATOR_EXPAND, "$", current, 1) +static func _tokenize_dollar_sign(_input: String, current_char: int) -> Token: + return Token.new(Token.Type.OPERATOR_EXPAND, "$", current_char, 1) -static func _tokenize_parenthesis(input: String, current: int) -> Token: - if input[current] == "(": - return Token.new(Token.Type.OPERATOR_OPENING_PARENTHESIS, "(", current, 1) +static func _tokenize_parenthesis(input: String, current_char: int) -> Token: + if input[current_char] == "(": + return Token.new(Token.Type.OPERATOR_OPENING_PARENTHESIS, "(", current_char, 1) else: - return Token.new(Token.Type.OPERATOR_CLOSING_PARENTHESIS, ")", current, 1) + return Token.new(Token.Type.OPERATOR_CLOSING_PARENTHESIS, ")", current_char, 1) -static func _tokenize_quoted_text(input: String, current: int) -> Token: - var start_char = current +static func _tokenize_quote(input: String, current_char: int) -> Token: var content: String = "" - var quote_type: String = input[current] - current += 1 # skip the opening quote + var quote_type: String = input[current_char] - while true: - if current >= input.length(): + # Skip the opening quote and start on the char right after + for i in range(current_char + 1, input.length()): + if input[i] == quote_type and input[max(0, i - 1)] != "\\": return Token.new( - Token.Type.WORD_UNTERMINATED, - "unterminated token", - start_char, - content.length() + 1 # accounts for the starting quote - ) - - if input[current] == quote_type: - if input[max(0, current-1)] != "\\": - return Token.new( - Token.Type.WORD, - content.c_unescape(), - start_char, - content.length() + 2 # accounts for the starting and ending quotes - ) - - content += input[current] - current += 1 - - return null + Token.Type.WORD, + content.c_unescape(), + current_char, + content.length() + 2 # accounts for the starting and ending quotes + ) + content += input[i] + # End of input was reached without finding a closing quote + return Token.new( + Token.Type.WORD_UNTERMINATED, + content.c_unescape(), + current_char, + content.length() + 1 # accounts for the starting quote + ) -static func _tokenize_text(input: String, current: int) -> Token: - var start_char = current +static func _tokenize_text(input: String, current_char: int) -> Token: var content: String = "" - while true: - if current == input.length() or input[current] in [" ", ";", "&", "\"", "\'", ")"]: - return Token.new( - Token.Type.WORD, - content.c_unescape(), - start_char, - content.length() - ) - - if input[current] in ["|", "!", "$", "("]: - return Token.new( - Token.Type.ERROR, - "unexpected token", - start_char, - content.length() + 1 - ) - - content += input[current] - current += 1 + for i in range(current_char, input.length()): + if input[i] in [" ", ";", "&", "|", "!", "$", "(", ")", "\"", "\'"]: + break + content += input[i] + + return Token.new( + Token.Type.WORD, + content.c_unescape(), + current_char, + content.length() + ) + + +## Merges WORD tokens if they are not separated by any other token +static func merge_word_tokens(tokens: Array[Token]) -> Array[Token]: + if tokens.is_empty(): + return tokens + # We now know that tokens is not empty so we append the first token for later simplification + var merged_tokens: Array[Token] = [tokens[0]] - return null - - -# Merges WORD tokens if they are not separated by any other token -static func _merge_word_tokens(tokens: Array[Token]) -> Array[Token]: - var merged_tokens: Array[Token] = [] - var last_token_type: Token.Type = Token.Type.ERROR - var word_temp: Token = Token.new(Token.Type.WORD, "", 0, -1) - var current: int = 0 - while current < tokens.size(): - if tokens[current].type == Token.Type.WORD: - if last_token_type == Token.Type.WORD: # Merge words - # Append to word_temp - word_temp.content += tokens[current].content - word_temp.consumed += tokens[current].consumed - current += 1 - elif last_token_type != Token.Type.WORD: # Prepare for possible future merge - # Setup a new word_temp - word_temp.start_char_index = tokens[current].start_char_index - word_temp.content = tokens[current].content - word_temp.consumed = tokens[current].consumed - current += 1 - last_token_type = Token.Type.WORD + # Start from the second token as we already appended the first + for i in range(1, tokens.size()): + if tokens[i].type == Token.Type.WORD and merged_tokens[-1].type == Token.Type.WORD: + merged_tokens[-1].content += tokens[i].content + merged_tokens[-1].consumed += tokens[i].consumed else: - if word_temp.consumed != -1: # If the word_temp is not empty - # Append and then clear word_temp - merged_tokens.append(word_temp) - word_temp = Token.new(Token.Type.WORD, "", 0, -1) - # Append the current non-word token - merged_tokens.append(tokens[current]) - last_token_type = tokens[current].type - current += 1 - # Append the word_temp if it is not empty - if word_temp.consumed != -1: - merged_tokens.append(word_temp) + merged_tokens.append(tokens[i]) return merged_tokens From b07f1470690bbc9851563f0d4f3aad82a8630f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Sun, 20 Aug 2023 22:58:41 +0200 Subject: [PATCH 03/20] Some work on the new parser idk --- .../gdshell/scripts/gdshell_command_parser.gd | 352 ++++++++++++------ .../gdshell/scripts/gdshell_command_runner.gd | 255 ++++++++----- addons/gdshell/scripts/gdshell_main.gd | 279 +++++++------- project.godot | 9 +- 4 files changed, 537 insertions(+), 358 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd index 2eb9140..b1af307 100644 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ b/addons/gdshell/scripts/gdshell_command_parser.gd @@ -12,18 +12,32 @@ class ParserResult: var status: Status var input: String - var result: Array[Token] + var ast: ASTNode + var tokens: Array[Token] + var rpn_tokens: Array[Token] var err_token_index: int var err_string: String = "" - func _init(_status: Status, _input: String, _result: Array[Token], _err_token_index: int = -1, _err_string: String = ""): + func _init(_status: Status, _input: String, _ast: ASTNode, _tokens: Array[Token], _err_token_index: int = -1, _err_string: String = ""): status = _status input = _input - result = _result + ast = _ast + tokens = _tokens err_token_index = _err_token_index err_string = _err_string +class ASTNode: + var token: Token + var left: ASTNode + var right: ASTNode + + func _init(_token: Token, _left: ASTNode = null, _right: ASTNode = null): + token = _token + left = _left + right = _right + + class Token: enum Type { SPACE, @@ -54,128 +68,254 @@ class Token: static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: var tokens: Array[Token] = tokenize(input) - if tokens.is_empty(): +# if tokens.is_empty(): +# return ParserResult.new( +# ParserResult.Status.OK, +# input, +# tokens, +# ) + if tokens[-1].type == Token.Type.WORD_UNTERMINATED: return ParserResult.new( - ParserResult.Status.OK, - input, - tokens, - ) - elif tokens[-1].type == Token.Type.WORD_UNTERMINATED: - return ParserResult.new( - ParserResult.Status.UNTERMINATED, - input, - tokens, - tokens[-1].start_char_index, # This points at the start of the token - opening quote - "The input is not terminated with corrent quote so another appended input is required. - See GDShell Docs for help", + ParserResult.Status.UNTERMINATED, + input, + ASTNode.new(null), + tokens, + tokens.size() - 1, + "The input is not terminated with corrent quote so another appended input is required. See GDShell Docs for help", ) - # Filters out Token.Type.SPACE tokens because their absence simplifies next processes - tokens = tokens.filter(func(t: Token): return t.type != Token.Type.SPACE) + # Expand aliases and OPERATOR_EXPAND tokens + tokens = expand(tokens, command_db) -# tokens = _expand_aliases(tokens, command_db) - # Validate all the operators - var err_operator_index: int = validate_operators(tokens) - if err_operator_index != -1: - printerr("Bad operator on index: ", err_operator_index) - return ParserResult.new( - ParserResult.Status.ERROR, - input, - tokens, - tokens[err_operator_index].start_char_index, - "bad operator usage", - ) - printerr("operators OK") + var ast: ASTNode = create_ast(tokens) + + # Validates logic of the expression +# var err: Dictionary = validate_expression(tokens) +# if err.err_token_index != -1: +# return ParserResult.new( +# ParserResult.Status.ERROR, +# input, +# tokens, +# err.err_token_index, +# err.err_string, +# ) - return ParserResult.new(ParserResult.Status.OK, input, tokens) + return ParserResult.new(ParserResult.Status.OK, input, ast, tokens) -static func _expand_aliases(tokens: Array[Token], command_db: GDShellCommandDB, forbidden_aliases: Array[String] = []) -> Array[Token]: - for i in tokens.size(): - if tokens[i].type != Token.Type.WORD: - # try to expand the next one - pass - - return [] +static func expand(tokens: Array[Token], command_db: GDShellCommandDB) -> Array[Token]: + return tokens -static func validate_operators(tokens: Array[Token]) -> int: + +static func create_ast(tokens: Array[Token]) -> ASTNode: + var ast_root: ASTNode + var token_stack: Array = [] + + for i in tokens.size(): + match tokens[i].type: + Token.Type.SPACE: + break + Token.Type.WORD: + pass +# command_construction.append(tokens[i].content) + Token.Type.OPERATOR_OPENING_PARENTHESIS: + pass + Token.Type.OPERATOR_CLOSING_PARENTHESIS: + pass + Token.Type.OPERATOR_PIPE: + pass + Token.Type.OPERATOR_AND: + pass + Token.Type.OPERATOR_OR: + pass + Token.Type.OPERATOR_NOT: + pass + Token.Type.OPERATOR_BACKGROUND: + pass + Token.Type.OPERATOR_SEQUENCE: + pass + Token.Type.OPERATOR_EXPAND: + pass + + + + + return ast_root + + + +static func _is_operator(token: Token) -> bool: + return token.type in [ + Token.Type.OPERATOR_AND, + Token.Type.OPERATOR_BACKGROUND, + Token.Type.OPERATOR_OR, + Token.Type.OPERATOR_PIPE, + Token.Type.OPERATOR_NOT, + Token.Type.OPERATOR_SEQUENCE, + ] + + + +# !(false || echo a) && echo b + +# man idk& && ((echo "success"); echo ":)") || (echo "failed"; echo ":(") ; !true& && echo hello +# (true --idk&) && ((echo "success"); (echo ":)")) || ((echo "failed"); (echo ":(")) ; (!true&) && (echo hello) + + +## Returns the index and error message for the first invalid operator +#static func validate_expression(tokens: Array[Token]) -> Dictionary: +# var err: Dictionary = { +# "err_token_index": -1, +# "err_string": "" +# } +# +# err = _validate_parentheses(tokens) +# if err.err_token_index != -1: +# return err +# +# for i in tokens.size(): +# match tokens[i].type: +# Token.Type.OPERATOR_PIPE: +# err = _validate_pipe(tokens, i) +# Token.Type.OPERATOR_AND: +# err = _validate_and(tokens, i) +# Token.Type.OPERATOR_OR: +# err = _validate_or(tokens, i) +# Token.Type.OPERATOR_NOT: +# err = _validate_not(tokens, i) +# Token.Type.OPERATOR_BACKGROUND: +# err = _validate_background(tokens, i) +# Token.Type.OPERATOR_SEQUENCE: +# err = _validate_sequence(tokens, i) +# Token.Type.OPERATOR_EXPAND: +# pass +# _: # SPACE, WORD, WORD_UNTERMINATED, OPERATOR_OPENING_PARENTHESIS, OPERATOR_CLOSING_PARENTHESIS +# pass +# +# if err.err_token_index != -1: +# return err +# +# return err + + + +static func _validate_parentheses(tokens: Array[Token]) -> Dictionary: var parenthesis_level: int = 0 - var open_parenthesis_index: int = -1 + var firt_opening_parenthesis_index: int = -1 for i in tokens.size(): - if tokens[i].type == Token.Type.WORD: - continue - if not _is_operator_valid(tokens, i): - return i - - # validate parenthesis pairs if tokens[i].type == Token.Type.OPERATOR_OPENING_PARENTHESIS: - if parenthesis_level == 0: - open_parenthesis_index = i # tracks the parenthesis that would be closed last parenthesis_level += 1 - + if firt_opening_parenthesis_index == -1: + firt_opening_parenthesis_index = i elif tokens[i].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS: parenthesis_level -= 1 - if parenthesis_level < 0: - return i # more closing parenthesis than opening ones - - return -1 if parenthesis_level == 0 else open_parenthesis_index - - -static func _is_operator_valid(tokens: Array[Token], operator_token_index: int) -> bool: - - # TODO : make sure no word is outside parenthesis - - - - if operator_token_index < 0 or operator_token_index >= tokens.size(): - push_error("[GDShell] 'operator_token_index' (index: %s) is out of range of 'tokens' (size: %s) - Handled as an invalid operator" % [operator_token_index, tokens.size()]) - return false + if parenthesis_level < 0: # Found closing parenthesis with no opening one + return { + "err_token_index": i, + "err_string": "Closing \")\" token on index [%d] does not have an opening counterpart." % i, + } - match tokens[operator_token_index].type: - # binary operators (operand operator operand) - Token.Type.OPERATOR_PIPE, Token.Type.OPERATOR_AND, Token.Type.OPERATOR_OR: - if operator_token_index-1 < 0 or operator_token_index+1 >= tokens.size(): - return false - # check the left operand - if (not (tokens[operator_token_index-1].type == Token.Type.WORD - or tokens[operator_token_index-1].type == Token.Type.OPERATOR_BACKGROUND - or tokens[operator_token_index-1].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS) - ): - return false - # check the right operand - if (not (tokens[operator_token_index+1].type == Token.Type.WORD - or tokens[operator_token_index+1].type == Token.Type.OPERATOR_NOT - or tokens[operator_token_index+1].type == Token.Type.OPERATOR_OPENING_PARENTHESIS)): - return false - - # left operators (operator operand) - Token.Type.OPERATOR_NOT, Token.Type.OPERATOR_EXPAND: - if operator_token_index+1 >= tokens.size(): - return false - # check the right operand - if tokens[operator_token_index+1].type != Token.Type.WORD: - return false - - # right operators (operand operator) - Token.Type.OPERATOR_BACKGROUND, Token.Type.OPERATOR_SEQUENCE: - if operator_token_index-1 < 0: - return false - # check the left operand - if tokens[operator_token_index-1].type != Token.Type.WORD: - return false - - # standalone operators - Token.Type.OPERATOR_OPENING_PARENTHESIS, Token.Type.OPERATOR_CLOSING_PARENTHESIS: - return true - - _: - push_error("[GDHell] Non-operator token: 'Token.Type.%s'" % str(Token.Type.find_key(tokens[operator_token_index].type))) - return false + if parenthesis_level != 0: # Did not find a closing parenthesis for all opening parentheses + return { + "err_token_index": firt_opening_parenthesis_index, + "err_string": "Opening \"(\" token on index [%d] does not have a closing counterpart." % firt_opening_parenthesis_index, + } - return true + return { + "err_token_index": -1, + "err_string": "" + } + + + +#static func _expand_aliases(tokens: Array[Token], command_db: GDShellCommandDB, forbidden_aliases: Array[String] = []) -> Array[Token]: +# for i in tokens.size(): +# if tokens[i].type != Token.Type.WORD: +# # try to expand the next one +# pass +# +# return [] +# +# +#static func validate_operators(tokens: Array[Token]) -> int: +# var parenthesis_level: int = 0 +# var open_parenthesis_index: int = -1 +# +# for i in tokens.size(): +# if tokens[i].type == Token.Type.WORD: +# continue +# if not _is_operator_valid(tokens, i): +# return i +# +# # validate parenthesis pairs +# if tokens[i].type == Token.Type.OPERATOR_OPENING_PARENTHESIS: +# if parenthesis_level == 0: +# open_parenthesis_index = i # tracks the parenthesis that would be closed last +# parenthesis_level += 1 +# +# elif tokens[i].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS: +# parenthesis_level -= 1 +# if parenthesis_level < 0: +# return i # more closing parenthesis than opening ones +# +# return -1 if parenthesis_level == 0 else open_parenthesis_index +# +# +#static func _is_operator_valid(tokens: Array[Token], operator_token_index: int) -> bool: +# +# # TODO : make sure no word is outside parenthesis +# +# +# +# if operator_token_index < 0 or operator_token_index >= tokens.size(): +# push_error("[GDShell] 'operator_token_index' (index: %s) is out of range of 'tokens' (size: %s) - Handled as an invalid operator" % [operator_token_index, tokens.size()]) +# return false +# +# match tokens[operator_token_index].type: +# # binary operators (operand operator operand) +# Token.Type.OPERATOR_PIPE, Token.Type.OPERATOR_AND, Token.Type.OPERATOR_OR: +# if operator_token_index-1 < 0 or operator_token_index+1 >= tokens.size(): +# return false +# # check the left operand +# if (not (tokens[operator_token_index-1].type == Token.Type.WORD +# or tokens[operator_token_index-1].type == Token.Type.OPERATOR_BACKGROUND +# or tokens[operator_token_index-1].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS) +# ): +# return false +# # check the right operand +# if (not (tokens[operator_token_index+1].type == Token.Type.WORD +# or tokens[operator_token_index+1].type == Token.Type.OPERATOR_NOT +# or tokens[operator_token_index+1].type == Token.Type.OPERATOR_OPENING_PARENTHESIS)): +# return false +# +# # left operators (operator operand) +# Token.Type.OPERATOR_NOT, Token.Type.OPERATOR_EXPAND: +# if operator_token_index+1 >= tokens.size(): +# return false +# # check the right operand +# if tokens[operator_token_index+1].type != Token.Type.WORD: +# return false +# +# # right operators (operand operator) +# Token.Type.OPERATOR_BACKGROUND, Token.Type.OPERATOR_SEQUENCE: +# if operator_token_index-1 < 0: +# return false +# # check the left operand +# if tokens[operator_token_index-1].type != Token.Type.WORD: +# return false +# +# # standalone operators +# Token.Type.OPERATOR_OPENING_PARENTHESIS, Token.Type.OPERATOR_CLOSING_PARENTHESIS: +# return true +# +# _: +# push_error("[GDHell] Non-operator token: 'Token.Type.%s'" % str(Token.Type.find_key(tokens[operator_token_index].type))) +# return false +# +# return true static func tokenize(input: String) -> Array[Token]: diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd b/addons/gdshell/scripts/gdshell_command_runner.gd index 6ac2259..498fc69 100644 --- a/addons/gdshell/scripts/gdshell_command_runner.gd +++ b/addons/gdshell/scripts/gdshell_command_runner.gd @@ -4,115 +4,193 @@ extends Node # Command execution flags -const F_EXECUTE_CONDITION_MET: int = 1 << 0 -const F_PIPE_PREVIOUS: int = 1 << 1 -const F_BACKGROUND: int = 1 << 2 -const F_NEGATED: int = 1 << 3 +const F_EXECUTE_CONDITION_MET: int = 1 #1 << 0 +const F_PIPE_PREVIOUS: int = 2 #1 << 1 +const F_BACKGROUND: int = 4 #1 << 2 +const F_NEGATED: int = 8 #1 << 3 var _PARENT_GDSHELL: GDShellMain var _background_commands: Array[GDShellCommand] = [] -var _is_running_command: bool = false + +func execute(parser_result: GDShellCommandParser.ParserResult) -> Dictionary: + if parser_result.status != GDShellCommandParser.ParserResult.Status.OK: + push_error("lmao bad input") + return {} + + return _execute_helper(parser_result.result) + -func execute(command_sequence: Dictionary) -> Dictionary: - if command_sequence["status"] != GDShellCommandParser.ParserResultStatus.OK: - return { - "error": 1, - "error_string": 'Cannot execute command sequence. See "data" for the command_sequence', - "data": command_sequence, - } +func _execute_helper(tokens: Array[GDShellCommandParser.Token], piped_data: Variant=null) -> Dictionary: + var last_command_result: Dictionary = {} + var word_accum: Array[GDShellCommandParser.Token] = [] + var current_command_flags: int = F_EXECUTE_CONDITION_MET - _is_running_command = true + var current: int = 0 - var current_token: int = 0 - var current_command_flags: int = F_EXECUTE_CONDITION_MET - var last_command_result: Dictionary = GDShellCommand.DEFAULT_COMMAND_RESULT - @warning_ignore("unsafe_method_access") - while current_token < command_sequence["result"].size(): - match command_sequence["result"][current_token]["type"]: - GDShellCommandParser.ParserBlockType.COMMAND: - var command: String = command_sequence["result"][current_token]["data"]["command"] - var params: Dictionary = command_sequence["result"][current_token]["data"]["params"] - - if not current_command_flags & F_EXECUTE_CONDITION_MET: - current_command_flags = F_EXECUTE_CONDITION_MET - continue - - if current_command_flags & F_PIPE_PREVIOUS: - params["data"] = last_command_result["data"] - - if current_command_flags & F_BACKGROUND: - last_command_result = GDShellCommand.DEFAULT_COMMAND_RESULT - _execute_command(command, params) - else: - last_command_result = await _execute_command(command, params) - - if current_command_flags & F_NEGATED: - last_command_result["error"] = 0 if last_command_result["error"] else 1 - - current_command_flags = F_EXECUTE_CONDITION_MET + var execute_command_helper: Callable = func() -> void: + if not current_command_flags & F_EXECUTE_CONDITION_MET: + current_command_flags = F_EXECUTE_CONDITION_MET + word_accum.clear() + return + + if current_command_flags & F_BACKGROUND: + last_command_result = GDShellCommand.DEFAULT_COMMAND_RESULT + _execute_command( + word_accum, + last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, + true + ) + else: + last_command_result = await _execute_command( + word_accum, + last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, + false + ) + + if current_command_flags & F_NEGATED: + last_command_result["error"] = 0 if last_command_result["error"] else 1 + + current_command_flags = F_EXECUTE_CONDITION_MET + word_accum.clear() + + + var execute_parenthesis_helper: Callable = func() -> int: + # find the closing parenthesis index + var parenthesis_level: int = 1 + while current < tokens.size(): + if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + parenthesis_level += 1 + elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + parenthesis_level -= 1 + if parenthesis_level == 0: + break + current += 1 + + + + return -1 + + + while current < tokens.size(): + match tokens[current]: + GDShellCommandParser.Token.Type.WORD: + word_accum.append(tokens[current]) - GDShellCommandParser.ParserBlockType.BACKGROUND: - current_command_flags |= F_BACKGROUND + GDShellCommandParser.Token.Type.OPERATOR_PIPE: + current_command_flags |= F_PIPE_PREVIOUS + execute_command_helper.call() + + GDShellCommandParser.Token.Type.OPERATOR_AND: + execute_command_helper.call() + if last_command_result["error"] != 0: + current_command_flags &= ~F_EXECUTE_CONDITION_MET - GDShellCommandParser.ParserBlockType.NOT: + GDShellCommandParser.Token.Type.OPERATOR_OR: + execute_command_helper.call() + if last_command_result["error"] == 0: + current_command_flags &= ~F_EXECUTE_CONDITION_MET + + GDShellCommandParser.Token.Type.OPERATOR_NOT: current_command_flags |= F_NEGATED - GDShellCommandParser.ParserBlockType.PIPE: - current_command_flags |= F_PIPE_PREVIOUS + GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: + current_command_flags |= F_BACKGROUND + execute_command_helper.call() - GDShellCommandParser.ParserBlockType.AND: - if last_command_result["error"]: - current_command_flags ^= F_EXECUTE_CONDITION_MET - else: - current_command_flags |= F_EXECUTE_CONDITION_MET + GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: + execute_command_helper.call() - GDShellCommandParser.ParserBlockType.OR: - if last_command_result["error"]: - current_command_flags |= F_EXECUTE_CONDITION_MET - else: - current_command_flags ^= F_EXECUTE_CONDITION_MET + GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + execute_parenthesis_helper.call() + + + +# var matching_parenthesis_index: int = _find_matching_parenthesis_index(tokens, current) +# if matching_parenthesis_index == -1: +# push_error("no matching parenthesis") +# return {} +# +# +# if not current_command_flags & F_EXECUTE_CONDITION_MET: # negace + background (pipe) +# current = matching_parenthesis_index + 1 # check if it is a & +# continue +# +# if tokens[matching_parenthesis_index+1].type == GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: +# current_command_flags |= F_BACKGROUND +# +# +# last_command_result = _execute_helper(tokens.slice(current+1, matching_parenthesis_index+1)) +# +# +# +# var original: Array[GDShellCommandParser.Token] = parser_result.result +# parser_result.result = parser_result.result.slice(current+1, matching_parenthesis_index+1) +# last_command_result = execute(parser_result) +# parser_result.result = original + + + _: # ERROR, SPACE, WORD_UNTERMINATED, OPERATOR_EXPAND, OPERATOR_CLOSING_PARENTHESIS + push_error("Unexpected token. These tokens should be handled earlier") + return {} - current_token += 1 + current += 1 - _is_running_command = false - return last_command_result + return {} + -func _execute_command(path: String, params: Dictionary, in_background: bool = false) -> Dictionary: - @warning_ignore("unsafe_method_access", "unsafe_cast") - var command: GDShellCommand = ResourceLoader.load(path, "GDScript").new() as GDShellCommand +func _execute_command(words: Array[GDShellCommandParser.Token], piped_data: Variant=null, background: bool=false) -> Dictionary: + var command_script: Resource = ResourceLoader.load(_find_command_path(words[0].content), "GDScript") + if not command_script: + push_error("can't load (non existent command)") + return {} + + var command: GDShellCommand = command_script.new() add_child(command) - command._PARENT_PROCESS = self - if in_background: + + if background: _background_commands.append(command) - @warning_ignore("redundant_await") - var result = await command._main(params["argv"], params["data"]) + var command_result: Dictionary = await command._main( + words.map(func(word: GDShellCommandParser.Token) -> String: return word.content), + piped_data + ) - if typeof(result) != TYPE_DICTIONARY: - push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY.\n'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." - % params["argv"][0] - ) + # Validate the command result + if typeof(command_result) != TYPE_DICTIONARY: + push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY. + 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." % words[0].content) # This assert statement acts as a hard error in the editor - assert( - typeof(result) == TYPE_DICTIONARY, - """[GDShell] The command does not return a value of TYPE_DICTIONARY. + assert(typeof(command_result) == TYPE_DICTIONARY, """[GDShell] The command does not return a value of TYPE_DICTIONARY. 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead. - See the Errors for more information about the failing command.""" - ) - result = GDShellCommand.DEFAULT_COMMAND_RESULT + See the Errors for more information about the failing command.""") + command_result = GDShellCommand.DEFAULT_COMMAND_RESULT else: - @warning_ignore("unsafe_method_access") - result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) +# @warning_ignore("unsafe_method_access") + command_result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) - command.queue_free() _background_commands.erase(command) + command.queue_free() + return command_result + + +func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: + var current: int = opening_parenthesis_index + var parenthesis_level: int = 1 - return result + while current < tokens.size(): + if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + parenthesis_level += 1 + elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + parenthesis_level -= 1 + if parenthesis_level == 0: + return current + return -1 + ############################################## @@ -123,20 +201,5 @@ func _execute_command(path: String, params: Dictionary, in_background: bool = fa func _handle_execute(command: String) -> Dictionary: return await _PARENT_GDSHELL.execute(command) - -func _handle_input(command: GDShellCommand, out: String) -> String: - if command in _background_commands: - return "" - return await _PARENT_GDSHELL._request_input_from_ui_handler(out) - - -func _handle_output(out: String, append_new_line: bool = true) -> void: - _PARENT_GDSHELL._request_output_from_ui_handler(out, append_new_line) - - -func _handle_get_ui_handler() -> GDShellUIHandler: - return _PARENT_GDSHELL.get_ui_handler() - - -func _handle_get_ui_handler_rich_text_label() -> RichTextLabel: - return _PARENT_GDSHELL.get_ui_handler_rich_text_label() +func _find_command_path(command_name: String) -> String: + return "" diff --git a/addons/gdshell/scripts/gdshell_main.gd b/addons/gdshell/scripts/gdshell_main.gd index 3472598..6e62cd9 100644 --- a/addons/gdshell/scripts/gdshell_main.gd +++ b/addons/gdshell/scripts/gdshell_main.gd @@ -20,155 +20,138 @@ var _is_command_awaiting_input: bool = false var _input_buffer: String = "" -func _ready() -> void: - if get_parent() == get_tree().root: # is singleton - setup_as_singleton() - else: - push_warning("GDShellMain was instanced directly so don't forget to set it up manually. For reference checkout GDShellMain.setup_as_singleton()") - - -func setup_as_singleton() -> void: - setup_command_runner() - - setup_command_db( - ProjectSettings.get_setting( - GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES, - GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES_DEFAULT - ) - ) - - setup_ui_handler( - GDShellMain.load_ui_handler_from_path( - ProjectSettings.get_setting( - GDShellEditorPlugin.UI_SCENE_PATH, - GDShellEditorPlugin.UI_SCENE_PATH_DEFAULT - ) - ), - true, - ProjectSettings.get_setting( - GDShellEditorPlugin.UI_CANVAS_LAYER, - GDShellEditorPlugin.UI_CANVAS_LAYER_DEFAULT - ) - ) - - execute_autorun() - -func setup_command_runner() -> void: - command_runner = GDShellCommandRunner.new() - command_runner._PARENT_GDSHELL = self - add_child(command_runner) +# a + (b - c) + !d& - -func setup_command_db(command_dir_paths: Array) -> void: - command_db = GDShellCommandDB.new() - - if ( - command_dir_paths.is_empty() - or command_dir_paths.all( - func(path): return typeof(path) != TYPE_STRING - ) - or not command_dir_paths.any( - func(path): return DirAccess.dir_exists_absolute(path) - ) - ): - push_error("[GDShell] No commands were loaded as there are no dir paths in 'Project/ProjectSettings/%s'" % GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES) - return - - for dir in command_dir_paths: - if typeof(dir) == TYPE_STRING: - command_db.add_commands_in_directory(dir) - - -func setup_ui_handler(handler: GDShellUIHandler, add_as_child: bool = true, canvas_layer: int = 100) -> void: - ui_handler = handler - ui_handler._PARENT_GDSHELL = self - ui_handler.set_visible(false) +func _ready() -> void: + var x: GDShellCommandParser.ParserResult = GDShellCommandParser.parse("echo | echo", null) + print("Input: ", x.input) + print("Status: ", x.status) + print("AST: ", x.ast) + print("Tokens: ", x.tokens) + print("Err index: ", x.err_token_index) + print("Err: ", x.err_string) - if add_as_child: - var cl: CanvasLayer = CanvasLayer.new() - cl.layer = canvas_layer - cl.add_child(handler) - add_child(cl) - - -func execute_autorun() -> void: - if "autorun" in command_db.get_all_command_names(): - @warning_ignore("return_value_discarded") - execute("autorun") - - -func execute(command: String) -> Dictionary: - var command_sequence: Dictionary = GDShellCommandParser.parse(command, command_db) - if command_sequence["status"] == GDShellCommandParser.ParserResultStatus.OK: - return await command_runner.execute(command_sequence) - return command_sequence - - -func get_ui_handler() -> GDShellUIHandler: - return ui_handler - - -func get_ui_handler_rich_text_label() -> RichTextLabel: - return ui_handler._get_output_rich_text_label() - - -func _request_input_from_ui_handler(out: String = "") -> String: - _is_command_awaiting_input = true - ui_handler._input_requested.emit(out) - return await _input_submitted - - -func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: - ui_handler._output_requested.emit(output, append_new_line) - - -func _submit_input(input: String) -> void: - if _is_command_awaiting_input: - _is_command_awaiting_input = false - _request_output_from_ui_handler(input, true) - _input_submitted.emit(input) - return + printerr("tokens:") + for token in x.tokens: + print(token.content) +# print(x.result) +# var a = "" +# for i in x.err_index: +# a += " " +# print(a+"^") +# print(GDShellCommandParser._tokenize_text("a 'abc'", 0).consumed) +# print(x.status) + + +#func setup_with_default_values() -> void: +# setup_command_runner() +# setup_command_db(COMMAND_DIR_PATH) +# setup_ui_handler(load_ui_handler_from_path(UI_HANDLER_PATH), true) - _input_buffer += input - var command_sequence: Dictionary = GDShellCommandParser.parse(_input_buffer, command_db) - match command_sequence["status"]: - GDShellCommandParser.ParserResultStatus.OK: - _request_output_from_ui_handler( - (ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true - ) - _input_buffer = "" - await command_runner.execute(command_sequence) - ui_handler._input_requested.emit("") - GDShellCommandParser.ParserResultStatus.UNTERMINATED: - _request_output_from_ui_handler( - (ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true - ) - ui_handler._input_requested.emit("> ") - GDShellCommandParser.ParserResultStatus.ERROR: - _request_output_from_ui_handler( - ui_handler._get_input_prompt() + _input_buffer, true - ) - _input_buffer = "" - # TODO better error announcement - _request_output_from_ui_handler( - "[color=red]%s[/color]" % command_sequence["result"]["error"], true - ) - ui_handler._input_requested.emit("") - - -static func load_ui_handler_from_path(path: String) -> GDShellUIHandler: - @warning_ignore("unsafe_cast", "unsafe_method_access") - return load(path).instantiate() as GDShellUIHandler - - -static func get_gdshell_version() -> String: - var config: ConfigFile = ConfigFile.new() - if config.load("res://addons/gdshell/plugin.cfg"): - return "Unknown" - return str(config.get_value("plugin", "version", "Unknown")) - - -func _input(event: InputEvent) -> void: - if event.is_action(UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): - ui_handler.toggle_visible() +# if execute_autorun_on_startup: +# execute_autorun() + + +#func setup_command_runner() -> void: +# command_runner = GDShellCommandRunner.new() +# command_runner._PARENT_GDSHELL = self +# add_child(command_runner) +# +# +#func setup_command_db(command_dir_path: String="") -> void: +# command_db = GDShellCommandDB.new() +# if not command_dir_path.is_empty(): +# command_db.add_commands_in_directory(command_dir_path) +# +# +#func setup_ui_handler(handler: GDShellUIHandler, add_as_child: bool=true) -> void: +# ui_handler = handler +# ui_handler._PARENT_GDSHELL = self +# ui_handler.set_visible(false) +# +# if add_as_child: +# var canvas_layer: CanvasLayer = CanvasLayer.new() +# canvas_layer.layer = GDSHELL_CANVAS_LAYER +# canvas_layer.add_child(handler) +# add_child(canvas_layer) + + +#func _input(event: InputEvent) -> void: +# if event.is_action(UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): +# ui_handler.toggle_visible() + + +#func execute_autorun() -> void: +# if "autorun" in command_db.get_all_command_names(): +# execute("autorun") + + +#func execute(command: String) -> Dictionary: +# var command_sequence: Dictionary = GDShellCommandParser.parse(command, command_db) +# if command_sequence["status"] == GDShellCommandParser.ParserResultStatus.OK: +# return await command_runner.execute(command_sequence) +# return command_sequence +# +# +#func get_ui_handler() -> GDShellUIHandler: +# return ui_handler +# +# +#func get_ui_handler_rich_text_label() -> RichTextLabel: +# return ui_handler._get_output_rich_text_label() +# +# +#func _request_input_from_ui_handler(out: String="") -> String: +# _is_command_awaiting_input = true +# ui_handler._input_requested.emit(out) +# return await _input_submitted +# +# +#func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: +# ui_handler._output_requested.emit(output, append_new_line) +# +# +#func _submit_input(input: String) -> void: +# if _is_command_awaiting_input: +# _is_command_awaiting_input = false +# _request_output_from_ui_handler(input, true) +# _input_submitted.emit(input) +# return +# +# +# _input_buffer += input +# var command_sequence: Dictionary = GDShellCommandParser.parse(_input_buffer, command_db) +# match command_sequence["status"]: +# GDShellCommandParser.ParserResultStatus.OK: +# _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) +# _input_buffer = "" +# await command_runner.execute(command_sequence) +# ui_handler._input_requested.emit("") +# GDShellCommandParser.ParserResultStatus.UNTERMINATED: +# _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) +# ui_handler._input_requested.emit("> ") +# GDShellCommandParser.ParserResultStatus.ERROR: +# _request_output_from_ui_handler(ui_handler._get_input_prompt() + _input_buffer, true) +# _input_buffer = "" +# # TODO better error announcement +# _request_output_from_ui_handler("[color=red]%s[/color]" % command_sequence["result"]["error"], true) +# ui_handler._input_requested.emit("") +# +# +#static func load_ui_handler_from_path(path: String) -> GDShellUIHandler: +# return load(path).instantiate() as GDShellUIHandler +# +# +#static func get_gdshell_version() -> String: +# var config: ConfigFile = ConfigFile.new() +# if config.load("res://addons/gdshell/plugin.cfg"): +# return "Unknown" +# return str(config.get_value("plugin", "version", "Unknown")) +# +# +#func _input(event: InputEvent) -> void: +# if not handle_gdshell_toggle_ui_action: +# return +# if event.is_action(GDSHELL_TOGGLE_UI_ACTION) and not event.is_echo() and event.is_pressed(): +# ui_handler.toggle_visible() diff --git a/project.godot b/project.godot index f38f1a4..b67ee0f 100644 --- a/project.godot +++ b/project.godot @@ -1,11 +1,3 @@ -; Engine configuration file. -; It's best edited using the editor UI and not directly, -; since the parameters that go here are not all obvious. -; -; Format: -; [section] ; section goes between [] -; param=value ; assign values to parameters - config_version=5 [application] @@ -15,6 +7,7 @@ config/description="Feature-packed customizable in-game console for development, run/main_scene="res://addons/gdshell/demo/demo.tscn" config/features=PackedStringArray("4.0", "Forward Plus") config/icon="res://addons/gdshell/icon.png" +config/tags=PackedStringArray() [autoload] From f4d3929ac6b48e84b9cb6e053cc7536437f1fb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Mon, 21 Aug 2023 18:27:17 +0200 Subject: [PATCH 04/20] NEW PARSER! (I hope it works) --- addons/gdshell/demo/demo.tscn | 2 +- .../gdshell/scripts/gdshell_command_parser.gd | 347 ++++++------------ 2 files changed, 123 insertions(+), 226 deletions(-) diff --git a/addons/gdshell/demo/demo.tscn b/addons/gdshell/demo/demo.tscn index 5e01afc..9f7567a 100644 --- a/addons/gdshell/demo/demo.tscn +++ b/addons/gdshell/demo/demo.tscn @@ -16,7 +16,7 @@ script = ExtResource("1_cwk1x") texture = ExtResource("2_hso4g") [node name="Label" type="Label" parent="GDShellIcon"] -anchors_preset = -1 +anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd index b1af307..33fdb51 100644 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ b/addons/gdshell/scripts/gdshell_command_parser.gd @@ -10,34 +10,19 @@ class ParserResult: ERROR, } - var status: Status - var input: String - var ast: ASTNode - var tokens: Array[Token] - var rpn_tokens: Array[Token] - var err_token_index: int - var err_string: String = "" + var status: Status ## Status of the parsed input. + var input: String ## The input that was provided by the user. + var tokens: Array[Token] ## Tokenized input. + var err_token_index: int ## Index of the Token that caused an error. If the index is -1, no error occured. + var err_string: String = "" ## Description of the error. If empty, no error occured. - func _init(_status: Status, _input: String, _ast: ASTNode, _tokens: Array[Token], _err_token_index: int = -1, _err_string: String = ""): + func _init(_status: Status, _input: String, _tokens: Array[Token], _err_token_index: int = -1, _err_string: String = ""): status = _status input = _input - ast = _ast tokens = _tokens err_token_index = _err_token_index err_string = _err_string - -class ASTNode: - var token: Token - var left: ASTNode - var right: ASTNode - - func _init(_token: Token, _left: ASTNode = null, _right: ASTNode = null): - token = _token - left = _left - right = _right - - class Token: enum Type { SPACE, @@ -49,7 +34,7 @@ class Token: OPERATOR_NOT, OPERATOR_BACKGROUND, OPERATOR_SEQUENCE, - OPERATOR_EXPAND, +# OPERATOR_EXPAND, OPERATOR_OPENING_PARENTHESIS, OPERATOR_CLOSING_PARENTHESIS, } @@ -67,83 +52,61 @@ class Token: static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: - var tokens: Array[Token] = tokenize(input) -# if tokens.is_empty(): -# return ParserResult.new( -# ParserResult.Status.OK, -# input, -# tokens, -# ) + var tokens: Array[Token] = _tokenize(input) + + if tokens.is_empty(): + return ParserResult.new( + ParserResult.Status.OK, + input, + tokens, + ) + if tokens[-1].type == Token.Type.WORD_UNTERMINATED: return ParserResult.new( ParserResult.Status.UNTERMINATED, input, - ASTNode.new(null), tokens, tokens.size() - 1, - "The input is not terminated with corrent quote so another appended input is required. See GDShell Docs for help", + "The input is not terminated with corrent quote so another appended input is required. See GDShell Docs for help.", ) - # Expand aliases and OPERATOR_EXPAND tokens - tokens = expand(tokens, command_db) - - - var ast: ASTNode = create_ast(tokens) + var validated: Dictionary = _validate_operators(tokens) + if validated["err_token_index"] != -1: + return ParserResult.new( + ParserResult.Status.ERROR, + input, + tokens, + validated["err_token_index"], + validated["err_string"] + ) - # Validates logic of the expression -# var err: Dictionary = validate_expression(tokens) -# if err.err_token_index != -1: -# return ParserResult.new( -# ParserResult.Status.ERROR, -# input, -# tokens, -# err.err_token_index, -# err.err_string, -# ) + tokens = _expand(tokens, command_db) - return ParserResult.new(ParserResult.Status.OK, input, ast, tokens) - - - -static func expand(tokens: Array[Token], command_db: GDShellCommandDB) -> Array[Token]: - return tokens + return ParserResult.new( + ParserResult.Status.OK, + input, + tokens + ) -static func create_ast(tokens: Array[Token]) -> ASTNode: - var ast_root: ASTNode - var token_stack: Array = [] +static func _expand(tokens: Array[Token], command_db: GDShellCommandDB) -> Array[Token]: + var expanded_tokens: Array[Token] = [] + var last_token_type: Token.Type = Token.Type.OPERATOR_AND for i in tokens.size(): - match tokens[i].type: - Token.Type.SPACE: - break - Token.Type.WORD: - pass -# command_construction.append(tokens[i].content) - Token.Type.OPERATOR_OPENING_PARENTHESIS: - pass - Token.Type.OPERATOR_CLOSING_PARENTHESIS: - pass - Token.Type.OPERATOR_PIPE: - pass - Token.Type.OPERATOR_AND: - pass - Token.Type.OPERATOR_OR: - pass - Token.Type.OPERATOR_NOT: - pass - Token.Type.OPERATOR_BACKGROUND: - pass - Token.Type.OPERATOR_SEQUENCE: - pass - Token.Type.OPERATOR_EXPAND: - pass - - - + if last_token_type != Token.Type.WORD: + expanded_tokens.append_array(_expand_token(tokens[i], command_db)) + else: + expanded_tokens.append(tokens[i]) + last_token_type = tokens[i].type - return ast_root + return expanded_tokens + +static func _expand_token(token: Token, command_db: GDShellCommandDB) -> Array[Token]: + if token.content in command_db._aliases.keys(): + return _tokenize(command_db._aliases[token.content]) + return [token] static func _is_operator(token: Token) -> bool: @@ -157,71 +120,55 @@ static func _is_operator(token: Token) -> bool: ] - -# !(false || echo a) && echo b - -# man idk& && ((echo "success"); echo ":)") || (echo "failed"; echo ":(") ; !true& && echo hello -# (true --idk&) && ((echo "success"); (echo ":)")) || ((echo "failed"); (echo ":(")) ; (!true&) && (echo hello) - - -## Returns the index and error message for the first invalid operator -#static func validate_expression(tokens: Array[Token]) -> Dictionary: -# var err: Dictionary = { -# "err_token_index": -1, -# "err_string": "" -# } -# -# err = _validate_parentheses(tokens) -# if err.err_token_index != -1: -# return err -# -# for i in tokens.size(): -# match tokens[i].type: -# Token.Type.OPERATOR_PIPE: -# err = _validate_pipe(tokens, i) -# Token.Type.OPERATOR_AND: -# err = _validate_and(tokens, i) -# Token.Type.OPERATOR_OR: -# err = _validate_or(tokens, i) -# Token.Type.OPERATOR_NOT: -# err = _validate_not(tokens, i) -# Token.Type.OPERATOR_BACKGROUND: -# err = _validate_background(tokens, i) -# Token.Type.OPERATOR_SEQUENCE: -# err = _validate_sequence(tokens, i) -# Token.Type.OPERATOR_EXPAND: -# pass -# _: # SPACE, WORD, WORD_UNTERMINATED, OPERATOR_OPENING_PARENTHESIS, OPERATOR_CLOSING_PARENTHESIS -# pass -# -# if err.err_token_index != -1: -# return err -# -# return err - +static func _validate_operators(tokens: Array[Token]) -> Dictionary: + var parenthesis_validation: Dictionary = _validate_parentheses(tokens) + if parenthesis_validation["err_token_index"] != -1: + return parenthesis_validation + + for i in tokens.size(): + if tokens[i].type == Token.Type.WORD: + continue + + if _token_requires_left_operand(tokens[i]): + if i == 0 or not _token_can_be_considered_operand(tokens[i-1]): + return { + "err_token_index": i, + "err_string": "\"%s\" does not have a required left operand." % tokens[i].content, + } + if _token_requires_right_operand(tokens[i]): + if i == tokens.size()-1 or not _token_can_be_considered_operand(tokens[i+1]): + return { + "err_token_index": i, + "err_string": "\"%s\" does not have a required right operand." % tokens[i].content, + } + + return { + "err_token_index": -1, + "err_string": "", + } static func _validate_parentheses(tokens: Array[Token]) -> Dictionary: var parenthesis_level: int = 0 - var firt_opening_parenthesis_index: int = -1 + var firt_opening_parenthesis_index: int for i in tokens.size(): if tokens[i].type == Token.Type.OPERATOR_OPENING_PARENTHESIS: parenthesis_level += 1 - if firt_opening_parenthesis_index == -1: + if parenthesis_level == 1: firt_opening_parenthesis_index = i elif tokens[i].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS: parenthesis_level -= 1 if parenthesis_level < 0: # Found closing parenthesis with no opening one return { "err_token_index": i, - "err_string": "Closing \")\" token on index [%d] does not have an opening counterpart." % i, + "err_string": "Closing \")\" on index [%d] does not have an opening counterpart." % i, } - if parenthesis_level != 0: # Did not find a closing parenthesis for all opening parentheses + if parenthesis_level > 0: # Did not find a closing parenthesis for all opening parentheses return { "err_token_index": firt_opening_parenthesis_index, - "err_string": "Opening \"(\" token on index [%d] does not have a closing counterpart." % firt_opening_parenthesis_index, + "err_string": "Opening \"(\" on index [%d] does not have a closing counterpart." % firt_opening_parenthesis_index, } return { @@ -230,95 +177,36 @@ static func _validate_parentheses(tokens: Array[Token]) -> Dictionary: } +static func _token_requires_left_operand(token: Token) -> bool: + return token.type in [ + Token.Type.OPERATOR_PIPE, + Token.Type.OPERATOR_AND, + Token.Type.OPERATOR_OR, + Token.Type.OPERATOR_BACKGROUND, + Token.Type.OPERATOR_SEQUENCE, + ] + -#static func _expand_aliases(tokens: Array[Token], command_db: GDShellCommandDB, forbidden_aliases: Array[String] = []) -> Array[Token]: -# for i in tokens.size(): -# if tokens[i].type != Token.Type.WORD: -# # try to expand the next one -# pass -# -# return [] -# -# -#static func validate_operators(tokens: Array[Token]) -> int: -# var parenthesis_level: int = 0 -# var open_parenthesis_index: int = -1 -# -# for i in tokens.size(): -# if tokens[i].type == Token.Type.WORD: -# continue -# if not _is_operator_valid(tokens, i): -# return i -# -# # validate parenthesis pairs -# if tokens[i].type == Token.Type.OPERATOR_OPENING_PARENTHESIS: -# if parenthesis_level == 0: -# open_parenthesis_index = i # tracks the parenthesis that would be closed last -# parenthesis_level += 1 -# -# elif tokens[i].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS: -# parenthesis_level -= 1 -# if parenthesis_level < 0: -# return i # more closing parenthesis than opening ones -# -# return -1 if parenthesis_level == 0 else open_parenthesis_index -# -# -#static func _is_operator_valid(tokens: Array[Token], operator_token_index: int) -> bool: -# -# # TODO : make sure no word is outside parenthesis -# -# -# -# if operator_token_index < 0 or operator_token_index >= tokens.size(): -# push_error("[GDShell] 'operator_token_index' (index: %s) is out of range of 'tokens' (size: %s) - Handled as an invalid operator" % [operator_token_index, tokens.size()]) -# return false -# -# match tokens[operator_token_index].type: -# # binary operators (operand operator operand) -# Token.Type.OPERATOR_PIPE, Token.Type.OPERATOR_AND, Token.Type.OPERATOR_OR: -# if operator_token_index-1 < 0 or operator_token_index+1 >= tokens.size(): -# return false -# # check the left operand -# if (not (tokens[operator_token_index-1].type == Token.Type.WORD -# or tokens[operator_token_index-1].type == Token.Type.OPERATOR_BACKGROUND -# or tokens[operator_token_index-1].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS) -# ): -# return false -# # check the right operand -# if (not (tokens[operator_token_index+1].type == Token.Type.WORD -# or tokens[operator_token_index+1].type == Token.Type.OPERATOR_NOT -# or tokens[operator_token_index+1].type == Token.Type.OPERATOR_OPENING_PARENTHESIS)): -# return false -# -# # left operators (operator operand) -# Token.Type.OPERATOR_NOT, Token.Type.OPERATOR_EXPAND: -# if operator_token_index+1 >= tokens.size(): -# return false -# # check the right operand -# if tokens[operator_token_index+1].type != Token.Type.WORD: -# return false -# -# # right operators (operand operator) -# Token.Type.OPERATOR_BACKGROUND, Token.Type.OPERATOR_SEQUENCE: -# if operator_token_index-1 < 0: -# return false -# # check the left operand -# if tokens[operator_token_index-1].type != Token.Type.WORD: -# return false -# -# # standalone operators -# Token.Type.OPERATOR_OPENING_PARENTHESIS, Token.Type.OPERATOR_CLOSING_PARENTHESIS: -# return true -# -# _: -# push_error("[GDHell] Non-operator token: 'Token.Type.%s'" % str(Token.Type.find_key(tokens[operator_token_index].type))) -# return false -# -# return true - - -static func tokenize(input: String) -> Array[Token]: +static func _token_requires_right_operand(token: Token) -> bool: + return token.type in [ + Token.Type.OPERATOR_PIPE, + Token.Type.OPERATOR_AND, + Token.Type.OPERATOR_OR, + Token.Type.OPERATOR_NOT, + ] + + +static func _token_can_be_considered_operand(token: Token) -> bool: + return token.type in [ + Token.Type.WORD, + Token.Type.OPERATOR_NOT, + Token.Type.OPERATOR_BACKGROUND, + Token.Type.OPERATOR_OPENING_PARENTHESIS, + Token.Type.OPERATOR_CLOSING_PARENTHESIS, + ] + + +static func _tokenize(input: String) -> Array[Token]: var tokens: Array[Token] = [] var current_char: int = 0 @@ -334,8 +222,8 @@ static func tokenize(input: String) -> Array[Token]: tokens.push_back(_tokenize_pipe(input, current_char)) "!": tokens.push_back(_tokenize_exclamation(input, current_char)) - "$": - tokens.push_back(_tokenize_dollar_sign(input, current_char)) +# "$": +# tokens.push_back(_tokenize_dollar_sign(input, current_char)) "(", ")": tokens.push_back(_tokenize_parenthesis(input, current_char)) "\"", "\'": @@ -345,7 +233,7 @@ static func tokenize(input: String) -> Array[Token]: current_char += tokens[-1].consumed - return merge_word_tokens(tokens) + return _filter_out_space_tokens(_merge_word_tokens(tokens)) static func _tokenize_space(_input: String, current_char: int) -> Token: @@ -372,8 +260,8 @@ static func _tokenize_exclamation(_input: String, current_char: int) -> Token: return Token.new(Token.Type.OPERATOR_NOT, "!", current_char, 1) -static func _tokenize_dollar_sign(_input: String, current_char: int) -> Token: - return Token.new(Token.Type.OPERATOR_EXPAND, "$", current_char, 1) +#static func _tokenize_dollar_sign(_input: String, current_char: int) -> Token: +# return Token.new(Token.Type.OPERATOR_EXPAND, "$", current_char, 1) static func _tokenize_parenthesis(input: String, current_char: int) -> Token: @@ -410,7 +298,8 @@ static func _tokenize_text(input: String, current_char: int) -> Token: var content: String = "" for i in range(current_char, input.length()): - if input[i] in [" ", ";", "&", "|", "!", "$", "(", ")", "\"", "\'"]: + # check if the character should end the WORD token. + if input[i] in [" ", ";", "&", "|", "!", "(", ")", "\"", "\'"]: break content += input[i] @@ -423,7 +312,7 @@ static func _tokenize_text(input: String, current_char: int) -> Token: ## Merges WORD tokens if they are not separated by any other token -static func merge_word_tokens(tokens: Array[Token]) -> Array[Token]: +static func _merge_word_tokens(tokens: Array[Token]) -> Array[Token]: if tokens.is_empty(): return tokens # We now know that tokens is not empty so we append the first token for later simplification @@ -438,3 +327,11 @@ static func merge_word_tokens(tokens: Array[Token]) -> Array[Token]: merged_tokens.append(tokens[i]) return merged_tokens + + +## Filters out SPACE tokens. After _merge_word_tokens() they are useless and it simplifies next operations. +static func _filter_out_space_tokens(tokens: Array[Token]) -> Array[Token]: + return tokens.filter( + func is_token_not_space(token: Token) -> bool: + return token.type != Token.Type.SPACE + ) From 2d5bfebcb4388e194eb3e9083ff10df4a79e76c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Mon, 21 Aug 2023 18:51:21 +0200 Subject: [PATCH 05/20] Clean up GDShellCommandDB --- .../commands/default_commands/alias.gd | 11 +-- addons/gdshell/scripts/gdshell_command_db.gd | 79 ++++++++++--------- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/addons/gdshell/commands/default_commands/alias.gd b/addons/gdshell/commands/default_commands/alias.gd index 9ec8f09..d465189 100644 --- a/addons/gdshell/commands/default_commands/alias.gd +++ b/addons/gdshell/commands/default_commands/alias.gd @@ -15,17 +15,12 @@ func _main(argv: Array, data) -> Dictionary: return {"error": 1, "error_string": "Not enough arguments"} if "-r" in argv[1] or "--remove" in argv[1]: - success = _PARENT_PROCESS._PARENT_GDSHELL.command_db.remove_alias(argv[2]) - if success: - output("Alias '%s' removed" % argv[2]) + _PARENT_PROCESS._PARENT_GDSHELL.command_db.remove_alias(argv[2]) return DEFAULT_COMMAND_RESULT - success = _PARENT_PROCESS._PARENT_GDSHELL.command_db.add_alias(argv[1], argv[2]) - if not success: - output("Could not add alias '%s'" % argv[1]) - return {"error": 1, "error_string": "Could not add alias"} + _PARENT_PROCESS._PARENT_GDSHELL.command_db.add_alias(argv[1], argv[2]) - output("Alias '%s' added" % argv[1]) +# output("Alias '%s' added" % argv[1]) return DEFAULT_COMMAND_RESULT diff --git a/addons/gdshell/scripts/gdshell_command_db.gd b/addons/gdshell/scripts/gdshell_command_db.gd index cbd0f59..18225f1 100644 --- a/addons/gdshell/scripts/gdshell_command_db.gd +++ b/addons/gdshell/scripts/gdshell_command_db.gd @@ -7,22 +7,25 @@ var _commands: Dictionary = {} var _aliases: Dictionary = {} -func add_command(path: String) -> bool: - var name_and_auto_aliases: Dictionary = get_command_name_and_auto_aliases(path) +## Returns the command name on success or empty String on failure +func add_command(path: String) -> String: + var name_and_auto_aliases: Dictionary = GDShellCommandDB.get_command_name_and_auto_aliases(path) if name_and_auto_aliases.is_empty(): - return false + return "" _commands[name_and_auto_aliases["name"]] = path _aliases.merge(name_and_auto_aliases["aliases"], true) - return true + return name_and_auto_aliases["name"] func add_commands_in_directory(path: String, recursive: bool = true) -> void: - for command in get_command_file_paths_in_directory(path, recursive): + for command in GDShellCommandDB.get_command_file_paths_in_directory(path, recursive): + @warning_ignore("return_value_discarded") add_command(command) -func remove_command(command_name: String) -> bool: - return _commands.erase(command_name) +func remove_command(command_name: String) -> void: + @warning_ignore("return_value_discarded") + _commands.erase(command_name) func get_command_path(command_name: String) -> String: @@ -30,57 +33,55 @@ func get_command_path(command_name: String) -> String: func get_all_command_names() -> Array[String]: - # This is required as GD4 doesn't allow upcast from Array -> Array[String] + # This is required as GD4 doesn't allow upcast from Array to Array[String] # see: https://www.reddit.com/r/godot/comments/10rqh9g/problem_with_typed_arrays_since_40_beta_17/ - # see: https://docs.godotengine.org/en/latest/classes/class_dictionary.html#class-dictionary-method-keys - var keys: Array[String] - for key in _commands.keys(): - if key is String: - keys.append(key) + var names: Array[String] = [] + names.assign(_commands.keys()) + return names - return keys - -func add_alias(alias: String, command: String) -> bool: - # This prevents the stupidest cyclic dependency, but the aliases can still be locked into a cycle - # TODO: Somehow detect the cyclic alias/command dependency and return an error? - if alias == command: - return false +func add_alias(alias: String, command: String) -> void: _aliases[alias] = command - return true -func remove_alias(alias: String) -> bool: - return _aliases.erase(alias) +func remove_alias(alias: String) -> void: + @warning_ignore("return_value_discarded") + _aliases.erase(alias) func get_all_aliases() -> Dictionary: return _aliases.duplicate() -static func get_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: +static func _get_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: var paths: Array[String] = [] var dir: DirAccess = DirAccess.open(path) - if dir != null: - dir.list_dir_begin() + if dir == null: + push_error("[GDShell] Cannot get file paths in directory \"%s\" - %s." % [path, DirAccess.get_open_error()]) + return [] + + var err: int = dir.list_dir_begin() + if err: + push_error("[GDShell] Cannot get file paths in directory \"%s\" - %s." % [path, error_string(err)]) + return [] + path = dir.get_next() + while path: + if dir.current_is_dir(): + if recursive: + paths.append_array(_get_file_paths_in_directory(dir.get_current_dir().path_join(path), true)) + else: + paths.append(dir.get_current_dir().path_join(path)) path = dir.get_next() - while path: - if dir.current_is_dir(): - if recursive: - paths.append_array(get_file_paths_in_directory(dir.get_current_dir().path_join(path), true)) - else: - paths.append(dir.get_current_dir().path_join(path)) - path = dir.get_next() - dir.list_dir_end() - if paths.is_empty(): - push_warning( - "[GDShell] No commands found in directory. Check the'GDShellCommandDB.get_file_paths_from_directory() argument'" - ) + dir.list_dir_end() + return paths static func get_command_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: - return get_file_paths_in_directory(path, recursive).filter(func(x): return is_file_gdshell_command(x)) + return _get_file_paths_in_directory(path, recursive).filter( + func(file_path): + return is_file_gdshell_command(file_path) + ) static func is_file_gdshell_command(path: String) -> bool: From b60fe4952fa618ba42f47c67e51b3a4b9fe02fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Tue, 22 Aug 2023 00:05:33 +0200 Subject: [PATCH 06/20] Make GDShellCommand use CommandResult instead of Dictionaries --- addons/gdshell/commands/autorun.gd | 6 + .../commands/default_commands/alias.gd | 12 +- .../commands/default_commands/autorun.gd | 6 - .../gdshell/commands/default_commands/bool.gd | 31 +- .../commands/default_commands/clear.gd | 4 +- .../gdshell/commands/default_commands/echo.gd | 4 +- .../commands/default_commands/gdfetch.gd | 4 +- .../gdshell/commands/default_commands/man.gd | 14 +- .../monitor_overlay/monitor.gd | 30 +- addons/gdshell/scripts/gdshell_command.gd | 40 ++- .../gdshell/scripts/gdshell_command_runner.gd | 308 +++++++++--------- 11 files changed, 235 insertions(+), 224 deletions(-) create mode 100644 addons/gdshell/commands/autorun.gd delete mode 100644 addons/gdshell/commands/default_commands/autorun.gd diff --git a/addons/gdshell/commands/autorun.gd b/addons/gdshell/commands/autorun.gd new file mode 100644 index 0000000..bf3a8f3 --- /dev/null +++ b/addons/gdshell/commands/autorun.gd @@ -0,0 +1,6 @@ +extends GDShellCommand + + +func _main(_argv: Array, _data) -> CommandResult: +# execute("gdfetch") + return CommandResult.new() diff --git a/addons/gdshell/commands/default_commands/alias.gd b/addons/gdshell/commands/default_commands/alias.gd index d465189..5f9771c 100644 --- a/addons/gdshell/commands/default_commands/alias.gd +++ b/addons/gdshell/commands/default_commands/alias.gd @@ -7,21 +7,21 @@ func _init(): } -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, data) -> CommandResult: var success: bool if not argv.size() > 2: output("Not enough arguments") - return {"error": 1, "error_string": "Not enough arguments"} + return CommandResult.new(1, "Not enough arguments") if "-r" in argv[1] or "--remove" in argv[1]: - _PARENT_PROCESS._PARENT_GDSHELL.command_db.remove_alias(argv[2]) - return DEFAULT_COMMAND_RESULT + _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db.remove_alias(argv[2]) + return CommandResult.new() - _PARENT_PROCESS._PARENT_GDSHELL.command_db.add_alias(argv[1], argv[2]) + _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db.add_alias(argv[1], argv[2]) # output("Alias '%s' added" % argv[1]) - return DEFAULT_COMMAND_RESULT + return CommandResult.new() func _get_manual() -> String: diff --git a/addons/gdshell/commands/default_commands/autorun.gd b/addons/gdshell/commands/default_commands/autorun.gd deleted file mode 100644 index c93c0fe..0000000 --- a/addons/gdshell/commands/default_commands/autorun.gd +++ /dev/null @@ -1,6 +0,0 @@ -extends GDShellCommand - - -func _main(_argv: Array, _data) -> Dictionary: -# execute("gdfetch") - return DEFAULT_COMMAND_RESULT diff --git a/addons/gdshell/commands/default_commands/bool.gd b/addons/gdshell/commands/default_commands/bool.gd index 5961da5..22748a7 100644 --- a/addons/gdshell/commands/default_commands/bool.gd +++ b/addons/gdshell/commands/default_commands/bool.gd @@ -1,15 +1,16 @@ extends GDShellCommand -const TRUE: Dictionary = { - "error": 0, - "data": "true", -} -const FALSE: Dictionary = { - "error": 1, - "error_string": "This is not an error, but false.", - "data": "false", -} +var TRUE: CommandResult = CommandResult.new( + 0, + "", + true +) +var FALSE: CommandResult = CommandResult.new( + 1, + "This is not an error, but false from bool command.", + false +) func _init(): @@ -20,7 +21,7 @@ func _init(): } -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, _data) -> CommandResult: if not argv.size() > 1: return TRUE @@ -33,11 +34,11 @@ func _main(argv: Array, data) -> Dictionary: randomize() return TRUE if randi() % 2 else FALSE _: - return { - "error": ERR_INVALID_PARAMETER, - "error_string": "Parameter '%s' not recognized" % argv[1], - "data": null, - } + return CommandResult.new( + ERR_INVALID_PARAMETER, + "Parameter '%s' not recognized" % argv[1], + null + ) func _get_manual() -> String: diff --git a/addons/gdshell/commands/default_commands/clear.gd b/addons/gdshell/commands/default_commands/clear.gd index 1428802..a0c6173 100644 --- a/addons/gdshell/commands/default_commands/clear.gd +++ b/addons/gdshell/commands/default_commands/clear.gd @@ -7,10 +7,10 @@ func _init(): } -func _main(_argv: Array, _data) -> Dictionary: +func _main(_argv: Array, _data) -> CommandResult: # Truly unbelieveable programming skills get_ui_handler_rich_text_label().clear() - return DEFAULT_COMMAND_RESULT + return CommandResult.new() func _get_manual() -> String: diff --git a/addons/gdshell/commands/default_commands/echo.gd b/addons/gdshell/commands/default_commands/echo.gd index 0141e1e..56ab98a 100644 --- a/addons/gdshell/commands/default_commands/echo.gd +++ b/addons/gdshell/commands/default_commands/echo.gd @@ -1,7 +1,7 @@ extends GDShellCommand -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, data) -> CommandResult: var out: String = "" if data != null: @@ -13,7 +13,7 @@ func _main(argv: Array, data) -> Dictionary: output(out) @warning_ignore("incompatible_ternary") - return {"data": null if out.is_empty() else out} + return CommandResult.new(0, "", null if out.is_empty() else out) func _get_manual() -> String: diff --git a/addons/gdshell/commands/default_commands/gdfetch.gd b/addons/gdshell/commands/default_commands/gdfetch.gd index f95042e..a83f2ac 100644 --- a/addons/gdshell/commands/default_commands/gdfetch.gd +++ b/addons/gdshell/commands/default_commands/gdfetch.gd @@ -41,7 +41,7 @@ func _init(): } -func _main(argv: Array, data) -> Dictionary: +func _main(argv: Array, data) -> CommandResult: var info: Dictionary = get_info() if "--i-am-a-linux-nerd-and-tried-to-use-neofetch" in argv: @@ -50,7 +50,7 @@ func _main(argv: Array, data) -> Dictionary: if not ("-s" in argv or "--silent" in argv): output(construct_output(LOGO, info), false) - return {"data": info} + return CommandResult.new(0, "", info) func construct_output(graphics: String, info: Dictionary, skip_lines: int = 3) -> String: diff --git a/addons/gdshell/commands/default_commands/man.gd b/addons/gdshell/commands/default_commands/man.gd index cd3c556..5a67583 100644 --- a/addons/gdshell/commands/default_commands/man.gd +++ b/addons/gdshell/commands/default_commands/man.gd @@ -12,20 +12,20 @@ func _init(): } -func _main(argv: Array, _data) -> Dictionary: +func _main(argv: Array, _data) -> CommandResult: if not argv.size() > 1: output("What manual page do you want? For example, try '[b]man man[/b]'\nTo see the list of all commands run '[b]man --list[/b]'") - return DEFAULT_COMMAND_RESULT + return CommandResult.new() var options: Dictionary = GDShellCommand.argv_parse_options(argv, true, false) if LIST_FLAGS.any(func(option): return option in options): # If any LIST_FLAG is in options output("[b][color=AQUAMARINE]Available GDShell commands:[/color][/b]") - for command_name in _PARENT_PROCESS._PARENT_GDSHELL.command_db.get_all_command_names(): + for command_name in _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db.get_all_command_names(): output("[color=BISQUE]%s[/color]" % command_name) if not argv.size() > options.keys().size() + 1: - return DEFAULT_COMMAND_RESULT + return CommandResult.new() var manual: String = "" for i in range(1, argv.size()): # first non-option arg @@ -34,17 +34,17 @@ func _main(argv: Array, _data) -> Dictionary: break if manual.is_empty(): - return {"error": 1, "error_string": "No manual", "data": null} + return CommandResult.new(1, "No manual", null) if not SILENT_FLAGS.any(func(option): return option in options): # If NOT any LIST_FLAG is in options var line: int = get_ui_handler_rich_text_label().get_line_count() output(manual) get_ui_handler_rich_text_label().call_deferred(&"scroll_to_line", line) - return {"data": manual} + return CommandResult.new(0, "", manual) func get_command_manual(command_name: String) -> String: - var command_db: GDShellCommandDB = _PARENT_PROCESS._PARENT_GDSHELL.command_db + var command_db: GDShellCommandDB = _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.command_db # unalias the name while true: if not command_name in command_db._aliases: diff --git a/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd b/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd index 423dd69..7b39fa2 100644 --- a/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd +++ b/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd @@ -49,23 +49,23 @@ const TYPE_NAMES: Array[String] = [ ] -func _main(argv: Array, _data) -> Dictionary: +func _main(argv: Array, _data) -> CommandResult: var monitor: Node = _get_monitor_overlay() if monitor == null: output("Cannot access Monitor Overlay. Make sure you have 'Monitor Overlay' plugin installed and try again") - return { - "error": 1, - "error_string": "Cannot access Monitor Overlay. Make sure you have 'Monitor Overlay' plugin installed and try again", - "data": null, - } + return CommandResult.new( + 1, + "Cannot access Monitor Overlay. Make sure you have 'Monitor Overlay' plugin installed and try again", + null + ) if argv.size() == 1: output("Not enought arguments. Run 'man monitor' to see all available options") - return { - "error": 2, - "error_string": "Not enought arguments. Run 'man monitor' to see all available options", - "data": null, - } + return CommandResult.new( + 2, + "Not enought arguments. Run 'man monitor' to see all available options", + null + ) var safe_to_edit_properties: Array[Dictionary] = _get_monitor_overlay_safe_to_edit_properties(monitor) var options: Dictionary = GDShellCommand.argv_parse_options(argv, true, false) @@ -79,11 +79,11 @@ func _main(argv: Array, _data) -> Dictionary: _edit_monitor_properties_with_options(monitor, safe_to_edit_properties, options) - return DEFAULT_COMMAND_RESULT + return CommandResult.new() func _get_monitor_overlay() -> Node: - if not _PARENT_PROCESS._PARENT_GDSHELL.has_node(NodePath(MONITOR_NODE_NAME)): + if not _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.has_node(NodePath(MONITOR_NODE_NAME)): if not ResourceLoader.exists(MONITOR_FILE_PATH): return null # MonitorOverlay is not installed @@ -94,9 +94,9 @@ func _get_monitor_overlay() -> Node: monitor.unique_name_in_owner = true # disable the fps monitor as it is enabled by default monitor.set("fps", false) - _PARENT_PROCESS._PARENT_GDSHELL.add_child(monitor) + _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.add_child(monitor) - return _PARENT_PROCESS._PARENT_GDSHELL.get_node(NodePath(MONITOR_NODE_NAME)) + return _PARENT_COMMAND_RUNNER._PARENT_GDSHELL.get_node(NodePath(MONITOR_NODE_NAME)) # returns a list of properties that are used for MonitorOverlay UI control diff --git a/addons/gdshell/scripts/gdshell_command.gd b/addons/gdshell/scripts/gdshell_command.gd index 646cb82..8c1d76b 100644 --- a/addons/gdshell/scripts/gdshell_command.gd +++ b/addons/gdshell/scripts/gdshell_command.gd @@ -3,43 +3,53 @@ class_name GDShellCommand extends Node +class CommandResult: + var err: int + var err_string: String + var data: Variant + + func _init(Err: int=0, ErrString: String="", Data: Variant=null) -> void: + err = Err + data = Data + err_string = "No error description." if Err != 0 and ErrString.is_empty() else ErrString + + signal command_end -const DEFAULT_COMMAND_RESULT: Dictionary = { - "error": 0, - "error_string": "No error description", - "data": null, -} @warning_ignore("unsafe_method_access") var COMMAND_NAME: String = get_script().get_path().get_file().get_basename() var COMMAND_AUTO_ALIASES: Dictionary = {} -var _PARENT_PROCESS: GDShellCommandRunner +var _PARENT_COMMAND_RUNNER: GDShellCommandRunner -func _main(_argv: Array, _data) -> Dictionary: - return DEFAULT_COMMAND_RESULT +func _main(_argv: Array[String], _data) -> CommandResult: + return CommandResult.new() -func execute(command: String) -> Dictionary: - return await _PARENT_PROCESS._handle_execute(command) +func execute(command: String) -> CommandResult: + return await _PARENT_COMMAND_RUNNER._handle_execute(command) func input(out: String = "") -> String: - return await _PARENT_PROCESS._handle_input(self, out) + return await _PARENT_COMMAND_RUNNER._handle_input(self, out) func output(out, append_new_line: bool = true) -> void: - _PARENT_PROCESS._handle_output(str(out), append_new_line) + _PARENT_COMMAND_RUNNER._handle_output(str(out), append_new_line) + + +func get_parent_command_runner() -> GDShellCommandRunner: + return _PARENT_COMMAND_RUNNER func get_ui_handler() -> GDShellUIHandler: - return _PARENT_PROCESS._handle_get_ui_handler() + return _PARENT_COMMAND_RUNNER._handle_get_ui_handler() func get_ui_handler_rich_text_label() -> RichTextLabel: - return _PARENT_PROCESS._handle_get_ui_handler_rich_text_label() + return _PARENT_COMMAND_RUNNER._handle_get_ui_handler_rich_text_label() func _get_manual() -> String: @@ -62,7 +72,7 @@ func _get_manual() -> String: ) -static func argv_parse_options(argv: Array, strip_name_dashes: bool = false, next_arg_as_value: bool = false) -> Dictionary: +static func argv_parse_options(argv: Array[String], strip_name_dashes: bool = false, next_arg_as_value: bool = false) -> Dictionary: var options: Dictionary = {} for i in argv.size(): if argv[i][0] == "-": diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd b/addons/gdshell/scripts/gdshell_command_runner.gd index 498fc69..0dcdf80 100644 --- a/addons/gdshell/scripts/gdshell_command_runner.gd +++ b/addons/gdshell/scripts/gdshell_command_runner.gd @@ -14,101 +14,101 @@ var _PARENT_GDSHELL: GDShellMain var _background_commands: Array[GDShellCommand] = [] -func execute(parser_result: GDShellCommandParser.ParserResult) -> Dictionary: +func execute(parser_result: GDShellCommandParser.ParserResult, piped_data: Variant=null) -> GDShellCommand.CommandResult: if parser_result.status != GDShellCommandParser.ParserResult.Status.OK: - push_error("lmao bad input") - return {} - - return _execute_helper(parser_result.result) - - - -func _execute_helper(tokens: Array[GDShellCommandParser.Token], piped_data: Variant=null) -> Dictionary: - var last_command_result: Dictionary = {} - var word_accum: Array[GDShellCommandParser.Token] = [] - var current_command_flags: int = F_EXECUTE_CONDITION_MET - - var current: int = 0 - - - var execute_command_helper: Callable = func() -> void: - if not current_command_flags & F_EXECUTE_CONDITION_MET: - current_command_flags = F_EXECUTE_CONDITION_MET - word_accum.clear() - return - - if current_command_flags & F_BACKGROUND: - last_command_result = GDShellCommand.DEFAULT_COMMAND_RESULT - _execute_command( - word_accum, - last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, - true - ) - else: - last_command_result = await _execute_command( - word_accum, - last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, - false - ) - - if current_command_flags & F_NEGATED: - last_command_result["error"] = 0 if last_command_result["error"] else 1 - - current_command_flags = F_EXECUTE_CONDITION_MET - word_accum.clear() - - - var execute_parenthesis_helper: Callable = func() -> int: - # find the closing parenthesis index - var parenthesis_level: int = 1 - while current < tokens.size(): - if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: - parenthesis_level += 1 - elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: - parenthesis_level -= 1 - if parenthesis_level == 0: - break - current += 1 - - - - return -1 - - - while current < tokens.size(): - match tokens[current]: - GDShellCommandParser.Token.Type.WORD: - word_accum.append(tokens[current]) - - GDShellCommandParser.Token.Type.OPERATOR_PIPE: - current_command_flags |= F_PIPE_PREVIOUS - execute_command_helper.call() - - GDShellCommandParser.Token.Type.OPERATOR_AND: - execute_command_helper.call() - if last_command_result["error"] != 0: - current_command_flags &= ~F_EXECUTE_CONDITION_MET - - GDShellCommandParser.Token.Type.OPERATOR_OR: - execute_command_helper.call() - if last_command_result["error"] == 0: - current_command_flags &= ~F_EXECUTE_CONDITION_MET - - GDShellCommandParser.Token.Type.OPERATOR_NOT: - current_command_flags |= F_NEGATED - - GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: - current_command_flags |= F_BACKGROUND - execute_command_helper.call() - - GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: - execute_command_helper.call() - - GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: - execute_parenthesis_helper.call() - - - + push_error("[GDShell] Attempted to execute invalid GDShellCommandParser.ParserResult.") + return null + return null +# return _execute_helper(parser_result.tokens) + + + +#func _execute_helper(tokens: Array[GDShellCommandParser.Token], piped_data: Variant=null) -> Dictionary: +# var last_command_result: Dictionary = {} +# var word_accum: Array[GDShellCommandParser.Token] = [] +# var current_command_flags: int = F_EXECUTE_CONDITION_MET +# +# var current: int = 0 +# +# +# var execute_command_helper: Callable = func() -> void: +# if not current_command_flags & F_EXECUTE_CONDITION_MET: +# current_command_flags = F_EXECUTE_CONDITION_MET +# word_accum.clear() +# return +# +# if current_command_flags & F_BACKGROUND: +## last_command_result = GDShellCommand.CommandResult.new() +# _execute_command( +# word_accum, +# last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, +# true +# ) +# else: +# last_command_result = await _execute_command( +# word_accum, +# last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, +# false +# ) +# +# if current_command_flags & F_NEGATED: +# last_command_result["error"] = 0 if last_command_result["error"] else 1 +# +# current_command_flags = F_EXECUTE_CONDITION_MET +# word_accum.clear() +# +# +# var execute_parenthesis_helper: Callable = func() -> int: +# # find the closing parenthesis index +# var parenthesis_level: int = 1 +# while current < tokens.size(): +# if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: +# parenthesis_level += 1 +# elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: +# parenthesis_level -= 1 +# if parenthesis_level == 0: +# break +# current += 1 +# +# +# +# return -1 +# +# +# while current < tokens.size(): +# match tokens[current]: +# GDShellCommandParser.Token.Type.WORD: +# word_accum.append(tokens[current]) +# +# GDShellCommandParser.Token.Type.OPERATOR_PIPE: +# current_command_flags |= F_PIPE_PREVIOUS +# execute_command_helper.call() +# +# GDShellCommandParser.Token.Type.OPERATOR_AND: +# execute_command_helper.call() +# if last_command_result["error"] != 0: +# current_command_flags &= ~F_EXECUTE_CONDITION_MET +# +# GDShellCommandParser.Token.Type.OPERATOR_OR: +# execute_command_helper.call() +# if last_command_result["error"] == 0: +# current_command_flags &= ~F_EXECUTE_CONDITION_MET +# +# GDShellCommandParser.Token.Type.OPERATOR_NOT: +# current_command_flags |= F_NEGATED +# +# GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: +# current_command_flags |= F_BACKGROUND +# execute_command_helper.call() +# +# GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: +# execute_command_helper.call() +# +# GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: +# execute_parenthesis_helper.call() + + + # var matching_parenthesis_index: int = _find_matching_parenthesis_index(tokens, current) # if matching_parenthesis_index == -1: # push_error("no matching parenthesis") @@ -131,65 +131,65 @@ func _execute_helper(tokens: Array[GDShellCommandParser.Token], piped_data: Vari # parser_result.result = parser_result.result.slice(current+1, matching_parenthesis_index+1) # last_command_result = execute(parser_result) # parser_result.result = original - - - _: # ERROR, SPACE, WORD_UNTERMINATED, OPERATOR_EXPAND, OPERATOR_CLOSING_PARENTHESIS - push_error("Unexpected token. These tokens should be handled earlier") - return {} - - current += 1 - - return {} - - - -func _execute_command(words: Array[GDShellCommandParser.Token], piped_data: Variant=null, background: bool=false) -> Dictionary: - var command_script: Resource = ResourceLoader.load(_find_command_path(words[0].content), "GDScript") - if not command_script: - push_error("can't load (non existent command)") - return {} - - var command: GDShellCommand = command_script.new() - add_child(command) - - if background: - _background_commands.append(command) - - var command_result: Dictionary = await command._main( - words.map(func(word: GDShellCommandParser.Token) -> String: return word.content), - piped_data - ) - - # Validate the command result - if typeof(command_result) != TYPE_DICTIONARY: - push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY. - 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." % words[0].content) - # This assert statement acts as a hard error in the editor - assert(typeof(command_result) == TYPE_DICTIONARY, """[GDShell] The command does not return a value of TYPE_DICTIONARY. - 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead. - See the Errors for more information about the failing command.""") - command_result = GDShellCommand.DEFAULT_COMMAND_RESULT - else: -# @warning_ignore("unsafe_method_access") - command_result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) - - _background_commands.erase(command) - command.queue_free() - return command_result - - -func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: - var current: int = opening_parenthesis_index - var parenthesis_level: int = 1 - - while current < tokens.size(): - if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: - parenthesis_level += 1 - elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: - parenthesis_level -= 1 - if parenthesis_level == 0: - return current - return -1 + + +# _: # ERROR, SPACE, WORD_UNTERMINATED, OPERATOR_EXPAND, OPERATOR_CLOSING_PARENTHESIS +# push_error("Unexpected token. These tokens should be handled earlier") +# return {} +# +# current += 1 +# +# return {} + + + +#func _execute_command(words: Array[GDShellCommandParser.Token], piped_data: Variant=null, background: bool=false) -> Dictionary: +# var command_script: Resource = ResourceLoader.load(_find_command_path(words[0].content), "GDScript") +# if not command_script: +# push_error("can't load (non existent command)") +# return {} +# +# var command: GDShellCommand = command_script.new() +# add_child(command) +# +# if background: +# _background_commands.append(command) +# +# var command_result: Dictionary = await command._main( +# words.map(func(word: GDShellCommandParser.Token) -> String: return word.content), +# piped_data +# ) +# +# # Validate the command result +# if typeof(command_result) != TYPE_DICTIONARY: +# push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY. +# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." % words[0].content) +# # This assert statement acts as a hard error in the editor +# assert(typeof(command_result) == TYPE_DICTIONARY, """[GDShell] The command does not return a value of TYPE_DICTIONARY. +# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead. +# See the Errors for more information about the failing command.""") +# command_result = GDShellCommand.DEFAULT_COMMAND_RESULT +# else: +## @warning_ignore("unsafe_method_access") +# command_result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) +# +# _background_commands.erase(command) +# command.queue_free() +# return command_result + + +#func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: +# var current: int = opening_parenthesis_index +# var parenthesis_level: int = 1 +# +# while current < tokens.size(): +# if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: +# parenthesis_level += 1 +# elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: +# parenthesis_level -= 1 +# if parenthesis_level == 0: +# return current +# return -1 @@ -198,7 +198,7 @@ func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], ############################################## -func _handle_execute(command: String) -> Dictionary: +func _handle_execute(command: String) -> GDShellCommand.CommandResult: return await _PARENT_GDSHELL.execute(command) func _find_command_path(command_name: String) -> String: From 7326dae18b00236900fe620d976f79b82a61ddfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Thu, 24 Aug 2023 23:53:48 +0200 Subject: [PATCH 07/20] Improve GDShellCommandDB a bit --- addons/gdshell/scripts/gdshell_command_db.gd | 80 +++++++++++++++----- 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_db.gd b/addons/gdshell/scripts/gdshell_command_db.gd index 18225f1..e41c29a 100644 --- a/addons/gdshell/scripts/gdshell_command_db.gd +++ b/addons/gdshell/scripts/gdshell_command_db.gd @@ -7,9 +7,13 @@ var _commands: Dictionary = {} var _aliases: Dictionary = {} +func has_command(command: String) -> bool: + return command in _commands + + ## Returns the command name on success or empty String on failure func add_command(path: String) -> String: - var name_and_auto_aliases: Dictionary = GDShellCommandDB.get_command_name_and_auto_aliases(path) + var name_and_auto_aliases: Dictionary = {}#GDShellCommandDB.get_command_name_and_auto_aliases(path) if name_and_auto_aliases.is_empty(): return "" _commands[name_and_auto_aliases["name"]] = path @@ -28,6 +32,7 @@ func remove_command(command_name: String) -> void: _commands.erase(command_name) +## Returns command path if the command is registered. Returns empty String if not. func get_command_path(command_name: String) -> String: return _commands.get(command_name, "") @@ -40,10 +45,22 @@ func get_all_command_names() -> Array[String]: return names +func get_all_commands() -> Dictionary: + return _commands.duplicate() + + +func has_alias(alias: String) -> bool: + return alias in _aliases + + func add_alias(alias: String, command: String) -> void: _aliases[alias] = command +func get_alias_value(alias: String) -> String: + return _aliases.get(alias, "") + + func remove_alias(alias: String) -> void: @warning_ignore("return_value_discarded") _aliases.erase(alias) @@ -78,34 +95,57 @@ static func _get_file_paths_in_directory(path: String, recursive: bool = true) - static func get_command_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: - return _get_file_paths_in_directory(path, recursive).filter( - func(file_path): - return is_file_gdshell_command(file_path) - ) + return _get_file_paths_in_directory(path, recursive).filter(is_file_gdshell_command) static func is_file_gdshell_command(path: String) -> bool: var res: Resource = ResourceLoader.load(path, "GDScript") if not res is GDScript: return false - - var script: Object = (res as GDScript).new() - if not script is GDShellCommand: - return false - - return true + return _is_script_gdshell_command(res as GDScript) + +static func _is_script_gdshell_command(script: Script) -> bool: + # HACK: + # This is super error prone. If the name of GDShellCommand script ever changes, this breaks. + # I haven't found or figured out a better workaround so this is just to make it work. + return script.get_script_property_list().any( + func(property: Dictionary) -> bool: + return str(property["name"]) == "gdshell_command.gd" + ) -static func get_command_name_and_auto_aliases(path: String) -> Dictionary: + +func get_command_name_and_auto_aliases(command: String) -> Dictionary: + return ( + GDShellCommandDB.get_file_command_name_and_auto_aliases(_commands[command]) if command in _commands + else {"name": "", "aliases": []} + ) + + +static func get_file_command_name_and_auto_aliases(path: String) -> Dictionary: var out: Dictionary = {"name": "", "aliases": []} - var res: Resource = ResourceLoader.load(path, "GDScript") - if not res is GDScript: + var command: GDShellCommand = get_file_gdshell_command_instance(path) + if command == null: return out + out["name"] = command.COMMAND_NAME + out["aliases"] = command.COMMAND_AUTO_ALIASES + return out + + +# This is a if monster, but it just checks if the command really exists +func get_gdshell_command_instance(command_name: String) -> GDShellCommand: + var command_script_path: String = get_command_path(command_name) + if command_script_path.is_empty(): + return null + return GDShellCommandDB.get_file_gdshell_command_instance(command_script_path) + + +static func get_file_gdshell_command_instance(path: String) -> GDShellCommand: + if not is_file_gdshell_command(path): + return null - var script: Object = (res as GDScript).new() - if not script is GDShellCommand: - return out + var command_script: Resource = ResourceLoader.load(path, "GDScript", ResourceLoader.CACHE_MODE_REUSE) + if command_script == null: + return null - out["name"] = (script as GDShellCommand).COMMAND_NAME - out["aliases"] = (script as GDShellCommand).COMMAND_AUTO_ALIASES - return out + return (command_script as GDScript).new() as GDShellCommand From 3c006efbc0357cb79727d5326150e4f8215e95fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Aug 2023 00:36:55 +0200 Subject: [PATCH 08/20] Just uncomment forgotten code --- addons/gdshell/scripts/gdshell_command_db.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/gdshell/scripts/gdshell_command_db.gd b/addons/gdshell/scripts/gdshell_command_db.gd index e41c29a..bfd856b 100644 --- a/addons/gdshell/scripts/gdshell_command_db.gd +++ b/addons/gdshell/scripts/gdshell_command_db.gd @@ -13,7 +13,7 @@ func has_command(command: String) -> bool: ## Returns the command name on success or empty String on failure func add_command(path: String) -> String: - var name_and_auto_aliases: Dictionary = {}#GDShellCommandDB.get_command_name_and_auto_aliases(path) + var name_and_auto_aliases: Dictionary = GDShellCommandDB.get_file_command_name_and_auto_aliases(path) if name_and_auto_aliases.is_empty(): return "" _commands[name_and_auto_aliases["name"]] = path From 5d9dd39287395f6b458da498a46f352e92b838b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Aug 2023 00:37:16 +0200 Subject: [PATCH 09/20] update GDShellCommand so it is consistent --- addons/gdshell/scripts/gdshell_command.gd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command.gd b/addons/gdshell/scripts/gdshell_command.gd index 8c1d76b..43d7093 100644 --- a/addons/gdshell/scripts/gdshell_command.gd +++ b/addons/gdshell/scripts/gdshell_command.gd @@ -8,10 +8,10 @@ class CommandResult: var err_string: String var data: Variant - func _init(Err: int=0, ErrString: String="", Data: Variant=null) -> void: - err = Err - data = Data - err_string = "No error description." if Err != 0 and ErrString.is_empty() else ErrString + func _init(_err: int=0, _err_string: String="", _data: Variant=null) -> void: + err = _err + data = _data + err_string = "No error description." if _err != 0 and _err_string.is_empty() else _err_string signal command_end From e8eafe803b5a5a1ea437aa1e21ddba26873a80df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Aug 2023 00:37:51 +0200 Subject: [PATCH 10/20] Add command_db to GDShellCommandParser.ParserResult --- addons/gdshell/scripts/gdshell_command_parser.gd | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd index 33fdb51..eb779e9 100644 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ b/addons/gdshell/scripts/gdshell_command_parser.gd @@ -13,16 +13,19 @@ class ParserResult: var status: Status ## Status of the parsed input. var input: String ## The input that was provided by the user. var tokens: Array[Token] ## Tokenized input. + var command_db: GDShellCommandDB ## GDShellCommandDB, that was used for parsing and should be used for execution. var err_token_index: int ## Index of the Token that caused an error. If the index is -1, no error occured. var err_string: String = "" ## Description of the error. If empty, no error occured. - func _init(_status: Status, _input: String, _tokens: Array[Token], _err_token_index: int = -1, _err_string: String = ""): + func _init(_status: Status, _input: String, _tokens: Array[Token], _command_db: GDShellCommandDB, _err_token_index: int = -1, _err_string: String = ""): status = _status input = _input tokens = _tokens + command_db = _command_db err_token_index = _err_token_index err_string = _err_string + class Token: enum Type { SPACE, @@ -59,6 +62,7 @@ static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: ParserResult.Status.OK, input, tokens, + command_db ) if tokens[-1].type == Token.Type.WORD_UNTERMINATED: @@ -66,6 +70,7 @@ static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: ParserResult.Status.UNTERMINATED, input, tokens, + command_db, tokens.size() - 1, "The input is not terminated with corrent quote so another appended input is required. See GDShell Docs for help.", ) @@ -76,6 +81,7 @@ static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: ParserResult.Status.ERROR, input, tokens, + command_db, validated["err_token_index"], validated["err_string"] ) @@ -85,7 +91,8 @@ static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: return ParserResult.new( ParserResult.Status.OK, input, - tokens + tokens, + command_db ) From 1be1d3d75976f2eace7de4d572198a5c6e332494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Aug 2023 00:38:34 +0200 Subject: [PATCH 11/20] Initial work for GDShellCommandRunner --- .../gdshell/scripts/gdshell_command_runner.gd | 111 ++++++++++++++++-- 1 file changed, 100 insertions(+), 11 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd b/addons/gdshell/scripts/gdshell_command_runner.gd index 0dcdf80..5caa100 100644 --- a/addons/gdshell/scripts/gdshell_command_runner.gd +++ b/addons/gdshell/scripts/gdshell_command_runner.gd @@ -4,24 +4,113 @@ extends Node # Command execution flags -const F_EXECUTE_CONDITION_MET: int = 1 #1 << 0 -const F_PIPE_PREVIOUS: int = 2 #1 << 1 -const F_BACKGROUND: int = 4 #1 << 2 -const F_NEGATED: int = 8 #1 << 3 +#const F_EXECUTE_CONDITION_MET: int = 1 #1 << 0 +#const F_PIPE_PREVIOUS: int = 2 #1 << 1 +#const F_BACKGROUND: int = 4 #1 << 2 +#const F_NEGATED: int = 8 #1 << 3 -var _PARENT_GDSHELL: GDShellMain +#var _PARENT_GDSHELL: GDShellMain var _background_commands: Array[GDShellCommand] = [] func execute(parser_result: GDShellCommandParser.ParserResult, piped_data: Variant=null) -> GDShellCommand.CommandResult: if parser_result.status != GDShellCommandParser.ParserResult.Status.OK: - push_error("[GDShell] Attempted to execute invalid GDShellCommandParser.ParserResult.") + push_error("[GDShell] Attempted to run invalid GDShellCommandParser.ParserResult.") return null + if parser_result.tokens.is_empty(): + push_error("[GDShell] Attempted to run an empty input.") + return null + if parser_result.command_db == null: + push_error("[GDShell] Attempted to run a command, but GDShellCommandParser.ParserResult.command_db is null.") + return null + + + return null # return _execute_helper(parser_result.tokens) +# This is a if monster, but it just checks if the right file is loaded. +func _execute_command(parser_result: GDShellCommandParser.ParserResult, start_word_token_index: int=0, piped_data: Variant=null, in_background: bool=false) -> GDShellCommand.CommandResult: + var command: GDShellCommand = parser_result.command_db.get_gdshell_command_instance(parser_result.tokens[start_word_token_index].content) + if command == null: + return null + + command.name = "GDShellCommand: " + command.COMMAND_NAME + add_child(command, true) + if in_background: + command.name += " (in background)" + _background_commands.append(command) + + var last_word_token_index: int = start_word_token_index + while true: + start_word_token_index += 1 + if last_word_token_index > parser_result.tokens.size(): + break + if parser_result.tokens[last_word_token_index].type != GDShellCommandParser.Token.Type.WORD: + break + + var words: Array[GDShellCommandParser.Token] = parser_result.tokens.slice(start_word_token_index, last_word_token_index) + + return null + + + + + + + +# print(command_script.new().get_script == ) +# var command_gdscript: GDScript = (command_script as GDScript).new() +# if not command_gdscript is GDShellCommand: +# pass +# push_error("can't load (non existent command)") +# return {} +# +# var command: GDShellCommand = command_script.new() +# add_child(command) +# +# if background: +# _background_commands.append(command) +# +# var command_result: Dictionary = await command._main( +# words.map(func(word: GDShellCommandParser.Token) -> String: return word.content), +# piped_data +# ) +# +# # Validate the command result +# if typeof(command_result) != TYPE_DICTIONARY: +# push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY. +# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." % words[0].content) +# # This assert statement acts as a hard error in the editor +# assert(typeof(command_result) == TYPE_DICTIONARY, """[GDShell] The command does not return a value of TYPE_DICTIONARY. +# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead. +# See the Errors for more information about the failing command.""") +# command_result = GDShellCommand.DEFAULT_COMMAND_RESULT +# else: +## @warning_ignore("unsafe_method_access") +# command_result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) +# +# _background_commands.erase(command) +# command.queue_free() +# return command_result + + +#func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: +# var current: int = opening_parenthesis_index +# var parenthesis_level: int = 1 +# +# while current < tokens.size(): +# if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: +# parenthesis_level += 1 +# elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: +# parenthesis_level -= 1 +# if parenthesis_level == 0: +# return current +# return -1 +# return null + #func _execute_helper(tokens: Array[GDShellCommandParser.Token], piped_data: Variant=null) -> Dictionary: # var last_command_result: Dictionary = {} @@ -198,8 +287,8 @@ func execute(parser_result: GDShellCommandParser.ParserResult, piped_data: Varia ############################################## -func _handle_execute(command: String) -> GDShellCommand.CommandResult: - return await _PARENT_GDSHELL.execute(command) - -func _find_command_path(command_name: String) -> String: - return "" +#func _handle_execute(command: String) -> GDShellCommand.CommandResult: +# return await _PARENT_GDSHELL.execute(command) +# +#func _find_command_path(command_name: String) -> String: +# return "" From 9dfa7c74639aa2ddefc9312869b79ddd14a49feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Aug 2023 14:51:19 +0200 Subject: [PATCH 12/20] Just style corrections --- addons/gdshell/scripts/gdshell_command_db.gd | 2 +- addons/gdshell/scripts/gdshell_ui_handler.gd | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_db.gd b/addons/gdshell/scripts/gdshell_command_db.gd index bfd856b..d6d6752 100644 --- a/addons/gdshell/scripts/gdshell_command_db.gd +++ b/addons/gdshell/scripts/gdshell_command_db.gd @@ -95,7 +95,7 @@ static func _get_file_paths_in_directory(path: String, recursive: bool = true) - static func get_command_file_paths_in_directory(path: String, recursive: bool = true) -> Array[String]: - return _get_file_paths_in_directory(path, recursive).filter(is_file_gdshell_command) + return _get_file_paths_in_directory(path, recursive).filter(GDShellCommandDB.is_file_gdshell_command) static func is_file_gdshell_command(path: String) -> bool: diff --git a/addons/gdshell/scripts/gdshell_ui_handler.gd b/addons/gdshell/scripts/gdshell_ui_handler.gd index 5b3d623..ca7be46 100644 --- a/addons/gdshell/scripts/gdshell_ui_handler.gd +++ b/addons/gdshell/scripts/gdshell_ui_handler.gd @@ -11,33 +11,42 @@ var _PARENT_GDSHELL: GDShellMain var history: Array = [] var hist_index = -1 + func submit_input(input: String) -> void: _PARENT_GDSHELL._submit_input(input) history.push_front(input) history_reset_index() + func autocomplete(input: String) -> String: var all_commands = _PARENT_GDSHELL.command_db.get_all_command_names() - var matches = all_commands.filter(func(str: String): return str.begins_with(input)) + var matches = all_commands.filter( + func(m: String): + return m.begins_with(input) + ) if matches.size() > 0: return matches[0] return input - + + func history_get_next() -> String: if (history.size() == 0): return "" hist_index = clamp(hist_index + 1, 0, history.size() - 1) return history[hist_index] - + + func history_get_previous() -> String: if (history.size() == 0): return "" hist_index = clamp(hist_index - 1, 0, history.size() - 1) return history[hist_index] - + + func history_reset_index() -> void: hist_index = -1 + func toggle_visible() -> void: visible = not visible From 3dde9dcc3455e72cf500a586aa8fba92d0cdb4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Aug 2023 14:51:42 +0200 Subject: [PATCH 13/20] Better operator operand recognition and special parentheses error message --- .../gdshell/scripts/gdshell_command_parser.gd | 54 ++++++++++++++----- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd index eb779e9..359cb36 100644 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ b/addons/gdshell/scripts/gdshell_command_parser.gd @@ -137,16 +137,18 @@ static func _validate_operators(tokens: Array[Token]) -> Dictionary: continue if _token_requires_left_operand(tokens[i]): - if i == 0 or not _token_can_be_considered_operand(tokens[i-1]): + if i == 0 or not _token_can_be_considered_operand(tokens[i], tokens[i-1]): return { "err_token_index": i, - "err_string": "\"%s\" does not have a required left operand." % tokens[i].content, + "err_string": "\"%s\" does not have a required left operand." % tokens[i].content if tokens[i].type != Token.Type.OPERATOR_CLOSING_PARENTHESIS + else "Parentheses pair does not have at least one required command.", } if _token_requires_right_operand(tokens[i]): - if i == tokens.size()-1 or not _token_can_be_considered_operand(tokens[i+1]): + if i == tokens.size()-1 or not _token_can_be_considered_operand(tokens[i], tokens[i+1]): return { "err_token_index": i, - "err_string": "\"%s\" does not have a required right operand." % tokens[i].content, + "err_string": "\"%s\" does not have a required right operand." % tokens[i].content if tokens[i].type != Token.Type.OPERATOR_OPENING_PARENTHESIS + else "Parentheses pair does not have at least one required command.", } return { @@ -191,6 +193,7 @@ static func _token_requires_left_operand(token: Token) -> bool: Token.Type.OPERATOR_OR, Token.Type.OPERATOR_BACKGROUND, Token.Type.OPERATOR_SEQUENCE, + Token.Type.OPERATOR_CLOSING_PARENTHESIS, ] @@ -200,19 +203,44 @@ static func _token_requires_right_operand(token: Token) -> bool: Token.Type.OPERATOR_AND, Token.Type.OPERATOR_OR, Token.Type.OPERATOR_NOT, - ] - - -static func _token_can_be_considered_operand(token: Token) -> bool: - return token.type in [ - Token.Type.WORD, - Token.Type.OPERATOR_NOT, - Token.Type.OPERATOR_BACKGROUND, Token.Type.OPERATOR_OPENING_PARENTHESIS, - Token.Type.OPERATOR_CLOSING_PARENTHESIS, ] +static func _token_can_be_considered_operand(operator: Token, operand: Token) -> bool: + match operator.type: + Token.Type.OPERATOR_PIPE, Token.Type.OPERATOR_AND, Token.Type.OPERATOR_OR, Token.Type.OPERATOR_SEQUENCE: + return operand.type in [ + Token.Type.WORD, + Token.Type.OPERATOR_NOT, + Token.Type.OPERATOR_BACKGROUND, + Token.Type.OPERATOR_OPENING_PARENTHESIS, + Token.Type.OPERATOR_CLOSING_PARENTHESIS, + ] + Token.Type.OPERATOR_NOT: + return operand.type in [ + Token.Type.WORD, + Token.Type.OPERATOR_OPENING_PARENTHESIS, + ] + Token.Type.OPERATOR_BACKGROUND: + return operand.type in [ + Token.Type.WORD, + ] + Token.Type.OPERATOR_OPENING_PARENTHESIS: + return operand.type in [ + Token.Type.WORD, + Token.Type.OPERATOR_NOT, + Token.Type.OPERATOR_OPENING_PARENTHESIS, + ] + Token.Type.OPERATOR_CLOSING_PARENTHESIS: + return operand.type in [ + Token.Type.WORD, + Token.Type.OPERATOR_BACKGROUND, + Token.Type.OPERATOR_CLOSING_PARENTHESIS, + ] + + return false + static func _tokenize(input: String) -> Array[Token]: var tokens: Array[Token] = [] var current_char: int = 0 From d3d750ebc19e45cce977d78dcff8c21ee2aa920f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Aug 2023 14:59:12 +0200 Subject: [PATCH 14/20] Update project.godot --- project.godot | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/project.godot b/project.godot index b67ee0f..000e816 100644 --- a/project.godot +++ b/project.godot @@ -1,13 +1,20 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + config_version=5 [application] config/name="GDShell" -config/description="Feature-packed customizable in-game console for development, debugging, cheats, etc... for Godot 4" +config/description="Light-weight customizable in-game console for development, debugging, cheats, etc... for Godot 4." run/main_scene="res://addons/gdshell/demo/demo.tscn" -config/features=PackedStringArray("4.0", "Forward Plus") +config/features=PackedStringArray("4.1") config/icon="res://addons/gdshell/icon.png" -config/tags=PackedStringArray() [autoload] @@ -16,16 +23,20 @@ GDShell="*res://addons/gdshell/scripts/gdshell_main.gd" [debug] gdscript/warnings/exclude_addons=false -gdscript/warnings/return_value_discarded=1 gdscript/warnings/unsafe_property_access=1 gdscript/warnings/unsafe_method_access=1 gdscript/warnings/unsafe_cast=1 gdscript/warnings/unsafe_call_argument=1 +gdscript/warnings/return_value_discarded=1 [editor_plugins] enabled=PackedStringArray("res://addons/gdshell/plugin.cfg") +[filesystem] + +import/blender/enabled=false + [input] gdshell_toggle_ui={ @@ -33,3 +44,7 @@ gdshell_toggle_ui={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) ] } + +[rendering] + +renderer/rendering_method="gl_compatibility" From 4d736c51dcafee2debdf29e65eb0cad3e6370b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Sat, 26 Aug 2023 19:21:57 +0200 Subject: [PATCH 15/20] GDShell rewrite is now working! --- addons/gdshell/scripts/gdshell_command.gd | 6 +- .../gdshell/scripts/gdshell_command_runner.gd | 392 +++++++----------- addons/gdshell/scripts/gdshell_main.gd | 205 +++++---- 3 files changed, 246 insertions(+), 357 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command.gd b/addons/gdshell/scripts/gdshell_command.gd index 43d7093..c984ee9 100644 --- a/addons/gdshell/scripts/gdshell_command.gd +++ b/addons/gdshell/scripts/gdshell_command.gd @@ -8,10 +8,10 @@ class CommandResult: var err_string: String var data: Variant - func _init(_err: int=0, _err_string: String="", _data: Variant=null) -> void: + func _init(_err: int=OK, _err_string: String="", _data: Variant=null) -> void: err = _err + err_string = "No error description." if _err != OK and _err_string.is_empty() else _err_string data = _data - err_string = "No error description." if _err != 0 and _err_string.is_empty() else _err_string signal command_end @@ -24,7 +24,7 @@ var COMMAND_AUTO_ALIASES: Dictionary = {} var _PARENT_COMMAND_RUNNER: GDShellCommandRunner -func _main(_argv: Array[String], _data) -> CommandResult: +func _main(_argv: Array[String], _data: CommandResult) -> CommandResult: return CommandResult.new() diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd b/addons/gdshell/scripts/gdshell_command_runner.gd index 5caa100..f84bf40 100644 --- a/addons/gdshell/scripts/gdshell_command_runner.gd +++ b/addons/gdshell/scripts/gdshell_command_runner.gd @@ -4,17 +4,21 @@ extends Node # Command execution flags -#const F_EXECUTE_CONDITION_MET: int = 1 #1 << 0 -#const F_PIPE_PREVIOUS: int = 2 #1 << 1 -#const F_BACKGROUND: int = 4 #1 << 2 -#const F_NEGATED: int = 8 #1 << 3 +const F_EXECUTE_CONDITION_MET: int = 1 +const F_PIPE_PREVIOUS: int = 2 +const F_BACKGROUND: int = 4 +const F_NEGATED: int = 8 -#var _PARENT_GDSHELL: GDShellMain +var _PARENT_GDSHELL: GDShellMain var _background_commands: Array[GDShellCommand] = [] -func execute(parser_result: GDShellCommandParser.ParserResult, piped_data: Variant=null) -> GDShellCommand.CommandResult: +func _init() -> void: + name = "GDShellCommandRunner - " + + +func execute(parser_result: GDShellCommandParser.ParserResult, piped_result: GDShellCommand.CommandResult=null) -> GDShellCommand.CommandResult: if parser_result.status != GDShellCommandParser.ParserResult.Status.OK: push_error("[GDShell] Attempted to run invalid GDShellCommandParser.ParserResult.") return null @@ -25,270 +29,174 @@ func execute(parser_result: GDShellCommandParser.ParserResult, piped_data: Varia push_error("[GDShell] Attempted to run a command, but GDShellCommandParser.ParserResult.command_db is null.") return null + var command_execution_flags: int = F_EXECUTE_CONDITION_MET + var last_command_result: GDShellCommand.CommandResult = piped_result + + var current_token_index: int = 0 + while current_token_index < parser_result.tokens.size(): + match parser_result.tokens[current_token_index].type: + + GDShellCommandParser.Token.Type.WORD: + var next_non_word_token_index: int = _get_next_non_word_token_index(parser_result.tokens, current_token_index) + var executed_command_result: GDShellCommand.CommandResult = await _execute_words( + parser_result.tokens.slice(current_token_index, next_non_word_token_index), + parser_result.command_db, + last_command_result, + command_execution_flags + ) + + if executed_command_result != null: + last_command_result = executed_command_result + + command_execution_flags &= F_EXECUTE_CONDITION_MET + current_token_index = next_non_word_token_index + + GDShellCommandParser.Token.Type.OPERATOR_PIPE: + pass + + GDShellCommandParser.Token.Type.OPERATOR_AND: + if not last_command_result.err: + command_execution_flags |= F_BACKGROUND + else: + command_execution_flags ^= F_BACKGROUND + + GDShellCommandParser.Token.Type.OPERATOR_OR: + if last_command_result.err: + command_execution_flags |= F_BACKGROUND + else: + command_execution_flags ^= F_BACKGROUND + + GDShellCommandParser.Token.Type.OPERATOR_NOT: + command_execution_flags |= F_NEGATED + + GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: + command_execution_flags |= F_BACKGROUND + + GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: + command_execution_flags |= F_EXECUTE_CONDITION_MET + + GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + var parentheses_content: Array[GDShellCommandParser.Token] = _get_parenthesis_inner_tokens(parser_result.tokens, current_token_index) + var executed_parentheses_result: GDShellCommand.CommandResult = await execute( + GDShellCommandParser.ParserResult.new( + GDShellCommandParser.ParserResult.Status.OK, + "", + parentheses_content, + parser_result.command_db + ) + ) + if executed_parentheses_result == null: + return null + + last_command_result = executed_parentheses_result + + GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + pass # Do nothing (don't delete this. This token should not trigger an error) + + var token_type: + push_error("[GDShell] GDShellCommandRunner encountered an unexpected '%s' token." % str(GDShellCommandParser.Token.Type.find_key(token_type))) + return null + + current_token_index += 1 + + return last_command_result + + +func _execute_words(tokens: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, last_result: GDShellCommand.CommandResult, command_execution_flags: int) -> GDShellCommand.CommandResult: + if not command_execution_flags & F_EXECUTE_CONDITION_MET: + return null + + var command_result: GDShellCommand.CommandResult = await _execute_command( + tokens, + command_db, + last_result if command_execution_flags & F_PIPE_PREVIOUS else null, + command_execution_flags & F_BACKGROUND + ) + if command_execution_flags & F_NEGATED: + command_result.err = OK if command_result.err else FAILED - return null -# return _execute_helper(parser_result.tokens) + return command_result -# This is a if monster, but it just checks if the right file is loaded. -func _execute_command(parser_result: GDShellCommandParser.ParserResult, start_word_token_index: int=0, piped_data: Variant=null, in_background: bool=false) -> GDShellCommand.CommandResult: - var command: GDShellCommand = parser_result.command_db.get_gdshell_command_instance(parser_result.tokens[start_word_token_index].content) +func _execute_command(words: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, piped_result: GDShellCommand.CommandResult=null, in_background: bool=false) -> GDShellCommand.CommandResult: + # Get argv + var argv: Array = words.map( + func(token: GDShellCommandParser.Token) -> String: + return token.content + ) + + # Create command instance + var command: GDShellCommand = command_db.get_gdshell_command_instance(argv[0]) if command == null: return null + # Set up command + command._PARENT_COMMAND_RUNNER = self command.name = "GDShellCommand: " + command.COMMAND_NAME add_child(command, true) if in_background: command.name += " (in background)" _background_commands.append(command) - var last_word_token_index: int = start_word_token_index - while true: - start_word_token_index += 1 - if last_word_token_index > parser_result.tokens.size(): - break - if parser_result.tokens[last_word_token_index].type != GDShellCommandParser.Token.Type.WORD: - break - - var words: Array[GDShellCommandParser.Token] = parser_result.tokens.slice(start_word_token_index, last_word_token_index) + # Run command + @warning_ignore("redundant_await") # We don't know if the user override will have await + var command_result: GDShellCommand.CommandResult = await command._main(argv, piped_result) - return null + # Cleanup command + _background_commands.erase(command) + command.queue_free() + return command_result +func _get_next_non_word_token_index(tokens: Array[GDShellCommandParser.Token], starting_from: int) -> int: + var next_non_word_token_index: int = starting_from + 1 + while next_non_word_token_index < tokens.size(): + if tokens[next_non_word_token_index].type != GDShellCommandParser.Token.Type.WORD: + break + next_non_word_token_index += 1 + return next_non_word_token_index +func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: + var current: int = opening_parenthesis_index + var parenthesis_level: int = 1 + + while current < tokens.size(): + if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + parenthesis_level += 1 + elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + parenthesis_level -= 1 + if parenthesis_level == 0: + return current + return -1 +func _get_parenthesis_inner_tokens(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> Array[GDShellCommandParser.Token]: + var matching_parenthesis_index: int = _find_matching_parenthesis_index(tokens, opening_parenthesis_index) + if matching_parenthesis_index == -1: + return [] + return tokens.slice(opening_parenthesis_index + 1, matching_parenthesis_index) -# print(command_script.new().get_script == ) -# var command_gdscript: GDScript = (command_script as GDScript).new() -# if not command_gdscript is GDShellCommand: -# pass -# push_error("can't load (non existent command)") -# return {} -# -# var command: GDShellCommand = command_script.new() -# add_child(command) -# -# if background: -# _background_commands.append(command) -# -# var command_result: Dictionary = await command._main( -# words.map(func(word: GDShellCommandParser.Token) -> String: return word.content), -# piped_data -# ) -# -# # Validate the command result -# if typeof(command_result) != TYPE_DICTIONARY: -# push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY. -# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." % words[0].content) -# # This assert statement acts as a hard error in the editor -# assert(typeof(command_result) == TYPE_DICTIONARY, """[GDShell] The command does not return a value of TYPE_DICTIONARY. -# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead. -# See the Errors for more information about the failing command.""") -# command_result = GDShellCommand.DEFAULT_COMMAND_RESULT -# else: -## @warning_ignore("unsafe_method_access") -# command_result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) -# -# _background_commands.erase(command) -# command.queue_free() -# return command_result - - -#func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: -# var current: int = opening_parenthesis_index -# var parenthesis_level: int = 1 -# -# while current < tokens.size(): -# if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: -# parenthesis_level += 1 -# elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: -# parenthesis_level -= 1 -# if parenthesis_level == 0: -# return current -# return -1 -# return null - - -#func _execute_helper(tokens: Array[GDShellCommandParser.Token], piped_data: Variant=null) -> Dictionary: -# var last_command_result: Dictionary = {} -# var word_accum: Array[GDShellCommandParser.Token] = [] -# var current_command_flags: int = F_EXECUTE_CONDITION_MET -# -# var current: int = 0 -# -# -# var execute_command_helper: Callable = func() -> void: -# if not current_command_flags & F_EXECUTE_CONDITION_MET: -# current_command_flags = F_EXECUTE_CONDITION_MET -# word_accum.clear() -# return -# -# if current_command_flags & F_BACKGROUND: -## last_command_result = GDShellCommand.CommandResult.new() -# _execute_command( -# word_accum, -# last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, -# true -# ) -# else: -# last_command_result = await _execute_command( -# word_accum, -# last_command_result if current_command_flags & F_PIPE_PREVIOUS else null, -# false -# ) -# -# if current_command_flags & F_NEGATED: -# last_command_result["error"] = 0 if last_command_result["error"] else 1 -# -# current_command_flags = F_EXECUTE_CONDITION_MET -# word_accum.clear() -# -# -# var execute_parenthesis_helper: Callable = func() -> int: -# # find the closing parenthesis index -# var parenthesis_level: int = 1 -# while current < tokens.size(): -# if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: -# parenthesis_level += 1 -# elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: -# parenthesis_level -= 1 -# if parenthesis_level == 0: -# break -# current += 1 -# -# -# -# return -1 -# -# -# while current < tokens.size(): -# match tokens[current]: -# GDShellCommandParser.Token.Type.WORD: -# word_accum.append(tokens[current]) -# -# GDShellCommandParser.Token.Type.OPERATOR_PIPE: -# current_command_flags |= F_PIPE_PREVIOUS -# execute_command_helper.call() -# -# GDShellCommandParser.Token.Type.OPERATOR_AND: -# execute_command_helper.call() -# if last_command_result["error"] != 0: -# current_command_flags &= ~F_EXECUTE_CONDITION_MET -# -# GDShellCommandParser.Token.Type.OPERATOR_OR: -# execute_command_helper.call() -# if last_command_result["error"] == 0: -# current_command_flags &= ~F_EXECUTE_CONDITION_MET -# -# GDShellCommandParser.Token.Type.OPERATOR_NOT: -# current_command_flags |= F_NEGATED -# -# GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: -# current_command_flags |= F_BACKGROUND -# execute_command_helper.call() -# -# GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: -# execute_command_helper.call() -# -# GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: -# execute_parenthesis_helper.call() - - - -# var matching_parenthesis_index: int = _find_matching_parenthesis_index(tokens, current) -# if matching_parenthesis_index == -1: -# push_error("no matching parenthesis") -# return {} -# -# -# if not current_command_flags & F_EXECUTE_CONDITION_MET: # negace + background (pipe) -# current = matching_parenthesis_index + 1 # check if it is a & -# continue -# -# if tokens[matching_parenthesis_index+1].type == GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: -# current_command_flags |= F_BACKGROUND -# -# -# last_command_result = _execute_helper(tokens.slice(current+1, matching_parenthesis_index+1)) -# -# -# -# var original: Array[GDShellCommandParser.Token] = parser_result.result -# parser_result.result = parser_result.result.slice(current+1, matching_parenthesis_index+1) -# last_command_result = execute(parser_result) -# parser_result.result = original - - -# _: # ERROR, SPACE, WORD_UNTERMINATED, OPERATOR_EXPAND, OPERATOR_CLOSING_PARENTHESIS -# push_error("Unexpected token. These tokens should be handled earlier") -# return {} -# -# current += 1 -# -# return {} +func _handle_execute(command: String) -> GDShellCommand.CommandResult: + return await _PARENT_GDSHELL.execute(command) -#func _execute_command(words: Array[GDShellCommandParser.Token], piped_data: Variant=null, background: bool=false) -> Dictionary: -# var command_script: Resource = ResourceLoader.load(_find_command_path(words[0].content), "GDScript") -# if not command_script: -# push_error("can't load (non existent command)") -# return {} -# -# var command: GDShellCommand = command_script.new() -# add_child(command) -# -# if background: -# _background_commands.append(command) -# -# var command_result: Dictionary = await command._main( -# words.map(func(word: GDShellCommandParser.Token) -> String: return word.content), -# piped_data -# ) -# -# # Validate the command result -# if typeof(command_result) != TYPE_DICTIONARY: -# push_error("[GDShell] The '%s' command does not return a value of TYPE_DICTIONARY. -# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead." % words[0].content) -# # This assert statement acts as a hard error in the editor -# assert(typeof(command_result) == TYPE_DICTIONARY, """[GDShell] The command does not return a value of TYPE_DICTIONARY. -# 'GDShellCommand.DEFAULT_COMMAND_RESULT' will be returned instead. -# See the Errors for more information about the failing command.""") -# command_result = GDShellCommand.DEFAULT_COMMAND_RESULT -# else: -## @warning_ignore("unsafe_method_access") -# command_result.merge(GDShellCommand.DEFAULT_COMMAND_RESULT) -# -# _background_commands.erase(command) -# command.queue_free() -# return command_result +func _handle_input(command: GDShellCommand, out: String) -> String: + if command in _background_commands: + return "" + return await _PARENT_GDSHELL._request_input_from_ui_handler(out) -#func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: -# var current: int = opening_parenthesis_index -# var parenthesis_level: int = 1 -# -# while current < tokens.size(): -# if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: -# parenthesis_level += 1 -# elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: -# parenthesis_level -= 1 -# if parenthesis_level == 0: -# return current -# return -1 +func _handle_output(out: String, append_new_line: bool = true) -> void: + _PARENT_GDSHELL._request_output_from_ui_handler(out, append_new_line) +func _handle_get_ui_handler() -> GDShellUIHandler: + return _PARENT_GDSHELL.get_ui_handler() -############################################## -# GDShellCommand-GDShell interface functions # -############################################## +func _handle_get_ui_handler_rich_text_label() -> RichTextLabel: + return _PARENT_GDSHELL.get_ui_handler_rich_text_label() -#func _handle_execute(command: String) -> GDShellCommand.CommandResult: -# return await _PARENT_GDSHELL.execute(command) -# -#func _find_command_path(command_name: String) -> String: -# return "" diff --git a/addons/gdshell/scripts/gdshell_main.gd b/addons/gdshell/scripts/gdshell_main.gd index 6e62cd9..72111b7 100644 --- a/addons/gdshell/scripts/gdshell_main.gd +++ b/addons/gdshell/scripts/gdshell_main.gd @@ -21,65 +21,47 @@ var _input_buffer: String = "" -# a + (b - c) + !d& - func _ready() -> void: - var x: GDShellCommandParser.ParserResult = GDShellCommandParser.parse("echo | echo", null) - print("Input: ", x.input) - print("Status: ", x.status) - print("AST: ", x.ast) - print("Tokens: ", x.tokens) - print("Err index: ", x.err_token_index) - print("Err: ", x.err_string) - - printerr("tokens:") - for token in x.tokens: - print(token.content) -# print(x.result) -# var a = "" -# for i in x.err_index: -# a += " " -# print(a+"^") -# print(GDShellCommandParser._tokenize_text("a 'abc'", 0).consumed) -# print(x.status) - - -#func setup_with_default_values() -> void: -# setup_command_runner() -# setup_command_db(COMMAND_DIR_PATH) -# setup_ui_handler(load_ui_handler_from_path(UI_HANDLER_PATH), true) + setup_with_default_values() + + +func setup_with_default_values() -> void: + setup_command_runner() + setup_command_db("res://addons/gdshell/commands/") + setup_ui_handler(load_ui_handler_from_path("res://addons/gdshell/ui/default_ui/default_ui.tscn"), true) + execute("autorun") # if execute_autorun_on_startup: # execute_autorun() -#func setup_command_runner() -> void: -# command_runner = GDShellCommandRunner.new() -# command_runner._PARENT_GDSHELL = self -# add_child(command_runner) -# -# -#func setup_command_db(command_dir_path: String="") -> void: -# command_db = GDShellCommandDB.new() -# if not command_dir_path.is_empty(): -# command_db.add_commands_in_directory(command_dir_path) -# -# -#func setup_ui_handler(handler: GDShellUIHandler, add_as_child: bool=true) -> void: -# ui_handler = handler -# ui_handler._PARENT_GDSHELL = self -# ui_handler.set_visible(false) -# -# if add_as_child: -# var canvas_layer: CanvasLayer = CanvasLayer.new() -# canvas_layer.layer = GDSHELL_CANVAS_LAYER -# canvas_layer.add_child(handler) -# add_child(canvas_layer) +func setup_command_runner() -> void: + command_runner = GDShellCommandRunner.new() + command_runner._PARENT_GDSHELL = self + add_child(command_runner) -#func _input(event: InputEvent) -> void: -# if event.is_action(UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): -# ui_handler.toggle_visible() +func setup_command_db(command_dir_path: String="") -> void: + command_db = GDShellCommandDB.new() + if not command_dir_path.is_empty(): + command_db.add_commands_in_directory(command_dir_path) + + +func setup_ui_handler(handler: GDShellUIHandler, add_as_child: bool=true) -> void: + ui_handler = handler + ui_handler._PARENT_GDSHELL = self + ui_handler.set_visible(false) + + if add_as_child: + var canvas_layer: CanvasLayer = CanvasLayer.new() + canvas_layer.layer = 100 + canvas_layer.add_child(handler) + add_child(canvas_layer) + + +func _input(event: InputEvent) -> void: + if event.is_action(UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): + ui_handler.toggle_visible() #func execute_autorun() -> void: @@ -87,67 +69,66 @@ func _ready() -> void: # execute("autorun") -#func execute(command: String) -> Dictionary: -# var command_sequence: Dictionary = GDShellCommandParser.parse(command, command_db) -# if command_sequence["status"] == GDShellCommandParser.ParserResultStatus.OK: -# return await command_runner.execute(command_sequence) -# return command_sequence -# -# -#func get_ui_handler() -> GDShellUIHandler: -# return ui_handler -# -# -#func get_ui_handler_rich_text_label() -> RichTextLabel: -# return ui_handler._get_output_rich_text_label() -# -# -#func _request_input_from_ui_handler(out: String="") -> String: -# _is_command_awaiting_input = true -# ui_handler._input_requested.emit(out) -# return await _input_submitted -# -# -#func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: -# ui_handler._output_requested.emit(output, append_new_line) -# -# -#func _submit_input(input: String) -> void: -# if _is_command_awaiting_input: -# _is_command_awaiting_input = false -# _request_output_from_ui_handler(input, true) -# _input_submitted.emit(input) -# return -# -# -# _input_buffer += input -# var command_sequence: Dictionary = GDShellCommandParser.parse(_input_buffer, command_db) -# match command_sequence["status"]: -# GDShellCommandParser.ParserResultStatus.OK: -# _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) -# _input_buffer = "" -# await command_runner.execute(command_sequence) -# ui_handler._input_requested.emit("") -# GDShellCommandParser.ParserResultStatus.UNTERMINATED: -# _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) -# ui_handler._input_requested.emit("> ") -# GDShellCommandParser.ParserResultStatus.ERROR: -# _request_output_from_ui_handler(ui_handler._get_input_prompt() + _input_buffer, true) -# _input_buffer = "" -# # TODO better error announcement -# _request_output_from_ui_handler("[color=red]%s[/color]" % command_sequence["result"]["error"], true) -# ui_handler._input_requested.emit("") -# -# -#static func load_ui_handler_from_path(path: String) -> GDShellUIHandler: -# return load(path).instantiate() as GDShellUIHandler -# -# -#static func get_gdshell_version() -> String: -# var config: ConfigFile = ConfigFile.new() -# if config.load("res://addons/gdshell/plugin.cfg"): -# return "Unknown" -# return str(config.get_value("plugin", "version", "Unknown")) +func execute(command: String) -> GDShellCommand.CommandResult: + var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(command, command_db) + if parser_result.status == GDShellCommandParser.ParserResult.Status.OK: + return await command_runner.execute(parser_result) + return null + + +func get_ui_handler() -> GDShellUIHandler: + return ui_handler + + +func get_ui_handler_rich_text_label() -> RichTextLabel: + return ui_handler._get_output_rich_text_label() + + +func _request_input_from_ui_handler(out: String="") -> String: + _is_command_awaiting_input = true + ui_handler._input_requested.emit(out) + return await _input_submitted + + +func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: + ui_handler._output_requested.emit(output, append_new_line) + + +func _submit_input(input: String) -> void: + if _is_command_awaiting_input: + _is_command_awaiting_input = false + _request_output_from_ui_handler(input, true) + _input_submitted.emit(input) + return + + _input_buffer += input + var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(_input_buffer, command_db) + match parser_result.status: + GDShellCommandParser.ParserResult.Status.OK: + _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) + _input_buffer = "" + await command_runner.execute(parser_result) + ui_handler._input_requested.emit("") + GDShellCommandParser.ParserResult.Status.UNTERMINATED: + _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) + ui_handler._input_requested.emit("> ") + GDShellCommandParser.ParserResult.Status.ERROR: + _request_output_from_ui_handler(ui_handler._get_input_prompt() + _input_buffer, true) + _input_buffer = "" + # TODO better error announcement +# _request_output_from_ui_handler("[color=red]%s[/color]" % parser_result["result"]["error"], true) + ui_handler._input_requested.emit("") + + +static func load_ui_handler_from_path(path: String) -> GDShellUIHandler: + return load(path).instantiate() as GDShellUIHandler + + +static func get_gdshell_version() -> String: + var config: ConfigFile = ConfigFile.new() + if config.load("res://addons/gdshell/plugin.cfg"): + return "Unknown" + return str(config.get_value("plugin", "version", "Unknown")) # # #func _input(event: InputEvent) -> void: From 25c0273f1be7ccd24f60c5ce220102133c349313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Sun, 27 Aug 2023 14:40:40 +0200 Subject: [PATCH 16/20] Make argv Array correctly typed --- addons/gdshell/scripts/gdshell_command_runner.gd | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd b/addons/gdshell/scripts/gdshell_command_runner.gd index f84bf40..0f3887e 100644 --- a/addons/gdshell/scripts/gdshell_command_runner.gd +++ b/addons/gdshell/scripts/gdshell_command_runner.gd @@ -121,10 +121,11 @@ func _execute_words(tokens: Array[GDShellCommandParser.Token], command_db: GDShe func _execute_command(words: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, piped_result: GDShellCommand.CommandResult=null, in_background: bool=false) -> GDShellCommand.CommandResult: # Get argv - var argv: Array = words.map( + # Fancy way to make the Array typed + var argv: Array = Array(words.map( func(token: GDShellCommandParser.Token) -> String: return token.content - ) + ), TYPE_STRING, "", null) # Create command instance var command: GDShellCommand = command_db.get_gdshell_command_instance(argv[0]) From 362cdf21dfada43d3fd1b0b3430de0b89f62defb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Wed, 5 Mar 2025 20:26:07 +0100 Subject: [PATCH 17/20] changes --- addons/gdshell/commands/autorun.gd | 4 + .../commands/default_commands/alias.gd | 16 +- .../gdshell/commands/default_commands/bool.gd | 20 +- .../commands/default_commands/clear.gd | 16 +- .../gdshell/commands/default_commands/echo.gd | 6 +- .../commands/default_commands/gdfetch.gd | 16 +- .../gdshell/commands/default_commands/man.gd | 18 +- .../monitor_overlay/monitor.gd | 5 +- addons/gdshell/demo/demo.gd | 15 +- addons/gdshell/demo/demo.tscn | 3 + addons/gdshell/plugin.cfg | 2 +- .../gdshell_expression_compiler.gd | 181 +++++++++ .../gdshell_expression_tokenizer.gd | 272 +++++++++++++ .../gsdhell_expression_parser.gd | 319 +++++++++++++++ addons/gdshell/scripts/gdshell_command.gd | 25 +- addons/gdshell/scripts/gdshell_command_db.gd | 4 +- .../gdshell/scripts/gdshell_command_parser.gd | 372 ------------------ .../gdshell/scripts/gdshell_command_runner.gd | 251 ++++-------- .../scripts/gdshell_command_runner_old.gd | 203 ++++++++++ addons/gdshell/scripts/gdshell_main.gd | 254 +++++++----- addons/gdshell/scripts/gdshell_ui_handler.gd | 49 ++- addons/gdshell/ui/default_ui/default_ui.gd | 29 +- .../roboto_mono/RobotoMono-Bold.ttf.import | 1 + .../RobotoMono-BoldItalic.ttf.import | 1 + .../roboto_mono/RobotoMono-Italic.ttf.import | 1 + .../roboto_mono/RobotoMono-Regular.ttf.import | 1 + project.godot | 6 +- 27 files changed, 1353 insertions(+), 737 deletions(-) rename addons/gdshell/commands/{plugin_integrations => plugin_integration_commands}/monitor_overlay/monitor.gd (97%) create mode 100644 addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd create mode 100644 addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd create mode 100644 addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd delete mode 100644 addons/gdshell/scripts/gdshell_command_parser.gd create mode 100644 addons/gdshell/scripts/gdshell_command_runner_old.gd diff --git a/addons/gdshell/commands/autorun.gd b/addons/gdshell/commands/autorun.gd index bf3a8f3..ba8eedf 100644 --- a/addons/gdshell/commands/autorun.gd +++ b/addons/gdshell/commands/autorun.gd @@ -2,5 +2,9 @@ extends GDShellCommand func _main(_argv: Array, _data) -> CommandResult: + output("Autorun here...") + #var x = await input("gimme text: ") # TODO input() blocks all following input + #output("hi %s" % x) + #execute("echo hi") # execute("gdfetch") return CommandResult.new() diff --git a/addons/gdshell/commands/default_commands/alias.gd b/addons/gdshell/commands/default_commands/alias.gd index 5f9771c..5280026 100644 --- a/addons/gdshell/commands/default_commands/alias.gd +++ b/addons/gdshell/commands/default_commands/alias.gd @@ -1,12 +1,6 @@ extends GDShellCommand -func _init(): - COMMAND_AUTO_ALIASES = { - "unalias": "alias -r", - } - - func _main(argv: Array, data) -> CommandResult: var success: bool @@ -24,6 +18,12 @@ func _main(argv: Array, data) -> CommandResult: return CommandResult.new() +func _get_command_auto_aliases() -> Dictionary: + return { + "unalias": "alias -r", + } + + func _get_manual() -> String: return ( """ @@ -58,8 +58,8 @@ func _get_manual() -> String: -Same as [i]alias -r print[/i] """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/bool.gd b/addons/gdshell/commands/default_commands/bool.gd index 22748a7..ebcba0c 100644 --- a/addons/gdshell/commands/default_commands/bool.gd +++ b/addons/gdshell/commands/default_commands/bool.gd @@ -13,14 +13,6 @@ var FALSE: CommandResult = CommandResult.new( ) -func _init(): - COMMAND_AUTO_ALIASES = { - "true": "bool -t", - "false": "bool -f", - "random": "bool -r", - } - - func _main(argv: Array, _data) -> CommandResult: if not argv.size() > 1: return TRUE @@ -41,6 +33,14 @@ func _main(argv: Array, _data) -> CommandResult: ) +func _get_command_auto_aliases(): + return { + "true": "bool -t", + "false": "bool -f", + "random": "bool -r", + } + + func _get_manual() -> String: return ( """ @@ -79,8 +79,8 @@ func _get_manual() -> String: Same as: random && echo "true" || echo "false" """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/clear.gd b/addons/gdshell/commands/default_commands/clear.gd index a0c6173..1754be9 100644 --- a/addons/gdshell/commands/default_commands/clear.gd +++ b/addons/gdshell/commands/default_commands/clear.gd @@ -1,18 +1,18 @@ extends GDShellCommand -func _init(): - COMMAND_AUTO_ALIASES = { - "cls": "clear", - } - - func _main(_argv: Array, _data) -> CommandResult: # Truly unbelieveable programming skills get_ui_handler_rich_text_label().clear() return CommandResult.new() +func _get_command_auto_aliases(): + return { + "cls": "clear", + } + + func _get_manual() -> String: return ( """ @@ -33,8 +33,8 @@ func _get_manual() -> String: -Same as [i]clear[/i] """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/echo.gd b/addons/gdshell/commands/default_commands/echo.gd index 56ab98a..ae15a26 100644 --- a/addons/gdshell/commands/default_commands/echo.gd +++ b/addons/gdshell/commands/default_commands/echo.gd @@ -13,7 +13,7 @@ func _main(argv: Array, data) -> CommandResult: output(out) @warning_ignore("incompatible_ternary") - return CommandResult.new(0, "", null if out.is_empty() else out) + return CommandResult.new(OK, "", null if out.is_empty() else out) func _get_manual() -> String: @@ -40,8 +40,8 @@ SYNOPSIS -Prints Hello 1 World! """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/gdfetch.gd b/addons/gdshell/commands/default_commands/gdfetch.gd index a83f2ac..6022c4e 100644 --- a/addons/gdshell/commands/default_commands/gdfetch.gd +++ b/addons/gdshell/commands/default_commands/gdfetch.gd @@ -35,12 +35,6 @@ const LOGO: String = ( ) -func _init(): - COMMAND_AUTO_ALIASES = { - "neofetch": "gdfetch --i-am-a-linux-nerd-and-tried-to-use-neofetch", - } - - func _main(argv: Array, data) -> CommandResult: var info: Dictionary = get_info() @@ -85,6 +79,12 @@ static func get_info() -> Dictionary: } +func _get_command_auto_aliases(): + return { + "neofetch": "gdfetch --i-am-a-linux-nerd-and-tried-to-use-neofetch", + } + + func _get_manual() -> String: return ( """ @@ -116,8 +116,8 @@ func _get_manual() -> String: Can be used as a input for other commands when called silently. """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/default_commands/man.gd b/addons/gdshell/commands/default_commands/man.gd index 5a67583..c0b6fba 100644 --- a/addons/gdshell/commands/default_commands/man.gd +++ b/addons/gdshell/commands/default_commands/man.gd @@ -5,13 +5,6 @@ const LIST_FLAGS: Array[String] = ["l", "L", "list", "LIST"] const SILENT_FLAGS: Array[String] = ["s", "S", "silent", "SILENT"] -func _init(): - COMMAND_AUTO_ALIASES = { - "manual": "man", - "help": "man", - } - - func _main(argv: Array, _data) -> CommandResult: if not argv.size() > 1: output("What manual page do you want? For example, try '[b]man man[/b]'\nTo see the list of all commands run '[b]man --list[/b]'") @@ -67,6 +60,13 @@ func get_command_manual(command_name: String) -> String: return manual +func _get_command_auto_aliases() -> Dictionary: + return { + "manual": "man", + "help": "man", + } + + func _get_manual() -> String: return ( """ @@ -90,8 +90,8 @@ func _get_manual() -> String: -Prints the manual for the [i]man[/i] command """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) diff --git a/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd similarity index 97% rename from addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd rename to addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd index 7b39fa2..e135496 100644 --- a/addons/gdshell/commands/plugin_integrations/monitor_overlay/monitor.gd +++ b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd @@ -6,6 +6,7 @@ const MONITOR_NODE_NAME: String = "GDShellMonitorOverlayIntegration" const OPTIONS_FLAGS: Array[String] = ["o", "O", "options", "OPTIONS"] +# TODO implement my own PR # Workaround until https://github.com/godotengine/godot/pull/69624 gets merged const TYPE_NAMES: Array[String] = [ "Nil", @@ -165,6 +166,6 @@ func _get_manual() -> String: [i]monitor -fps=true --process=true --physics_process=false --sampling_rate=10[/i] -Enables fps and process monitors, disables physics_process monitor and sets sampling rate to 10 """.format({ - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), }) diff --git a/addons/gdshell/demo/demo.gd b/addons/gdshell/demo/demo.gd index a84c259..e926a71 100644 --- a/addons/gdshell/demo/demo.gd +++ b/addons/gdshell/demo/demo.gd @@ -3,21 +3,28 @@ extends CanvasItem const ICON_TO_VIEWPORT_RATIO: float = 0.309018 -var icon: Sprite2D +@onready var icon: Sprite2D = %GDShellIcon +@onready var label: Label = %Label func _ready() -> void: - $GDShellIcon/Label.text = "Press '%s' to toggle GDShell" % InputMap.action_get_events(GDShell.UI_TOGGLE_ACTION)[0].as_text_keycode() icon = $GDShellIcon + @warning_ignore("return_value_discarded") get_viewport().size_changed.connect(update_icon) update_icon() + + var gdshell_ui_toggle_action_input_events: Array[InputEvent] = InputMap.action_get_events(GDShell.ui_handler._UI_TOGGLE_ACTION) + if gdshell_ui_toggle_action_input_events.is_empty(): + label.text = "No InputEvent is set for GDShell Ui Toggle Action.\nSet an action in settings at 'gdshell/settings/ui/ui_toggle_action'." + else: + label.text = "Press '%s' to toggle GDShell" % (gdshell_ui_toggle_action_input_events[0] as InputEventKey).as_text_keycode() # responsive icon func update_icon() -> void: # scale the icon so that it takes up ICON_TO_VIEWPORT_RATIO of the viewport - var min_viewport_side: int = min(get_viewport_rect().size.x, get_viewport_rect().size.y) - var max_texture_side: int = max(icon.texture.get_size().x, icon.texture.get_size().y) + var min_viewport_side: float = min(get_viewport_rect().size.x, get_viewport_rect().size.y) + var max_texture_side: float = max(icon.texture.get_size().x, icon.texture.get_size().y) var scale_factor: float = (min_viewport_side / max_texture_side) * ICON_TO_VIEWPORT_RATIO icon.scale = Vector2(scale_factor, scale_factor) diff --git a/addons/gdshell/demo/demo.tscn b/addons/gdshell/demo/demo.tscn index 9f7567a..838a17c 100644 --- a/addons/gdshell/demo/demo.tscn +++ b/addons/gdshell/demo/demo.tscn @@ -13,9 +13,11 @@ grow_vertical = 2 script = ExtResource("1_cwk1x") [node name="GDShellIcon" type="Sprite2D" parent="."] +unique_name_in_owner = true texture = ExtResource("2_hso4g") [node name="Label" type="Label" parent="GDShellIcon"] +unique_name_in_owner = true anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 @@ -27,3 +29,4 @@ offset_right = -13.0 offset_bottom = 32.0 grow_horizontal = 2 text = "SAMPLE TEXT" +horizontal_alignment = 1 diff --git a/addons/gdshell/plugin.cfg b/addons/gdshell/plugin.cfg index 7c49478..353e40b 100644 --- a/addons/gdshell/plugin.cfg +++ b/addons/gdshell/plugin.cfg @@ -3,5 +3,5 @@ name="GDShell" description="Feature-packed customizable in-game console for development, debugging, cheats, etc... for Godot 4" author="Jakub Janšta (Kubulambula)" -version="1.0-dev2" +version="1.0-dev3" script="gdshell_editor_plugin.gd" diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd new file mode 100644 index 0000000..216636b --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd @@ -0,0 +1,181 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellExpressionCompiler +extends RefCounted + + +class CompilerResult extends RefCounted: + enum Status { + OK, + ERROR, + UNTERMINATED, + } + + var result: Dictionary + var status: Status + var input_expression: String + var error_description: String + var input_expression_error_start_index: int + var input_expression_error_length: int + + func _init(_result: Dictionary, _status: Status, _input_expression: String, _error_description: String, _input_expression_error_start_index: int, _input_expression_error_length: int) -> void: + result = _result + status = _status + input_expression = _input_expression + error_description = _error_description + input_expression_error_start_index = _input_expression_error_start_index + input_expression_error_length = _input_expression_error_length + + func _to_string() -> String: + return "{CompilerResult: %s, error_description: \"%s\"}" % [Status.find_key(status), error_description] + + +static func compile(input_expression: String) -> CompilerResult: + var tokenizer_result: GDShellExpressionTokenizer.TokenizerResult = GDShellExpressionTokenizer.tokenize(input_expression) + if tokenizer_result.status == GDShellExpressionTokenizer.TokenizerResult.Status.ERROR: + return CompilerResult.new( + {}, + CompilerResult.Status.ERROR, + input_expression, + tokenizer_result.description, + tokenizer_result.result[-1].start_char_index, + tokenizer_result.result[-1].consumed_chars + ) + if tokenizer_result.status == GDShellExpressionTokenizer.TokenizerResult.Status.UNTERMINATED: + return CompilerResult.new( + {}, + CompilerResult.Status.UNTERMINATED, + input_expression, + "unterminated expression", + tokenizer_result.result[-1].start_char_index, + tokenizer_result.result[-1].consumed_chars + ) + if tokenizer_result.result.is_empty(): # empty input - dont even bother with parsing + return CompilerResult.new( + {}, + CompilerResult.Status.OK, + input_expression, + tokenizer_result.description, + 0, + 0 + ) + + var parser_result: GDShellExpressionParser.ParserResult = GDShellExpressionParser.parse(tokenizer_result.result) + if parser_result.status == GDShellExpressionParser.ParserResult.Status.ERROR: + return CompilerResult.new( + parser_result.result, + CompilerResult.Status.ERROR, + input_expression, + parser_result.description, + parser_result.input_expression_error_start_index, + parser_result.input_expression_error_length + ) + + return CompilerResult.new( + parser_result.result, + CompilerResult.Status.OK, + input_expression, + parser_result.description, + parser_result.input_expression_error_start_index, + parser_result.input_expression_error_length + ) + + + +static func is_command_expression_valid(command_expression: Dictionary, error_info: bool = false, command_db: GDShellCommandDB = null) -> bool: + if not command_expression.has_all(["type", "name", "args"]): + return false + # type + if command_expression["type"] != "command": + return false + # name + if typeof(command_expression["name"]) != TYPE_STRING: + return false + if command_db != null: + push_error("CommandDB validation not yet implemented.") + # args + if typeof(command_expression["args"]) != TYPE_ARRAY: + return false + @warning_ignore("unsafe_method_access") + if command_expression["args"].any( + func(arg: Variant) -> bool: + return typeof(arg) != TYPE_STRING + ): + return false + # for reporting where the error occurred + if error_info: + if not command_expression.has_all(["index", "lenght"]): + return false + # index + if typeof(command_expression["index"]) != TYPE_INT: + return false + if command_expression["index"] < 0: + return false + # length + if typeof(command_expression["length"]) != TYPE_INT: + return false + if command_expression["length"] < 1: + return false + + return true + + +static func is_operator_expression_valid(operator_expression: Dictionary, error_info: bool = false) -> bool: + if not operator_expression.has_all(["type", "operator"]): + return false + # type + if operator_expression["type"] != "operator": + return false + # for reporting where the error occurred + if error_info: + if not operator_expression.has_all(["index", "lenght"]): + return false + # index + if typeof(operator_expression["index"]) != TYPE_INT: + return false + if operator_expression["index"] < 0: + return false + # length + if typeof(operator_expression["length"]) != TYPE_INT: + return false + if operator_expression["length"] < 1: + return false + # operator + if typeof(operator_expression["operator"]) != TYPE_STRING: + return false + match operator_expression["operator"]: + "!": + if not operator_expression.has("right"): + return false + if typeof(operator_expression["right"]) != TYPE_DICTIONARY: + return false + "&": + if not operator_expression.has("left"): + return false + if typeof(operator_expression["left"]) != TYPE_DICTIONARY: + return false + "|", "||", "&&", ";": + if not operator_expression.has_all(["left", "right"]): + return false + if typeof(operator_expression["left"]) != TYPE_DICTIONARY: + return false + if typeof(operator_expression["right"]) != TYPE_DICTIONARY: + return false + + return true + + +static func is_expression_valid(expression: Dictionary, error_info: bool = false, command_db: GDShellCommandDB = null) -> bool: + if expression.get("type") == "command": + return is_command_expression_valid(expression, error_info, command_db) + + elif expression.get("type") == "operator": + if is_operator_expression_valid(expression, error_info) == false: + return false + if expression.has("left"): + if is_expression_valid(expression["left"], error_info, command_db) == false: + return false + if expression.has("right"): + if is_expression_valid(expression["right"], error_info, command_db) == false: + return false + + return true diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd new file mode 100644 index 0000000..729db27 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd @@ -0,0 +1,272 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellExpressionTokenizer +extends RefCounted + + +class Token extends RefCounted: + enum Type { + # Error token - content of the token is an error message + ERROR, + # Parser tokens + EXPRESSION, + EXPRESSION_END, + EXPRESSION_HANDLE, + # Helper tokenizer token + SPACE, + # Tokenizer tokens + WORD, + WORD_UNTERMINATED, +# OPERATOR_EXPAND, + OPERATOR_NOT, + OPERATOR_BACKGROUND, + OPERATOR_AND, + OPERATOR_PIPE, + OPERATOR_OR, + OPERATOR_SEQUENCE, + OPERATOR_OPENING_PARENTHESIS, + OPERATOR_CLOSING_PARENTHESIS, + } + + var type: Type + var content: String + var start_char_index: int + var consumed_chars: int + + func _init(_type: Type, _content: String, _start_char_index: int, _consumed_chars: int) -> void: + self.type = _type + self.content = _content + self.start_char_index = _start_char_index + self.consumed_chars = _consumed_chars + + func _to_string() -> String: + return "{Token: %s, Content: \"%s\", Start char index: %s}" % [str(Type.find_key(type)), content, start_char_index] + + +class TokenizerResult extends RefCounted: + enum Status { + OK, + ERROR, + UNTERMINATED, + } + + var result: Array[Token] + var status: Status + var description: String + + func _init(_result: Array[Token], _status: TokenizerResult.Status, _description: String) -> void: + result = _result + status = _status + description = _description + + +static func tokenize(input_expression: String) -> TokenizerResult: + var tokens: Array[Token] = [] + var current_token: Token = null + var current_char_index: int = 0 + + if input_expression.is_empty(): + return TokenizerResult.new([], TokenizerResult.Status.OK, "empty input expression") + + while current_char_index < input_expression.length(): + match input_expression[current_char_index]: + " ": + current_token = _tokenize_space(input_expression, current_char_index) + ";": + current_token = _tokenize_semicolon(input_expression, current_char_index) + "!": + current_token = _tokenize_exclamation(input_expression, current_char_index) + #"$": # TODO variable operator + #tokens.push_back(_tokenize_dollar_sign(input_expression, current_char)) + "&": + current_token = _tokenize_and(input_expression, current_char_index) + "|": + current_token = _tokenize_vertical_slash(input_expression, current_char_index) + "(", ")": + current_token = _tokenize_parenthesis(input_expression, current_char_index) + "\"", "\'": + current_token = _tokenize_quote(input_expression, current_char_index) + _: + current_token = _tokenize_text(input_expression, current_char_index) + + current_char_index += current_token.consumed_chars + tokens.push_back(current_token) + + if current_token.type == Token.Type.ERROR: + return TokenizerResult.new( + tokens, + TokenizerResult.Status.ERROR, + current_token.content + ) + if current_token.type == Token.Type.WORD_UNTERMINATED: + return TokenizerResult.new( + tokens, + TokenizerResult.Status.UNTERMINATED, + "unterminated expression" + ) + + return TokenizerResult.new( + _filter_out_space_tokens(_merge_word_tokens(tokens)), + TokenizerResult.Status.OK, + "OK" + ) + + +static func _tokenize_space(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != " ": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as space" % input_expression[start_char_index], + start_char_index, + 0 + ) + + var space_chars_consumed: int = 0 + while start_char_index + space_chars_consumed < input_expression.length() and input_expression[start_char_index + space_chars_consumed] == " ": + space_chars_consumed += 1 + return Token.new(Token.Type.SPACE, " ", start_char_index, space_chars_consumed) + + +static func _tokenize_semicolon(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != ";": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as semicolon" % input_expression[start_char_index], + start_char_index, + 0 + ) + + return Token.new(Token.Type.OPERATOR_SEQUENCE, ";", start_char_index, 1) + + +static func _tokenize_exclamation(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "!": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as exclamation" % input_expression[start_char_index], + start_char_index, + 0 + ) + + return Token.new(Token.Type.OPERATOR_NOT, "!", start_char_index, 1) + + +static func _tokenize_and(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "&": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as and" % input_expression[start_char_index], + start_char_index, + 0 + ) + + if start_char_index < input_expression.length() - 1 and input_expression[start_char_index + 1] == "&": + return Token.new(Token.Type.OPERATOR_AND, "&&", start_char_index, 2) + return Token.new(Token.Type.OPERATOR_BACKGROUND, "&", start_char_index, 1) + + +static func _tokenize_vertical_slash(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "|": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as vertical slash" % input_expression[start_char_index], + start_char_index, + 0 + ) + + if start_char_index < input_expression.length() - 1 and input_expression[start_char_index + 1] == "|": + return Token.new(Token.Type.OPERATOR_OR, "||", start_char_index, 2) + return Token.new(Token.Type.OPERATOR_PIPE, "|", start_char_index, 1) + + +static func _tokenize_parenthesis(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] == "(": + return Token.new(Token.Type.OPERATOR_OPENING_PARENTHESIS, "(", start_char_index, 1) + elif input_expression[start_char_index] == ")": + return Token.new(Token.Type.OPERATOR_CLOSING_PARENTHESIS, ")", start_char_index, 1) + else: + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as parenthesis" % input_expression[start_char_index], + start_char_index, + 0 + ) + + +static func _tokenize_quote(input_expression: String, start_char_index: int) -> Token: + if input_expression[start_char_index] != "\"" and input_expression[start_char_index] != "'": + return Token.new( + Token.Type.ERROR, + "cannot tokenize \"%s\" as quote" % input_expression[start_char_index], + start_char_index, + 0 + ) + + var content: String = "" + for i: int in range(start_char_index + 1, input_expression.length()): # Skip the opening quote and start on the char right after + if input_expression[i] == input_expression[start_char_index] and input_expression[i - 1] != "\\": # check for string end + return Token.new( + Token.Type.WORD, + content.c_unescape(), + start_char_index, + content.length() + 2 # accounts for the starting and ending quotes + ) + content += input_expression[i] + + # End of input_expression was reached without finding a closing quote + return Token.new( + Token.Type.WORD_UNTERMINATED, + content.c_unescape(), + start_char_index, + content.length() + 1 # accounts just for the starting quote + ) + + +static func _tokenize_text(input_expression: String, start_char_index: int) -> Token: + var content: String = "" + + for i: int in range(start_char_index, input_expression.length()): + # check if the character should end the WORD token. + if input_expression[i] in [" ", ";", "&", "|", "!", "(", ")", "\'", "\"", "\\", "\a", "\b", "\f", "\n", "\r", "\t", "\v"]: + break + content += input_expression[i] + + if content.is_empty(): + return Token.new( + Token.Type.ERROR, + "cannot start tokenizing word", + start_char_index, + 0 + ) + + return Token.new( + Token.Type.WORD, + content, + start_char_index, + content.length() + ) + + +## Merges WORD tokens if they are not separated by any other token +static func _merge_word_tokens(tokens: Array[Token]) -> Array[Token]: + if tokens.is_empty(): + return tokens + # We now know that tokens is not empty so we append the first token for later simplification + var merged_tokens: Array[Token] = [tokens[0]] + + # Start from the second token as we already appended the first + for i: int in range(1, tokens.size()): + if tokens[i].type == Token.Type.WORD and merged_tokens[-1].type == Token.Type.WORD: + merged_tokens[-1].content += tokens[i].content + merged_tokens[-1].consumed_chars += tokens[i].consumed_chars + else: + merged_tokens.append(tokens[i]) + + return merged_tokens + + +## Filters out SPACE tokens as after _merge_word_tokens() they are useless and it simplifies next operations. +static func _filter_out_space_tokens(tokens: Array[Token]) -> Array[Token]: + return tokens.filter( + func is_token_not_space(token: Token) -> bool: + return token.type != Token.Type.SPACE + ) diff --git a/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd new file mode 100644 index 0000000..dea4bcf --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd @@ -0,0 +1,319 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellExpressionParser +extends RefCounted + + +const _PRECEDENCE_TABLE: Array[Array] = [ + # ! & && | || ; WORD ( ) $ + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &"!", &">"], # ! + [&">", &"!", &">", &">", &">", &">", &"!", &"!", &">", &">"], # & + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # && + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # | + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # || + [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &">", &">"], # ; + [&"!", &">", &">", &">", &">", &">", &"=", &"!", &">", &">"], # WORD + [&"<", &"<", &"<", &"<", &"<", &"<", &"<", &"<", &"=", &"!"], # ( + [&"<", &">", &">", &">", &">", &">", &"!", &"!", &">", &">"], # ) + [&"<", &"<", &"<", &"<", &"<", &"<", &"<", &"<", &"!", &"."], # $ +] + +const _PRECEDENCE_TABLE_KEYS: Array[GDShellExpressionTokenizer.Token.Type] = [ + GDShellExpressionTokenizer.Token.Type.OPERATOR_NOT, + GDShellExpressionTokenizer.Token.Type.OPERATOR_BACKGROUND, + GDShellExpressionTokenizer.Token.Type.OPERATOR_AND, + GDShellExpressionTokenizer.Token.Type.OPERATOR_PIPE, + GDShellExpressionTokenizer.Token.Type.OPERATOR_OR, + GDShellExpressionTokenizer.Token.Type.OPERATOR_SEQUENCE, + GDShellExpressionTokenizer.Token.Type.WORD, + GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, + GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS, + GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, +] + + +class ParserResult extends RefCounted: + enum Status { + OK, + ERROR, + } + + var result: Dictionary + var status: Status + var description: String + var input_expression_error_start_index: int + var input_expression_error_length: int + + func _init(_result: Dictionary, _status: Status, _description: String, _input_expression_error_start_index: int, _input_expression_error_length: int) -> void: + result = _result + status = _status + description = _description + input_expression_error_start_index = _input_expression_error_start_index + input_expression_error_length = _input_expression_error_length + + +static func parse(tokens: Array[GDShellExpressionTokenizer.Token]) -> ParserResult: + tokens.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)) # For easier precedence + var current_token_index: int = 0 # Index into tokens array + var token_stack: Array[GDShellExpressionTokenizer.Token] = [GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)] # Helper stack for precedence + var expression_node_stack: Array[Dictionary] = [] # Expresion tree building stack + + while current_token_index < tokens.size(): + var topmost_terminal_index: int = _parse_precedence_get_topmost_terminal_index(token_stack) + match _parse_get_precedence_action(token_stack[topmost_terminal_index], tokens[current_token_index]): + &"<": # Shift + if token_stack.insert(topmost_terminal_index + 1, GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, "<", 0, 0)) != OK: # isert handle for expression reduction + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "cannot insert EXPRESSION_HANDLE", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + token_stack.push_back(tokens[current_token_index]) + current_token_index += 1 + + &">": # Reduce + if _parse_precedence_reduce_to_expression(token_stack, expression_node_stack) == false: + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "cannot reduce stack to EXPRESSION. Token stack: '%s'" % str(token_stack), + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + &"=": # Push + token_stack.push_back(tokens[current_token_index]) + current_token_index += 1 + + &"!": # Error + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "error in precedence table at [%s,%s] with symbols [%s,%s]" % [ + _parse_get_precedence_index(token_stack[topmost_terminal_index]), + _parse_get_precedence_index(tokens[current_token_index]), + GDShellExpressionTokenizer.Token.Type.find_key(_parse_get_precedence_index(token_stack[topmost_terminal_index])), + GDShellExpressionTokenizer.Token.Type.find_key(_parse_get_precedence_index(tokens[current_token_index])) + ], + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + &".": # OK + return ParserResult.new( + {} if expression_node_stack.is_empty() else expression_node_stack[0], + ParserResult.Status.OK, + "OK", + 0, + 0 + ) + + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "parsing ended prematurely due to token buffer out of bounds.", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + +static func _parse_get_reduceable_tokens(token_stack: Array[GDShellExpressionTokenizer.Token]) -> Array[GDShellExpressionTokenizer.Token]: + for i: int in range(token_stack.size() - 1, -1, -1): + if token_stack[i].type == GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE: + return token_stack.slice(i) + return [] + + +static func _parse_precedence_is_token_array_type_patern_match(tokens: Array[GDShellExpressionTokenizer.Token], pattern: Array[GDShellExpressionTokenizer.Token.Type]) -> bool: + if tokens.size() != pattern.size(): + return false + for i: int in tokens.size(): + if tokens[i].type != pattern[i]: + return false + return true + + +static func _parse_precedence_reduce_to_expression(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: + var reduceable_tokens: Array[GDShellExpressionTokenizer.Token] = _parse_get_reduceable_tokens(token_stack) + match reduceable_tokens.map(func(token: GDShellExpressionTokenizer.Token) -> GDShellExpressionTokenizer.Token.Type: return token.type): + # E -> WORD+ + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.WORD, ..]: + _parse_reduce_words(token_stack, expression_node_stack) + # E -> (E) + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS]: + _parse_reduce_parenthesis(token_stack) + # E -> !E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_NOT, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + _parse_reduce_not(token_stack, expression_node_stack) + # E -> E& + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_BACKGROUND]: + _parse_reduce_background(token_stack, expression_node_stack) + # E -> E && E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_AND, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + _parse_reduce_and(token_stack, expression_node_stack) + # E -> E | E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_PIPE, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + _parse_reduce_pipe(token_stack, expression_node_stack) + # E -> E || E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_OR, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + _parse_reduce_or(token_stack, expression_node_stack) + # E -> E ; E + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_SEQUENCE, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: + _parse_reduce_sequence(token_stack, expression_node_stack) + # ! -> () + [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS]: + return false + # ! -> .. + var _unmatched_reduceable_tokens_pattern: # Unknown unreduceable stack + return false + return true + + +# E -> WORD* +static func _parse_reduce_words(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: + var word_tokens: Array[GDShellExpressionTokenizer.Token] = [] + while token_stack.back() != null: + var current_token: GDShellExpressionTokenizer.Token = token_stack.pop_back() + if current_token.type == GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE: + break + word_tokens.push_front(current_token) + + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + expression_node_stack.push_back({ + "type": "command", + "index": word_tokens[0].start_char_index, + "length": word_tokens[0].consumed_chars, + "name": word_tokens.pop_front().content, + "args": word_tokens.map( + func(word_token: GDShellExpressionTokenizer.Token) -> String: + return word_token.content + ), + }) + + +# E -> (E) +static func _parse_reduce_parenthesis(token_stack: Array[GDShellExpressionTokenizer.Token]) -> void: + token_stack.pop_back() # ) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # ( + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + + +static func _parse_reduce_not(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: + var right: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": 1, + "operator": "!", + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_NOT + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + + +static func _parse_reduce_background(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-1].start_char_index, + "length": 1, + "operator": "&", + "left": left, + }) + token_stack.pop_back() # OPERATOR_BACKGROUND + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + + +static func _parse_reduce_and(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": 2, + "operator": "&&", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_AND + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + + +static func _parse_reduce_pipe(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": 1, + "operator": "|", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_PIPE + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + + +static func _parse_reduce_or(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": 2, + "operator": "||", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_OR + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + + +static func _parse_reduce_sequence(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: + var right: Dictionary = expression_node_stack.pop_back() + var left: Dictionary = expression_node_stack.pop_back() + expression_node_stack.push_back({ + "type": "operator", + "index": token_stack[-2].start_char_index, + "length": 1, + "operator": ";", + "left": left, + "right": right, + }) + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # OPERATOR_SEQUENCE + token_stack.pop_back() # EXPRESSION + token_stack.pop_back() # EXPRESSION_HANDLE + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + + +static func _parse_precedence_get_topmost_terminal_index(token_stack: Array[GDShellExpressionTokenizer.Token]) -> int: + for i: int in range(token_stack.size() - 1, -1, -1): + if token_stack[i].type == GDShellExpressionTokenizer.Token.Type.EXPRESSION: + continue + return i + return 0 + + +static func _parse_get_precedence_index(token: GDShellExpressionTokenizer.Token) -> int: + return _PRECEDENCE_TABLE_KEYS.find(token.type) + + +static func _parse_get_precedence_action(stack_token: GDShellExpressionTokenizer.Token, current_token: GDShellExpressionTokenizer.Token) -> String: + return _PRECEDENCE_TABLE[_parse_get_precedence_index(stack_token)][_parse_get_precedence_index(current_token)] diff --git a/addons/gdshell/scripts/gdshell_command.gd b/addons/gdshell/scripts/gdshell_command.gd index c984ee9..2860e41 100644 --- a/addons/gdshell/scripts/gdshell_command.gd +++ b/addons/gdshell/scripts/gdshell_command.gd @@ -17,10 +17,6 @@ class CommandResult: signal command_end -@warning_ignore("unsafe_method_access") -var COMMAND_NAME: String = get_script().get_path().get_file().get_basename() -var COMMAND_AUTO_ALIASES: Dictionary = {} - var _PARENT_COMMAND_RUNNER: GDShellCommandRunner @@ -36,14 +32,10 @@ func input(out: String = "") -> String: return await _PARENT_COMMAND_RUNNER._handle_input(self, out) -func output(out, append_new_line: bool = true) -> void: +func output(out: Variant, append_new_line: bool = true) -> void: _PARENT_COMMAND_RUNNER._handle_output(str(out), append_new_line) -func get_parent_command_runner() -> GDShellCommandRunner: - return _PARENT_COMMAND_RUNNER - - func get_ui_handler() -> GDShellUIHandler: return _PARENT_COMMAND_RUNNER._handle_get_ui_handler() @@ -52,6 +44,15 @@ func get_ui_handler_rich_text_label() -> RichTextLabel: return _PARENT_COMMAND_RUNNER._handle_get_ui_handler_rich_text_label() +func _get_command_name() -> String: + @warning_ignore("unsafe_method_access") + return get_script().get_path().get_file().get_basename() + + +func _get_command_auto_aliases() -> Dictionary: + return {} + + func _get_manual() -> String: return ( """ @@ -65,8 +66,8 @@ func _get_manual() -> String: -Override the [b]_get_manual()[/b] function for a custom manual page. """.format( { - "COMMAND_NAME": COMMAND_NAME, - "COMMAND_AUTO_ALIASES": COMMAND_AUTO_ALIASES, + "COMMAND_NAME": _get_command_name(), + "COMMAND_AUTO_ALIASES": _get_command_auto_aliases(), } ) ) @@ -74,7 +75,7 @@ func _get_manual() -> String: static func argv_parse_options(argv: Array[String], strip_name_dashes: bool = false, next_arg_as_value: bool = false) -> Dictionary: var options: Dictionary = {} - for i in argv.size(): + for i: int in argv.size(): if argv[i][0] == "-": var option_name: String = argv[i].get_slice("=", 0).lstrip("-") if strip_name_dashes else argv[i].get_slice("=", 0) var option_value: String = argv[i].get_slice("=", 1) if "=" in argv[i] else "" diff --git a/addons/gdshell/scripts/gdshell_command_db.gd b/addons/gdshell/scripts/gdshell_command_db.gd index d6d6752..f6cea56 100644 --- a/addons/gdshell/scripts/gdshell_command_db.gd +++ b/addons/gdshell/scripts/gdshell_command_db.gd @@ -127,8 +127,8 @@ static func get_file_command_name_and_auto_aliases(path: String) -> Dictionary: var command: GDShellCommand = get_file_gdshell_command_instance(path) if command == null: return out - out["name"] = command.COMMAND_NAME - out["aliases"] = command.COMMAND_AUTO_ALIASES + out["name"] = command._get_command_name() + out["aliases"] = command._get_command_auto_aliases() return out diff --git a/addons/gdshell/scripts/gdshell_command_parser.gd b/addons/gdshell/scripts/gdshell_command_parser.gd deleted file mode 100644 index 359cb36..0000000 --- a/addons/gdshell/scripts/gdshell_command_parser.gd +++ /dev/null @@ -1,372 +0,0 @@ -@icon("res://addons/gdshell/icon.png") -class_name GDShellCommandParser -extends RefCounted - - -class ParserResult: - enum Status { - OK, - UNTERMINATED, - ERROR, - } - - var status: Status ## Status of the parsed input. - var input: String ## The input that was provided by the user. - var tokens: Array[Token] ## Tokenized input. - var command_db: GDShellCommandDB ## GDShellCommandDB, that was used for parsing and should be used for execution. - var err_token_index: int ## Index of the Token that caused an error. If the index is -1, no error occured. - var err_string: String = "" ## Description of the error. If empty, no error occured. - - func _init(_status: Status, _input: String, _tokens: Array[Token], _command_db: GDShellCommandDB, _err_token_index: int = -1, _err_string: String = ""): - status = _status - input = _input - tokens = _tokens - command_db = _command_db - err_token_index = _err_token_index - err_string = _err_string - - -class Token: - enum Type { - SPACE, - WORD, - WORD_UNTERMINATED, - OPERATOR_PIPE, - OPERATOR_AND, - OPERATOR_OR, - OPERATOR_NOT, - OPERATOR_BACKGROUND, - OPERATOR_SEQUENCE, -# OPERATOR_EXPAND, - OPERATOR_OPENING_PARENTHESIS, - OPERATOR_CLOSING_PARENTHESIS, - } - - var type: Type - var content: String - var start_char_index: int - var consumed: int - - func _init(_type: Type, _content: String, _start_char_index: int, _consumed: int): - type = _type - content = _content - start_char_index = _start_char_index - consumed = _consumed - - -static func parse(input: String, command_db: GDShellCommandDB) -> ParserResult: - var tokens: Array[Token] = _tokenize(input) - - if tokens.is_empty(): - return ParserResult.new( - ParserResult.Status.OK, - input, - tokens, - command_db - ) - - if tokens[-1].type == Token.Type.WORD_UNTERMINATED: - return ParserResult.new( - ParserResult.Status.UNTERMINATED, - input, - tokens, - command_db, - tokens.size() - 1, - "The input is not terminated with corrent quote so another appended input is required. See GDShell Docs for help.", - ) - - var validated: Dictionary = _validate_operators(tokens) - if validated["err_token_index"] != -1: - return ParserResult.new( - ParserResult.Status.ERROR, - input, - tokens, - command_db, - validated["err_token_index"], - validated["err_string"] - ) - - tokens = _expand(tokens, command_db) - - return ParserResult.new( - ParserResult.Status.OK, - input, - tokens, - command_db - ) - - -static func _expand(tokens: Array[Token], command_db: GDShellCommandDB) -> Array[Token]: - var expanded_tokens: Array[Token] = [] - var last_token_type: Token.Type = Token.Type.OPERATOR_AND - - for i in tokens.size(): - if last_token_type != Token.Type.WORD: - expanded_tokens.append_array(_expand_token(tokens[i], command_db)) - else: - expanded_tokens.append(tokens[i]) - last_token_type = tokens[i].type - - return expanded_tokens - - -static func _expand_token(token: Token, command_db: GDShellCommandDB) -> Array[Token]: - if token.content in command_db._aliases.keys(): - return _tokenize(command_db._aliases[token.content]) - return [token] - - -static func _is_operator(token: Token) -> bool: - return token.type in [ - Token.Type.OPERATOR_AND, - Token.Type.OPERATOR_BACKGROUND, - Token.Type.OPERATOR_OR, - Token.Type.OPERATOR_PIPE, - Token.Type.OPERATOR_NOT, - Token.Type.OPERATOR_SEQUENCE, - ] - - -static func _validate_operators(tokens: Array[Token]) -> Dictionary: - var parenthesis_validation: Dictionary = _validate_parentheses(tokens) - if parenthesis_validation["err_token_index"] != -1: - return parenthesis_validation - - for i in tokens.size(): - if tokens[i].type == Token.Type.WORD: - continue - - if _token_requires_left_operand(tokens[i]): - if i == 0 or not _token_can_be_considered_operand(tokens[i], tokens[i-1]): - return { - "err_token_index": i, - "err_string": "\"%s\" does not have a required left operand." % tokens[i].content if tokens[i].type != Token.Type.OPERATOR_CLOSING_PARENTHESIS - else "Parentheses pair does not have at least one required command.", - } - if _token_requires_right_operand(tokens[i]): - if i == tokens.size()-1 or not _token_can_be_considered_operand(tokens[i], tokens[i+1]): - return { - "err_token_index": i, - "err_string": "\"%s\" does not have a required right operand." % tokens[i].content if tokens[i].type != Token.Type.OPERATOR_OPENING_PARENTHESIS - else "Parentheses pair does not have at least one required command.", - } - - return { - "err_token_index": -1, - "err_string": "", - } - - -static func _validate_parentheses(tokens: Array[Token]) -> Dictionary: - var parenthesis_level: int = 0 - var firt_opening_parenthesis_index: int - - for i in tokens.size(): - if tokens[i].type == Token.Type.OPERATOR_OPENING_PARENTHESIS: - parenthesis_level += 1 - if parenthesis_level == 1: - firt_opening_parenthesis_index = i - elif tokens[i].type == Token.Type.OPERATOR_CLOSING_PARENTHESIS: - parenthesis_level -= 1 - if parenthesis_level < 0: # Found closing parenthesis with no opening one - return { - "err_token_index": i, - "err_string": "Closing \")\" on index [%d] does not have an opening counterpart." % i, - } - - if parenthesis_level > 0: # Did not find a closing parenthesis for all opening parentheses - return { - "err_token_index": firt_opening_parenthesis_index, - "err_string": "Opening \"(\" on index [%d] does not have a closing counterpart." % firt_opening_parenthesis_index, - } - - return { - "err_token_index": -1, - "err_string": "" - } - - -static func _token_requires_left_operand(token: Token) -> bool: - return token.type in [ - Token.Type.OPERATOR_PIPE, - Token.Type.OPERATOR_AND, - Token.Type.OPERATOR_OR, - Token.Type.OPERATOR_BACKGROUND, - Token.Type.OPERATOR_SEQUENCE, - Token.Type.OPERATOR_CLOSING_PARENTHESIS, - ] - - -static func _token_requires_right_operand(token: Token) -> bool: - return token.type in [ - Token.Type.OPERATOR_PIPE, - Token.Type.OPERATOR_AND, - Token.Type.OPERATOR_OR, - Token.Type.OPERATOR_NOT, - Token.Type.OPERATOR_OPENING_PARENTHESIS, - ] - - -static func _token_can_be_considered_operand(operator: Token, operand: Token) -> bool: - match operator.type: - Token.Type.OPERATOR_PIPE, Token.Type.OPERATOR_AND, Token.Type.OPERATOR_OR, Token.Type.OPERATOR_SEQUENCE: - return operand.type in [ - Token.Type.WORD, - Token.Type.OPERATOR_NOT, - Token.Type.OPERATOR_BACKGROUND, - Token.Type.OPERATOR_OPENING_PARENTHESIS, - Token.Type.OPERATOR_CLOSING_PARENTHESIS, - ] - Token.Type.OPERATOR_NOT: - return operand.type in [ - Token.Type.WORD, - Token.Type.OPERATOR_OPENING_PARENTHESIS, - ] - Token.Type.OPERATOR_BACKGROUND: - return operand.type in [ - Token.Type.WORD, - ] - Token.Type.OPERATOR_OPENING_PARENTHESIS: - return operand.type in [ - Token.Type.WORD, - Token.Type.OPERATOR_NOT, - Token.Type.OPERATOR_OPENING_PARENTHESIS, - ] - Token.Type.OPERATOR_CLOSING_PARENTHESIS: - return operand.type in [ - Token.Type.WORD, - Token.Type.OPERATOR_BACKGROUND, - Token.Type.OPERATOR_CLOSING_PARENTHESIS, - ] - - return false - -static func _tokenize(input: String) -> Array[Token]: - var tokens: Array[Token] = [] - var current_char: int = 0 - - while current_char < input.length(): - match input[current_char]: - " ": - tokens.push_back(_tokenize_space(input, current_char)) - ";": - tokens.push_back(_tokenize_semicolon(input, current_char)) - "&": - tokens.push_back(_tokenize_and(input, current_char)) - "|": - tokens.push_back(_tokenize_pipe(input, current_char)) - "!": - tokens.push_back(_tokenize_exclamation(input, current_char)) -# "$": -# tokens.push_back(_tokenize_dollar_sign(input, current_char)) - "(", ")": - tokens.push_back(_tokenize_parenthesis(input, current_char)) - "\"", "\'": - tokens.push_back(_tokenize_quote(input, current_char)) - _: - tokens.push_back(_tokenize_text(input, current_char)) - - current_char += tokens[-1].consumed - - return _filter_out_space_tokens(_merge_word_tokens(tokens)) - - -static func _tokenize_space(_input: String, current_char: int) -> Token: - return Token.new(Token.Type.SPACE, " ", current_char, 1) - - -static func _tokenize_semicolon(_input: String, current_char: int) -> Token: - return Token.new(Token.Type.OPERATOR_SEQUENCE, ";", current_char, 1) - - -static func _tokenize_and(input: String, current_char: int) -> Token: - if current_char < input.length()-1 and input[current_char+1] == "&": - return Token.new(Token.Type.OPERATOR_AND, "&&", current_char, 2) - return Token.new(Token.Type.OPERATOR_BACKGROUND, "&", current_char, 1) - - -static func _tokenize_pipe(input: String, current_char: int) -> Token: - if current_char < input.length()-1 and input[current_char+1] == "|": - return Token.new(Token.Type.OPERATOR_OR, "||", current_char, 2) - return Token.new(Token.Type.OPERATOR_PIPE, "|", current_char, 1) - - -static func _tokenize_exclamation(_input: String, current_char: int) -> Token: - return Token.new(Token.Type.OPERATOR_NOT, "!", current_char, 1) - - -#static func _tokenize_dollar_sign(_input: String, current_char: int) -> Token: -# return Token.new(Token.Type.OPERATOR_EXPAND, "$", current_char, 1) - - -static func _tokenize_parenthesis(input: String, current_char: int) -> Token: - if input[current_char] == "(": - return Token.new(Token.Type.OPERATOR_OPENING_PARENTHESIS, "(", current_char, 1) - else: - return Token.new(Token.Type.OPERATOR_CLOSING_PARENTHESIS, ")", current_char, 1) - - -static func _tokenize_quote(input: String, current_char: int) -> Token: - var content: String = "" - var quote_type: String = input[current_char] - - # Skip the opening quote and start on the char right after - for i in range(current_char + 1, input.length()): - if input[i] == quote_type and input[max(0, i - 1)] != "\\": - return Token.new( - Token.Type.WORD, - content.c_unescape(), - current_char, - content.length() + 2 # accounts for the starting and ending quotes - ) - content += input[i] - # End of input was reached without finding a closing quote - return Token.new( - Token.Type.WORD_UNTERMINATED, - content.c_unescape(), - current_char, - content.length() + 1 # accounts for the starting quote - ) - - -static func _tokenize_text(input: String, current_char: int) -> Token: - var content: String = "" - - for i in range(current_char, input.length()): - # check if the character should end the WORD token. - if input[i] in [" ", ";", "&", "|", "!", "(", ")", "\"", "\'"]: - break - content += input[i] - - return Token.new( - Token.Type.WORD, - content.c_unescape(), - current_char, - content.length() - ) - - -## Merges WORD tokens if they are not separated by any other token -static func _merge_word_tokens(tokens: Array[Token]) -> Array[Token]: - if tokens.is_empty(): - return tokens - # We now know that tokens is not empty so we append the first token for later simplification - var merged_tokens: Array[Token] = [tokens[0]] - - # Start from the second token as we already appended the first - for i in range(1, tokens.size()): - if tokens[i].type == Token.Type.WORD and merged_tokens[-1].type == Token.Type.WORD: - merged_tokens[-1].content += tokens[i].content - merged_tokens[-1].consumed += tokens[i].consumed - else: - merged_tokens.append(tokens[i]) - - return merged_tokens - - -## Filters out SPACE tokens. After _merge_word_tokens() they are useless and it simplifies next operations. -static func _filter_out_space_tokens(tokens: Array[Token]) -> Array[Token]: - return tokens.filter( - func is_token_not_space(token: Token) -> bool: - return token.type != Token.Type.SPACE - ) diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd b/addons/gdshell/scripts/gdshell_command_runner.gd index 0f3887e..ed80fef 100644 --- a/addons/gdshell/scripts/gdshell_command_runner.gd +++ b/addons/gdshell/scripts/gdshell_command_runner.gd @@ -1,203 +1,98 @@ @icon("res://addons/gdshell/icon.png") -class_name GDShellCommandRunner +#class_name GDShellCommandRunner extends Node - -# Command execution flags -const F_EXECUTE_CONDITION_MET: int = 1 -const F_PIPE_PREVIOUS: int = 2 -const F_BACKGROUND: int = 4 -const F_NEGATED: int = 8 - -var _PARENT_GDSHELL: GDShellMain - var _background_commands: Array[GDShellCommand] = [] -func _init() -> void: - name = "GDShellCommandRunner - " -func execute(parser_result: GDShellCommandParser.ParserResult, piped_result: GDShellCommand.CommandResult=null) -> GDShellCommand.CommandResult: - if parser_result.status != GDShellCommandParser.ParserResult.Status.OK: - push_error("[GDShell] Attempted to run invalid GDShellCommandParser.ParserResult.") - return null - if parser_result.tokens.is_empty(): - push_error("[GDShell] Attempted to run an empty input.") - return null - if parser_result.command_db == null: - push_error("[GDShell] Attempted to run a command, but GDShellCommandParser.ParserResult.command_db is null.") - return null +class RunnerResult extends RefCounted: + enum Status { + OK, + COMPILE_ERROR, + VALIDATION_ERROR, + RUNTIME_ERROR, + } - var command_execution_flags: int = F_EXECUTE_CONDITION_MET - var last_command_result: GDShellCommand.CommandResult = piped_result + var status: Status + var error_description: String + var result: GDShellCommand.CommandResult + var error_index: int + var error_length: int - var current_token_index: int = 0 - while current_token_index < parser_result.tokens.size(): - match parser_result.tokens[current_token_index].type: - - GDShellCommandParser.Token.Type.WORD: - var next_non_word_token_index: int = _get_next_non_word_token_index(parser_result.tokens, current_token_index) - var executed_command_result: GDShellCommand.CommandResult = await _execute_words( - parser_result.tokens.slice(current_token_index, next_non_word_token_index), - parser_result.command_db, - last_command_result, - command_execution_flags - ) - - if executed_command_result != null: - last_command_result = executed_command_result - - command_execution_flags &= F_EXECUTE_CONDITION_MET - current_token_index = next_non_word_token_index - - GDShellCommandParser.Token.Type.OPERATOR_PIPE: - pass - - GDShellCommandParser.Token.Type.OPERATOR_AND: - if not last_command_result.err: - command_execution_flags |= F_BACKGROUND - else: - command_execution_flags ^= F_BACKGROUND - - GDShellCommandParser.Token.Type.OPERATOR_OR: - if last_command_result.err: - command_execution_flags |= F_BACKGROUND - else: - command_execution_flags ^= F_BACKGROUND - - GDShellCommandParser.Token.Type.OPERATOR_NOT: - command_execution_flags |= F_NEGATED - - GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: - command_execution_flags |= F_BACKGROUND - - GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: - command_execution_flags |= F_EXECUTE_CONDITION_MET - - GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: - var parentheses_content: Array[GDShellCommandParser.Token] = _get_parenthesis_inner_tokens(parser_result.tokens, current_token_index) - var executed_parentheses_result: GDShellCommand.CommandResult = await execute( - GDShellCommandParser.ParserResult.new( - GDShellCommandParser.ParserResult.Status.OK, - "", - parentheses_content, - parser_result.command_db - ) - ) - if executed_parentheses_result == null: - return null - - last_command_result = executed_parentheses_result - - GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: - pass # Do nothing (don't delete this. This token should not trigger an error) - - var token_type: - push_error("[GDShell] GDShellCommandRunner encountered an unexpected '%s' token." % str(GDShellCommandParser.Token.Type.find_key(token_type))) - return null - - current_token_index += 1 - return last_command_result - - -func _execute_words(tokens: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, last_result: GDShellCommand.CommandResult, command_execution_flags: int) -> GDShellCommand.CommandResult: - if not command_execution_flags & F_EXECUTE_CONDITION_MET: - return null - - var command_result: GDShellCommand.CommandResult = await _execute_command( - tokens, - command_db, - last_result if command_execution_flags & F_PIPE_PREVIOUS else null, - command_execution_flags & F_BACKGROUND - ) + func _init(_status: Status, _error_description: String, _result: GDShellCommand.CommandResult, _error_index: int, _error_length: int) -> void: + status = _status + error_description = _error_description + result = _result + error_index = _error_index + error_length = _error_length + + +func execute(expression: String, command_db: GDShellCommandDB) -> RunnerResult: + var compiler_result: GDShellExpressionCompiler.CompilerResult = GDShellExpressionCompiler.compile(expression) + if compiler_result.status != GDShellExpressionCompiler.CompilerResult.Status.OK: + return RunnerResult.new(RunnerResult.Status.COMPILE_ERROR, compiler_result.error_description, null, -1, -1) - if command_execution_flags & F_NEGATED: - command_result.err = OK if command_result.err else FAILED + return execute_compiled(compiler_result.result, command_db) + + +func execute_compiled(compiled_expression: Dictionary, command_db: GDShellCommandDB) -> RunnerResult: + if not GDShellExpressionCompiler.is_expression_valid(compiled_expression, false, command_db): + return RunnerResult.new(RunnerResult.Status.VALIDATION_ERROR, "invalid compiled expression", null, -1, -1) + return _execute(compiled_expression, command_db) + + +func _execute(expression: Dictionary, command_db: GDShellCommandDB) -> RunnerResult: + return null + + +func _execute_operator(operator_expression: Dictionary, command_db: GDShellCommandDB) -> RunnerResult: + match operator_expression["operator"]: + "!": + var right_operand_result: RunnerResult = _execute(operator_expression["right"], command_db) + if right_operand_result.status == OK: + right_operand_result.result.err = FAILED if right_operand_result.result.err == OK else OK + return right_operand_result + "&": + pass + "|": + pass + "||": + pass + "&&": + pass + ";": + @warning_ignore("redundant_await") + await _execute(operator_expression["left"], command_db) + return await _execute(operator_expression["right"], command_db) - return command_result + return null -func _execute_command(words: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, piped_result: GDShellCommand.CommandResult=null, in_background: bool=false) -> GDShellCommand.CommandResult: - # Get argv - # Fancy way to make the Array typed - var argv: Array = Array(words.map( - func(token: GDShellCommandParser.Token) -> String: - return token.content - ), TYPE_STRING, "", null) - - # Create command instance - var command: GDShellCommand = command_db.get_gdshell_command_instance(argv[0]) +func _execute_command(command_expression: Dictionary, piped_data: Variant, command_db: GDShellCommandDB, in_background: bool = false) -> RunnerResult: + # Create command + var command: GDShellCommand = command_db.get_gdshell_command_instance(command_expression["name"]) if command == null: - return null - - # Set up command - command._PARENT_COMMAND_RUNNER = self - command.name = "GDShellCommand: " + command.COMMAND_NAME + return RunnerResult.new(RunnerResult.Status.RUNTIME_ERROR, "cannot create a command instance", null, -1, -1) + # Setup command + command.name = "GDShellCommand: " + command._get_command_name() add_child(command, true) if in_background: command.name += " (in background)" _background_commands.append(command) - # Run command - @warning_ignore("redundant_await") # We don't know if the user override will have await - var command_result: GDShellCommand.CommandResult = await command._main(argv, piped_result) - + var command_result: GDShellCommand.CommandResult + if in_background: + command._main(command_expression["args"], piped_data) + command_result = GDShellCommand.CommandResult.new() + else: + @warning_ignore("redundant_await") # We don't know if the user override will have await + command_result = await command._main(command_expression["args"], piped_data) + # TODO - does this work with both background and normal commands? Won't background commands be freed prematurely? # Cleanup command _background_commands.erase(command) command.queue_free() - return command_result - - -func _get_next_non_word_token_index(tokens: Array[GDShellCommandParser.Token], starting_from: int) -> int: - var next_non_word_token_index: int = starting_from + 1 - while next_non_word_token_index < tokens.size(): - if tokens[next_non_word_token_index].type != GDShellCommandParser.Token.Type.WORD: - break - next_non_word_token_index += 1 - return next_non_word_token_index - - -func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: - var current: int = opening_parenthesis_index - var parenthesis_level: int = 1 - while current < tokens.size(): - if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: - parenthesis_level += 1 - elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: - parenthesis_level -= 1 - if parenthesis_level == 0: - return current - return -1 - - -func _get_parenthesis_inner_tokens(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> Array[GDShellCommandParser.Token]: - var matching_parenthesis_index: int = _find_matching_parenthesis_index(tokens, opening_parenthesis_index) - if matching_parenthesis_index == -1: - return [] - return tokens.slice(opening_parenthesis_index + 1, matching_parenthesis_index) - - -func _handle_execute(command: String) -> GDShellCommand.CommandResult: - return await _PARENT_GDSHELL.execute(command) - - -func _handle_input(command: GDShellCommand, out: String) -> String: - if command in _background_commands: - return "" - return await _PARENT_GDSHELL._request_input_from_ui_handler(out) - - -func _handle_output(out: String, append_new_line: bool = true) -> void: - _PARENT_GDSHELL._request_output_from_ui_handler(out, append_new_line) - - -func _handle_get_ui_handler() -> GDShellUIHandler: - return _PARENT_GDSHELL.get_ui_handler() - - -func _handle_get_ui_handler_rich_text_label() -> RichTextLabel: - return _PARENT_GDSHELL.get_ui_handler_rich_text_label() - + return RunnerResult.new(RunnerResult.Status.OK, "OK", command_result, -1, -1) diff --git a/addons/gdshell/scripts/gdshell_command_runner_old.gd b/addons/gdshell/scripts/gdshell_command_runner_old.gd new file mode 100644 index 0000000..3125eb5 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_runner_old.gd @@ -0,0 +1,203 @@ +@icon("res://addons/gdshell/icon.png") +class_name GDShellCommandRunner +extends Node + + +# Command execution flags +const F_EXECUTE_CONDITION_MET: int = 1 +const F_PIPE_PREVIOUS: int = 2 +const F_BACKGROUND: int = 4 +const F_NEGATED: int = 8 + +var _PARENT_GDSHELL: GDShellMain + +var _background_commands: Array[GDShellCommand] = [] + + +func _init() -> void: + name = "GDShellCommandRunner - " + + +#func execute(parser_result: GDShellCommandParser.ParserResult, piped_result: GDShellCommand.CommandResult=null) -> GDShellCommand.CommandResult: + #if parser_result.status != GDShellCommandParser.ParserResult.Status.OK: + #push_error("[GDShell] Attempted to run invalid GDShellCommandParser.ParserResult.") + #return null + #if parser_result.tokens.is_empty(): + #push_error("[GDShell] Attempted to run an empty input.") + #return null + #if parser_result.command_db == null: + #push_error("[GDShell] Attempted to run a command, but GDShellCommandParser.ParserResult.command_db is null.") + #return null + # + #var command_execution_flags: int = F_EXECUTE_CONDITION_MET + #var last_command_result: GDShellCommand.CommandResult = piped_result + # + #var current_token_index: int = 0 + #while current_token_index < parser_result.tokens.size(): + #match parser_result.tokens[current_token_index].type: + # + #GDShellCommandParser.Token.Type.WORD: + #var next_non_word_token_index: int = _get_next_non_word_token_index(parser_result.tokens, current_token_index) + #var executed_command_result: GDShellCommand.CommandResult = await _execute_words( + #parser_result.tokens.slice(current_token_index, next_non_word_token_index), + #parser_result.command_db, + #last_command_result, + #command_execution_flags + #) + # + #if executed_command_result != null: + #last_command_result = executed_command_result + # + #command_execution_flags &= F_EXECUTE_CONDITION_MET + #current_token_index = next_non_word_token_index + # + #GDShellCommandParser.Token.Type.OPERATOR_PIPE: + #pass + # + #GDShellCommandParser.Token.Type.OPERATOR_AND: + #if not last_command_result.err: + #command_execution_flags |= F_BACKGROUND + #else: + #command_execution_flags ^= F_BACKGROUND + # + #GDShellCommandParser.Token.Type.OPERATOR_OR: + #if last_command_result.err: + #command_execution_flags |= F_BACKGROUND + #else: + #command_execution_flags ^= F_BACKGROUND + # + #GDShellCommandParser.Token.Type.OPERATOR_NOT: + #command_execution_flags |= F_NEGATED + # + #GDShellCommandParser.Token.Type.OPERATOR_BACKGROUND: + #command_execution_flags |= F_BACKGROUND + # + #GDShellCommandParser.Token.Type.OPERATOR_SEQUENCE: + #command_execution_flags |= F_EXECUTE_CONDITION_MET + # + #GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + #var parentheses_content: Array[GDShellCommandParser.Token] = _get_parenthesis_inner_tokens(parser_result.tokens, current_token_index) + #var executed_parentheses_result: GDShellCommand.CommandResult = await execute( + #GDShellCommandParser.ParserResult.new( + #GDShellCommandParser.ParserResult.Status.OK, + #"", + #parentheses_content, + #parser_result.command_db + #) + #) + #if executed_parentheses_result == null: + #return null + # + #last_command_result = executed_parentheses_result + # + #GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + #pass # Do nothing (don't delete this. This token should not trigger an error) + # + #var token_type: + #push_error("[GDShell] GDShellCommandRunner encountered an unexpected '%s' token." % str(GDShellCommandParser.Token.Type.find_key(token_type))) + #return null + # + #current_token_index += 1 + # + #return last_command_result +# +# +#func _execute_words(tokens: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, last_result: GDShellCommand.CommandResult, command_execution_flags: int) -> GDShellCommand.CommandResult: + #if not command_execution_flags & F_EXECUTE_CONDITION_MET: + #return null + # + #var command_result: GDShellCommand.CommandResult = await _execute_command( + #tokens, + #command_db, + #last_result if command_execution_flags & F_PIPE_PREVIOUS else null, + #command_execution_flags & F_BACKGROUND + #) + # + #if command_execution_flags & F_NEGATED: + #command_result.err = OK if command_result.err else FAILED + # + #return command_result +# + +#func _execute_command(words: Array[GDShellCommandParser.Token], command_db: GDShellCommandDB, piped_result: GDShellCommand.CommandResult=null, in_background: bool=false) -> GDShellCommand.CommandResult: + ## Get argv + ## Fancy way to make the Array typed + #var argv: Array = Array(words.map( + #func(token: GDShellCommandParser.Token) -> String: + #return token.content + #), TYPE_STRING, "", null) + # + ## Create command instance + #var command: GDShellCommand = command_db.get_gdshell_command_instance(argv[0]) + #if command == null: + #return null + # + ## Set up command + #command._PARENT_COMMAND_RUNNER = self + #command.name = "GDShellCommand: " + command._get_command_name() + #add_child(command, true) + #if in_background: + #command.name += " (in background)" + #_background_commands.append(command) + # + ## Run command + #@warning_ignore("redundant_await") # We don't know if the user override will have await + #var command_result: GDShellCommand.CommandResult = await command._main(argv, piped_result) + # + ## Cleanup command + #_background_commands.erase(command) + #command.queue_free() + #return command_result +# +# +#func _get_next_non_word_token_index(tokens: Array[GDShellCommandParser.Token], starting_from: int) -> int: + #var next_non_word_token_index: int = starting_from + 1 + #while next_non_word_token_index < tokens.size(): + #if tokens[next_non_word_token_index].type != GDShellCommandParser.Token.Type.WORD: + #break + #next_non_word_token_index += 1 + #return next_non_word_token_index +# +# +#func _find_matching_parenthesis_index(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> int: + #var current: int = opening_parenthesis_index + #var parenthesis_level: int = 1 + # + #while current < tokens.size(): + #if tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_OPENING_PARENTHESIS: + #parenthesis_level += 1 + #elif tokens[current].type == GDShellCommandParser.Token.Type.OPERATOR_CLOSING_PARENTHESIS: + #parenthesis_level -= 1 + #if parenthesis_level == 0: + #return current + #return -1 +# +# +#func _get_parenthesis_inner_tokens(tokens: Array[GDShellCommandParser.Token], opening_parenthesis_index: int) -> Array[GDShellCommandParser.Token]: + #var matching_parenthesis_index: int = _find_matching_parenthesis_index(tokens, opening_parenthesis_index) + #if matching_parenthesis_index == -1: + #return [] + #return tokens.slice(opening_parenthesis_index + 1, matching_parenthesis_index) + + +func _handle_execute(command: String) -> GDShellCommand.CommandResult: + return await _PARENT_GDSHELL.execute(command) + + +func _handle_input(command: GDShellCommand, out: String) -> String: + if command in _background_commands: + return "" + return await _PARENT_GDSHELL._request_input_from_ui_handler(out) + + +func _handle_output(out: String, append_new_line: bool = true) -> void: + _PARENT_GDSHELL._request_output_from_ui_handler(out, append_new_line) + + +func _handle_get_ui_handler() -> GDShellUIHandler: + return _PARENT_GDSHELL.get_ui_handler() + + +func _handle_get_ui_handler_rich_text_label() -> RichTextLabel: + return _PARENT_GDSHELL.get_ui_handler_rich_text_label() + diff --git a/addons/gdshell/scripts/gdshell_main.gd b/addons/gdshell/scripts/gdshell_main.gd index 72111b7..fe25050 100644 --- a/addons/gdshell/scripts/gdshell_main.gd +++ b/addons/gdshell/scripts/gdshell_main.gd @@ -3,125 +3,210 @@ class_name GDShellMain extends Node -signal _input_submitted(input: String) - - -var UI_TOGGLE_ACTION: String = ProjectSettings.get_setting( - GDShellEditorPlugin.UI_TOGGLE_ACTION, - GDShellEditorPlugin.UI_TOGGLE_ACTION_DEFAULT -) +signal _requested_input_submitted(input: String) var command_runner: GDShellCommandRunner var command_db: GDShellCommandDB var ui_handler: GDShellUIHandler # Internal helper variables -var _is_command_awaiting_input: bool = false +var _ui_handler_canvas_layer: CanvasLayer var _input_buffer: String = "" - +var _input_requested: bool = false func _ready() -> void: + _ui_handler_canvas_layer = CanvasLayer.new() + _ui_handler_canvas_layer.layer = 100 + add_child(_ui_handler_canvas_layer) + setup_with_default_values() + + var result = GDShellExpressionCompiler.compile("a && b ; (c | d)") + print(GDShellExpressionCompiler.is_expression_valid(result.result)) + print(result) + print(result.result) + return + + #if "autorun" in command_db.get_all_command_names(): + #@warning_ignore("return_value_discarded") + #execute("autorun") func setup_with_default_values() -> void: - setup_command_runner() - setup_command_db("res://addons/gdshell/commands/") - setup_ui_handler(load_ui_handler_from_path("res://addons/gdshell/ui/default_ui/default_ui.tscn"), true) + # GDShellCommandRunner + set_command_runner(GDShellCommandRunner.new(), true) - execute("autorun") -# if execute_autorun_on_startup: -# execute_autorun() - + # GDShellCommandDB + var database: GDShellCommandDB = GDShellCommandDB.new() + for directory: String in ProjectSettings.get_setting( + GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES, + GDShellEditorPlugin.COMMAND_SCANNED_DIRECTORIES_DEFAULT + ): + database.add_commands_in_directory(directory) + set_command_db(database) + + #GDShellUIHandler + set_ui_handler( + GDShellMain._get_ui_handler_instance_from_path( + str(ProjectSettings.get_setting( + GDShellEditorPlugin.UI_SCENE_PATH, + GDShellEditorPlugin.UI_SCENE_PATH_DEFAULT + )) + ), + true + ) + + +func set_command_db(new_command_db: GDShellCommandDB) -> void: + if new_command_db == null: + push_error("[GDShell] Attempted to set GDShellCommandDB, but null value was given.") + return + command_db = new_command_db -func setup_command_runner() -> void: - command_runner = GDShellCommandRunner.new() - command_runner._PARENT_GDSHELL = self - add_child(command_runner) +func unset_command_db() -> void: + command_db = null -func setup_command_db(command_dir_path: String="") -> void: - command_db = GDShellCommandDB.new() - if not command_dir_path.is_empty(): - command_db.add_commands_in_directory(command_dir_path) +func set_command_runner(new_command_runner: GDShellCommandRunner, add_as_child: bool = true) -> void: + if new_command_runner == null: + push_error("[GDShell] Attempted to set GDShellCommandRunner, but null value was given.") + return + # if new command runner is the same as the onld one, do nothing unless it is requested to be reparented + if new_command_runner == command_runner: + if add_as_child and new_command_runner.get_parent() != self: + new_command_runner.reparent(self) + return + + unset_command_runner() + + if add_as_child: + if new_command_runner.get_parent() != null: + push_error("[GDShell] Attempted to set GDShellCommandRunner, but runner already has a parent Node. Set 'add_as_child' to false if you wish to manage the runner yourself.") + return + add_child(new_command_runner) + + command_runner = new_command_runner + command_runner._PARENT_GDSHELL = self + + if command_runner.get_parent() == null: + push_warning("[GDShell] GDShellCommandRunner was set, but it has no parent. Make sure to give it a parent or remember to free it manually to prevent leaks if you wish to manage the runner yourself.") -func setup_ui_handler(handler: GDShellUIHandler, add_as_child: bool=true) -> void: - ui_handler = handler - ui_handler._PARENT_GDSHELL = self - ui_handler.set_visible(false) - if add_as_child: - var canvas_layer: CanvasLayer = CanvasLayer.new() - canvas_layer.layer = 100 - canvas_layer.add_child(handler) - add_child(canvas_layer) +func unset_command_runner() -> void: + if ui_handler == null: + push_warning("[GDShell] Attempted to unset GDShellCommandRunner, but none was set.") + return + if command_runner.get_parent() == self: + command_runner.queue_free() + elif command_runner.get_parent() == null: + push_warning("[GDShell] Unset GDShellCommandRunner has no parent and was not freed. Remember to free it manually to prevent leaks.") + command_runner = null -func _input(event: InputEvent) -> void: - if event.is_action(UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): - ui_handler.toggle_visible() +func set_ui_handler(new_ui_handler: GDShellUIHandler, add_as_child: bool = true) -> void: + if new_ui_handler == null: + push_error("[GDShell] Attempted to set GDShellUIHandler, but null value was given.") + return + # if new handler is the same as the onld one, do nothing unless it is requested to be reparented + if new_ui_handler == ui_handler: + if add_as_child and new_ui_handler.get_parent() != _ui_handler_canvas_layer: + new_ui_handler.reparent(_ui_handler_canvas_layer) + return + + unset_ui_handler() + + if add_as_child: + if new_ui_handler.get_parent() != null: + push_error("[GDShell] Attempted to set GDShellUIHandler, but new_ui_handler already has a parent Node. Set 'add_as_child' to false if you wish to manage the handler yourself.") + return + _ui_handler_canvas_layer.add_child(new_ui_handler) + + ui_handler = new_ui_handler + ui_handler.set_visible(false) + if not ui_handler.input_submitted.is_connected(self._on_ui_handler_input_submitted): + @warning_ignore("return_value_discarded") + ui_handler.input_submitted.connect(self._on_ui_handler_input_submitted) + + if ui_handler.get_parent() == null: + push_warning("[GDShell] GDShellUIHandler was set, but it has no parent. Make sure to give it a parent or remember to free it manually to prevent leaks if you wish to manage the handler yourself.") -#func execute_autorun() -> void: -# if "autorun" in command_db.get_all_command_names(): -# execute("autorun") +func unset_ui_handler() -> void: + if ui_handler == null: + push_warning("[GDShell] Attempted to unset GDShellUIHandler, but none was set.") + return + + if ui_handler.input_submitted.is_connected(self._on_ui_handler_input_submitted): + ui_handler.input_submitted.disconnect(self._on_ui_handler_input_submitted) + if ui_handler.get_parent() == self: + ui_handler.queue_free() + elif ui_handler.get_parent() == null: + push_warning("[GDShell] Unset GDShellUIHandler has no parent and was not freed. Remember to free it manually to prevent leaks.") + ui_handler = null func execute(command: String) -> GDShellCommand.CommandResult: - var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(command, command_db) - if parser_result.status == GDShellCommandParser.ParserResult.Status.OK: - return await command_runner.execute(parser_result) + #var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(command, command_db) + #if parser_result.status == GDShellCommandParser.ParserResult.Status.OK: + #return await command_runner.execute(parser_result) return null -func get_ui_handler() -> GDShellUIHandler: - return ui_handler - - -func get_ui_handler_rich_text_label() -> RichTextLabel: - return ui_handler._get_output_rich_text_label() - - -func _request_input_from_ui_handler(out: String="") -> String: - _is_command_awaiting_input = true - ui_handler._input_requested.emit(out) - return await _input_submitted +func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: + if ui_handler == null: + push_warning("[GDShell] No GDShellUIHandler is set. No output is outputted.") + return + ui_handler._output_requested(output) -func _request_output_from_ui_handler(output: String, append_new_line: bool) -> void: - ui_handler._output_requested.emit(output, append_new_line) +func _request_input_from_ui_handler(output: String = "") -> String: + if ui_handler == null: + push_warning("[GDShell] No GDShellUIHandler is set. Empty input is returned.") + return "" + ui_handler._input_requested(output) + return await _requested_input_submitted -func _submit_input(input: String) -> void: - if _is_command_awaiting_input: - _is_command_awaiting_input = false - _request_output_from_ui_handler(input, true) - _input_submitted.emit(input) +func _on_ui_handler_input_submitted(input: String) -> void: + if _input_requested: # The input is requested by a command. Do nothing and just forward it to it + _input_requested = false + _requested_input_submitted.emit(input) return _input_buffer += input - var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(_input_buffer, command_db) - match parser_result.status: - GDShellCommandParser.ParserResult.Status.OK: - _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) - _input_buffer = "" - await command_runner.execute(parser_result) - ui_handler._input_requested.emit("") - GDShellCommandParser.ParserResult.Status.UNTERMINATED: - _request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) - ui_handler._input_requested.emit("> ") - GDShellCommandParser.ParserResult.Status.ERROR: - _request_output_from_ui_handler(ui_handler._get_input_prompt() + _input_buffer, true) - _input_buffer = "" - # TODO better error announcement -# _request_output_from_ui_handler("[color=red]%s[/color]" % parser_result["result"]["error"], true) - ui_handler._input_requested.emit("") - - -static func load_ui_handler_from_path(path: String) -> GDShellUIHandler: - return load(path).instantiate() as GDShellUIHandler + #var parser_result: GDShellCommandParser.ParserResult = GDShellCommandParser.parse(_input_buffer, command_db) + #match parser_result.status: + #GDShellCommandParser.ParserResult.Status.OK: + #_request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) + #_input_buffer = "" + #await command_runner.execute(parser_result) + ##ui_handler._input_requested.emit("") + #GDShellCommandParser.ParserResult.Status.UNTERMINATED: + #_request_output_from_ui_handler((ui_handler._get_input_prompt() if input == _input_buffer else "") + input, true) + ##ui_handler._input_requested.emit("> ") + #GDShellCommandParser.ParserResult.Status.ERROR: + #_request_output_from_ui_handler(ui_handler._get_input_prompt() + _input_buffer, true) + #_input_buffer = "" + ## TODO better error announcement +## _request_output_from_ui_handler("[color=red]%s[/color]" % parser_result["result"]["error"], true) + ##ui_handler._input_requested.emit("") + + +static func _get_ui_handler_instance_from_path(path: String) -> GDShellUIHandler: + var scene: Resource = load(path) + if not scene is PackedScene: + push_error("[GDShell] Attempted to get a GDShellUIHandler instance from '%s' but resource is to a scene." % path) + return null + + var instance: Node = (scene as PackedScene).instantiate() + if not instance is GDShellUIHandler: + push_error("[GDShell] Attempted to get a GDShellUIHandler instance from '%s' but scene root is not of GDShellUIHandler type." % path) + instance.queue_free() + return null + + return instance as GDShellUIHandler static func get_gdshell_version() -> String: @@ -129,10 +214,3 @@ static func get_gdshell_version() -> String: if config.load("res://addons/gdshell/plugin.cfg"): return "Unknown" return str(config.get_value("plugin", "version", "Unknown")) -# -# -#func _input(event: InputEvent) -> void: -# if not handle_gdshell_toggle_ui_action: -# return -# if event.is_action(GDSHELL_TOGGLE_UI_ACTION) and not event.is_echo() and event.is_pressed(): -# ui_handler.toggle_visible() diff --git a/addons/gdshell/scripts/gdshell_ui_handler.gd b/addons/gdshell/scripts/gdshell_ui_handler.gd index ca7be46..4fff6c3 100644 --- a/addons/gdshell/scripts/gdshell_ui_handler.gd +++ b/addons/gdshell/scripts/gdshell_ui_handler.gd @@ -3,48 +3,61 @@ class_name GDShellUIHandler extends Control -signal _input_requested(output: String) -signal _output_requested(output: String, append_new_line: bool) +signal input_submitted(input: String) + + +var _UI_TOGGLE_ACTION: String = str(ProjectSettings.get_setting( + GDShellEditorPlugin.UI_TOGGLE_ACTION, + GDShellEditorPlugin.UI_TOGGLE_ACTION_DEFAULT +)) -var _PARENT_GDSHELL: GDShellMain var history: Array = [] -var hist_index = -1 +var history_index: int = -1 + + +func _input_requested(output: String) -> void: + pass + + +func _output_requested(output: String, append_new_lide: bool = true) -> void: + pass func submit_input(input: String) -> void: - _PARENT_GDSHELL._submit_input(input) history.push_front(input) history_reset_index() + input_submitted.emit(input) func autocomplete(input: String) -> String: - var all_commands = _PARENT_GDSHELL.command_db.get_all_command_names() - var matches = all_commands.filter( - func(m: String): - return m.begins_with(input) - ) - if matches.size() > 0: - return matches[0] - return input + #var all_commands = _PARENT_GDSHELL.command_db.get_all_command_names() + #var matches = all_commands.filter( + #func(m: String): + #return m.begins_with(input) + #) + #if matches.size() > 0: + #return matches[0] + #return input + return "" func history_get_next() -> String: if (history.size() == 0): return "" - hist_index = clamp(hist_index + 1, 0, history.size() - 1) - return history[hist_index] + history_index = clamp(history_index + 1, 0, history.size() - 1) + return history[history_index] func history_get_previous() -> String: if (history.size() == 0): return "" - hist_index = clamp(hist_index - 1, 0, history.size() - 1) - return history[hist_index] + history_index = clamp(history_index - 1, 0, history.size() - 1) + return history[history_index] func history_reset_index() -> void: - hist_index = -1 + history_index = -1 func toggle_visible() -> void: diff --git a/addons/gdshell/ui/default_ui/default_ui.gd b/addons/gdshell/ui/default_ui/default_ui.gd index b4bcafa..d093f66 100644 --- a/addons/gdshell/ui/default_ui/default_ui.gd +++ b/addons/gdshell/ui/default_ui/default_ui.gd @@ -1,6 +1,5 @@ @tool extends GDShellUIHandler -# The default ui extends a PanelContainer instead of a plain Control const DEFAULT_FONT: Font = preload("res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf") const BOLD_FONT: Font = preload("res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf") @@ -9,7 +8,6 @@ const BOLD_ITALICS_FONT: Font = preload("res://addons/gdshell/ui/fonts/roboto_mo # This looks scary, doesn't it? @export_category("GDShell UI") - @export_group("Fonts") @export var default_font: Font = DEFAULT_FONT: set(value): @@ -86,13 +84,18 @@ var _is_input_requested: bool = true: ) -func _ready(): +func _ready() -> void: visibility_changed.connect(_on_visibility_changed) - _input_requested.connect(_handle_input) - _output_requested.connect(_handle_output) + #input_requested.connect(_handle_input) + #output_requested.connect(_handle_output) set_deferred(&"_is_input_requested", true) +func _input(event: InputEvent) -> void: + if event.is_action(_UI_TOGGLE_ACTION) and not event.is_echo() and event.is_pressed(): + toggle_visible() + + func _handle_input(out: String) -> void: set_deferred(&"_is_input_requested", true) _handle_output(out, false) @@ -124,25 +127,27 @@ func _on_visibility_changed() -> void: else: input_line_edit.release_focus() -func set_line_edit_caret_to_end(): +func set_line_edit_caret_to_end() -> void: input_line_edit.grab_focus() input_line_edit.caret_column = input_line_edit.text.length() -func set_line_edit_caret_to_beginning(): +func set_line_edit_caret_to_beginning() -> void: input_line_edit.caret_column = 0 - + + func text_before_caret() -> String: return input_line_edit.text.substr(0, input_line_edit.caret_column) -func _on_input_line_edit_gui_input(event): + +func _on_input_line_edit_gui_input(event: InputEvent) -> void: if (event is InputEventKey and event.pressed): - if event.keycode == KEY_TAB: + if (event as InputEventKey).keycode == KEY_TAB: input_line_edit.text = autocomplete(text_before_caret()) set_line_edit_caret_to_end.call_deferred() - elif event.keycode == KEY_UP: + elif (event as InputEventKey).keycode == KEY_UP: input_line_edit.text = history_get_next() set_line_edit_caret_to_end.call_deferred() - elif event.keycode == KEY_DOWN: + elif (event as InputEventKey).keycode == KEY_DOWN: input_line_edit.text = history_get_previous() set_line_edit_caret_to_end.call_deferred() else: diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import index e44ee67..5219755 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Bold.ttf-4b2e484e86ae1add3920f3eb2 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import index 3e62b44..d4c0da9 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-BoldItalic.ttf-504bb32bf16b4fd66c1 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import index 85f6092..0c68da4 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Italic.ttf-badc5e851feaad0fe475224 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import index a9160bb..3ff6a6e 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import @@ -15,6 +15,7 @@ dest_files=["res://.godot/imported/RobotoMono-Regular.ttf-fc30ee082ec3388fa60a14 Rendering=null antialiasing=1 generate_mipmaps=false +disable_embedded_bitmaps=true multichannel_signed_distance_field=false msdf_pixel_range=8 msdf_size=48 diff --git a/project.godot b/project.godot index 000e816..1b2384f 100644 --- a/project.godot +++ b/project.godot @@ -13,7 +13,7 @@ config_version=5 config/name="GDShell" config/description="Light-weight customizable in-game console for development, debugging, cheats, etc... for Godot 4." run/main_scene="res://addons/gdshell/demo/demo.tscn" -config/features=PackedStringArray("4.1") +config/features=PackedStringArray("4.3") config/icon="res://addons/gdshell/icon.png" [autoload] @@ -23,6 +23,8 @@ GDShell="*res://addons/gdshell/scripts/gdshell_main.gd" [debug] gdscript/warnings/exclude_addons=false +gdscript/warnings/untyped_declaration=1 +gdscript/warnings/inferred_declaration=1 gdscript/warnings/unsafe_property_access=1 gdscript/warnings/unsafe_method_access=1 gdscript/warnings/unsafe_cast=1 @@ -41,7 +43,7 @@ import/blender/enabled=false gdshell_toggle_ui={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"echo":false,"script":null) +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":96,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } From 7baa0774f6d54cacbfe1535d20e09457f93f6108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Tue, 11 Mar 2025 17:32:39 +0100 Subject: [PATCH 18/20] Update to Godot 4.4 --- addons/gdshell/commands/autorun.gd.uid | 1 + addons/gdshell/commands/default_commands/alias.gd.uid | 1 + addons/gdshell/commands/default_commands/bool.gd.uid | 1 + addons/gdshell/commands/default_commands/clear.gd.uid | 1 + addons/gdshell/commands/default_commands/echo.gd.uid | 1 + addons/gdshell/commands/default_commands/gdfetch.gd.uid | 1 + addons/gdshell/commands/default_commands/man.gd.uid | 1 + .../plugin_integration_commands/monitor_overlay/monitor.gd.uid | 1 + addons/gdshell/demo/demo.gd.uid | 1 + addons/gdshell/demo/demo.tscn | 2 +- addons/gdshell/gdshell_editor_plugin.gd.uid | 1 + .../expression_compiler/gdshell_expression_compiler.gd.uid | 1 + .../expression_compiler/gdshell_expression_tokenizer.gd.uid | 1 + .../expression_compiler/gsdhell_expression_parser.gd.uid | 1 + addons/gdshell/scripts/gdshell_command.gd.uid | 1 + addons/gdshell/scripts/gdshell_command_db.gd.uid | 1 + addons/gdshell/scripts/gdshell_command_runner.gd.uid | 1 + addons/gdshell/scripts/gdshell_command_runner_old.gd.uid | 1 + addons/gdshell/scripts/gdshell_main.gd.uid | 1 + addons/gdshell/scripts/gdshell_ui_handler.gd.uid | 1 + addons/gdshell/ui/default_ui/default_ui.gd.uid | 1 + addons/gdshell/ui/default_ui/default_ui.tscn | 2 +- addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import | 1 + .../ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import | 1 + .../gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import | 1 + .../gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import | 1 + project.godot | 2 +- 27 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 addons/gdshell/commands/autorun.gd.uid create mode 100644 addons/gdshell/commands/default_commands/alias.gd.uid create mode 100644 addons/gdshell/commands/default_commands/bool.gd.uid create mode 100644 addons/gdshell/commands/default_commands/clear.gd.uid create mode 100644 addons/gdshell/commands/default_commands/echo.gd.uid create mode 100644 addons/gdshell/commands/default_commands/gdfetch.gd.uid create mode 100644 addons/gdshell/commands/default_commands/man.gd.uid create mode 100644 addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd.uid create mode 100644 addons/gdshell/demo/demo.gd.uid create mode 100644 addons/gdshell/gdshell_editor_plugin.gd.uid create mode 100644 addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd.uid create mode 100644 addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd.uid create mode 100644 addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd.uid create mode 100644 addons/gdshell/scripts/gdshell_command.gd.uid create mode 100644 addons/gdshell/scripts/gdshell_command_db.gd.uid create mode 100644 addons/gdshell/scripts/gdshell_command_runner.gd.uid create mode 100644 addons/gdshell/scripts/gdshell_command_runner_old.gd.uid create mode 100644 addons/gdshell/scripts/gdshell_main.gd.uid create mode 100644 addons/gdshell/scripts/gdshell_ui_handler.gd.uid create mode 100644 addons/gdshell/ui/default_ui/default_ui.gd.uid diff --git a/addons/gdshell/commands/autorun.gd.uid b/addons/gdshell/commands/autorun.gd.uid new file mode 100644 index 0000000..a25168c --- /dev/null +++ b/addons/gdshell/commands/autorun.gd.uid @@ -0,0 +1 @@ +uid://dhwdbfprk0w34 diff --git a/addons/gdshell/commands/default_commands/alias.gd.uid b/addons/gdshell/commands/default_commands/alias.gd.uid new file mode 100644 index 0000000..49cd92c --- /dev/null +++ b/addons/gdshell/commands/default_commands/alias.gd.uid @@ -0,0 +1 @@ +uid://dqkp7k311c7qq diff --git a/addons/gdshell/commands/default_commands/bool.gd.uid b/addons/gdshell/commands/default_commands/bool.gd.uid new file mode 100644 index 0000000..5ccbfba --- /dev/null +++ b/addons/gdshell/commands/default_commands/bool.gd.uid @@ -0,0 +1 @@ +uid://0jqep47mskax diff --git a/addons/gdshell/commands/default_commands/clear.gd.uid b/addons/gdshell/commands/default_commands/clear.gd.uid new file mode 100644 index 0000000..613e165 --- /dev/null +++ b/addons/gdshell/commands/default_commands/clear.gd.uid @@ -0,0 +1 @@ +uid://cy2ajeeqr4rvm diff --git a/addons/gdshell/commands/default_commands/echo.gd.uid b/addons/gdshell/commands/default_commands/echo.gd.uid new file mode 100644 index 0000000..6f1a9e4 --- /dev/null +++ b/addons/gdshell/commands/default_commands/echo.gd.uid @@ -0,0 +1 @@ +uid://dk2m8fw8cijpa diff --git a/addons/gdshell/commands/default_commands/gdfetch.gd.uid b/addons/gdshell/commands/default_commands/gdfetch.gd.uid new file mode 100644 index 0000000..a4c3ec7 --- /dev/null +++ b/addons/gdshell/commands/default_commands/gdfetch.gd.uid @@ -0,0 +1 @@ +uid://dpionqq57kuh3 diff --git a/addons/gdshell/commands/default_commands/man.gd.uid b/addons/gdshell/commands/default_commands/man.gd.uid new file mode 100644 index 0000000..14b5cdd --- /dev/null +++ b/addons/gdshell/commands/default_commands/man.gd.uid @@ -0,0 +1 @@ +uid://dwjw45cqys264 diff --git a/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd.uid b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd.uid new file mode 100644 index 0000000..ae88a01 --- /dev/null +++ b/addons/gdshell/commands/plugin_integration_commands/monitor_overlay/monitor.gd.uid @@ -0,0 +1 @@ +uid://pw1ksoelnhof diff --git a/addons/gdshell/demo/demo.gd.uid b/addons/gdshell/demo/demo.gd.uid new file mode 100644 index 0000000..0579cdf --- /dev/null +++ b/addons/gdshell/demo/demo.gd.uid @@ -0,0 +1 @@ +uid://bg5laxgioyd86 diff --git a/addons/gdshell/demo/demo.tscn b/addons/gdshell/demo/demo.tscn index 838a17c..53208cc 100644 --- a/addons/gdshell/demo/demo.tscn +++ b/addons/gdshell/demo/demo.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=3 format=3 uid="uid://cil8b81ctftx3"] -[ext_resource type="Script" path="res://addons/gdshell/demo/demo.gd" id="1_cwk1x"] +[ext_resource type="Script" uid="uid://bg5laxgioyd86" path="res://addons/gdshell/demo/demo.gd" id="1_cwk1x"] [ext_resource type="Texture2D" uid="uid://bmmfysofbp50p" path="res://addons/gdshell/icon.png" id="2_hso4g"] [node name="GDShellDemo" type="Control"] diff --git a/addons/gdshell/gdshell_editor_plugin.gd.uid b/addons/gdshell/gdshell_editor_plugin.gd.uid new file mode 100644 index 0000000..e48fdb7 --- /dev/null +++ b/addons/gdshell/gdshell_editor_plugin.gd.uid @@ -0,0 +1 @@ +uid://cdl3y2evxe7ir diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd.uid b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd.uid new file mode 100644 index 0000000..e353dc6 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd.uid @@ -0,0 +1 @@ +uid://d0a8e4sbpnpa3 diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd.uid b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd.uid new file mode 100644 index 0000000..e5378a2 --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_tokenizer.gd.uid @@ -0,0 +1 @@ +uid://drg1k066fmiyl diff --git a/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd.uid b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd.uid new file mode 100644 index 0000000..bc4880e --- /dev/null +++ b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd.uid @@ -0,0 +1 @@ +uid://dpyjxkrr2illb diff --git a/addons/gdshell/scripts/gdshell_command.gd.uid b/addons/gdshell/scripts/gdshell_command.gd.uid new file mode 100644 index 0000000..fee7be1 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command.gd.uid @@ -0,0 +1 @@ +uid://b1ixb01gw3wcg diff --git a/addons/gdshell/scripts/gdshell_command_db.gd.uid b/addons/gdshell/scripts/gdshell_command_db.gd.uid new file mode 100644 index 0000000..363d2b4 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_db.gd.uid @@ -0,0 +1 @@ +uid://c1lh7iliyrra1 diff --git a/addons/gdshell/scripts/gdshell_command_runner.gd.uid b/addons/gdshell/scripts/gdshell_command_runner.gd.uid new file mode 100644 index 0000000..b28223b --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_runner.gd.uid @@ -0,0 +1 @@ +uid://dai6np6gwb82 diff --git a/addons/gdshell/scripts/gdshell_command_runner_old.gd.uid b/addons/gdshell/scripts/gdshell_command_runner_old.gd.uid new file mode 100644 index 0000000..c96485b --- /dev/null +++ b/addons/gdshell/scripts/gdshell_command_runner_old.gd.uid @@ -0,0 +1 @@ +uid://sp64qq7g876c diff --git a/addons/gdshell/scripts/gdshell_main.gd.uid b/addons/gdshell/scripts/gdshell_main.gd.uid new file mode 100644 index 0000000..1525f16 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_main.gd.uid @@ -0,0 +1 @@ +uid://crc2wmm7dhwew diff --git a/addons/gdshell/scripts/gdshell_ui_handler.gd.uid b/addons/gdshell/scripts/gdshell_ui_handler.gd.uid new file mode 100644 index 0000000..765b826 --- /dev/null +++ b/addons/gdshell/scripts/gdshell_ui_handler.gd.uid @@ -0,0 +1 @@ +uid://cbm77ncihcsgl diff --git a/addons/gdshell/ui/default_ui/default_ui.gd.uid b/addons/gdshell/ui/default_ui/default_ui.gd.uid new file mode 100644 index 0000000..a454d0e --- /dev/null +++ b/addons/gdshell/ui/default_ui/default_ui.gd.uid @@ -0,0 +1 @@ +uid://dyo5rm5v27am7 diff --git a/addons/gdshell/ui/default_ui/default_ui.tscn b/addons/gdshell/ui/default_ui/default_ui.tscn index 73a0205..fe0a073 100644 --- a/addons/gdshell/ui/default_ui/default_ui.tscn +++ b/addons/gdshell/ui/default_ui/default_ui.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=14 format=3 uid="uid://dkixrvtb3a1f8"] -[ext_resource type="Script" path="res://addons/gdshell/ui/default_ui/default_ui.gd" id="2_7mabp"] +[ext_resource type="Script" uid="uid://dyo5rm5v27am7" path="res://addons/gdshell/ui/default_ui/default_ui.gd" id="2_7mabp"] [ext_resource type="FontFile" uid="uid://b8fo1gccd11mc" path="res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf" id="2_cto07"] [ext_resource type="FontFile" uid="uid://by5705qqqugvi" path="res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf" id="2_ncxo2"] [ext_resource type="FontFile" uid="uid://6d4ig2mw72j" path="res://addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf" id="2_xle7x"] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import index 5219755..25454c0 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Bold.ttf.import @@ -23,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import index d4c0da9..c41a92e 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-BoldItalic.ttf.import @@ -23,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import index 0c68da4..c701d50 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Italic.ttf.import @@ -23,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import index 3ff6a6e..04f6075 100644 --- a/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import +++ b/addons/gdshell/ui/fonts/roboto_mono/RobotoMono-Regular.ttf.import @@ -23,6 +23,7 @@ allow_system_fallback=true force_autohinter=false hinting=1 subpixel_positioning=1 +keep_rounding_remainders=true oversampling=0.0 Fallbacks=null fallbacks=[] diff --git a/project.godot b/project.godot index 1b2384f..120b573 100644 --- a/project.godot +++ b/project.godot @@ -13,7 +13,7 @@ config_version=5 config/name="GDShell" config/description="Light-weight customizable in-game console for development, debugging, cheats, etc... for Godot 4." run/main_scene="res://addons/gdshell/demo/demo.tscn" -config/features=PackedStringArray("4.3") +config/features=PackedStringArray("4.4") config/icon="res://addons/gdshell/icon.png" [autoload] From d22c0e978226de328c39ad3101ae2bbee8b8ce56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Thu, 17 Apr 2025 15:25:03 +0200 Subject: [PATCH 19/20] parser comments & error handling --- .../gsdhell_expression_parser.gd | 173 +++++++++++++----- 1 file changed, 128 insertions(+), 45 deletions(-) diff --git a/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd index dea4bcf..916df02 100644 --- a/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd +++ b/addons/gdshell/scripts/expression_compiler/gsdhell_expression_parser.gd @@ -3,6 +3,13 @@ class_name GDShellExpressionParser extends RefCounted +## Two dimensional Array for precedence parser action lookup. +## [br] - < Shift +## [br] - > Reduce +## [br] - = Push +## [br] - ! Error +## [br] - ? Unknown token (not present in this table, but used internally for errors) +## [br] - . End const _PRECEDENCE_TABLE: Array[Array] = [ # ! & && | || ; WORD ( ) $ [&"<", &"<", &">", &">", &">", &">", &"<", &"<", &"!", &">"], # ! @@ -17,6 +24,7 @@ const _PRECEDENCE_TABLE: Array[Array] = [ [&"<", &"<", &"<", &"<", &"<", &"<", &"<", &"<", &"!", &"."], # $ ] +## The index of the Token in this array is the index to _PRECEDENCE_TABLE array. const _PRECEDENCE_TABLE_KEYS: Array[GDShellExpressionTokenizer.Token.Type] = [ GDShellExpressionTokenizer.Token.Type.OPERATOR_NOT, GDShellExpressionTokenizer.Token.Type.OPERATOR_BACKGROUND, @@ -52,16 +60,29 @@ class ParserResult extends RefCounted: static func parse(tokens: Array[GDShellExpressionTokenizer.Token]) -> ParserResult: - tokens.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)) # For easier precedence + # Append EXPRESSION_END Token to the input tokens to make precedence easier + tokens.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)) var current_token_index: int = 0 # Index into tokens array - var token_stack: Array[GDShellExpressionTokenizer.Token] = [GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)] # Helper stack for precedence - var expression_node_stack: Array[Dictionary] = [] # Expresion tree building stack + # Helper Token stack for precedence - initialize it with EXPRESSION_END Token + var token_stack: Array[GDShellExpressionTokenizer.Token] = [GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_END, "$", 0, 0)] + # Expresion stack for expression tree building + var expression_node_stack: Array[Dictionary] = [] while current_token_index < tokens.size(): var topmost_terminal_index: int = _parse_precedence_get_topmost_terminal_index(token_stack) + if topmost_terminal_index == -1: + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "no terminal found in token stack", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + match _parse_get_precedence_action(token_stack[topmost_terminal_index], tokens[current_token_index]): &"<": # Shift - if token_stack.insert(topmost_terminal_index + 1, GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, "<", 0, 0)) != OK: # isert handle for expression reduction + # Insert handle for expression reduction + if token_stack.insert(topmost_terminal_index + 1, GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, "<", 0, 0)) != OK: return ParserResult.new( {}, ParserResult.Status.ERROR, @@ -77,7 +98,7 @@ static func parse(tokens: Array[GDShellExpressionTokenizer.Token]) -> ParserResu return ParserResult.new( {}, ParserResult.Status.ERROR, - "cannot reduce stack to EXPRESSION. Token stack: '%s'" % str(token_stack), + "cannot reduce stack to EXPRESSION", tokens[current_token_index].start_char_index, tokens[current_token_index].consumed_chars ) @@ -90,12 +111,16 @@ static func parse(tokens: Array[GDShellExpressionTokenizer.Token]) -> ParserResu return ParserResult.new( {}, ParserResult.Status.ERROR, - "error in precedence table at [%s,%s] with symbols [%s,%s]" % [ - _parse_get_precedence_index(token_stack[topmost_terminal_index]), - _parse_get_precedence_index(tokens[current_token_index]), - GDShellExpressionTokenizer.Token.Type.find_key(_parse_get_precedence_index(token_stack[topmost_terminal_index])), - GDShellExpressionTokenizer.Token.Type.find_key(_parse_get_precedence_index(tokens[current_token_index])) - ], + "unexpected token", + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) + + &"?": # Unexpected token while parsing + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "unknown, unparseable or unimplemented Token", tokens[current_token_index].start_char_index, tokens[current_token_index].consumed_chars ) @@ -108,21 +133,32 @@ static func parse(tokens: Array[GDShellExpressionTokenizer.Token]) -> ParserResu 0, 0 ) + + var unknown_action: # Unknown action + return ParserResult.new( + {}, + ParserResult.Status.ERROR, + "unknown precedence parser action '%s'" % str(unknown_action), + tokens[current_token_index].start_char_index, + tokens[current_token_index].consumed_chars + ) return ParserResult.new( {}, ParserResult.Status.ERROR, - "parsing ended prematurely due to token buffer out of bounds.", + "parsing ended prematurely due to token buffer out of bounds", tokens[current_token_index].start_char_index, tokens[current_token_index].consumed_chars ) static func _parse_get_reduceable_tokens(token_stack: Array[GDShellExpressionTokenizer.Token]) -> Array[GDShellExpressionTokenizer.Token]: + # Traverse token_stack array backwards + # From the back get all the tokens until expression handle is encountered for i: int in range(token_stack.size() - 1, -1, -1): if token_stack[i].type == GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE: return token_stack.slice(i) - return [] + return [] # No expression handle found - nothing can be reduced static func _parse_precedence_is_token_array_type_patern_match(tokens: Array[GDShellExpressionTokenizer.Token], pattern: Array[GDShellExpressionTokenizer.Token.Type]) -> bool: @@ -136,49 +172,73 @@ static func _parse_precedence_is_token_array_type_patern_match(tokens: Array[GDS static func _parse_precedence_reduce_to_expression(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var reduceable_tokens: Array[GDShellExpressionTokenizer.Token] = _parse_get_reduceable_tokens(token_stack) - match reduceable_tokens.map(func(token: GDShellExpressionTokenizer.Token) -> GDShellExpressionTokenizer.Token.Type: return token.type): + if reduceable_tokens.is_empty(): + push_error("[GDShell] No reduceable tokens found. Missing EXPRESSION_HANDLE Token?") + return false + + var reduceable_tokens_types: Array = reduceable_tokens.map( + func(token: GDShellExpressionTokenizer.Token) -> GDShellExpressionTokenizer.Token.Type: + return token.type + ) + + match reduceable_tokens_types: # E -> WORD+ [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.WORD, ..]: - _parse_reduce_words(token_stack, expression_node_stack) + # Check if all Tokens after EXPRESSION_HANDLE are WORDs because of the open ended pattern + if reduceable_tokens_types.slice(1).all( + func(token_type: GDShellExpressionTokenizer.Token.Type) -> bool: + return token_type == GDShellExpressionTokenizer.Token.Type.WORD + ): + return _parse_reduce_words(token_stack, expression_node_stack) # E -> (E) [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS]: - _parse_reduce_parenthesis(token_stack) + return _parse_reduce_parenthesis(token_stack) # E -> !E [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_NOT, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: - _parse_reduce_not(token_stack, expression_node_stack) + return _parse_reduce_not(token_stack, expression_node_stack) # E -> E& [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_BACKGROUND]: - _parse_reduce_background(token_stack, expression_node_stack) + return _parse_reduce_background(token_stack, expression_node_stack) # E -> E && E [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_AND, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: - _parse_reduce_and(token_stack, expression_node_stack) + return _parse_reduce_and(token_stack, expression_node_stack) # E -> E | E [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_PIPE, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: - _parse_reduce_pipe(token_stack, expression_node_stack) + return _parse_reduce_pipe(token_stack, expression_node_stack) # E -> E || E [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_OR, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: - _parse_reduce_or(token_stack, expression_node_stack) + return _parse_reduce_or(token_stack, expression_node_stack) # E -> E ; E [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.EXPRESSION, GDShellExpressionTokenizer.Token.Type.OPERATOR_SEQUENCE, GDShellExpressionTokenizer.Token.Type.EXPRESSION]: - _parse_reduce_sequence(token_stack, expression_node_stack) + return _parse_reduce_sequence(token_stack, expression_node_stack) # ! -> () [GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE, GDShellExpressionTokenizer.Token.Type.OPERATOR_OPENING_PARENTHESIS, GDShellExpressionTokenizer.Token.Type.OPERATOR_CLOSING_PARENTHESIS]: + # Tried to reduce empty parantheses. This is an error in the input command string. return false - # ! -> .. - var _unmatched_reduceable_tokens_pattern: # Unknown unreduceable stack - return false - return true + + # ! -> .. + push_error("[GDShell] Cannot reduce Tokens. No matching reduction rule found for pattern: %s" % str( + reduceable_tokens_types.map( + func(token_type: GDShellExpressionTokenizer.Token.Type) -> String: + return str(GDShellExpressionTokenizer.Token.Type.find_key(token_type))) + ) + ) + return false -# E -> WORD* -static func _parse_reduce_words(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: +## E -> WORD+ +static func _parse_reduce_words(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var word_tokens: Array[GDShellExpressionTokenizer.Token] = [] while token_stack.back() != null: var current_token: GDShellExpressionTokenizer.Token = token_stack.pop_back() - if current_token.type == GDShellExpressionTokenizer.Token.Type.EXPRESSION_HANDLE: + if current_token.type != GDShellExpressionTokenizer.Token.Type.WORD: break word_tokens.push_front(current_token) + if word_tokens.is_empty(): + push_error("[GDShell] No reduceable WORDs.") + return false + token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) expression_node_stack.push_back({ "type": "command", @@ -190,23 +250,26 @@ static func _parse_reduce_words(token_stack: Array[GDShellExpressionTokenizer.To return word_token.content ), }) + return true -# E -> (E) -static func _parse_reduce_parenthesis(token_stack: Array[GDShellExpressionTokenizer.Token]) -> void: +## E -> (E) +static func _parse_reduce_parenthesis(token_stack: Array[GDShellExpressionTokenizer.Token]) -> bool: token_stack.pop_back() # ) token_stack.pop_back() # EXPRESSION token_stack.pop_back() # ( token_stack.pop_back() # EXPRESSION_HANDLE token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true -static func _parse_reduce_not(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: +## E -> !E +static func _parse_reduce_not(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var right: Dictionary = expression_node_stack.pop_back() expression_node_stack.push_back({ "type": "operator", "index": token_stack[-2].start_char_index, - "length": 1, + "length": token_stack[-2].consumed_chars, "operator": "!", "right": right, }) @@ -214,14 +277,16 @@ static func _parse_reduce_not(token_stack: Array[GDShellExpressionTokenizer.Toke token_stack.pop_back() # OPERATOR_NOT token_stack.pop_back() # EXPRESSION_HANDLE token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true -static func _parse_reduce_background(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: +## E -> E& +static func _parse_reduce_background(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var left: Dictionary = expression_node_stack.pop_back() expression_node_stack.push_back({ "type": "operator", "index": token_stack[-1].start_char_index, - "length": 1, + "length": token_stack[-2].consumed_chars, "operator": "&", "left": left, }) @@ -229,15 +294,17 @@ static func _parse_reduce_background(token_stack: Array[GDShellExpressionTokeniz token_stack.pop_back() # EXPRESSION token_stack.pop_back() # EXPRESSION_HANDLE token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true -static func _parse_reduce_and(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: +## E -> E && E +static func _parse_reduce_and(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var right: Dictionary = expression_node_stack.pop_back() var left: Dictionary = expression_node_stack.pop_back() expression_node_stack.push_back({ "type": "operator", "index": token_stack[-2].start_char_index, - "length": 2, + "length": token_stack[-2].consumed_chars, "operator": "&&", "left": left, "right": right, @@ -247,15 +314,17 @@ static func _parse_reduce_and(token_stack: Array[GDShellExpressionTokenizer.Toke token_stack.pop_back() # EXPRESSION token_stack.pop_back() # EXPRESSION_HANDLE token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true -static func _parse_reduce_pipe(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: +## E -> E | E +static func _parse_reduce_pipe(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var right: Dictionary = expression_node_stack.pop_back() var left: Dictionary = expression_node_stack.pop_back() expression_node_stack.push_back({ "type": "operator", "index": token_stack[-2].start_char_index, - "length": 1, + "length": token_stack[-2].consumed_chars, "operator": "|", "left": left, "right": right, @@ -265,15 +334,17 @@ static func _parse_reduce_pipe(token_stack: Array[GDShellExpressionTokenizer.Tok token_stack.pop_back() # EXPRESSION token_stack.pop_back() # EXPRESSION_HANDLE token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true -static func _parse_reduce_or(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: +## E -> E || E +static func _parse_reduce_or(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var right: Dictionary = expression_node_stack.pop_back() var left: Dictionary = expression_node_stack.pop_back() expression_node_stack.push_back({ "type": "operator", "index": token_stack[-2].start_char_index, - "length": 2, + "length": token_stack[-2].consumed_chars, "operator": "||", "left": left, "right": right, @@ -283,15 +354,17 @@ static func _parse_reduce_or(token_stack: Array[GDShellExpressionTokenizer.Token token_stack.pop_back() # EXPRESSION token_stack.pop_back() # EXPRESSION_HANDLE token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true -static func _parse_reduce_sequence(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> void: +## E -> E ; E +static func _parse_reduce_sequence(token_stack: Array[GDShellExpressionTokenizer.Token], expression_node_stack: Array[Dictionary]) -> bool: var right: Dictionary = expression_node_stack.pop_back() var left: Dictionary = expression_node_stack.pop_back() expression_node_stack.push_back({ "type": "operator", "index": token_stack[-2].start_char_index, - "length": 1, + "length": token_stack[-2].consumed_chars, "operator": ";", "left": left, "right": right, @@ -301,19 +374,29 @@ static func _parse_reduce_sequence(token_stack: Array[GDShellExpressionTokenizer token_stack.pop_back() # EXPRESSION token_stack.pop_back() # EXPRESSION_HANDLE token_stack.push_back(GDShellExpressionTokenizer.Token.new(GDShellExpressionTokenizer.Token.Type.EXPRESSION, "", 0, 0)) + return true +## Finds the topmost terminal (not Expression) on the [param token_stack]. static func _parse_precedence_get_topmost_terminal_index(token_stack: Array[GDShellExpressionTokenizer.Token]) -> int: + # Traverse token_stack array backwards for i: int in range(token_stack.size() - 1, -1, -1): if token_stack[i].type == GDShellExpressionTokenizer.Token.Type.EXPRESSION: continue return i - return 0 + push_error("[GDShell] No terminal present in the Token stack. Missing EXPRESSION_END Token?") + return -1 +## Finds the tokens index in the [member _PRECEDENCE_TABLE_KEYS]. static func _parse_get_precedence_index(token: GDShellExpressionTokenizer.Token) -> int: return _PRECEDENCE_TABLE_KEYS.find(token.type) +## Given two [GDShellExpressionTokenizer.Token]s, returns the required action for the precedence parser. static func _parse_get_precedence_action(stack_token: GDShellExpressionTokenizer.Token, current_token: GDShellExpressionTokenizer.Token) -> String: - return _PRECEDENCE_TABLE[_parse_get_precedence_index(stack_token)][_parse_get_precedence_index(current_token)] + var stack_token_precedence_index: int = _parse_get_precedence_index(stack_token) + var current_token_precedence_index: int = _parse_get_precedence_index(current_token) + if stack_token_precedence_index == -1 or current_token_precedence_index == -1: + return &"?" # Unparseable unknown token - might be unimplemented + return _PRECEDENCE_TABLE[stack_token_precedence_index][current_token_precedence_index] From ea781c25e9282c996b91fef9e120ad03627b38c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jan=C5=A1ta?= Date: Fri, 25 Apr 2025 13:38:44 +0200 Subject: [PATCH 20/20] Cleanup --- .../gdshell_expression_compiler.gd | 27 +++++++------------ .../scripts/gdshell_command_runner_old.gd | 1 - addons/gdshell/scripts/gdshell_main.gd | 11 ++++---- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd index 216636b..ca4b5e0 100644 --- a/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd +++ b/addons/gdshell/scripts/expression_compiler/gdshell_expression_compiler.gd @@ -13,24 +13,26 @@ class CompilerResult extends RefCounted: var result: Dictionary var status: Status var input_expression: String - var error_description: String + var description: String var input_expression_error_start_index: int var input_expression_error_length: int - func _init(_result: Dictionary, _status: Status, _input_expression: String, _error_description: String, _input_expression_error_start_index: int, _input_expression_error_length: int) -> void: + func _init(_result: Dictionary, _status: Status, _input_expression: String, _description: String, _input_expression_error_start_index: int, _input_expression_error_length: int) -> void: result = _result status = _status input_expression = _input_expression - error_description = _error_description + description = _description input_expression_error_start_index = _input_expression_error_start_index input_expression_error_length = _input_expression_error_length func _to_string() -> String: - return "{CompilerResult: %s, error_description: \"%s\"}" % [Status.find_key(status), error_description] + return "{CompilerResult: %s, description: \"%s\"}" % [Status.find_key(status), description] static func compile(input_expression: String) -> CompilerResult: + # Tokenize the input var tokenizer_result: GDShellExpressionTokenizer.TokenizerResult = GDShellExpressionTokenizer.tokenize(input_expression) + if tokenizer_result.status == GDShellExpressionTokenizer.TokenizerResult.Status.ERROR: return CompilerResult.new( {}, @@ -49,7 +51,8 @@ static func compile(input_expression: String) -> CompilerResult: tokenizer_result.result[-1].start_char_index, tokenizer_result.result[-1].consumed_chars ) - if tokenizer_result.result.is_empty(): # empty input - dont even bother with parsing + # empty input - dont even bother with parsing + if tokenizer_result.result.is_empty(): return CompilerResult.new( {}, CompilerResult.Status.OK, @@ -59,20 +62,11 @@ static func compile(input_expression: String) -> CompilerResult: 0 ) + # Parse the tokenized input var parser_result: GDShellExpressionParser.ParserResult = GDShellExpressionParser.parse(tokenizer_result.result) - if parser_result.status == GDShellExpressionParser.ParserResult.Status.ERROR: - return CompilerResult.new( - parser_result.result, - CompilerResult.Status.ERROR, - input_expression, - parser_result.description, - parser_result.input_expression_error_start_index, - parser_result.input_expression_error_length - ) - return CompilerResult.new( parser_result.result, - CompilerResult.Status.OK, + CompilerResult.Status.OK if parser_result.status == GDShellExpressionParser.ParserResult.Status.OK else CompilerResult.Status.ERROR, input_expression, parser_result.description, parser_result.input_expression_error_start_index, @@ -80,7 +74,6 @@ static func compile(input_expression: String) -> CompilerResult: ) - static func is_command_expression_valid(command_expression: Dictionary, error_info: bool = false, command_db: GDShellCommandDB = null) -> bool: if not command_expression.has_all(["type", "name", "args"]): return false diff --git a/addons/gdshell/scripts/gdshell_command_runner_old.gd b/addons/gdshell/scripts/gdshell_command_runner_old.gd index 3125eb5..b4536ae 100644 --- a/addons/gdshell/scripts/gdshell_command_runner_old.gd +++ b/addons/gdshell/scripts/gdshell_command_runner_old.gd @@ -200,4 +200,3 @@ func _handle_get_ui_handler() -> GDShellUIHandler: func _handle_get_ui_handler_rich_text_label() -> RichTextLabel: return _PARENT_GDSHELL.get_ui_handler_rich_text_label() - diff --git a/addons/gdshell/scripts/gdshell_main.gd b/addons/gdshell/scripts/gdshell_main.gd index fe25050..bbf1f95 100644 --- a/addons/gdshell/scripts/gdshell_main.gd +++ b/addons/gdshell/scripts/gdshell_main.gd @@ -15,17 +15,18 @@ var _input_buffer: String = "" var _input_requested: bool = false -func _ready() -> void: +func _ready() -> void: _ui_handler_canvas_layer = CanvasLayer.new() _ui_handler_canvas_layer.layer = 100 add_child(_ui_handler_canvas_layer) setup_with_default_values() - var result = GDShellExpressionCompiler.compile("a && b ; (c | d)") - print(GDShellExpressionCompiler.is_expression_valid(result.result)) - print(result) - print(result.result) + var result = GDShellExpressionCompiler.compile("'a''a'") + #var result = GDShellExpressionCompiler.compile("a && b ; (c | d)") + printerr(GDShellExpressionCompiler.is_expression_valid(result.result)) + print(JSON.stringify(result.result, "\t", false)) + #print(result.result) return #if "autorun" in command_db.get_all_command_names():