1 /**
2 Command line tool that executes a command while preserving header lines.
3 
4 Copyright (c) 2018-2021, eBay Inc.
5 Initially written by Jon Degenhardt
6 
7 License: Boost License 1.0 (http://boost.org/LICENSE_1_0.txt)
8 */
9 module tsv_utils.keep_header;
10 
11 auto helpText = q"EOS
12 Execute a command against one or more files in a header aware fashion.
13 The first line of each file is assumed to be a header. The first header
14 is output unchanged. Remaining lines are sent to the given command via
15 standard input, excluding the header lines of subsequent files. Output
16 from the command is appended to the initial header line.
17 
18 A double dash (--) delimits the command, similar to how the pipe
19 operator (|) delimits commands. Examples:
20 
21     $ keep-header file1.txt -- sort
22     $ keep-header file1.txt file2.txt -- sort -k1,1nr
23 
24 These sort the files as usual, but preserve the header as the first line
25 output. Data can also be read from standard input. Example:
26 
27     $ cat file1.txt | keep-header -- grep red
28 
29 Options:
30 
31 -V      --version   Print version information and exit.
32 -h         --help   This help information.
33 EOS";
34 
35 static if (__VERSION__ >= 2085) extern(C) __gshared string[] rt_options = [ "gcopt=cleanup:none" ];
36 
37 /** keep-header is a simple program, it is implemented entirely in main.
38  */
39 int main(string[] args)
40 {
41     import std.algorithm : findSplit, joiner;
42     import std.path : baseName, stripExtension;
43     import std.process : pipeProcess, ProcessPipes, Redirect, wait;
44     import std.range;
45     import std.stdio;
46     import std.typecons : tuple;
47 
48     /* When running in DMD code coverage mode, turn on report merging. */
49     version(D_Coverage) version(DigitalMars)
50     {
51         import core.runtime : dmd_coverSetMerge;
52         dmd_coverSetMerge(true);
53     }
54 
55     auto programName = (args.length > 0) ? args[0].stripExtension.baseName : "Unknown_program_name";
56     auto splitArgs = findSplit(args, ["--"]);
57 
58     if (splitArgs[1].length == 0 || splitArgs[2].length == 0)
59     {
60         auto cmdArgs = splitArgs[0][1 .. $];
61         stderr.writefln("Synopsis: %s [file...] -- program [args]", programName);
62         if (cmdArgs.length > 0 &&
63             (cmdArgs[0] == "-h" || cmdArgs[0] == "--help" || cmdArgs[0] == "--help-verbose"))
64         {
65             stderr.writeln();
66             stderr.writeln(helpText);
67         }
68         else if (cmdArgs.length > 0 &&
69                  (cmdArgs[0] == "-V" || cmdArgs[0] == "--V" ||  cmdArgs[0] == "--version"))
70         {
71             import tsv_utils.common.tsvutils_version;
72             stderr.writeln();
73             stderr.writeln(tsvutilsVersionNotice("keep-header"));
74         }
75         return 0;
76     }
77 
78     ProcessPipes pipe;
79     try pipe = pipeProcess(splitArgs[2], Redirect.stdin);
80     catch (Exception exc)
81     {
82         stderr.writefln("[%s] Command failed: '%s'", programName, splitArgs[2].joiner(" "));
83         stderr.writeln(exc.msg);
84         return 1;
85     }
86 
87     int status = 0;
88     {
89         scope(exit)
90         {
91             auto pipeStatus = wait(pipe.pid);
92             if (pipeStatus != 0) status = pipeStatus;
93         }
94 
95         bool headerWritten = false;
96         foreach (filename; splitArgs[0].length > 1 ? splitArgs[0][1..$] : ["-"])
97         {
98             bool isStdin = (filename == "-");
99             File inputStream;
100 
101             if (isStdin) inputStream = stdin;
102             else
103             {
104                 try inputStream = filename.File();
105                 catch (Exception exc)
106                 {
107                     stderr.writefln("[%s] Unable to open file: '%s'", programName, filename);
108                     stderr.writeln(exc.msg);
109                     status = 1;
110                     break;
111                 }
112             }
113 
114             auto firstLine = inputStream.readln();
115 
116             if (inputStream.eof && firstLine.length == 0) continue;
117 
118             if (!headerWritten)
119             {
120                 write(firstLine);
121                 stdout.flush;
122                 headerWritten = true;
123             }
124 
125             if (isStdin)
126             {
127                 foreach (line; inputStream.byLine(KeepTerminator.yes))
128                 {
129                     pipe.stdin.write(line);
130                 }
131             }
132             else
133             {
134                 ubyte[1024 * 128] readBuffer;
135                 foreach (ubyte[] chunk; inputStream.byChunk(readBuffer))
136                 {
137                     pipe.stdin.write(cast(char[])chunk);
138                 }
139             }
140             pipe.stdin.flush;
141         }
142         pipe.stdin.close;
143     }
144     return status;
145 }