1 /**
2 A simple version of the unix 'nl' program.
3 
4 This program is a simpler version of the unix 'nl' (number lines) program. It reads
5 text from files or standard input and adds a line number to each line.
6 
7 Copyright (c) 2015-2018, eBay Software Foundation
8 Initially written by Jon Degenhardt
9 
10 License: Boost Licence 1.0 (http://boost.org/LICENSE_1_0.txt)
11 */
12 module number_lines;
13 
14 import std.stdio;
15 import std.typecons : tuple;
16 
17 auto helpText = q"EOS
18 Synopsis: number-lines [options] [file...]
19 
20 number-lines reads from files or standard input and writes each line to standard
21 output preceded by a line number. It is a simplified version of the unix 'nl'
22 program. It supports one feature 'nl' does not: the ability to treat the first
23 line of files as a header. This is useful when working with tab-separated-value
24 files. If header processing used, a header line is written for the first file,
25 and the header lines are dropped from any subsequent files.
26 
27 Examples:
28    number-lines myfile.txt
29    cat myfile.txt | number-lines --header linenum
30    number-lines *.txt
31 
32 Options:
33 EOS";
34 
35 /**
36 Container for command line options.
37  */
38 struct NumberLinesOptions
39 {
40     enum defaultHeaderString = "line";
41 
42     string programName;
43     bool hasHeader = false;       // --H|header
44     string headerString = "";     // --s|header-string
45     long startNum = 1;            // --n|start-num
46     char delim = '\t';            // --d|delimiter
47     bool versionWanted = false;   // --V|version
48 
49     /* Returns a tuple. First value is true if command line arguments were successfully
50      * processed and execution should continue, or false if an error occurred or the user
51      * asked for help. If false, the second value is the appropriate exit code (0 or 1).
52      */
53     auto processArgs (ref string[] cmdArgs)
54     {
55         import std.algorithm : any, each;
56         import std.getopt;
57         import std.path : baseName, stripExtension;
58 
59         programName = (cmdArgs.length > 0) ? cmdArgs[0].stripExtension.baseName : "Unknown_program_name";
60 
61         try
62         {
63             auto r = getopt(
64                 cmdArgs,
65                 std.getopt.config.caseSensitive,
66                 "H|header",        "     Treat the first line of each file as a header. The first input file's header is output, subsequent file headers are discarded.", &hasHeader,
67                 std.getopt.config.caseInsensitive,
68                 "s|header-string", "STR  String to use in the header row. Implies --header. Default: 'line'", &headerString,
69                 "n|start-number",  "NUM  Number to use for the first line. Default: 1", &startNum,
70                 "d|delimiter",     "CHR  Character appended to line number, preceding the rest of the line. Default: TAB (Single byte UTF-8 characters only.)", &delim,
71                 std.getopt.config.caseSensitive,
72                 "V|version",       "     Print version information and exit.", &versionWanted,
73                 std.getopt.config.caseInsensitive,
74             );
75 
76             if (r.helpWanted)
77             {
78                 defaultGetoptPrinter(helpText, r.options);
79                 return tuple(false, 0);
80             }
81             else if (versionWanted)
82             {
83                 import tsvutils_version;
84                 writeln(tsvutilsVersionNotice("number-lines"));
85                 return tuple(false, 0);
86             }
87 
88             /* Derivations. */
89             if (headerString.length > 0) hasHeader = true;
90             else headerString = defaultHeaderString;
91         }
92         catch (Exception exc)
93         {
94             stderr.writefln("[%s] Error processing command line arguments: %s", programName, exc.msg);
95             return tuple(false, 1);
96         }
97         return tuple(true, 0);
98     }
99 }
100 
101 int main(string[] cmdArgs)
102 {
103     /* When running in DMD code coverage mode, turn on report merging. */
104     version(D_Coverage) version(DigitalMars)
105     {
106         import core.runtime : dmd_coverSetMerge;
107         dmd_coverSetMerge(true);
108     }
109 
110     NumberLinesOptions cmdopt;
111     auto r = cmdopt.processArgs(cmdArgs);
112     if (!r[0]) return r[1];
113     try numberLines(cmdopt, cmdArgs[1..$]);
114     catch (Exception exc)
115     {
116         stderr.writefln("Error [%s]: %s", cmdopt.programName, exc.msg);
117         return 1;
118     }
119 
120     return 0;
121 }
122 
123 void numberLines(in NumberLinesOptions cmdopt, in string[] inputFiles)
124 {
125     import std.conv : to;
126     import std.range;
127     import tsvutil : BufferedOutputRange;
128 
129     auto bufferedOutput = BufferedOutputRange!(typeof(stdout))(stdout);
130 
131     long lineNum = cmdopt.startNum;
132     bool headerWritten = false;
133     foreach (filename; (inputFiles.length > 0) ? inputFiles : ["-"])
134     {
135         auto inputStream = (filename == "-") ? stdin : filename.File();
136         foreach (fileLineNum, line; inputStream.byLine(KeepTerminator.no).enumerate(1))
137         {
138             if (cmdopt.hasHeader && fileLineNum == 1)
139             {
140                 if (!headerWritten)
141                 {
142                     bufferedOutput.append(cmdopt.headerString);
143                     bufferedOutput.append(cmdopt.delim);
144                     bufferedOutput.appendln(line);
145                     headerWritten = true;
146                 }
147             }
148             else
149             {
150                 bufferedOutput.append(lineNum.to!string);
151                 bufferedOutput.append(cmdopt.delim);
152                 bufferedOutput.appendln(line);
153                 lineNum++;
154             }
155         }
156     }
157 }