3
votes

I'm trying to build my own version of MATLAB's dir function. My current code (below) almost works but I am having trouble parsing a certain combination of inputs.

What I would like it to do is this:

  • Don't list hidden folders.
  • Have a Boolean parameter (default is false) to return only directory names and not files.
  • Have an optional field for the path of the folder (default is current directory).

To be clearer, I would like to create the function dir2 which can handle these combinations:

  1. dir2 This should list every not-hidden file or folder in the current directory
  2. dir2('path_to_directory') This should list every not-hidden file or folder in the specified directory
  3. dir2('OnlyDirectories', true) This should list only the not-hidden folders in the current directory
  4. dir2('path_to_directory', 'OnlyDirectories', true) This should list only the not-hidden folders in the specified directory

My current version is this:

function list = dir2(varargin)
    p = inputParser;

    addOptional(p, 'name', '.', @ischar);
    addParameter(p, 'OnlyDirectories', false, @islogical);
    parse(p, varargin{:});

    list = dir(p.Results.name);
    if p.Results.OnlyDirectories
        dirFlags = [list.isdir];
        list = list(dirFlags); % Keeping only directories
    end
    % Deleting hidden folders from the list
    list = list(arrayfun(@(x) ~strcmp(x.name(1),'.'), list)); 
end

This works fine for cases 1, 2 and 4 but it doesn't work for case 3. In this case it gives me the error:

Expected a string scalar or character vector for the parameter name, instead the input type was 'logical'.

I think I might be missing something trivial about MATLAB's input parsing but I can't figure out what.

2

2 Answers

2
votes

You're right that the parser appears to give some odd results, a related question might be this one.

One work-around, which works for your function in its current form, would be to add a check for if 2 inputs are given. If there are 2 inputs, assume it's your OnlyDirectories flag and use the default name value. The code would look like this and passes all 4 of your example use cases.

function list = dir2(varargin)
    p = inputParser;
    addOptional(p, 'name', '.', @ischar);
    addParameter(p, 'OnlyDirectories', false, @islogical);
    if numel(varargin) == 2 
        varargin = [{'.'}, varargin]; 
    end
    parse(p, varargin{:});
    list = dir(p.Results.name);
    if p.Results.OnlyDirectories
        dirFlags = [list.isdir];
        list = list(dirFlags); 
    end
    list = list(arrayfun(@(x) ~strcmp(x.name(1),'.'), list)); 
end

This is a bit hacky though, and has scope to give confusing error messages. It would be better to just have both inputs as name-value pairs

function list = dir2(varargin)
    p = inputParser;
    addParameter(p, 'name', '.', @ischar);
    addParameter(p, 'OnlyDirectories', false, @islogical);
    % ... other code
end

Use: dir2('name', 'C:/Folder/MyStuff/', 'OnlyDirectories', true)

0
votes

Your problem is that your validation function for your optional argument is not specific enough. As you have it, it is only checking that it is a character array. When you only pass 'OnlyDirectories', true as the arguments for your third case, the string 'OnlyDirectories' passes your validation function and is used as the value for parameter 'name'. You then get an error because the remaining argument is a logical value and not a parameter name string.

Using this as a more stringent validation function works for me:

addOptional(p, 'name', '.', @(d) (exist(d, 'dir') == 7));

This uses exist to check if the optional argument is a valid folder. For your third case, the string 'OnlyDirectories' doesn't pass the validation function for your optional parameter 'name', thus the default value is used and the string is passed along to check for the next parameter.

EDIT:

To clarify, the inherent problem is that you're wanting to include an optional argument that is a character string, and parameter-value pairs are always going to begin with a parameter name which is a character string as well. There is some (potentially unavoidable) ambiguity in such a case, and the only way inputParser can clearly distinguish an optional character string from the parameter name of a subsequent parameter-value pair is via the validation function you provide. If the difference between them can't be made sufficiently clear, you should instead use required arguments or parameter-value pairs. Note that this wouldn't be a problem if your optional argument were anything other than a character array or string.

However, if you're set on using an optional parameter, the example I provide above for a more-specific validation function is just one approach. There are other options that may be more to your liking. You could instead check that the optional parameter string isn't any of the parameter names for your function:

addOptional(p, 'name', '.', @(d) ~ismember(d, {'OnlyDirectories', ...}));

This comes with the limitation that you could never search through a folder with the name of one of your input parameters (an ambiguity you may have to accept regardless of the solution you choose).

Still another solution would allow you to use optional arguments like 'path_to_folder/*.csv' (i.e. wildcard indicators and paths to files):

addOptional(p, 'name', '.', @name_check);
...

function isValid = name_check(d)
  try
    [dirPath, fileName, fileExt] = fileparts(d);
    isValid = ismember('*', d) ...  % A wildcard, not allowed in parameter names
              || (isempty(fileExt) && (exist(d, 'dir') == 7)) ...      % A folder path
              || (~isempty(fileExt) && (exist(dirPath, 'dir') == 7));  % A file path
  catch
    isValid = false;  % Any failure of the above indicates an invalid argument
  end
end