Module refinery.lib.scripts.ps1.token
Expand source code Browse git
from __future__ import annotations
import enum
import unicodedata
from dataclasses import dataclass
BACKTICK_ESCAPE = {
'0' : '\0',
'a' : '\a',
'b' : '\b',
'e' : '\x1b',
'f' : '\f',
'n' : '\n',
'r' : '\r',
't' : '\t',
'v' : '\v',
}
#: The inverse of `BACKTICK_ESCAPE`: how a character that cannot stand for itself inside a
#: double-quoted string is written. It belongs beside the table it inverts, and beside the lexer
#: that reads what it writes, so that reading and writing an escape stay one fact.
BACKTICK_ENCODE = {v: F'`{k}' for k, v in BACKTICK_ESCAPE.items()}
SINGLE_QUOTES = frozenset("'\u2018\u2019\u201A\u201B")
DOUBLE_QUOTES = frozenset('"\u201C\u201D\u201E')
DASHES = frozenset('-\u2013\u2014\u2015')
#: The Unicode general categories a space, a line separator and a paragraph separator carry. Above
#: Latin-1 these are the whole of PowerShell's whitespace rule, which asks `char.IsSeparator` and
#: nothing of its own.
_SEPARATOR_CATEGORIES = frozenset(('Zl', 'Zp', 'Zs'))
#: The ASCII characters PowerShell passes over between two tokens. A newline is deliberately not one
#: of them: it ends a statement, so it is a token rather than the space around one.
_ASCII_WHITESPACE = frozenset(' \t\v\f')
#: The two characters above ASCII and below `_SEPARATOR_CATEGORIES`' reach that PowerShell names one
#: at a time. Neither is settled by category: a no-break space is a separator and would be found
#: anyway, and a next-line control is not one and would not.
_LATIN1_WHITESPACE = frozenset('\u00A0\u0085')
#: The ASCII characters that end the token being read, whatever that token began with. This is what
#: makes `a#b` one word where `a{` is two.
_ASCII_FORCE_NEW_TOKEN = frozenset(' \t\n\v\f\r&(),;{|}')
#: The ASCII characters that end a token only where it began with a digit, which is the whole of the
#: difference between `7z` and `7+`. A `?` and a `:` are not among them: they end a numeral from 7.0
#: onwards, where a ternary operator may follow one, and on 5.1 `1?` is a single word.
_ASCII_FORCE_NEW_TOKEN_AFTER_NUMBER = frozenset('!#%*+-./<=>]')
def is_whitespace(c: str) -> bool:
"""
Whether `c` stands between two tokens without being one. Above Latin-1 the question is Unicode's
rather than PowerShell's \u2014 every separator counts, so an em space and a line separator part two
words exactly as a space does \u2014 and below it the answer is a table, which is why a vertical tab
and a form feed are whitespace while a carriage return is not.
"""
if c < '\x80':
return c in _ASCII_WHITESPACE
if c <= '\u0100':
return c in _LATIN1_WHITESPACE
return unicodedata.category(c) in _SEPARATOR_CATEGORIES
def forces_new_token(c: str) -> bool:
"""
Whether `c` ends the token being read, whatever that token began with. Above ASCII that is
exactly whitespace; below it there are a handful more.
The empty string stands for the end of the source, and it ends every token. PowerShell says the
same thing by reading a NUL past the last character and listing the NUL in the table, which is
a spelling this cannot borrow: the reader here asks about its own first character, where
PowerShell's has already taken one, so a NUL in the set would part a word beginning with one
that PowerShell keeps whole.
"""
if not c:
return True
if c < '\x80':
return c in _ASCII_FORCE_NEW_TOKEN
return is_whitespace(c)
def forces_new_token_after_number(c: str) -> bool:
"""
Whether `c` ends a token that began with a digit. A numeral ends on more characters than a word
does, which is the whole of why `7z` is one token and `7+` is two. Above ASCII a dash is the
only one, in each of the spellings `DASHES` accepts.
The end of the source is not among them and is not meant to be: it is `forces_new_token` that
answers for it, and a reader has to ask that one first anyway, because what ends every token
ends a numeral too.
"""
if c < '\x80':
return c in _ASCII_FORCE_NEW_TOKEN_AFTER_NUMBER
return c in DASHES
NORMALIZE_QUOTES = str.maketrans({
'\u2018': "'",
'\u2019': "'",
'\u201A': "'",
'\u201B': "'",
'\u201C': '"',
'\u201D': '"',
'\u201E': '"',
})
_VARIABLE_PATTERN_CORE = (
r'(?:[a-zA-Z0-9_]+:(?!:))?'
r'(?:\{[^}]+\}|[a-zA-Z0-9_][a-zA-Z0-9_?]*)'
r'|[$?^]'
)
def _strip_backtick_noop(name: str) -> str:
result: list[str] = []
i = 0
while i < len(name):
if name[i] == '`' and i + 1 < len(name):
result.append(name[i + 1])
i += 2
continue
result.append(name[i])
i += 1
return ''.join(result)
class Ps1TokenKind(enum.Enum):
INTEGER = 'integer' # noqa
REAL = 'real' # noqa
STRING_VERBATIM = 'sq-string' # noqa
STRING_EXPAND = 'dq-string' # noqa
HSTRING_VERBATIM = 'sq-hstring' # noqa
HSTRING_EXPAND = 'dq-hstring' # noqa
VARIABLE = 'variable' # noqa
SPLAT_VARIABLE = 'splat-variable' # noqa
LABEL = 'label' # noqa
PLUS = '+' # noqa
DASH = '-' # noqa
STAR = '*' # noqa
SLASH = '/' # noqa
PERCENT = '%' # noqa
DOT = '.' # noqa
DOTDOT = '..' # noqa
COMMA = ',' # noqa
SEMICOLON = ';' # noqa
INCREMENT = '++' # noqa
DECREMENT = '--' # noqa
EXCLAIM = '!' # noqa
DOUBLE_COLON = '::' # noqa
DOUBLE_AMPERSAND = '&&' # noqa
DOUBLE_PIPE = '||' # noqa
EQUALS = '=' # noqa
PLUS_ASSIGN = '+=' # noqa
DASH_ASSIGN = '-=' # noqa
STAR_ASSIGN = '*=' # noqa
SLASH_ASSIGN = '/=' # noqa
PERCENT_ASSIGN = '%=' # noqa
OPERATOR = 'operator' # noqa
PARAMETER = 'parameter' # noqa
GENERIC_TOKEN = 'generic-token' # noqa
GENERIC_EXPAND = 'generic-expand' # noqa
LPAREN = '(' # noqa
RPAREN = ')' # noqa
LBRACE = '{' # noqa
RBRACE = '}' # noqa
LBRACKET = '[' # noqa
RBRACKET = ']' # noqa
AT_LPAREN = '@(' # noqa
AT_LBRACE = '@{' # noqa
DOLLAR_LPAREN = '$(' # noqa
PIPE = '|' # noqa
AMPERSAND = '&' # noqa
REDIRECTION = 'redirection' # noqa
REDIRECT_IN = '<' # noqa
IF = 'if' # noqa
ELSEIF = 'elseif' # noqa
ELSE = 'else' # noqa
SWITCH = 'switch' # noqa
WHILE = 'while' # noqa
FOR = 'for' # noqa
FOREACH = 'foreach' # noqa
DO = 'do' # noqa
UNTIL = 'until' # noqa
FUNCTION = 'function' # noqa
FILTER = 'filter' # noqa
RETURN = 'return' # noqa
BREAK = 'break' # noqa
CONTINUE = 'continue' # noqa
THROW = 'throw' # noqa
EXIT = 'exit' # noqa
TRY = 'try' # noqa
CATCH = 'catch' # noqa
FINALLY = 'finally' # noqa
TRAP = 'trap' # noqa
DATA = 'data' # noqa
BEGIN = 'begin' # noqa
PROCESS = 'process' # noqa
END = 'end' # noqa
PARAM = 'param' # noqa
IN = 'in' # noqa
CLASS = 'class' # noqa
USING = 'using' # noqa
ENUM = 'enum' # noqa
DYNAMICPARAM = 'dynamicparam' # noqa
NEWLINE = 'newline' # noqa
EOF = 'eof' # noqa
@property
def is_keyword(self):
return self in _KEYWORDS_SET
@property
def is_assignment(self):
return self in _ASSIGNMENT_SET
@property
def mode_invariant(self):
"""
Whether the text of a token of this kind reads the same in every
`refinery.lib.scripts.ps1.lexer.Ps1LexerMode`. A parser holding one as lookahead may keep it
across a mode change, because scanning it again could not produce anything else. A kind
earns membership only by being witnessed in the material the invariance test drives: an
entry that test cannot reach is a claim nothing checks, and skipping the re-read is worth
nothing next to that.
"""
return self in _MODE_INVARIANT_SET
_KEYWORDS: dict[str, Ps1TokenKind] = {
'if' : Ps1TokenKind.IF,
'elseif' : Ps1TokenKind.ELSEIF,
'else' : Ps1TokenKind.ELSE,
'switch' : Ps1TokenKind.SWITCH,
'while' : Ps1TokenKind.WHILE,
'for' : Ps1TokenKind.FOR,
'foreach' : Ps1TokenKind.FOREACH,
'do' : Ps1TokenKind.DO,
'until' : Ps1TokenKind.UNTIL,
'function' : Ps1TokenKind.FUNCTION,
'filter' : Ps1TokenKind.FILTER,
'return' : Ps1TokenKind.RETURN,
'break' : Ps1TokenKind.BREAK,
'continue' : Ps1TokenKind.CONTINUE,
'throw' : Ps1TokenKind.THROW,
'exit' : Ps1TokenKind.EXIT,
'try' : Ps1TokenKind.TRY,
'catch' : Ps1TokenKind.CATCH,
'finally' : Ps1TokenKind.FINALLY,
'trap' : Ps1TokenKind.TRAP,
'data' : Ps1TokenKind.DATA,
'begin' : Ps1TokenKind.BEGIN,
'process' : Ps1TokenKind.PROCESS,
'end' : Ps1TokenKind.END,
'param' : Ps1TokenKind.PARAM,
'in' : Ps1TokenKind.IN,
'class' : Ps1TokenKind.CLASS,
'using' : Ps1TokenKind.USING,
'enum' : Ps1TokenKind.ENUM,
'dynamicparam' : Ps1TokenKind.DYNAMICPARAM,
}
_KEYWORDS_SET = frozenset(_KEYWORDS.values())
_ASSIGNMENT_SET = frozenset((
Ps1TokenKind.EQUALS,
Ps1TokenKind.PLUS_ASSIGN,
Ps1TokenKind.DASH_ASSIGN,
Ps1TokenKind.STAR_ASSIGN,
Ps1TokenKind.SLASH_ASSIGN,
Ps1TokenKind.PERCENT_ASSIGN,
))
_MODE_INVARIANT_SET = frozenset((
Ps1TokenKind.AMPERSAND,
Ps1TokenKind.AT_LBRACE,
Ps1TokenKind.AT_LPAREN,
Ps1TokenKind.COMMA,
Ps1TokenKind.DOLLAR_LPAREN,
Ps1TokenKind.EOF,
Ps1TokenKind.HSTRING_VERBATIM,
Ps1TokenKind.LBRACE,
Ps1TokenKind.LPAREN,
Ps1TokenKind.NEWLINE,
Ps1TokenKind.PIPE,
Ps1TokenKind.RBRACE,
Ps1TokenKind.RBRACKET,
Ps1TokenKind.RPAREN,
Ps1TokenKind.SEMICOLON,
Ps1TokenKind.STRING_EXPAND,
Ps1TokenKind.STRING_VERBATIM,
))
KEYWORD_SPELLING: dict[str, str] = {
'param': 'Param',
}
@dataclass
class Ps1Token:
kind: Ps1TokenKind
value: str
offset: int
def __repr__(self):
v = self.value
if len(v) > 40:
v = v[:37] + '...'
return F'Token({self.kind.name}, {v!r}, @{self.offset})'
Global variables
var BACKTICK_ENCODE-
The inverse of
BACKTICK_ESCAPE: how a character that cannot stand for itself inside a double-quoted string is written. It belongs beside the table it inverts, and beside the lexer that reads what it writes, so that reading and writing an escape stay one fact.
Functions
def is_whitespace(c)-
Whether
cstands between two tokens without being one. Above Latin-1 the question is Unicode's rather than PowerShell's — every separator counts, so an em space and a line separator part two words exactly as a space does — and below it the answer is a table, which is why a vertical tab and a form feed are whitespace while a carriage return is not.Expand source code Browse git
def is_whitespace(c: str) -> bool: """ Whether `c` stands between two tokens without being one. Above Latin-1 the question is Unicode's rather than PowerShell's \u2014 every separator counts, so an em space and a line separator part two words exactly as a space does \u2014 and below it the answer is a table, which is why a vertical tab and a form feed are whitespace while a carriage return is not. """ if c < '\x80': return c in _ASCII_WHITESPACE if c <= '\u0100': return c in _LATIN1_WHITESPACE return unicodedata.category(c) in _SEPARATOR_CATEGORIES def forces_new_token(c)-
Whether
cends the token being read, whatever that token began with. Above ASCII that is exactly whitespace; below it there are a handful more.The empty string stands for the end of the source, and it ends every token. PowerShell says the same thing by reading a NUL past the last character and listing the NUL in the table, which is a spelling this cannot borrow: the reader here asks about its own first character, where PowerShell's has already taken one, so a NUL in the set would part a word beginning with one that PowerShell keeps whole.
Expand source code Browse git
def forces_new_token(c: str) -> bool: """ Whether `c` ends the token being read, whatever that token began with. Above ASCII that is exactly whitespace; below it there are a handful more. The empty string stands for the end of the source, and it ends every token. PowerShell says the same thing by reading a NUL past the last character and listing the NUL in the table, which is a spelling this cannot borrow: the reader here asks about its own first character, where PowerShell's has already taken one, so a NUL in the set would part a word beginning with one that PowerShell keeps whole. """ if not c: return True if c < '\x80': return c in _ASCII_FORCE_NEW_TOKEN return is_whitespace(c) def forces_new_token_after_number(c)-
Whether
cends a token that began with a digit. A numeral ends on more characters than a word does, which is the whole of why7zis one token and7+is two. Above ASCII a dash is the only one, in each of the spellingsDASHESaccepts.The end of the source is not among them and is not meant to be: it is
forces_new_token()that answers for it, and a reader has to ask that one first anyway, because what ends every token ends a numeral too.Expand source code Browse git
def forces_new_token_after_number(c: str) -> bool: """ Whether `c` ends a token that began with a digit. A numeral ends on more characters than a word does, which is the whole of why `7z` is one token and `7+` is two. Above ASCII a dash is the only one, in each of the spellings `DASHES` accepts. The end of the source is not among them and is not meant to be: it is `forces_new_token` that answers for it, and a reader has to ask that one first anyway, because what ends every token ends a numeral too. """ if c < '\x80': return c in _ASCII_FORCE_NEW_TOKEN_AFTER_NUMBER return c in DASHES
Classes
class Ps1TokenKind (*args, **kwds)-
Create a collection of name/value pairs.
Example enumeration:
>>> class Color(Enum): ... RED = 1 ... BLUE = 2 ... GREEN = 3Access them by:
- attribute access:
Color.RED
- value lookup:
Color(1)
- name lookup:
Color['RED']
Enumerations can be iterated over, and know how many members they have:
>>> len(Color) 3>>> list(Color) [<Color.RED: 1>, <Color.BLUE: 2>, <Color.GREEN: 3>]Methods can be added to enumerations, and members can have their own attributes – see the documentation for details.
Expand source code Browse git
class Ps1TokenKind(enum.Enum): INTEGER = 'integer' # noqa REAL = 'real' # noqa STRING_VERBATIM = 'sq-string' # noqa STRING_EXPAND = 'dq-string' # noqa HSTRING_VERBATIM = 'sq-hstring' # noqa HSTRING_EXPAND = 'dq-hstring' # noqa VARIABLE = 'variable' # noqa SPLAT_VARIABLE = 'splat-variable' # noqa LABEL = 'label' # noqa PLUS = '+' # noqa DASH = '-' # noqa STAR = '*' # noqa SLASH = '/' # noqa PERCENT = '%' # noqa DOT = '.' # noqa DOTDOT = '..' # noqa COMMA = ',' # noqa SEMICOLON = ';' # noqa INCREMENT = '++' # noqa DECREMENT = '--' # noqa EXCLAIM = '!' # noqa DOUBLE_COLON = '::' # noqa DOUBLE_AMPERSAND = '&&' # noqa DOUBLE_PIPE = '||' # noqa EQUALS = '=' # noqa PLUS_ASSIGN = '+=' # noqa DASH_ASSIGN = '-=' # noqa STAR_ASSIGN = '*=' # noqa SLASH_ASSIGN = '/=' # noqa PERCENT_ASSIGN = '%=' # noqa OPERATOR = 'operator' # noqa PARAMETER = 'parameter' # noqa GENERIC_TOKEN = 'generic-token' # noqa GENERIC_EXPAND = 'generic-expand' # noqa LPAREN = '(' # noqa RPAREN = ')' # noqa LBRACE = '{' # noqa RBRACE = '}' # noqa LBRACKET = '[' # noqa RBRACKET = ']' # noqa AT_LPAREN = '@(' # noqa AT_LBRACE = '@{' # noqa DOLLAR_LPAREN = '$(' # noqa PIPE = '|' # noqa AMPERSAND = '&' # noqa REDIRECTION = 'redirection' # noqa REDIRECT_IN = '<' # noqa IF = 'if' # noqa ELSEIF = 'elseif' # noqa ELSE = 'else' # noqa SWITCH = 'switch' # noqa WHILE = 'while' # noqa FOR = 'for' # noqa FOREACH = 'foreach' # noqa DO = 'do' # noqa UNTIL = 'until' # noqa FUNCTION = 'function' # noqa FILTER = 'filter' # noqa RETURN = 'return' # noqa BREAK = 'break' # noqa CONTINUE = 'continue' # noqa THROW = 'throw' # noqa EXIT = 'exit' # noqa TRY = 'try' # noqa CATCH = 'catch' # noqa FINALLY = 'finally' # noqa TRAP = 'trap' # noqa DATA = 'data' # noqa BEGIN = 'begin' # noqa PROCESS = 'process' # noqa END = 'end' # noqa PARAM = 'param' # noqa IN = 'in' # noqa CLASS = 'class' # noqa USING = 'using' # noqa ENUM = 'enum' # noqa DYNAMICPARAM = 'dynamicparam' # noqa NEWLINE = 'newline' # noqa EOF = 'eof' # noqa @property def is_keyword(self): return self in _KEYWORDS_SET @property def is_assignment(self): return self in _ASSIGNMENT_SET @property def mode_invariant(self): """ Whether the text of a token of this kind reads the same in every `refinery.lib.scripts.ps1.lexer.Ps1LexerMode`. A parser holding one as lookahead may keep it across a mode change, because scanning it again could not produce anything else. A kind earns membership only by being witnessed in the material the invariance test drives: an entry that test cannot reach is a claim nothing checks, and skipping the re-read is worth nothing next to that. """ return self in _MODE_INVARIANT_SETAncestors
- enum.Enum
Class variables
var INTEGER-
The type of the None singleton.
var REAL-
The type of the None singleton.
var STRING_VERBATIM-
The type of the None singleton.
var STRING_EXPAND-
The type of the None singleton.
var HSTRING_VERBATIM-
The type of the None singleton.
var HSTRING_EXPAND-
The type of the None singleton.
var VARIABLE-
The type of the None singleton.
var SPLAT_VARIABLE-
The type of the None singleton.
var LABEL-
The type of the None singleton.
var PLUS-
The type of the None singleton.
var DASH-
The type of the None singleton.
var STAR-
The type of the None singleton.
var SLASH-
The type of the None singleton.
var PERCENT-
The type of the None singleton.
var DOT-
The type of the None singleton.
var DOTDOT-
The type of the None singleton.
var COMMA-
The type of the None singleton.
var SEMICOLON-
The type of the None singleton.
var INCREMENT-
The type of the None singleton.
var DECREMENT-
The type of the None singleton.
var EXCLAIM-
The type of the None singleton.
var DOUBLE_COLON-
The type of the None singleton.
var DOUBLE_AMPERSAND-
The type of the None singleton.
var DOUBLE_PIPE-
The type of the None singleton.
var EQUALS-
The type of the None singleton.
var PLUS_ASSIGN-
The type of the None singleton.
var DASH_ASSIGN-
The type of the None singleton.
var STAR_ASSIGN-
The type of the None singleton.
var SLASH_ASSIGN-
The type of the None singleton.
var PERCENT_ASSIGN-
The type of the None singleton.
var OPERATOR-
The type of the None singleton.
var PARAMETER-
The type of the None singleton.
var GENERIC_TOKEN-
The type of the None singleton.
var GENERIC_EXPAND-
The type of the None singleton.
var LPAREN-
The type of the None singleton.
var RPAREN-
The type of the None singleton.
var LBRACE-
The type of the None singleton.
var RBRACE-
The type of the None singleton.
var LBRACKET-
The type of the None singleton.
var RBRACKET-
The type of the None singleton.
var AT_LPAREN-
The type of the None singleton.
var AT_LBRACE-
The type of the None singleton.
var DOLLAR_LPAREN-
The type of the None singleton.
var PIPE-
The type of the None singleton.
var AMPERSAND-
The type of the None singleton.
var REDIRECTION-
The type of the None singleton.
var REDIRECT_IN-
The type of the None singleton.
var IF-
The type of the None singleton.
var ELSEIF-
The type of the None singleton.
var ELSE-
The type of the None singleton.
var SWITCH-
The type of the None singleton.
var WHILE-
The type of the None singleton.
var FOR-
The type of the None singleton.
var FOREACH-
The type of the None singleton.
var DO-
The type of the None singleton.
var UNTIL-
The type of the None singleton.
var FUNCTION-
The type of the None singleton.
var FILTER-
The type of the None singleton.
var RETURN-
The type of the None singleton.
var BREAK-
The type of the None singleton.
var CONTINUE-
The type of the None singleton.
var THROW-
The type of the None singleton.
var EXIT-
The type of the None singleton.
var TRY-
The type of the None singleton.
var CATCH-
The type of the None singleton.
var FINALLY-
The type of the None singleton.
var TRAP-
The type of the None singleton.
var DATA-
The type of the None singleton.
var BEGIN-
The type of the None singleton.
var PROCESS-
The type of the None singleton.
var END-
The type of the None singleton.
var PARAM-
The type of the None singleton.
var IN-
The type of the None singleton.
var CLASS-
The type of the None singleton.
var USING-
The type of the None singleton.
var ENUM-
The type of the None singleton.
var DYNAMICPARAM-
The type of the None singleton.
var NEWLINE-
The type of the None singleton.
var EOF-
The type of the None singleton.
Instance variables
var is_keyword-
Expand source code Browse git
@property def is_keyword(self): return self in _KEYWORDS_SET var is_assignment-
Expand source code Browse git
@property def is_assignment(self): return self in _ASSIGNMENT_SET var mode_invariant-
Whether the text of a token of this kind reads the same in every
Ps1LexerMode. A parser holding one as lookahead may keep it across a mode change, because scanning it again could not produce anything else. A kind earns membership only by being witnessed in the material the invariance test drives: an entry that test cannot reach is a claim nothing checks, and skipping the re-read is worth nothing next to that.Expand source code Browse git
@property def mode_invariant(self): """ Whether the text of a token of this kind reads the same in every `refinery.lib.scripts.ps1.lexer.Ps1LexerMode`. A parser holding one as lookahead may keep it across a mode change, because scanning it again could not produce anything else. A kind earns membership only by being witnessed in the material the invariance test drives: an entry that test cannot reach is a claim nothing checks, and skipping the re-read is worth nothing next to that. """ return self in _MODE_INVARIANT_SET
class Ps1Token (kind, value, offset)-
Ps1Token(kind: 'Ps1TokenKind', value: 'str', offset: 'int')
Expand source code Browse git
@dataclass class Ps1Token: kind: Ps1TokenKind value: str offset: int def __repr__(self): v = self.value if len(v) > 40: v = v[:37] + '...' return F'Token({self.kind.name}, {v!r}, @{self.offset})'Instance variables
var kind-
The type of the None singleton.
var value-
The type of the None singleton.
var offset-
The type of the None singleton.