4

I want to pass a list of directories to my C# console application as arguments which may be relative paths based on the current working directory, or contain drive letters and/or wildcards. E.g.:

myApp.exe someDir another\dir dirs\with\wild*cards* ..\..\a\relative\dir C:\an\absolute\dir\with\wild*cards*

So far I've come up with this:

static void Main(string[] args)
{
    List<string> directories = new List<string>();
    foreach (string arg in args)
    {
        if (Directory.Exists(arg))
        {
           // 'arg' by itself is a valid directory, and must not contain any wildcards
           // just add it to 'directories'
           directories.Add(arg);
        }
        else
        {
           // 'arg' must either be a non-existant directory or invalid directory name,
           // or else it contains wildcard(s). Find all matching directory names, starting
           // at the current directory (assuming 'arg' might be a relative path), and add
           // all matching directory names to 'directories'.
           string[] dirs = Directory.GetDirectories(Directory.GetCurrentDirectory(), arg, SearchOption.TopDirectoryOnly);
           directories.AddRange(dirs);
        }
     }

     Console.WriteLine("Full list of directories specified by the command line args:");
     foreach (string dir in directories)
     {
        Console.WriteLine("    " + dir);
     }

     // Now go do what I want to do for each of these directories...
}

This works great for someDir, another\dir, and dirs\with\wild*cards*, but won't work for ..\..\a\relative\dir or C:\an\abolute\dir\with\wild*cards*. For the relative dir, Directory.GetDirectories() throws a System.ArgumentException saying "Search pattern cannot contain '..' to move up directories and can be contained only internally in file/directory names, as in 'a..b'." For the drive-letter-based directory, it throws a System.ArgumentException saying "Second path fragment must not be a drive or UNC name."

How can I handle the ".." and drive letters? This has to be a solved problem, but I can't find examples of such for C#.

"Bonus question": My above code also doesn't handle a\path\with\wild*cards*\in\anything\other\than\the\top\directory. Any easy way to handle that too?

phonetagger
  • 7,701
  • 3
  • 31
  • 55
  • What happens if you combine something like `Path.Combine(Directory.GetCurrentDirectory(), arg)`? – Camilo Terevinto Apr 04 '18 at 17:33
  • @CamiloTerevinto If I do `string combinedPath = Path.Combine(Directory.GetCurrentDirectory(), arg);` that turns everything into `C:\whatever\directory\with\wild*cards*`, which still has the same problem when I call `Directory.GetDirectories()`. – phonetagger Apr 04 '18 at 18:27

2 Answers2

0

Couple of observations:

1) To check if path absolute or relative - you can use Path.IsRooted()

2) To resolve path with ".." to absolute path (be it relative or absolute) you can use:

path = new Uri(Path.Combine(Directory.GetCurrentDirectory(), path)).AbsolutePath;

Routing it though Uri expands those dots. Path.GetFullPath will fail in case of wildcards, but Uri will not.

3) To check if path contains wildcards you can just do path.Contains("*") || path.Contains("?") since both of those characters are not valid path chars, so cannot be present in a context other than being wildcards.

4) To resolve wildcard in absolute path (and match your "bonus" requirement) you need to find out first directory which does not contain wildcard. So you basically need to split path into to parts - before first wildcard and after first wildcard. For example:

C:\an\abolute\dir\with\wild*cards*

Path before wildcard is C:\an\abolute\dir\with, after (and including) wildcard: wild*cards*.

C:\an\abolu*e\dir\with\wild*cards*

Path before first wildcard: C:\an, after: abolu*e\dir\with\wild*cards*. There are different ways to do that of course, easiest I think is regex:

@"[\\/](?=[^\\/]*[\*?])"

It basically matches directory separator, but only if it is followed by 0 or more charactres which are NOT directory separators, then followed by wildcard symbol.

Combining this all together we have:

static IEnumerable<string> ResolveDirectories(string path) {
    if (path.Contains("*") || path.Contains("?")) {
        // we have a wildcard,
        // resolve it to absolute path if necessary
        if (!Path.IsPathRooted(path))
            path = Path.Combine(Directory.GetCurrentDirectory(), path);
        // resolve .. stuff if any
        path = new Uri(Path.Combine(Directory.GetCurrentDirectory(), path)).AbsolutePath;
        // split with regex above, only on first match (2 parts)
        var parts = new Regex(@"[\\/](?=[^\\/]*[\*?])").Split(path, 2);
        var searchRoot = parts[0];                
        var searchPatterns = parts[1].Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
        foreach (var dir in ResolveWildcards(searchRoot, searchPatterns))
            yield return dir;                
    }
    else {
        // this should work with "../.." type of paths too, will resolve relative to current directory
        // so no processing is necessary
        if (Directory.Exists(path)) {
            yield return path;
        }
        else {
            // invalid directory?
        }
    }
}

static IEnumerable<string> ResolveWildcards(string searchRoot, string [] searchPatterns) {
    // if we have path C:\an\abolu*e\dir\with\wild*cards*
    // search root is C:\an
    // and patterns are: [abolu*e, dir, with, wild*cards*]
    if (Directory.Exists(searchRoot)) {
        // use next pattern to search in a search root
        var next = searchPatterns[0];
        // leave the rest for recursion
        var rest = searchPatterns.Skip(1).ToArray();
        foreach (var dir in Directory.EnumerateDirectories(searchRoot, next, SearchOption.AllDirectories)) {
            // if nothing left (last pattern) - return it
            if (rest.Length == 0)
                yield return dir;
            else {
                // otherwise search with rest patterns in freshly found directory
                foreach (var sub in ResolveWildcards(dir, rest))
                    yield return sub;
            }
        }
    }
}

This is not properly tested, so take care.

Evk
  • 98,527
  • 8
  • 141
  • 191
0

'Evk' posted his/her answer a few minutes before I was about to post this, so I have not evaluated Evk's answer.

After finding this question+answer which might have worked fine but I didn't test it, I stumbled across Michael Ganss's Glob.cs NuGet package, and the results are very nice:

// Requires Glob.cs, (c) 2013 Michael Ganss, downloaded via the NuGet package manager:
// https://www.nuget.org/packages/Glob.cs
// (In Visual Studio, go to Tools->NuGet Package Manager->Package Manager Console.)
// PM> Install-Package Glob.cs -Version 1.3.0
using Glob;

namespace StackOverflow_cs
{
   class Program
   {
      static void Main(string[] args)
      {
         List<string> directories = new List<string>();
         foreach (string arg in args)
         {
            var dirs = Glob.Glob.Expand(arg, true, true);
            foreach (var dir in dirs)
            {
               directories.Add(dir.FullName);
            }
         }

         Console.WriteLine("Full list of directories specified by the command line args:");
         foreach (string dir in directories)
         {
            Console.WriteLine("    " + dir);
         }

         // Now go do what I want to do for each of these directories......
      }
   }
}

But I don't know why I have to say "Glob.Glob.Expand()" instead of simply "Glob.Expand()". Anyway, it works beautifully.

phonetagger
  • 7,701
  • 3
  • 31
  • 55