7
$\begingroup$

I have a list of folders and files in my current working directory as a result of a built-in function FileNames: FileNames["*", "", Infinity]].

It looks like this:

{Folder1\Folder11, Folder1\file11, Folder1\Folder12, Folder2\file21, etc.}

I need to transform it into a nested list looking exactly like follows (because I have a simple browsing interface based on OpenerView):

{"current working directory", {{"Folder1", {"Folder11", "Folder12", 
"file11"}}, {"Folder2", {"file21"}}, etc.}}

I found a couple of solutions here, but these algorithms require 'parentID' and they are not in Wolfram Language (so it's hard for me to understand them).

How can I do this in Mathematica?

Update

This is an example of a real flat files/folders list for "MyFolder" obtained with FileNames(with minimal adjustments):

{"Folder1\\file1.txt", "Folder2\\file3.dat", "Folder3\\file4.nb", 
"Folder3\\file5.m", "Folder3\\file6.m", 
"Folder3\\Folder31\\file11.log", "Folder3\\Folder31\\file12.log", 
"Folder3\\Folder31\\Folder32\\file13.log", 
"Folder3\\Folder31\\Folder32\\file14.log", 
"Folder3\\Folder31\\Folder32\\Folder33\\file111.log", 
"Folder3\\Folder31\\Folder32\\Folder33\\file21.xls"}

And expected result is:

{{"Folder1",{"file1.txt"}}, {"Folder2",{"file3.dat"}}, {"Folder3", {"file4.nb", "file5.m", "file6.m",{"Folder31", {"file11.log","file12.log", {"Folder32", {"file13.log", "file14.log",{"Folder33", {"file111.log", "file21.xls"}}}}}}}}}

I would like to find a method that works for folders of any deep.

$\endgroup$
  • $\begingroup$ @ciao I found a solution (in my answer below). I would be grateful for any improvement of my code. Thanks! $\endgroup$ – floyd17 Aug 24 '15 at 12:36
  • $\begingroup$ @belisarius Sorry, copy/paste error :/ Fixed this. $\endgroup$ – floyd17 Aug 24 '15 at 17:17
  • $\begingroup$ @belizarius I'm sorry. Now it is the way it should be, without MyFolder. $\endgroup$ – floyd17 Aug 24 '15 at 17:41
  • $\begingroup$ @belizarius Yes, it's true. Removed that. $\endgroup$ – floyd17 Aug 24 '15 at 17:50
  • $\begingroup$ Welcome to Mathematica.SE! I suggest the following: 0) Browse the common pitfalls question 1) As you receive help, try to give it too, by answering questions in your area of expertise. 2) Read the faq! 3) When you see good questions and answers, vote them up by clicking the gray triangles, because the credibility of the system is based on the reputation gained by users sharing their knowledge. Also, please remember to accept the answer, if any, that solves your problem, by clicking the checkmark sign! $\endgroup$ – Dr. belisarius Aug 25 '15 at 5:45
3
$\begingroup$

I think this is it. I hope that will be useful. And I would be grateful for any improvements.

First some auxiliary functions.

DeleteBracketsOnAtoms[lst_] := If[Length[#] == 1, First[#], #] & /@ lst

MyDirList[dir_] := Quiet@Module[{rawres, splitted},SetDirectory[dir];
rawres = Select[FileNames["*", "", Infinity], DirectoryQ[#] == False &]; 
splitted = FileNameSplit /@ rawres;{FileNameTake[dir], 
DeleteBracketsOnAtoms[splitted]}] (*makes list of files and non-empty folders*)

To find unique 1st-level folders in parent folder we use

unique1stLevelDirs[parent_, children_] := DeleteDuplicates@
(Select[children, Length[#] > 1 &][[All, 1]])

Next function groups the children of FirstLevelDir:

GroupChildren[children_, FirstLevelDir_] := {FirstLevelDir, 
  DeleteBracketsOnAtoms[Drop[#, 1] & /@ Select[Select[children, 
      Length[#] > 0 &], #[[1]] == FirstLevelDir &]]}

This groups parent and children like so {parent,{child1,child2}}:

CreateHierarchy[parent_, children_] := Module[
  {unique1stLevel = unique1stLevelDirs[parent, children],
   atoms = Select[children, Length[#] == 0 &]},
  {parent, Join[atoms, GroupChildren[children, #] & /@ unique1stLevel]}
  ]

It seems that we need to nest the last function on levels 0, 2, 4, 6, ... So

ToNextLevel[list_] := Module[
  {},
  level = level + 2;
  Apply[CreateHierarchy, list, {level}]
  ]

And finally

level = -2;
result = Nest[ToNextLevel,  MyDirList["My folder"], 100];

I guess that my solution is very clumsy, but it works nevertheless, even for directories with a very deep folder structure (tested on my local disk D:).

$\endgroup$
3
$\begingroup$

The following use DepthFirstScan (graph programming) and is cleaner IMHO:

f[ac_, son_, papa_, n_] := If[n != 0, 
         ac /. Longest[{papa, s_List} | papa] :> {papa, Append[{} ⋃ s, son]}, 
         Append[ac, papa]]

format1[fn_] := Module[{g, heads, ac = {}}, 
   g = Graph@Union@Flatten@Apply[Rule, Partition[#, 2, 1] & /@ FileNameSplit /@ fn, {2}];
   heads = Pick[#, VertexInDegree[g, #] == 0 & /@ #] &@VertexList@g;
   DepthFirstScan[g, #, {"DiscoverVertex" -> ((ac = f[ac, ##]) &)}] & /@  heads;
   ac]

Usage:

fn = {"Folder1\\file1.txt", "Folder2\\file3.dat", "Folder3\\file4.nb",
      "Folder3\\file5.m", "Folder3\\file6.m", 
      "Folder3\\Folder31\\file11.log", "Folder3\\Folder31\\file12.log", 
      "Folder3\\Folder31\\Folder32\\file13.log", 
      "Folder3\\Folder31\\Folder32\\file14.log", 
      "Folder3\\Folder31\\Folder32\\Folder33\\file111.log", 
      "Folder3\\Folder31\\Folder32\\Folder33\\file21.xls"};
format1[fn]

  (*
{{"Folder1", {"file1.txt"}}, 
 {"Folder2", {"file3.dat"}}, 
 {"Folder3", {"file4.nb", "file5.m", "file6.m", 
             {"Folder31", {"file11.log", "file12.log", 
                          {"Folder32", {"file13.log", "file14.log", 
                                       {"Folder33", {"file111.log", "file21.xls"}
}}}}}}}}
*)
$\endgroup$
  • 1
    $\begingroup$ Yes. Nicely implemented! $\endgroup$ – ciao Aug 25 '15 at 4:12
  • $\begingroup$ @belisarius Thank you! I could not imagine such a solution. But it seems to me that graph-based code is much slower than mine. I've tested both on my local drive D. My code takes ~25 s. Five minutes ago I started the graph-based code and it's still running... Is it particular for graphs? $\endgroup$ – floyd17 Aug 25 '15 at 4:55
  • $\begingroup$ @floyd17 Graph algos aren't particularly fast on Mma. Consider Mma as a nice modelling platform for them :) $\endgroup$ – Dr. belisarius Aug 25 '15 at 5:44
3
$\begingroup$

By using Leonid's makeTree from here:

makeTree[wrds_] := Reap[If[# =!= {}, Sow[Rest[#], First@#]] & /@ 
                                             wrds, _, #1 -> makeTree[#2] &][[2]]

usage

fn = {"Folder1\\file1.txt", "Folder2\\file3.dat", "Folder3\\file4.nb",
      "Folder3\\file5.m", "Folder3\\file6.m", 
      "Folder3\\Folder31\\file11.log", "Folder3\\Folder31\\file12.log", 
      "Folder3\\Folder31\\Folder32\\file13.log", 
      "Folder3\\Folder31\\Folder32\\file14.log", 
      "Folder3\\Folder31\\Folder32\\Folder33\\file111.log", 
      "Folder3\\Folder31\\Folder32\\Folder33\\file21.xls"};

makeTree[StringSplit[fn, "\\"]] /. (s_ -> {}) :> s


  (*
  {"Folder1" -> {"file1.txt"}, 
   "Folder2" -> {"file3.dat"}, 
   "Folder3" -> {"file4.nb", "file5.m", "file6.m", 
                "Folder31" -> {"file11.log", "file12.log", 
                              "Folder32" -> {"file13.log", "file14.log", 
                                            "Folder33" -> {"file111.log", "file21.xls"
    }}}}}
$\endgroup$
2
$\begingroup$

Using the Graph representation: A caveat at the end.

format[fn_] := Module[{g,  heads, rul, res},
g = Graph@ Union@Flatten@Apply[Rule, Partition[#, 2, 1] & /@ FileNameSplit /@ fn, {2}];
heads = Select[VertexList@g, Length@VertexInComponent[g, #] == 1 &];
rul   = ({# -> {"$" <> #, Rest@VertexOutComponent[g, #, 1]}} & /@  VertexList@g);
res   = heads //. Union @@ rul /. {} -> Sequence[] /. {a_String} :> a;
MapAll[If[Head[#] === String,  StringReplace[#, "$" ~~ a__ :> a], #] &, res]
      ]

usage

fn = {"Folder1\\file1.txt", "Folder2\\file3.dat", "Folder3\\file4.nb",
    "Folder3\\file5.m", "Folder3\\file6.m", 
   "Folder3\\Folder31\\file11.log", "Folder3\\Folder31\\file12.log", 
   "Folder3\\Folder31\\Folder32\\file13.log", 
   "Folder3\\Folder31\\Folder32\\file14.log", 
   "Folder3\\Folder31\\Folder32\\Folder33\\file111.log", 
   "Folder3\\Folder31\\Folder32\\Folder33\\file21.xls"};

format[fn]
(*
{{"Folder1", {"file1.txt"}}, 
 {"Folder2", {"file3.dat"}}, 
 {"Folder3", {"file4.nb", "file5.m", "file6.m", 
             {"Folder31", {"file11.log", "file12.log", 
                          {"Folder32", {"file13.log", "file14.log", 
                                       {"Folder33", {"file111.log", "fiile21.xls"
 }}}}}}}}}

Caveat: It won't work if you have equal names at different branches. For considering that case I recommend a preprocessing stage that adds a prefix identifying the branch.

$\endgroup$
  • $\begingroup$ Lol - was just going to comment "use graphs...". +1 $\endgroup$ – ciao Aug 24 '15 at 22:39
  • $\begingroup$ @ciao I believe this can be done better by using DepthFirstScan... but it resisted my try $\endgroup$ – Dr. belisarius Aug 24 '15 at 22:41
  • $\begingroup$ @ciao See my other answer :) $\endgroup$ – Dr. belisarius Aug 25 '15 at 2:16
1
$\begingroup$

A recursive approach that groups as required and will work for any folder depth and reoccurring folder names at different locations in the directory hierarchy.

fileNameList = {"Folder1\\file1.txt", "Folder2\\file3.dat", 
   "Folder3\\file4.nb", "Folder3\\file5.m", "Folder3\\file6.m", 
   "Folder3\\Folder31\\file11.log", "Folder3\\Folder31\\file12.log", 
   "Folder3\\Folder31\\Folder32\\file13.log", 
   "Folder3\\Folder31\\Folder32\\file14.log", 
   "Folder3\\Folder31\\Folder32\\Folder33\\file111.log", 
   "Folder3\\Folder31\\Folder32\\Folder33\\file21.xls"};

The recursive function makeFileHierarchy.

makeFileHierarchy[expr_List] :=
 If[Max@Dimensions@expr == 1,
  If[Length@(expr /. {} -> Nothing) != 0, 
   {Flatten@expr}, 
   Nothing
  ],
  {
   First@First@#, 
   makeFileHierarchy@#[[All, 2 ;;]]
  } & /@ GatherBy[expr, #[[1]] &]
 ]

 makeFileHierarchy[StringSplit[fileNameList, "\\"]] /. {x_} -> x

(*
{
 {"Folder1", 
  {"file1.txt"}
 }, 
 {"Folder2", 
  {"file3.dat"}
 }, 
 {"Folder3", 
  {"file4.nb", "file5.m", "file6.m", 
   {"Folder31", 
    {"file11.log", "file12.log", 
     {"Folder32", 
      {"file13.log", "file14.log", 
       {"Folder33", 
        {"file111.log", "file21.xls"}
 }}}}}}}
}
*)

Note that makeFileHierarchy takes the StringSplit of file names.

It takes a ragged list of StringSplit file names and first checks if the list is a single nested entry or an empty nested entry; both by products of the GatherBy. If it is a single entry then the Max of each of its Dimensions will be 1. Further, if the nested empty list is replaced with Nothing (new in 10.2) then there will be no entries (i.e. Length == 0). Else, return a list with the single entry.

If it is not a single entry in a nested list then Map (/@) over the list GatherBy the first entry in each list. From the GatherBy result, take the first entry of the first gathered list, the folder. Then pass the folder items (all gathered lists less their first entry (i.e. the folder)) on to be hierarchically grouped by makeFileHierarchy.

From this process all leaves of the hierarchy (the files) are returned as single item lists. The ReplaceAll (/.) removes them from these lists as required.

Hope this helps.

$\endgroup$
  • $\begingroup$ Thanks! But for my example from the top of the page your code returns very strange number 524.808 instead of name of each file. What is it? BTW, folder names are OK and nested structure as well. $\endgroup$ – floyd17 Aug 25 '15 at 13:30
  • $\begingroup$ fileNameList is a copy and paste of your example in the Update section of your question. My guess is that you have a value assigned to x and perhaps have ./ instead of /.. Try ClearAll on makeFileHierarchy and x, in a new notebook copy and paste my answer into it (the fileNameList, makeFileHierarchy definition, and makeFileHierarchy call. This should execute as shown in my answer. I just double checked this. It works as expected. $\endgroup$ – Edmund Aug 25 '15 at 13:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.