# Copyright 2008-2015 Nokia Networks
# Copyright 2016- Robot Framework Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
_CONTROL_WORDS = frozenset(("ELSE", "ELSE IF", "AND", "WITH NAME", "AS"))
_SEQUENCES_TO_BE_ESCAPED = ("\\", "${", "@{", "%{", "&{", "*{", "=")
[docs]
def escape(item):
if not isinstance(item, str):
return item
if item in _CONTROL_WORDS:
return "\\" + item
for seq in _SEQUENCES_TO_BE_ESCAPED:
if seq in item:
item = item.replace(seq, "\\" + seq)
return item
[docs]
def glob_escape(item):
# Python 3.4+ has `glob.escape()` but it has special handling for drives
# that we don't want.
for char in "[*?":
if char in item:
item = item.replace(char, f"[{char}]")
return item
[docs]
class Unescaper:
_escape_sequences = re.compile(
r"""
(\\+) # escapes
(n|r|t # n, r, or t
|x[0-9a-fA-F]{2} # x+HH
|u[0-9a-fA-F]{4} # u+HHHH
|U[0-9a-fA-F]{8} # U+HHHHHHHH
)? # optionally
""",
re.VERBOSE,
)
def __init__(self):
self._escape_handlers = {
"": lambda value: value,
"n": lambda value: "\n",
"r": lambda value: "\r",
"t": lambda value: "\t",
"x": self._hex_to_unichr,
"u": self._hex_to_unichr,
"U": self._hex_to_unichr,
}
def _hex_to_unichr(self, value):
ordinal = int(value, 16)
# No Unicode code points above 0x10FFFF
if ordinal > 0x10FFFF:
return "U" + value
# `chr` only supports ordinals up to 0xFFFF on narrow Python builds.
# This may not be relevant anymore.
if ordinal > 0xFFFF:
return eval(rf"'\U{ordinal:08x}'")
return chr(ordinal)
[docs]
def unescape(self, item):
if not isinstance(item, str) or "\\" not in item:
return item
return self._escape_sequences.sub(self._handle_escapes, item)
def _handle_escapes(self, match):
escapes, text = match.groups()
half, is_escaped = divmod(len(escapes), 2)
escapes = escapes[:half]
text = text or ""
if is_escaped:
marker, value = text[:1], text[1:]
text = self._escape_handlers[marker](value)
return escapes + text
unescape = Unescaper().unescape
[docs]
def split_from_equals(value):
from robot.variables import VariableMatches
if not isinstance(value, str) or "=" not in value:
return value, None
matches = VariableMatches(value, ignore_errors=True)
if not matches and "\\" not in value:
return tuple(value.split("=", 1))
try:
index = _find_split_index(value, matches)
except ValueError:
return value, None
return value[:index], value[index + 1 :]
def _find_split_index(string, matches):
remaining = string
relative_index = 0
for match in matches:
try:
return _find_split_index_from_part(match.before) + relative_index
except ValueError:
remaining = match.after
relative_index += match.end
return _find_split_index_from_part(remaining) + relative_index
def _find_split_index_from_part(string):
index = 0
while "=" in string[index:]:
index += string[index:].index("=")
if _not_escaping(string[:index]):
return index
index += 1
raise ValueError
def _not_escaping(name):
backslashes = len(name) - len(name.rstrip("\\"))
return backslashes % 2 == 0