1 /**
2 This tool concatenates all the files in a directory, with a line at the start of each
3 new file giving the name of the file. This is used for testing tools generating
4 multiple output files. It is similar to 'tail -n +1 dir/*'. The main difference is
5 that it assembles files in the same order on all platforms, a characteristic
6 necessary for testing.
7 
8 Copyright (c) 2020, eBay Inc.
9 Initially written by Jon Degenhardt
10 
11 License: Boost License 1.0 (http://boost.org/LICENSE_1_0.txt)
12 
13 */
14 module buildtools.dircat;
15 
16 import std.range;
17 import std.stdio;
18 import std.typecons : tuple;
19 
20 version(unittest)
21 {
22     // When running unit tests, use main from -main compiler switch.
23 }
24 else
25 {
26     int main(string[] cmdArgs)
27     {
28         /* When running in DMD code coverage mode, turn on report merging. */
29         version(D_Coverage) version(DigitalMars)
30         {
31             import core.runtime : dmd_coverSetMerge;
32             dmd_coverSetMerge(true);
33         }
34 
35         DirCatOptions cmdopt;
36         auto r = cmdopt.processArgs(cmdArgs);
37         if (!r[0]) return r[1];
38 
39         try concatenateDirectoryFiles(cmdopt);
40         catch (Exception e)
41         {
42             stderr.writefln("Error [%s]: %s", cmdopt.programName, e.msg);
43             return 1;
44         }
45         return 0;
46     }
47 }
48 
49 auto helpText = q"EOS
50 Synopsis: dircat [options] <directory>
51 
52 This tool concatenates all files in a directory, writing the contents to
53 standard output. The contents of each file is preceded with a line
54 containing the path of the file.
55 
56 The current features are very simple. The directory must contain only
57 regular files. It is an error if the directory contains subdirectories
58 or symbolic links.
59 
60 Exit status is '0' on success, '1' if an error occurred.
61 
62 Options:
63 EOS";
64 
65 struct DirCatOptions
66 {
67     string programName;
68     string dir;                          // Required argument
69 
70     /* Returns a tuple. First value is true if command line arguments were successfully
71      * processed and execution should continue, or false if an error occurred or the user
72      * asked for help. If false, the second value is the appropriate exit code (0 or 1).
73      *
74      * Returning true (execution continues) means args have been validated and derived
75      * values calculated.
76      */
77     auto processArgs (ref string[] cmdArgs)
78     {
79         import std.getopt;
80         import std.path : baseName, stripExtension;
81 
82         programName = (cmdArgs.length > 0) ? cmdArgs[0].stripExtension.baseName : "Unknown_program_name";
83 
84         try
85         {
86             auto r = getopt(cmdArgs);
87 
88             if (r.helpWanted)
89             {
90                 defaultGetoptPrinter(helpText, r.options);
91                 return tuple(false, 0);
92             }
93 
94             /* Get the directory path. Should be the one command line arg remaining. */
95             if (cmdArgs.length == 2) dir = cmdArgs[1];
96             else if (cmdArgs.length < 2) throw new Exception("A directory is required.");
97             else throw new Exception("Unexpected arguments.");
98         }
99         catch (Exception exc)
100         {
101             stderr.writefln("[%s] Error processing command line arguments: %s", programName, exc.msg);
102             return tuple(false, 1);
103         }
104         return tuple(true, 0);
105     }
106 }
107 
108 void concatenateDirectoryFiles(DirCatOptions cmdopt)
109 {
110     import std.algorithm : copy, sort;
111     import std.conv : to;
112     import std.exception : enforce;
113     import std.file : dirEntries, DirEntry, exists, isDir, SpanMode;
114     import std.format : format;
115     import std.path;
116 
117     string[] filepaths;
118 
119     enforce(cmdopt.dir.exists, format("Directory '%s' does not exist.", cmdopt.dir));
120     enforce(cmdopt.dir.isDir, format("File path '%s' is not a directory.", cmdopt.dir));
121 
122     foreach (DirEntry de; dirEntries(cmdopt.dir, SpanMode.shallow))
123     {
124         enforce(!de.isDir, format("Directory member '%s' is a directory.", de.name));
125         enforce(!de.isSymlink, format("Directory member '%s' is a symbolic link.", de.name));
126         enforce(de.isFile, format("Directory member '%s' is not a file.", de.name));
127 
128         filepaths ~= de.name;
129     }
130     filepaths.sort;
131     foreach (filenum, path; filepaths)
132     {
133         if (filenum > 0) writeln;
134         writefln("==> %s <==", path);
135         path.File.byChunk(1024L * 128L).copy(stdout.lockingTextWriter);
136     }
137 }