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 /** Container for command line options.
36  */
37 struct NumberLinesOptions
38 {
39     enum defaultHeaderString = "line";
40 
41     string programName;
42     bool hasHeader = false;       // --H|header
43     string headerString = "";     // --s|header-string
44     long startNum = 1;            // --n|start-num
45     char delim = '\t';            // --d|delimiter
46     bool versionWanted = false;   // --V|version
47 
48     /* Returns a tuple. First value is true if command line arguments were successfully
49      * processed and execution should continue, or false if an error occurred or the user
50      * asked for help. If false, the second value is the appropriate exit code (0 or 1).
51      */
52     auto processArgs (ref string[] cmdArgs)
53     {
54         import std.algorithm : any, each;
55         import std.getopt;
56         import std.path : baseName, stripExtension;
57 
58         programName = (cmdArgs.length > 0) ? cmdArgs[0].stripExtension.baseName : "Unknown_program_name";
59 
60         try
61         {
62             auto r = getopt(
63                 cmdArgs,
64                 std.getopt.config.caseSensitive,
65                 "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,
66                 std.getopt.config.caseInsensitive,
67                 "s|header-string", "STR  String to use in the header row. Implies --header. Default: 'line'", &headerString,
68                 "n|start-number",  "NUM  Number to use for the first line. Default: 1", &startNum,
69                 "d|delimiter",     "CHR  Character appended to line number, preceding the rest of the line. Default: TAB (Single byte UTF-8 characters only.)", &delim,
70                 std.getopt.config.caseSensitive,
71                 "V|version",       "     Print version information and exit.", &versionWanted,
72                 std.getopt.config.caseInsensitive,
73             );
74 
75             if (r.helpWanted)
76             {
77                 defaultGetoptPrinter(helpText, r.options);
78                 return tuple(false, 0);
79             }
80             else if (versionWanted)
81             {
82                 import tsvutils_version;
83                 writeln(tsvutilsVersionNotice("number-lines"));
84                 return tuple(false, 0);
85             }
86 
87             /* Derivations. */
88             if (headerString.length > 0) hasHeader = true;
89             else headerString = defaultHeaderString;
90         }
91         catch (Exception exc)
92         {
93             stderr.writefln("[%s] Error processing command line arguments: %s", programName, exc.msg);
94             return tuple(false, 1);
95         }
96         return tuple(true, 0);
97     }
98 }
99 
100 /** Main program. */
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 /** Implements the primary logic behind number lines.
124  *
125  * Reads lines lines from each file, outputing each with a line number prepended. The
126  * header from the first file is written, the header from subsequent files is dropped.
127  */
128 void numberLines(in NumberLinesOptions cmdopt, in string[] inputFiles)
129 {
130     import std.conv : to;
131     import std.range;
132     import tsvutil : BufferedOutputRange;
133 
134     auto bufferedOutput = BufferedOutputRange!(typeof(stdout))(stdout);
135 
136     long lineNum = cmdopt.startNum;
137     bool headerWritten = false;
138     foreach (filename; (inputFiles.length > 0) ? inputFiles : ["-"])
139     {
140         auto inputStream = (filename == "-") ? stdin : filename.File();
141         foreach (fileLineNum, line; inputStream.byLine(KeepTerminator.no).enumerate(1))
142         {
143             if (cmdopt.hasHeader && fileLineNum == 1)
144             {
145                 if (!headerWritten)
146                 {
147                     bufferedOutput.append(cmdopt.headerString);
148                     bufferedOutput.append(cmdopt.delim);
149                     bufferedOutput.appendln(line);
150                     headerWritten = true;
151                 }
152             }
153             else
154             {
155                 bufferedOutput.append(lineNum.to!string);
156                 bufferedOutput.append(cmdopt.delim);
157                 bufferedOutput.appendln(line);
158                 lineNum++;
159             }
160         }
161     }
162 }