@@ -53,56 +53,14 @@ def to_dict(value: str, of_type: tuple[type[Any], type[Any]]) -> Iterator[tuple[
5353 msg = f"dictionary lines must be of form key=value, found { row !r} "
5454 raise TypeError (msg )
5555
56- @staticmethod
57- def _win32_process_path_backslash (value : str , escape : str , special_chars : str ) -> str :
58- """Escape backslash in value that is not followed by a special character.
59-
60- This allows windows paths to be written without double backslash, while retaining the POSIX backslash escape
61- semantics for quotes and escapes.
62-
63- """
64- result = []
65- for ix , char in enumerate (value ):
66- result .append (char )
67- if char == escape :
68- last_char = value [ix - 1 : ix ]
69- if last_char == escape :
70- continue
71- next_char = value [ix + 1 : ix + 2 ]
72- if next_char not in {escape , * special_chars }:
73- result .append (escape ) # escape escapes that are not themselves escaping a special character
74- return "" .join (result )
75-
7656 @staticmethod
7757 def to_command (value : str ) -> Command :
7858 """At this point, ``value`` has already been substituted out, and all punctuation / escapes are final.
7959
8060 Value will typically be stripped of whitespace when coming from an ini file.
8161
8262 """
83- value = value .replace (r"\#" , "#" )
84- is_win = sys .platform == "win32"
85- if is_win : # pragma: win32 cover
86- s = shlex .shlex (posix = True )
87- value = StrConvert ._win32_process_path_backslash (
88- value ,
89- escape = s .escape ,
90- special_chars = s .quotes ,
91- )
92- splitter = shlex .shlex (value , posix = True )
93- splitter .whitespace_split = True
94- splitter .commenters = "" # comments handled earlier, and the shlex does not know escaped comment characters
95- args : list [str ] = []
96- pos = 0
97- try :
98- for arg in splitter :
99- if is_win and len (arg ) > 1 and arg [0 ] == arg [- 1 ] and arg .startswith (("'" , '"' )): # pragma: win32 cover
100- # on Windows quoted arguments will remain quoted, strip it
101- arg = arg [1 :- 1 ] # ruff:ignore[redefined-loop-name]
102- args .append (arg )
103- pos = cast ("StringIO" , splitter .instream ).tell ()
104- except ValueError :
105- args .append (value [pos :])
63+ args = split_args (value .replace (r"\#" , "#" ))
10664 if len (args ) == 0 :
10765 msg = f"attempting to parse { value !r} into a command failed"
10866 raise ValueError (msg )
@@ -134,4 +92,56 @@ def to_bool(value: str) -> bool:
13492 raise TypeError (msg )
13593
13694
137- __all__ = ("StrConvert" ,)
95+ def split_args (value : str ) -> list [str ]:
96+ """Split an already substituted shell-like string into its arguments.
97+
98+ :param value: the string to split, with all punctuation and escapes final
99+
100+ :returns: the arguments, with quoting resolved
101+
102+ """
103+ is_win = sys .platform == "win32"
104+ if is_win : # pragma: win32 cover
105+ s = shlex .shlex (posix = True )
106+ value = _win32_process_path_backslash (value , escape = s .escape , special_chars = s .quotes )
107+ splitter = shlex .shlex (value , posix = True )
108+ splitter .whitespace_split = True
109+ splitter .commenters = "" # comments handled earlier, and the shlex does not know escaped comment characters
110+ args : list [str ] = []
111+ pos = 0
112+ try :
113+ for arg in splitter :
114+ if is_win and len (arg ) > 1 and arg [0 ] == arg [- 1 ] and arg .startswith (("'" , '"' )): # pragma: win32 cover
115+ # on Windows quoted arguments will remain quoted, strip it
116+ arg = arg [1 :- 1 ] # ruff:ignore[redefined-loop-name]
117+ args .append (arg )
118+ pos = cast ("StringIO" , splitter .instream ).tell ()
119+ except ValueError :
120+ args .append (value [pos :])
121+ return args
122+
123+
124+ def _win32_process_path_backslash (value : str , escape : str , special_chars : str ) -> str :
125+ """Escape backslash in value that is not followed by a special character.
126+
127+ This allows windows paths to be written without double backslash, while retaining the POSIX backslash escape
128+ semantics for quotes and escapes.
129+
130+ """
131+ result = []
132+ for ix , char in enumerate (value ):
133+ result .append (char )
134+ if char == escape :
135+ last_char = value [ix - 1 : ix ]
136+ if last_char == escape :
137+ continue
138+ next_char = value [ix + 1 : ix + 2 ]
139+ if next_char not in {escape , * special_chars }:
140+ result .append (escape ) # escape escapes that are not themselves escaping a special character
141+ return "" .join (result )
142+
143+
144+ __all__ = (
145+ "StrConvert" ,
146+ "split_args" ,
147+ )
0 commit comments