@@ -28,74 +28,117 @@ class Parser:
2828 """
2929 Default parser.
3030
31- Relies on a dictionnary, arguments, with an almost-exhaustive list
32- of the launchers options to determine when the launcher stops and
33- when the command begins.
34-
35- All the --option=value will be caught and don't need to be present in
36- the dictionnary.
37-
38- A minimal launcher module can use this to simplify implementation:
39- ```
40- from e4s_cl.cf.launchers import Parser
41- SCRIPT_NAMES = [identifiers]
42- ARGUMENTS = {...}
43- PARSER = Parser(ARGUMENTS)
44- ```
31+ Uses a dictionary of known options when available, but applies robust
32+ fallbacks so unknown options don't prematurely terminate parsing.
33+
34+ Rules:
35+ - '--' ends option parsing; remainder is the program.
36+ - ':' (Open MPI app-context delimiter) is retained on the launcher side.
37+ Note: multi-app context still needs higher-level handling to inject the
38+ wrapper before each app.
39+ - Known options consume their declared nargs.
40+ - '--opt=value' is always accepted.
41+ - Unknown long options ('--opt') consume one value if the next token isn't
42+ an option; otherwise consume none.
43+ - Unknown short options consume one value if the next token isn't an option.
44+ For concatenated known 1-arg short opts (e.g., '-xFOO=bar'), the entire
45+ token is accepted as one.
4546 """
4647
4748 def __init__ (self , arguments : Dict [str , int ]):
48- self .arguments = arguments
49+ self .arguments = arguments or {}
50+
51+ @staticmethod
52+ def _is_option (tok : str ) -> bool :
53+ return tok .startswith ('-' ) and tok != '-'
54+
55+ @staticmethod
56+ def _has_inline_value (tok : str ) -> bool :
57+ return tok .startswith ('--' ) and '=' in tok
4958
5059 def parse (self , command : List [str ]) -> Tuple [List [str ], List [str ]]:
5160 """
5261 Separate a command line into launcher and program.
5362 """
54- position = 0
55- known = True
56- launcher = []
63+ if not command :
64+ return [], []
5765
58- launcher .append (command [position ])
66+ position = 0
67+ launcher = [command [position ]]
5968 position += 1
69+ n = len (command )
6070
61- while known and position < len ( command ) :
71+ while position < n :
6272 flag = command [position ]
6373
64- if flag in self .arguments :
65- to_skip = self .arguments [flag ]
66-
67- # Catch generic --flag=value
68- elif re .match (r'^--[\-A-Za-z0-9]+=.*$' , flag ):
69- to_skip = 0
70-
71- # Catch concatenated flag and value, -p gpu => -pgpu
72- # We know flag is not in self.arguments
73- elif re .match (r'^-[\w\-]+' , flag ):
74- # List arguments that match the start of flag and that take only one option
75- matches = list (
76- filter (
77- lambda option : (flag .startswith (option ) and self .
78- arguments [option ] == 1 ),
79- self .arguments ))
80-
81- if (matches ):
82- to_skip = 0
83- else :
84- known = False
85- break
86-
87- else :
88- known = False
74+ # End-of-options sentinel: keep it on the launcher side and stop
75+ if flag == '--' :
76+ launcher .append (flag )
77+ position += 1
8978 break
9079
91- for index in range (0 , to_skip + 1 ):
92- launcher .append (command [position + index ])
80+ # Open MPI app-context delimiter: retain and continue parsing
81+ if flag == ':' :
82+ launcher .append (flag )
83+ position += 1
84+ continue
85+
86+ # Known option: consume declared nargs
87+ if flag in self .arguments :
88+ nargs = self .arguments [flag ]
89+ launcher .append (flag )
90+ # Append up to nargs following tokens safely
91+ for k in range (min (nargs , n - position - 1 )):
92+ launcher .append (command [position + 1 + k ])
93+ position += 1 + nargs
94+ continue
95+
96+ # Generic '--opt=value'
97+ if self ._has_inline_value (flag ):
98+ launcher .append (flag )
99+ position += 1
100+ continue
101+
102+ # Unknown long option: '--opt'
103+ if flag .startswith ('--' ):
104+ launcher .append (flag )
105+ # Heuristic: if next token is not an option, treat as value
106+ if position + 1 < n and not self ._is_option (command [position + 1 ]):
107+ launcher .append (command [position + 1 ])
108+ position += 2
109+ else :
110+ position += 1
111+ continue
112+
113+ # Short options (unknown or concatenated)
114+ if flag .startswith ('-' ) and flag != '-' :
115+ # If any known 1-arg option is a prefix of this token, accept concatenated form
116+ concatenated = next (
117+ (
118+ opt for opt , nargs in self .arguments .items ()
119+ if nargs == 1 and flag .startswith (opt )
120+ ),
121+ None ,
122+ )
123+ if concatenated :
124+ launcher .append (flag )
125+ position += 1
126+ continue
127+
128+ # Otherwise, assume unknown short option possibly with one value
129+ launcher .append (flag )
130+ if position + 1 < n and not self ._is_option (command [position + 1 ]):
131+ launcher .append (command [position + 1 ])
132+ position += 2
133+ else :
134+ position += 1
135+ continue
93136
94- position += (to_skip + 1 )
137+ # First non-option token: program starts here
138+ break
95139
96140 return launcher , command [position :]
97141
98-
99142LAUNCHERS = {}
100143
101144for _ , _module_name , _ in walk_packages (path = __path__ , prefix = __name__ + '.' ):
0 commit comments