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 }