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 }