-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Internal shell now uses a specialized Win32 lexer (w32lex) instead of builtin shlex, avoiding ban of backslashes
- Loading branch information
Showing
4 changed files
with
272 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
COPYRIGHT = '''Copyright (C)2024, by maxpat78.''' | ||
__version__ = '1.0' | ||
__version__ = '1.1' | ||
__all__ = ["Vault", "init_vault", "backupDirIds"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,254 @@ | ||
COPYRIGHT = '''Copyright (C)2024, by maxpat78.''' | ||
|
||
__all__ = ["split", "quote", "join", "cmd_parse", "cmd_split", "cmd_quote"] | ||
__version__ = '0.9.9' | ||
|
||
class NotExpected(Exception): | ||
def __init__ (p, s): | ||
super().__init__(s + ' is not expected') | ||
|
||
|
||
SPLIT_SHELL32 = 0 # CommandLineToArgvW (and pre-2005 VC Runtime) mode (default) | ||
SPLIT_ARGV0 = 1 # full compatibility: simplified parsing of argv[0] | ||
SPLIT_VC2005 = 2 # enable VC2005+ handling of quoted double quote | ||
CMD_VAREXPAND = 4 # expand %variables% | ||
|
||
def split(s, mode=SPLIT_SHELL32): | ||
"""Split a command line like CommandLineToArgvW (SHELL32) or old parse_cmdline | ||
(VC Runtime) with mode=SPLIT_SHELL32 (default). With mode=SPLIT_ARGV0, do | ||
special simplified parsing for first argument; with mode=SPLIT_VC2005, emulate | ||
2005 and newer parse_cmdline.""" | ||
argv = [] # resulting arguments list | ||
arg = '' # current argument | ||
quoted = 0 # if current argument is quoted | ||
backslashes = 0 # backslashes in a row | ||
quotes = 0 # quotes in a row | ||
space = 0 # whitespace in a row | ||
|
||
if not s: return [] | ||
|
||
# Parse 1st argument (executable pathname) in a simplified way, parse_cmdline conformant. | ||
# Argument is everything up to first space if unquoted, or second quote otherwise | ||
if mode&1: | ||
i=0 | ||
for c in s: | ||
i += 1 | ||
if c == '"': | ||
quoted = not quoted | ||
continue | ||
if c in ' \t': | ||
if quoted: | ||
arg += c | ||
continue | ||
break | ||
arg += c | ||
argv += [arg] | ||
arg='' | ||
quoted = 0 | ||
s = s[i:] # strip processed string | ||
|
||
s = s.strip() # strip leading and trailing whitespace | ||
if not s: return argv | ||
|
||
# Special rules: | ||
# Quotes: " open block; "" open and close block; """ open, add literal " and close block | ||
# Backslashes, if followed by ": | ||
# 2n -> n, and open/close block | ||
# (2n+1) -> n, and add literal " | ||
for c in s: | ||
# count backslashes | ||
if c == '\\': | ||
space = 0 # reset count | ||
backslashes += 1 | ||
continue | ||
if c == '"': | ||
space = 0 # reset count | ||
if backslashes: | ||
# take 2n, emit n | ||
arg += '\\' * (backslashes//2) | ||
if backslashes%2: | ||
# if odd, add the escaped literal quote | ||
arg += c | ||
backslashes = 0 | ||
continue | ||
backslashes = 0 | ||
quoted = not quoted | ||
quotes += 1 | ||
# 3" in a row unquoted or 2" quoted -> add a literal " | ||
if quotes == 3 or quotes == 2 and quoted: | ||
arg += c | ||
quoted = not quoted | ||
if mode&2: | ||
quoted = not quoted # new parse_cmdline does NOT change quoting | ||
quotes = 0 | ||
continue | ||
if backslashes: | ||
# simply append the backslashes | ||
arg += '\\' * backslashes | ||
quotes = backslashes = 0 | ||
if c in ' \t': | ||
if quoted: | ||
# append whitespace | ||
arg += c | ||
continue | ||
# ignore whitespace in excess between arguments | ||
if not space: | ||
# append argument | ||
argv += [arg] | ||
arg = '' | ||
space += 1 | ||
continue | ||
space = 0 | ||
# append normal char | ||
arg += c | ||
if backslashes: | ||
arg += '\\' * backslashes | ||
# append last arg | ||
argv += [arg] | ||
return argv | ||
|
||
def quote(s): | ||
"Quote a string in a way suitable for the split function" | ||
backslashes = 0 # backslashes in a row | ||
if not s: return '""' | ||
arg = '' | ||
for c in s: | ||
# count backslashes | ||
if c == '\\': | ||
backslashes += 1 | ||
continue | ||
if c == '"': | ||
if backslashes: | ||
# take n, emit 2n | ||
arg += '\\' * (2*backslashes) | ||
backslashes = 0 | ||
# escape the " | ||
arg += '\\"' | ||
continue | ||
if backslashes: | ||
# add literally | ||
arg += backslashes*'\\' | ||
backslashes = 0 | ||
arg += c | ||
if backslashes: | ||
# double at end, since we quote hereafter | ||
arg += (2*backslashes)*'\\' | ||
arg = '"'+arg+'"' # always quote argument | ||
return arg | ||
|
||
def join(argv): | ||
"Quote and join list items, so that split returns the same" | ||
return ' '.join([quote(arg) for arg in argv]) | ||
|
||
def cmd_parse(s, mode=SPLIT_SHELL32|CMD_VAREXPAND): | ||
"Pre-process a command line like Windows CMD Command Prompt" | ||
escaped = 0 | ||
quoted = 0 | ||
percent = 0 | ||
meta = 0 # special chars in a row | ||
arg = '' | ||
argv = [] | ||
|
||
# remove (ignore) some leading chars | ||
for c in ' ;,=\t\x0B\x0C\xFF': s = s.lstrip(c) | ||
|
||
if not s or s[0] == ':': return [] | ||
|
||
# push special batch char | ||
if s[0] == '@': | ||
argv = ['@'] | ||
s = s[1:] | ||
# some combinations at line start are prohibited | ||
if s[0] in '|&<>': | ||
raise NotExpected(s[0]) | ||
if len(s)>1 and s[0:2] == '()': | ||
raise NotExpected(')') | ||
|
||
i = 0 | ||
while i < len(s): | ||
c = s[i] | ||
i += 1 | ||
if c == '"': | ||
if not escaped: quoted = not quoted | ||
if c == '^': | ||
if escaped or quoted: | ||
arg += c | ||
escaped = 0 | ||
else: | ||
escaped = 1 | ||
continue | ||
# %VAR% -> replace with os.environ['VAR'] *if set* and even if quoted | ||
# ^%VAR% -> same as above | ||
# %VAR^% | ||
# ^%VAR^% -> keep literal %VAR% | ||
# %%VAR%% -> replace internal %VAR% only | ||
if c == '%' and (mode&CMD_VAREXPAND): | ||
arg += c | ||
if percent and percent != i-1: | ||
if not escaped: | ||
vname = s[percent:i-1] | ||
val = os.environ.get(vname) | ||
#~ print('debug: "%s": trying to replace var "%s" with "%s"' %(s,vname,val)) | ||
if val: arg = arg.replace('%'+vname+'%', val) | ||
percent = 0 | ||
continue | ||
percent = i # record percent position | ||
continue | ||
# pipe, redirection, &, && and ||: break argument, and set aside special char/couple | ||
# multiple pipe, redirection, &, && and || in sequence are forbidden | ||
if c in '|<>&': | ||
if escaped or quoted: | ||
arg += c | ||
escaped = 0 | ||
continue | ||
meta += 1 | ||
# 3 specials in a row is forbidden | ||
if meta == 3: raise NotExpected(c) | ||
# if 2 specials undoubled | ||
if len(argv) >= 2 and argv[-1] in '|<>&' and c != argv[-1]: raise NotExpected(c) | ||
# push argument, if any, and special char/couple | ||
if arg: argv += [arg] | ||
argv += [c] | ||
# if doubled operator: ||, <<, >>, && | ||
if i < len(s) and s[i] == c: | ||
argv[-1] = 2*c | ||
i += 1 | ||
meta += 1 | ||
arg = '' | ||
continue | ||
if c in ' ,;=\t': | ||
percent = 0 | ||
# exception (Windows 2000+): starting special char escaped | ||
if i==2 and escaped and c in ',;=': | ||
argv += [c] | ||
escaped = 0 | ||
continue | ||
else: | ||
meta = 0 | ||
arg += c | ||
escaped = 0 | ||
argv += [arg] | ||
return argv | ||
|
||
def cmd_split(s, mode=SPLIT_SHELL32): | ||
"Post-process with split a command line parsed by cmd_parse (mimic mslex behavior)" | ||
argv = [] | ||
for tok in cmd_parse(s, mode): | ||
if tok in ('@','<','|','>','<<','>>','&','&&','||'): | ||
argv += [tok] | ||
continue | ||
argv += split(tok) | ||
return argv | ||
|
||
def cmd_quote(s): | ||
"Quote a string in a way suitable for the cmd_split function" | ||
# suitable means [x] == cmd_split(cmd_quote(x)) | ||
arg = '' | ||
for c in s: | ||
if c in ('^%<|>&'): arg += '^' # escape the escapable! | ||
arg += c | ||
if (' ' in arg) or ('\\' in arg): | ||
# quote only when special split chars inside, | ||
# since quote() always insert into double quotes! | ||
arg = quote(arg) | ||
return arg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters