comparison scripts/general/inputParser.m @ 19227:ff820f92cbb5

inputParser: classdef port of @inputParser from Octave Forge general pkg. * scripts/general/classdef.m: an almost Matlab compatible version of this class was part of the Octave Forge general package since 2011, and made use of @class syntax. With classdef now implemented in Octave, this is a port of that function with the incompatibilities fixed. The help text needs to be adapted after a new format is decided for this files. * scripts/general/module.mk: add new file to the build system. * NEWS: reference new class. * doc/interpreter/func.texi: add DOCSTRING on the manual.
author Carnë Draug <carandraug@octave.org>
date Wed, 20 Aug 2014 00:24:03 +0100
parents
children d900f863335c
comparison
equal deleted inserted replaced
19226:f707835af867 19227:ff820f92cbb5
1 ## Copyright (C) 2011-2014 Carnë Draug
2 ##
3 ## This file is part of Octave.
4 ##
5 # Octave is free software; you can redistribute it and/or modify it
6 ## under the terms of the GNU General Public License as published by
7 ## the Free Software Foundation; either version 3 of the License, or (at
8 ## your option) any later version.
9 ##
10 ## Octave is distributed in the hope that it will be useful, but
11 ## WITHOUT ANY WARRANTY; without even the implied warranty of
12 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 ## General Public License for more details.
14 ##
15 ## You should have received a copy of the GNU General Public License
16 ## along with Octave; see the file COPYING. If not, see
17 ## <http://www.gnu.org/licenses/>.
18
19 ## -*- texinfo -*-
20 ## @deftypefn {Function File} {} inputParser ()
21 ## Create object @var{parser} of the inputParser class.
22 ##
23 ## This class is designed to allow easy parsing of function arguments. This
24 ## class supports four types of arguments:
25 ##
26 ## @enumerate
27 ## @item mandatory (see @command{addRequired});
28 ## @item optional (see @command{addOptional});
29 ## @item named (see @command{addParamValue});
30 ## @item switch (see @command{addSwitch}).
31 ## @end enumerate
32 ##
33 ## After defining the function API with this methods, the supplied arguments
34 ## can be parsed with the @command{parse} method and the parsing results
35 ## accessed with the @command{Results} accessor.
36 ##
37 ## @deftypefnx {Accessor method} parser.Parameters
38 ## Return list of parameters name already defined.
39 ##
40 ## @deftypefnx {Accessor method} parser.Results
41 ## Return structure with argument names as fieldnames and corresponding values.
42 ##
43 ## @deftypefnx {Accessor method} parser.Unmatched
44 ## Return structure similar to @command{Results} for unmatched parameters. See
45 ## the @command{KeepUnmatched} property.
46 ##
47 ## @deftypefnx {Accessor method} parser.UsingDefaults
48 ## Return cell array with the names of arguments that are using default values.
49 ##
50 ## @deftypefnx {Class property} parser.CaseSensitive = @var{boolean}
51 ## Set whether matching of argument names should be case sensitive. Defaults to false.
52 ##
53 ## @deftypefnx {Class property} parser.FunctionName = @var{name}
54 ## Set function name to be used on error messages. Defauls to empty string.
55 ##
56 ## @deftypefnx {Class property} parser.KeepUnmatched = @var{boolean}
57 ## Set whether an error should be given for non-defined arguments. Defaults to
58 ## false. If set to true, the extra arguments can be accessed through
59 ## @code{Unmatched} after the @code{parse} method. Note that since @command{Switch}
60 ## and @command{ParamValue} arguments can be mixed, it is not possible to know
61 ## the unmatched type. If argument is found unmatched it is assumed to be of the
62 ## @command{ParamValue} type and it is expected to be followed by a value.
63 ##
64 ## @deftypefnx {Class property} parser.StructExpand = @var{boolean}
65 ## Set whether a structure can be passed to the function instead of parameter
66 ## value pairs. Defaults to true. Not implemented yet.
67 ##
68 ## The following example shows how to use this class:
69 ##
70 ## @example
71 ## @group
72 ## function check (varargin)
73 ## p = inputParser (); # create object
74 ## p.FunctionName = "check"; # set function name
75 ## p.addRequired ("pack", @@ischar); # create mandatory argument
76 ##
77 ## p.addOptional ("path", pwd(), @@ischar); # create optional argument
78 ##
79 ## ## one can create a function handle to anonymous functions for validators
80 ## val_mat = @@(x) isvector (x) && all (x <= 1) && all (x >= 0);
81 ## p.addOptional ("mat", [0 0], val_mat);
82 ##
83 ## ## create two ParamValue type of arguments
84 ## val_type = @@(x) any (strcmp (x, @{"linear", "quadratic"@}));
85 ## p.addParamValue ("type", "linear", val_type);
86 ## val_verb = @@(x) any (strcmp (x, @{"low", "medium", "high"@}));
87 ## p.addParamValue ("tolerance", "low", val_verb);
88 ##
89 ## ## create a switch type of argument
90 ## p.addSwitch ("verbose");
91 ##
92 ## p.parse (varargin@{:@});
93 ##
94 ## ## the rest of the function can access the input by accessing p.Results
95 ## ## for example, to access the value of tolerance, use p.Results.tolerance
96 ## endfunction
97 ##
98 ## check ("mech"); # valid, will use defaults for other arguments
99 ## check (); # error since at least one argument is mandatory
100 ## check (1); # error since !ischar
101 ## check ("mech", "~/dev"); # valid, will use defaults for other arguments
102 ##
103 ## check ("mech", "~/dev", [0 1 0 0], "type", "linear"); # valid
104 ##
105 ## ## the following is also valid. Note how the Switch type of argument can be
106 ## ## mixed into or before the ParamValue (but still after Optional)
107 ## check ("mech", "~/dev", [0 1 0 0], "verbose", "tolerance", "high");
108 ##
109 ## ## the following returns an error since not all optional arguments, `path' and
110 ## ## `mat', were given before the named argument `type'.
111 ## check ("mech", "~/dev", "type", "linear");
112 ## @end group
113 ## @end example
114 ##
115 ## @emph{Note 1}: a function can have any mixture of the four API types but they
116 ## must appear in a specific order. @command{Required} arguments must be the very
117 ## first which can be followed by @command{Optional} arguments. Only the
118 ## @command{ParamValue} and @command{Switch} arguments can be mixed together but
119 ## must be at the end.
120 ##
121 ## @emph{Note 2}: if both @command{Optional} and @command{ParamValue} arguments
122 ## are mixed in a function API, once a string Optional argument fails to validate
123 ## against, it will be considered the end of @command{Optional} arguments and the
124 ## first key for a @command{ParamValue} and @command{Switch} arguments.
125 ##
126 ## @seealso{nargin, validateattributes, validatestring, varargin}
127 ## @end deftypefn
128
129 ## -*- texinfo -*-
130 ## @deftypefnx {Function File} {} addOptional (@var{argname}, @var{default})
131 ## @deftypefnx {Function File} {} addOptional (@var{argname}, @var{default}, @var{validator})
132 ## Add new optional argument to the object @var{parser} of the class inputParser
133 ## to implement an ordered arguments type of API
134 ##
135 ## @var{argname} must be a string with the name of the new argument. The order
136 ## in which new arguments are added with @command{addOptional}, represents the
137 ## expected order of arguments.
138 ##
139 ## @var{default} will be the value used when the argument is not specified.
140 ##
141 ## @var{validator} is an optional anonymous function to validate the given values
142 ## for the argument with name @var{argname}. Alternatively, a function name
143 ## can be used.
144 ##
145 ## See @command{help inputParser} for examples.
146 ##
147 ## @emph{Note}: if a string argument does not validate, it will be considered a
148 ## ParamValue key. If an optional argument is not given a validator, anything
149 ## will be valid, and so any string will be considered will be the value of the
150 ## optional argument (in @sc{matlab}, if no validator is given and argument is
151 ## a string it will also be considered a ParamValue key).
152 ##
153 ## @end deftypefn
154
155 ## -*- texinfo -*-
156 ## @deftypefn {Function File} {} addParamValue (@var{argname}, @var{default})
157 ## @deftypefnx {Function File} {} addParamValue (@var{argname}, @var{default}, @var{validator})
158 ## Add new parameter to the object @var{parser} of the class inputParser to implement
159 ## a name/value pair type of API.
160 ##
161 ## @var{argname} must be a string with the name of the new parameter.
162 ##
163 ## @var{default} will be the value used when the parameter is not specified.
164 ##
165 ## @var{validator} is an optional function handle to validate the given values
166 ## for the parameter with name @var{argname}. Alternatively, a function name
167 ## can be used.
168 ##
169 ## See @command{help inputParser} for examples.
170 ##
171 ## @end deftypefn
172
173 ## -*- texinfo -*-
174 ## @deftypefn {Function File} {} addRequired (@var{argname})
175 ## @deftypefnx {Function File} {} addRequired (@var{argname}, @var{validator})
176 ## Add new mandatory argument to the object @var{parser} of inputParser class.
177 ##
178 ## This method belongs to the inputParser class and implements an ordered
179 ## arguments type of API.
180 ##
181 ## @var{argname} must be a string with the name of the new argument. The order
182 ## in which new arguments are added with @command{addrequired}, represents the
183 ## expected order of arguments.
184 ##
185 ## @var{validator} is an optional function handle to validate the given values
186 ## for the argument with name @var{argname}. Alternatively, a function name
187 ## can be used.
188 ##
189 ## See @command{help inputParser} for examples.
190 ##
191 ## @emph{Note}: this can be used together with the other type of arguments but
192 ## it must be the first (see @command{@@inputParser}).
193 ##
194 ## @end deftypefn
195
196 ## -*- texinfo -*-
197 ## @deftypefn {Function File} {} addSwitch (@var{argname})
198 ## Add new switch type of argument to the object @var{parser} of inputParser class.
199 ##
200 ## This method belongs to the inputParser class and implements a switch
201 ## arguments type of API.
202 ##
203 ## @var{argname} must be a string with the name of the new argument. Arguments
204 ## of this type can be specified at the end, after @code{Required} and @code{Optional},
205 ## and mixed between the @code{ParamValue}. They default to false. If one of the
206 ## arguments supplied is a string like @var{argname}, then after parsing the value
207 ## of @var{parse}.Results.@var{argname} will be true.
208 ##
209 ## See @command{help inputParser} for examples.
210 ##
211 ## @end deftypefn
212
213 ## -*- texinfo -*-
214 ## @deftypefn {Function File} {} parse (@var{varargin})
215 ## Parses and validates list of arguments according to object @var{parser} of the
216 ## class inputParser.
217 ##
218 ## After parsing, the results can be accessed with the @command{Results}
219 ## accessor. See @command{help inputParser} for a more complete description.
220 ##
221 ## @end deftypefn
222
223 ## Author: Carnë Draug <carandraug@octave.org>
224
225 classdef inputParser < handle
226 properties
227 ## TODO set input checking for this properties
228 CaseSensitive = false;
229 FunctionName = "";
230 KeepUnmatched = false;
231 # PartialMatching = true; # TODO unimplemented
232 # StructExpand = true; # TODO unimplemented
233 endproperties
234
235 properties (SetAccess = protected)
236 Parameters = cell ();
237 Results = struct ();
238 Unmatched = struct ();
239 UsingDefaults = cell ();
240 endproperties
241
242 properties (Access = protected)
243 ## Since Required and Optional are ordered, they get a cell array of
244 ## structs with the fields "name", "def" (default), and "val" (validator).
245 Required = cell ();
246 Optional = cell ();
247 ## ParamValue and Swicth are unordered so we have a struct whose fieldnames
248 ## are the argname, and values are a struct with fields "def" and "val"
249 ParamValue = struct ();
250 Switch = struct ();
251
252 ## List of ParamValues and Switch names to ease searches
253 ParamValueNames = cell ();
254 SwitchNames = cell ();
255
256 ## When checking for fieldnames in a Case Insensitive way, this variable
257 ## holds the correct identifier for the last searched named using the
258 ## is_argname method.
259 last_name = "";
260 endproperties
261
262 properties (Access = protected, Constant = true)
263 ## Default validator, always returns scalar true.
264 def_val = @() true;
265 endproperties
266
267 methods
268 function addRequired (this, name, val = inputParser.def_val)
269 if (nargin < 2 || nargin > 3)
270 print_usage ();
271 elseif (numel (this.Optional) || numel (fieldnames (this.ParamValue))
272 || numel (fieldnames (this.Switch)))
273 error (["inputParser.addRequired: can't have a Required argument " ...
274 "after Optional, ParamValue, or Switch"]);
275 endif
276 this.validate_name ("Required", name);
277 this.Required{end+1} = struct ("name", name, "val", val);
278 endfunction
279
280 function addOptional (this, name, def, val = inputParser.def_val)
281 if (nargin < 3 || nargin > 4)
282 print_usage ();
283 elseif (numel (fieldnames (this.ParamValue))
284 || numel (fieldnames (this.Switch)))
285 error (["inputParser.Optional: can't have Optional arguments " ...
286 "after ParamValue or Switch"]);
287 endif
288 this.validate_name ("Optional", name);
289 this.validate_default ("Optional", name, def, val);
290 this.Optional{end+1} = struct ("name", name, "def", def, "val", val);
291 endfunction
292
293 function addParamValue (this, name, def, val = inputParser.def_val)
294 if (nargin < 3 || nargin > 4)
295 print_usage ();
296 endif
297 this.validate_name ("ParamValue", name);
298 this.validate_default ("ParamValue", name, def, val);
299 this.ParamValue.(name).def = def;
300 this.ParamValue.(name).val = val;
301 endfunction
302
303 function addSwitch (this, name)
304 if (nargin != 2)
305 print_usage ();
306 endif
307 this.validate_name ("Switch", name);
308 this.Switch.(name).def = false;
309 endfunction
310
311 function parse (this, varargin)
312 if (numel (varargin) < numel (this.Required))
313 if (this.FunctionName)
314 print_usage (this.FunctionName);
315 else
316 this.error ("not enough input arguments");
317 endif
318 endif
319 pnargin = numel (varargin);
320
321 this.ParamValueNames = fieldnames (this.ParamValue);
322 this.SwitchNames = fieldnames (this.Switch);
323
324 ## Evaluate the Required arguments first
325 nReq = numel (this.Required);
326 for idx = 1:nReq
327 req = this.Required{idx};
328 this.validate_arg (req.name, req.val, varargin{idx});
329 endfor
330
331 vidx = nReq; # current index in varargin
332
333 ## Search for a list of Optional arguments
334 idx = 0; # current index on the array of Optional
335 nOpt = numel (this.Optional);
336 while (vidx < pnargin && idx < nOpt)
337 opt = this.Optional{++idx};
338 in = varargin{++vidx};
339 if (! opt.val (in))
340 ## If it does not match there's two options:
341 ## 1) input is actually wrong and we should error;
342 ## 2) it's a ParamValue or Switch name and we should use the
343 ## the default for the rest.
344 if (ischar (in))
345 idx--;
346 vidx--;
347 break
348 else
349 this.error (sprintf ("failed validation of %s",
350 toupper (opt.name)));
351 endif
352 endif
353 this.Results.(opt.name) = in;
354 endwhile
355
356 ## Fill in with defaults of missing Optional
357 while (idx++ < nOpt)
358 opt = this.Optional{idx};
359 this.UsingDefaults{end+1} = opt.name;
360 this.Results.(opt.name) = opt.def;
361 endwhile
362
363 ## Search unordered Options (Switch and ParamValue)
364 while (vidx++ < pnargin)
365 name = varargin{vidx};
366 if (this.is_argname ("ParamValue", name))
367 if (vidx++ > pnargin)
368 this.error (sprintf ("no matching value for option '%s'",
369 toupper (name)));
370 endif
371 this.validate_arg (this.last_name, this.ParamValue.(this.last_name).val,
372 varargin{vidx});
373 elseif (this.is_argname ("Switch", name))
374 this.Results.(this.last_name) = true;
375 else
376 if (vidx++ < pnargin && this.KeepUnmatched)
377 this.Unmatched.(name) = varargin{vidx};
378 else
379 this.error (sprintf ("argument '%s' is not a valid parameter",
380 toupper (name)));
381 endif
382 endif
383 endwhile
384 ## Add them to the UsingDeafults list
385 this.add_missing ("ParamValue");
386 this.add_missing ("Switch");
387
388 endfunction
389
390 function display (this)
391 if (nargin > 1)
392 print_usage ();
393 endif
394 printf ("inputParser object with properties:\n\n");
395 b2s = @(x) ifelse (any (x), "true", "false");
396 printf ([" CaseSensitive : %s\n FunctionName : %s\n" ...
397 " KeepUnmatched : %s\n PartialMatching : %s\n" ...
398 " StructExpand : %s\n\n"],
399 b2s (this.CaseSensitive), b2s (this.FunctionName),
400 b2s (this.KeepUnmatched), b2s (this.PartialMatching),
401 b2s (this.StructExpand));
402 printf ("Defined parameters:\n\n {%s}\n",
403 strjoin (this.Parameters, ", "));
404 endfunction
405 endmethods
406
407 methods (Access = private)
408 function validate_name (this, type, name)
409 if (! isvarname (name))
410 error ("inputParser.add%s: NAME is an invalid identifier", method);
411 elseif (any (strcmpi (this.Parameters, name)))
412 ## Even if CaseSensitive is "on", we still shouldn't allow
413 ## two args with the same name.
414 error ("inputParser.add%s: argname '%s' has already been specified",
415 type, name);
416 endif
417 this.Parameters{end+1} = name;
418 endfunction
419
420 function validate_default (this, type, name, def, val)
421 if (! feval (val, def))
422 error ("inputParser.add%s: failed validation for '%s' default value",
423 type, name);
424 endif
425 endfunction
426
427 function validate_arg (this, name, val, in)
428 if (! val (in))
429 this.error (sprintf ("failed validation of %s", toupper (name)));
430 endif
431 this.Results.(name) = in;
432 endfunction
433
434 function r = is_argname (this, type, name)
435 if (this.CaseSensitive)
436 r = isfield (this.(type), name);
437 this.last_name = name;
438 else
439 fnames = this.([type "Names"]);
440 l = strcmpi (name, fnames);
441 r = any (l(:));
442 if (r)
443 this.last_name = fnames{l};
444 endif
445 endif
446 endfunction
447
448 function add_missing (this, type)
449 unmatched = setdiff (fieldnames (this.(type)), fieldnames (this.Results));
450 for namec = unmatched(:)'
451 name = namec{1};
452 this.UsingDefaults{end+1} = name;
453 this.Results.(name) = this.(type).(name).def;
454 endfor
455 endfunction
456
457 function error (this, msg)
458 where = "";
459 if (this.FunctionName)
460 where = [this.FunctionName ": "];
461 endif
462 error ("%s%s", where, msg);
463 endfunction
464 endmethods
465
466 endclassdef
467
468 %!function p = create_p ()
469 %! p = inputParser ();
470 %! p.CaseSensitive = true;
471 %! p.addRequired ("req1", @(x) ischar (x));
472 %! p.addOptional ("op1", "val", @(x) any (strcmp (x, {"val", "foo"})));
473 %! p.addOptional ("op2", 78, @(x) x > 50);
474 %! p.addSwitch ("verbose");
475 %! p.addParamValue ("line", "tree", @(x) any (strcmp (x, {"tree", "circle"})));
476 %!endfunction
477
478 ## check normal use, only required are given
479 %!test
480 %! p = create_p ();
481 %! p.parse ("file");
482 %! r = p.Results;
483 %! assert (r.req1, "file");
484 %! assert (sort (p.UsingDefaults), sort ({"op1", "op2", "verbose", "line"}));
485 %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line},
486 %! {"file", "val", 78, false, "tree"});
487
488 ## check normal use, but give values different than defaults
489 %!test
490 %! p = create_p ();
491 %! p.parse ("file", "foo", 80, "line", "circle", "verbose");
492 %! r = p.Results;
493 %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line},
494 %! {"file", "foo", 80, true, "circle"});
495
496 ## check optional is skipped and considered ParamValue if unvalidated string
497 %!test
498 %! p = create_p ();
499 %! p.parse ("file", "line", "circle");
500 %! r = p.Results;
501 %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line},
502 %! {"file", "val", 78, false, "circle"});
503
504 ## check case insensitivity
505 %!test
506 %! p = create_p ();
507 %! p.CaseSensitive = false;
508 %! p.parse ("file", "foo", 80, "LiNE", "circle", "vERbOSe");
509 %! r = p.Results;
510 %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line},
511 %! {"file", "foo", 80, true, "circle"});
512
513 ## check KeepUnmatched
514 %!test
515 %! p = create_p ();
516 %! p.KeepUnmatched = true;
517 %! p.parse ("file", "foo", 80, "line", "circle", "verbose", "extra", 50);
518 %! assert (p.Unmatched.extra, 50)
519
520 ## check error when missing required
521 %!error <not enough input arguments>
522 %! p = create_p ();
523 %! p.parse ();
524
525 ## check error when given required does not validate
526 %!error <failed validation of >
527 %! p = create_p ();
528 %! p.parse (50);
529
530 ## check error when given optional does not validate
531 %!error <is not a valid parameter>
532 %! p = create_p ();
533 %! p.parse ("file", "no-val");
534
535 ## check error when given ParamValue does not validate
536 %!error <failed validation of >
537 %! p = create_p ();
538 %! p.parse ("file", "foo", 51, "line", "round");
539
540 ## check alternative method (obj, ...) API
541 %!function p2 = create_p2 ();
542 %! p2 = inputParser;
543 %! addRequired (p2, "req1", @(x) ischar (x));
544 %! addOptional (p2, "op1", "val", @(x) any (strcmp (x, {"val", "foo"})));
545 %! addOptional (p2, "op2", 78, @(x) x > 50);
546 %! addSwitch (p2, "verbose");
547 %! addParamValue (p2, "line", "tree", @(x) any (strcmp (x, {"tree", "circle"})));
548 %!endfunction
549
550 ## check normal use, only required are given
551 %!test
552 %! p2 = create_p2 ();
553 %! parse (p2, "file");
554 %! r = p2.Results;
555 %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line},
556 %! {"file", "val", 78, false, "tree"});
557 %! assert (sort (p2.UsingDefaults), sort ({"op1", "op2", "verbose", "line"}));
558
559 ## check normal use, but give values different than defaults
560 %!test
561 %! p2 = create_p2 ();
562 %! parse (p2, "file", "foo", 80, "line", "circle", "verbose");
563 %! r = p2.Results;
564 %! assert ({r.req1, r.op1, r.op2, r.verbose, r.line},
565 %! {"file", "foo", 80, true, "circle"});
566
567 ## FIXME: This somehow works in Matlab
568 #%!test
569 #%! p = inputParser;
570 #%! p.addOptional ("op1", "val");
571 #%! p.addParamValue ("line", "tree");
572 #%! p.parse ("line", "circle");
573 #%! assert (p.Results, struct ("op1", "val", "line", "circle"));