wolffd@0: function varargout = processArgs(userArgs, varargin) wolffd@0: % Process function arguments, allowing args to be passed by wolffd@0: % the user either as name/value pairs, or positionally, or both. wolffd@0: % wolffd@0: % This function also provides optional enforcement of required inputs, and wolffd@0: % optional type checking. Argument names must start with the '-' wolffd@0: % character and not precede any positional arguments. wolffd@0: % wolffd@0: % Matthew Dunham wolffd@0: % University of British Columbia wolffd@0: % Last updated January 14th, 2010 wolffd@0: % wolffd@0: %% USAGE: wolffd@0: % wolffd@0: % [out1,out2,...,outN] = processArgs(userArgs ,... wolffd@0: % '-name1' , default1 ,... wolffd@0: % '-name2' , default2 ,... wolffd@0: % '-nameN' , defaultN ); wolffd@0: % wolffd@0: % The 'userArgs' input is a cell array and in normal usage is simply the wolffd@0: % varargin cell array from the calling function. It contains 0 to N values wolffd@0: % or 0 to N name/value pairs. It may also contain a combination of wolffd@0: % positional and named arguments as long as no named argument precedes a wolffd@0: % positional one. wolffd@0: % wolffd@0: % Note, this function is CASE INSENSITIVE. wolffd@0: % wolffd@0: %% ENFORCING REQUIRED ARGUMENTS wolffd@0: % wolffd@0: % To ensure that certain arguments are passed in, (and error if not), add a wolffd@0: % '*' character to the corresponding name as in wolffd@0: % wolffd@0: % [out1,...] = processArgs(userArgs,'*-name1',default1,... wolffd@0: % wolffd@0: % wolffd@0: % Providing empty values, (e.g. {},[],'') for required arguments also wolffd@0: % errors unless these were explicitly passed as named arguments. wolffd@0: %% TYPE CHECKING wolffd@0: % wolffd@0: % To enforce that the input type, (class) is the same type as the default wolffd@0: % value, add a '+' character to the corresponding name as in wolffd@0: % wolffd@0: % [out1,...] = processArgs(userArgs,'+-name1',default1,... wolffd@0: % wolffd@0: % '+' and '*' can be combined as in wolffd@0: % wolffd@0: % [out1,...] = processArgs(userArgs,'*+-name1',default1,... wolffd@0: % wolffd@0: % or equivalently wolffd@0: % wolffd@0: % [out1,...] = processArgs(userArgs,'+*-name1',default1,... wolffd@0: %% OTHER CONSIDERATIONS wolffd@0: % wolffd@0: % If the user passes in arguments in positional mode, and uses [], {}, or wolffd@0: % '' as place holders, the default values are used in their place. When the wolffd@0: % user passes in values via name/value pairs, this behavior does not wolffd@0: % occur; the explicit value the user specified, (even if [], {}, '') is wolffd@0: % always used. wolffd@0: % wolffd@0: %% ADVANCED wolffd@0: % The programmer must specify the same number of output arguments as wolffd@0: % possible input arguments, OR exactly one output. If exactly one output wolffd@0: % is used, the outputs are all bundled into a single cell array and wolffd@0: % returned with each arg value preceded by its name. This can be useful for wolffd@0: % relaying some or all of the arguments to subsequent functions as is done wolffd@0: % with the extractArgs function. wolffd@0: % wolffd@0: %% DEPENDENCIES wolffd@0: % wolffd@0: % None wolffd@0: % wolffd@0: %% EXAMPLES wolffd@0: % These are all valid usages. Note that here the first and second arguments wolffd@0: % are required and the types of the second and fourth arguments are wolffd@0: % checked. wolffd@0: % wolffd@0: % outerFunction(obj,... wolffd@0: % '-first' , 1 ,... wolffd@0: % '-second' , MvnDist() ,... wolffd@0: % '-third' , 22 ,... wolffd@0: % '-fourth' , 10 ); wolffd@0: % wolffd@0: % outerFunction(obj,'-fourth',3,'-second',MvnDist(),'-first',12); wolffd@0: % outerFunction(obj,1,MvnDist(),3); wolffd@0: % outerFunction(obj,1,MvnDist(),3,[]); wolffd@0: % outerFunction(obj,'-first',1,'-second',DiscreteDist(),'-third',[]); wolffd@0: % outerFunction(obj,1,MvnDist(),'-fourth',10); wolffd@0: % wolffd@0: % wolffd@0: % function [a,b,c,d] = outerFunction(obj,varargin) wolffd@0: % [a,b,c,d] = processArgs(varargin ,... wolffd@0: % '*-first' , [] ,... wolffd@0: % '*+-second' , MvnDist() ,... wolffd@0: % '-third' , 18 ,... wolffd@0: % '+-fourth' , 23 ); wolffd@0: % end wolffd@0: %% CONSTANTS wolffd@0: PREFIX = '-'; % prefix that must precede the names of arguments. wolffd@0: REQ = '*'; % require the argument wolffd@0: TYPE = '+'; % check the type of the arg against the default type wolffd@0: wolffd@0: % set to true for more exhaustive error checking or false for faster wolffd@0: % execution. wolffd@0: FULL_ERROR_CHECK = true; wolffd@0: wolffd@0: %% PROCESS VARARGIN - PASSED BY PROGRAMMER wolffd@0: wolffd@0: %% Check Initial Inputs wolffd@0: if ~iscell(userArgs) ,throwAsCaller(MException('PROCESSARGS:noUserArgs','PROGRAMMER ERROR - you must pass in the user''s arguments in a cell array as in processArgs(varargin,''-name'',val,...)'));end wolffd@0: if isempty(varargin) ,throwAsCaller(MException('PROCESSARGS:emptyVarargin','PROGRAMMER ERROR - you have not passed in any name/default pairs to processArgs')); end wolffd@0: %% Extract Programmer Argument Names and Markers wolffd@0: progArgNames = varargin(1:2:end); wolffd@0: maxNargs = numel(progArgNames); wolffd@0: required = cellfun(@(c)any(REQ==c(1:min(3,end))),progArgNames); wolffd@0: typecheck = cellfun(@(c)any(TYPE==c(1:min(3,end))),progArgNames); wolffd@0: if ~iscellstr(progArgNames) ,throwAsCaller(MException('PROCESSARGS:notCellStr ',sprintf('PROGRAMMER ERROR - you must pass to processArgs name/default pairs'))); end wolffd@0: %% Remove * and + Markers wolffd@0: try wolffd@0: progArgNames(required | typecheck) = ... wolffd@0: cellfuncell(@(c)c(c~=REQ & c~=TYPE) ,... wolffd@0: progArgNames(required | typecheck)); wolffd@0: catch ME wolffd@0: if strcmp(ME.identifier,'MATLAB:UndefinedFunction') wolffd@0: err = MException('PROCESSARGS:missingExternalFunctions','ProcessArgs requires the following external functions available in PMTK2: catString, cellfuncell, interweave, isprefix. Please add these to your MATLAB path.'); wolffd@0: throw(addCause(err,ME)); wolffd@0: else wolffd@0: rethrow(ME); wolffd@0: end wolffd@0: end wolffd@0: %% Set Default Values wolffd@0: defaults = varargin(2:2:end); wolffd@0: varargout = defaults; wolffd@0: %% Check Programmer Supplied Arguments wolffd@0: if mod(numel(varargin),2) ,throwAsCaller(MException('PROCESSARGS:oddNumArgs',sprintf('PROGRAMMER ERROR - you have passed in an odd number of arguments to processArgs, which requires name/default pairs'))); end wolffd@0: if any(cellfun(@isempty,progArgNames)) ,throwAsCaller(MException('PROCESSARGS:emptyStrName ',sprintf('PROGRAMMER ERROR - empty-string names are not allowed')));end wolffd@0: if nargout ~= 1 && nargout ~= maxNargs ,throwAsCaller(MException('PROCESSARGS:wrongNumOutputs',sprintf('PROGRAMMER ERROR - processArgs requires the same number of output arguments as named/default input pairs'))); end wolffd@0: if ~isempty(PREFIX) && ... wolffd@0: ~all(cellfun(@(c)~isempty(c) &&... wolffd@0: c(1)==PREFIX,progArgNames)) wolffd@0: throwAsCaller(MException('PROCESSARGS:missingPrefix',sprintf('PROGRAMMER ERROR - processArgs requires that each argument name begin with the prefix %s',PREFIX))); wolffd@0: end wolffd@0: if FULL_ERROR_CHECK && ... wolffd@0: numel(unique(progArgNames)) ~= numel(progArgNames) ,throwAsCaller(MException('PROCESSARGS:duplicateName',sprintf('PROGRAMMER ERROR - you can not use the same argument name twice')));end wolffd@0: %% PROCESS USERARGS wolffd@0: wolffd@0: %% Error Check User Args wolffd@0: if numel(userArgs) == 0 && nargout > 1 wolffd@0: if any(required) ,throwAsCaller(MException('PROCESSARGS:missingReqArgs',sprintf('The following required arguments were not specified:\n%s',catString(progArgNames(required))))); wolffd@0: else return; wolffd@0: end wolffd@0: end wolffd@0: if FULL_ERROR_CHECK wolffd@0: % slow, but helpful in transition from process_options to processArgs wolffd@0: % checks for missing '-' wolffd@0: if ~isempty(PREFIX) wolffd@0: userstrings = lower(... wolffd@0: userArgs(cellfun(@(c)ischar(c) && size(c,1)==1,userArgs))); wolffd@0: problem = ismember(... wolffd@0: userstrings,cellfuncell(@(c)c(2:end),progArgNames)); wolffd@0: if any(problem) wolffd@0: if sum(problem) == 1, warning('processArgs:missingPrefix','The specified value ''%s'', matches an argument name, except for a missing prefix %s. It will be interpreted as a value, not a name.',userstrings{problem},PREFIX) wolffd@0: else warning('processArgs:missingPrefix','The following values match an argument name, except for missing prefixes %s:\n\n%s\n\nThey will be interpreted as values, not names.',PREFIX,catString(userstrings(problem))); wolffd@0: end wolffd@0: end wolffd@0: end wolffd@0: end wolffd@0: %% Find User Arg Names wolffd@0: userArgNamesNDX = find(cellfun(@(c)ischar(c) &&... wolffd@0: ~isempty(c) &&... wolffd@0: c(1)==PREFIX,userArgs)); wolffd@0: %% Check User Arg Names wolffd@0: if ~isempty(userArgNamesNDX) && ... wolffd@0: ~isequal(userArgNamesNDX,userArgNamesNDX(1):2:numel(userArgs)-1) wolffd@0: if isempty(PREFIX), throwAsCaller(MException('PROCESSARGS:missingVal',sprintf('\n(1) every named argument must be followed by its value\n(2) no positional argument may be used after the first named argument\n'))); wolffd@0: else throwAsCaller(MException('PROCESSARGS:posArgAfterNamedArg',sprintf('\n(1) every named argument must be followed by its value\n(2) no positional argument may be used after the first named argument\n(3) every argument name must begin with the ''%s'' character\n(4) values cannot be strings beginning with the %s character\n',PREFIX,PREFIX))); wolffd@0: end wolffd@0: end wolffd@0: if FULL_ERROR_CHECK && ... wolffd@0: ~isempty(userArgNamesNDX) && ... wolffd@0: numel(unique(userArgs(userArgNamesNDX))) ~= numel(userArgNamesNDX) wolffd@0: throwAsCaller(MException('PROCESSARGS:duplicateUserArg',sprintf('You have specified the same argument name twice'))); wolffd@0: end wolffd@0: %% Extract Positional Args wolffd@0: argsProvided = false(1,maxNargs); wolffd@0: if isempty(userArgNamesNDX) wolffd@0: positionalArgs = userArgs; wolffd@0: elseif userArgNamesNDX(1) == 1 wolffd@0: positionalArgs = {}; wolffd@0: else wolffd@0: positionalArgs = userArgs(1:userArgNamesNDX(1)-1); wolffd@0: end wolffd@0: %% Check For Too Many Inputs wolffd@0: if numel(positionalArgs) + numel(userArgNamesNDX) > maxNargs ,throwAsCaller(MException('PROCESSARGS:tooManyInputs',sprintf('You have specified %d too many arguments to the function',numel(positionalArgs)+numel(userArgNamesNDX)- maxNargs)));end wolffd@0: %% Process Positional Args wolffd@0: for i=1:numel(positionalArgs) wolffd@0: % don't overwrite default value if positional arg is wolffd@0: % empty, i.e. '',{},[] wolffd@0: if ~isempty(userArgs{i}) wolffd@0: argsProvided(i) = true; wolffd@0: if typecheck(i) && ~isa(userArgs{i},class(defaults{i})) ,throwAsCaller(MException('PROCESSARGS:argWrongType',sprintf('Argument %d must be of type %s',i,class(defaults{i})))); end wolffd@0: varargout{i} = userArgs{i}; wolffd@0: end wolffd@0: end wolffd@0: %% Process Named Args wolffd@0: userArgNames = userArgs(userArgNamesNDX); wolffd@0: userProgMap = zeros(1,numel(userArgNames)); wolffd@0: usedProgArgNames = false(1,numel(progArgNames)); wolffd@0: for i=1:numel(userArgNames) wolffd@0: for j=1:numel(progArgNames) wolffd@0: if ~usedProgArgNames(j) && strcmpi(userArgNames{i},progArgNames{j}) wolffd@0: userProgMap(i) = j; wolffd@0: usedProgArgNames(j) = true; wolffd@0: break; wolffd@0: end wolffd@0: end wolffd@0: end wolffd@0: %% Error Check User Args wolffd@0: if any(~userProgMap) ,throwAsCaller(MException('PROCESSARGS:invalidArgNames',sprintf('The following argument names are invalid: %s',catString(userArgNames(userProgMap == 0),' , ')))); end wolffd@0: if any(userProgMap <= numel(positionalArgs)) ,throwAsCaller(MException('PROCESSARGS:bothPosAndName' ,sprintf('You cannot specify an argument positionally, and by name in the same function call.')));end wolffd@0: %% Extract User Values wolffd@0: userValues = userArgs(userArgNamesNDX + 1); wolffd@0: %% Type Check User Args wolffd@0: if any(typecheck) wolffd@0: for i=1:numel(userArgNamesNDX) wolffd@0: if typecheck(userProgMap(i)) && ... wolffd@0: ~isa(userArgs{userArgNamesNDX(i)+1},... wolffd@0: class(defaults{userProgMap(i)})) wolffd@0: throwAsCaller(MException('PROCESSARGS:wrongType',sprintf('Argument %s must be of type %s',userArgs{userArgNamesNDX(i)},class(defaults{userProgMap(i)})))); wolffd@0: end wolffd@0: end wolffd@0: end wolffd@0: varargout(userProgMap) = userValues; wolffd@0: %% Check Required Args wolffd@0: argsProvided(userProgMap) = true; wolffd@0: if any(~argsProvided & required) ,throwAsCaller(MException('PROCESSARGS:emptyVals',sprintf('The following required arguments were either not specified, or were given empty values:\n%s',catString(progArgNames(~argsProvided & required))))); end wolffd@0: %% Relay Mode wolffd@0: if nargout == 1 && (numel(varargin) > 2 || (numel(varargin) == 1 && isprefix('-',varargin{1}))) wolffd@0: varargout = {interweave(progArgNames,varargout)}; wolffd@0: end wolffd@0: wolffd@0: end wolffd@0: wolffd@0: wolffd@0: function s = catString(c,delim) wolffd@0: % Converts a cell array of strings to a single string, (i.e. single-row wolffd@0: % character array). The specified delimiter, delim, is added between each wolffd@0: % entry. Include any spaces you want in delim. If delim is not specified, wolffd@0: % ', ' is used instead. If c is already a string, it is just returned. If c wolffd@0: % is empty, s = ''. wolffd@0: % wolffd@0: % EXAMPLE: wolffd@0: % wolffd@0: % s = catString({'touch /tmp/foo';'touch /tmp foo2';'mkdir /tmp/test'},' && ') wolffd@0: % s = wolffd@0: % touch /tmp/foo && touch /tmp foo2 && mkdir /tmp/test wolffd@0: wolffd@0: if nargin == 0; s = ''; return; end wolffd@0: if ischar(c), s=c; wolffd@0: if strcmp(s,','),s = '';end wolffd@0: return; wolffd@0: end wolffd@0: if isempty(c),s=''; return;end wolffd@0: if nargin < 2, delim = ', '; end wolffd@0: s = ''; wolffd@0: for i=1:numel(c) wolffd@0: s = [s, rowvec(c{i}),rowvec(delim)]; %#ok wolffd@0: end wolffd@0: s(end-numel(delim)+1:end) = []; wolffd@0: if strcmp(s,','),s = '';end wolffd@0: end wolffd@0: wolffd@0: wolffd@0: function out = cellfuncell(fun, C, varargin) wolffd@0: out = cellfun(fun, C, varargin{:},'UniformOutput',false); wolffd@0: end wolffd@0: wolffd@0: function C = interweave(A,B) wolffd@0: % If A, B are two cell arrays of length N1, N2, C is a cell array of length wolffd@0: % N1 + N2 where where C(1) = A(1), C(2) = B(1), C(3) = A(2), C(4) = B(2), ... etc wolffd@0: % Note, C is always a row vector. If one cell array is longer than the wolffd@0: % other the remaining elements of the longer cell array are added to the wolffd@0: % end of C. A and B are first converted to column vectors. wolffd@0: A = A(:); B = B(:); wolffd@0: C = cell(length(A)+length(B),1); wolffd@0: counter = 1; wolffd@0: while true wolffd@0: if ~isempty(A) wolffd@0: C(counter) = A(1); A(1) = []; wolffd@0: counter = counter + 1; wolffd@0: end wolffd@0: if ~isempty(B) wolffd@0: C(counter) = B(1); B(1) = []; wolffd@0: counter = counter + 1; wolffd@0: end wolffd@0: if isempty(A) && isempty(B) wolffd@0: break; wolffd@0: end wolffd@0: end wolffd@0: C = C'; wolffd@0: end wolffd@0: wolffd@0: function p = isprefix(short,long) wolffd@0: % ISPREFIX Tests if the first arg is a prefix of the second. wolffd@0: % The second arg may also be a cell array of strings, in which case, each wolffd@0: % is tested. CASE SENSITIVE! wolffd@0: % wolffd@0: % If the second argument is not a string, p = false, it does not error. wolffd@0: % wolffd@0: % EXAMPLES: wolffd@0: % wolffd@0: % isprefix('foo','foobar') wolffd@0: % ans = wolffd@0: % 1 wolffd@0: % wolffd@0: %isprefix('test_',{'test_MvnDist','test_DiscreteDist','UnitTest'}) wolffd@0: %ans = wolffd@0: % 1 1 0 wolffd@0: error(nargchk(2,2,nargin)); wolffd@0: if ischar(long) wolffd@0: p = strncmp(long,short,length(short)); wolffd@0: elseif iscell(long) wolffd@0: p = cellfun(@(c)isprefix(short,c),long); wolffd@0: else wolffd@0: p = false; wolffd@0: end wolffd@0: end wolffd@0: wolffd@0: wolffd@0: function x = rowvec(x) wolffd@0: x = x(:)'; wolffd@0: end wolffd@0: wolffd@0: function x = colvec(x) wolffd@0: x = x(:); wolffd@0: end wolffd@0: wolffd@0: wolffd@0: wolffd@0: wolffd@0: wolffd@0: wolffd@0: wolffd@0: wolffd@0: wolffd@0: wolffd@0: