1 /**
2 Utilities used by tsv-utils applications. InputFieldReordering, BufferedOututRange,
3 and a several others.
4 
5 Utilities in this file:
6 $(LIST
7     * [InputFieldReordering] - A class that creates a reordered subset of fields from
8       an input line. Fields in the subset are accessed by array indicies. This is
9       especially useful when processing the subset in a specific order, such as the
10       order listed on the command-line at run-time.
11 
12     * [BufferedOutputRange] - An OutputRange with an internal buffer used to buffer
13       output. Intended for use with stdout, it is a significant performance benefit.
14 
15     * [joinAppend] - A function that performs a join, but appending the join output to
16       an output stream. It is a performance improvement over using join or joiner with
17       writeln.
18 
19     * [getTsvFieldValue] - A convenience function when only a single value is needed from
20       an input line.
21 
22     * Field-lists: [parseFieldList], [makeFieldListOptionHandler] - Helper functions for
23       parsing field-lists entered on the command line.
24 
25     * [throwIfWindowsNewlineOnUnix] - A utility for Unix platform builds to detecting
26       Windows newlines in input.
27 )
28 
29 Copyright (c) 2015-2018, eBay Software Foundation
30 Initially written by Jon Degenhardt
31 
32 License: Boost Licence 1.0 (http://boost.org/LICENSE_1_0.txt)
33 */
34 
35 module tsv_utils.common.utils;
36 
37 import std.range;
38 import std.traits : isIntegral, isSomeChar, isSomeString, isUnsigned;
39 import std.typecons : Flag, No, Yes;
40 
41 // InputFieldReording class.
42 
43 /** Flag used by the InputFieldReordering template. */
44 alias EnablePartialLines = Flag!"enablePartialLines";
45 
46 /**
47 InputFieldReordering - Move select fields from an input line to an output array,
48 reordering along the way.
49 
50 The InputFieldReordering class is used to reorder a subset of fields from an input line.
51 The caller instantiates an InputFieldReordering object at the start of input processing.
52 The instance contains a mapping from input index to output index, plus a buffer holding
53 the reordered fields. The caller processes each input line by calling initNewLine,
54 splitting the line into fields, and calling processNextField on each field. The output
55 buffer is ready when the allFieldsFilled method returns true.
56 
57 Fields are not copied, instead the output buffer points to the fields passed by the caller.
58 The caller needs to use or copy the output buffer while the fields are still valid, which
59 is normally until reading the next input line. The program below illustrates the basic use
60 case. It reads stdin and outputs fields [3, 0, 2], in that order. (See also joinAppend,
61 below, which has a performance improvement over join used here.)
62 
63 ---
64 int main(string[] args)
65 {
66     import tsv_utils.common.utils;
67     import std.algorithm, std.array, std.range, std.stdio;
68     size_t[] fieldIndicies = [3, 0, 2];
69     auto fieldReordering = new InputFieldReordering!char(fieldIndicies);
70     foreach (line; stdin.byLine)
71     {
72         fieldReordering.initNewLine;
73         foreach(fieldIndex, fieldValue; line.splitter('\t').enumerate)
74         {
75             fieldReordering.processNextField(fieldIndex, fieldValue);
76             if (fieldReordering.allFieldsFilled) break;
77         }
78         if (fieldReordering.allFieldsFilled)
79         {
80             writeln(fieldReordering.outputFields.join('\t'));
81         }
82         else
83         {
84             writeln("Error: Insufficient number of field on the line.");
85         }
86     }
87     return 0;
88 }
89 ---
90 
91 Field indicies are zero-based. An individual field can be listed multiple times. The
92 outputFields array is not valid until all the specified fields have been processed. The
93 allFieldsFilled method tests this. If a line does not have enough fields the outputFields
94 buffer cannot be used. For most TSV applications this is okay, as it means the line is
95 invalid and cannot be used. However, if partial lines are okay, the template can be
96 instantiated with EnablePartialLines.yes. This will ensure that any fields not filled-in
97 are empty strings in the outputFields return.
98 */
99 class InputFieldReordering(C, EnablePartialLines partialLinesOk = EnablePartialLines.no)
100 if (isSomeChar!C)
101 {
102     /* Implementation: The class works by creating an array of tuples mapping the input
103      * field index to the location in the outputFields array. The 'fromToMap' array is
104      * sorted in input field order, enabling placement in the outputFields buffer during a
105      * pass over the input fields. The map is created by the constructor. An example:
106      *
107      *    inputFieldIndicies: [3, 0, 7, 7, 1, 0, 9]
108      *             fromToMap: [<0,1>, <0,5>, <1,4>, <3,0>, <7,2>, <7,3>, <9,6>]
109      *
110      * During processing of an a line, an array slice, mapStack, is used to track how
111      * much of the fromToMap remains to be processed.
112      */
113     import std.range;
114     import std.typecons : Tuple;
115 
116     alias TupleFromTo = Tuple!(size_t, "from", size_t, "to");
117 
118     private C[][] outputFieldsBuf;
119     private TupleFromTo[] fromToMap;
120     private TupleFromTo[] mapStack;
121 
122     final this(const ref size_t[] inputFieldIndicies, size_t start = 0) pure nothrow @safe
123     {
124         import std.algorithm : sort;
125 
126         outputFieldsBuf = new C[][](inputFieldIndicies.length);
127         fromToMap.reserve(inputFieldIndicies.length);
128 
129         foreach (to, from; inputFieldIndicies.enumerate(start))
130         {
131             fromToMap ~= TupleFromTo(from, to);
132         }
133 
134         sort(fromToMap);
135         initNewLine;
136     }
137 
138     /** initNewLine initializes the object for a new line. */
139     final void initNewLine() pure nothrow @safe
140     {
141         mapStack = fromToMap;
142         static if (partialLinesOk)
143         {
144             import std.algorithm : each;
145             outputFieldsBuf.each!((ref s) => s.length = 0);
146         }
147     }
148 
149     /** processNextField maps an input field to the correct locations in the outputFields
150      * array. It should be called once for each field on the line, in the order found.
151      */
152     final size_t processNextField(size_t fieldIndex, C[] fieldValue) pure nothrow @safe @nogc
153     {
154         size_t numFilled = 0;
155         while (!mapStack.empty && fieldIndex == mapStack.front.from)
156         {
157             outputFieldsBuf[mapStack.front.to] = fieldValue;
158             mapStack.popFront;
159             numFilled++;
160         }
161         return numFilled;
162     }
163 
164     /** allFieldsFilled returned true if all fields expected have been processed. */
165     final bool allFieldsFilled() const pure nothrow @safe @nogc
166     {
167         return mapStack.empty;
168     }
169 
170     /** outputFields is the assembled output fields. Unless partial lines are enabled,
171      * it is only valid after allFieldsFilled is true.
172      */
173     final C[][] outputFields() pure nothrow @safe @nogc
174     {
175         return outputFieldsBuf[];
176     }
177 }
178 
179 /* Tests using different character types. */
180 unittest
181 {
182     import std.conv : to;
183 
184     auto inputLines = [["r1f0", "r1f1", "r1f2",   "r1f3"],
185                        ["r2f0", "abc",  "ÀBCßßZ", "ghi"],
186                        ["r3f0", "123",  "456",    "789"]];
187 
188     size_t[] fields_2_0 = [2, 0];
189 
190     auto expected_2_0 = [["r1f2",   "r1f0"],
191                          ["ÀBCßßZ", "r2f0"],
192                          ["456",    "r3f0"]];
193 
194     char[][][]  charExpected_2_0 = to!(char[][][])(expected_2_0);
195     wchar[][][] wcharExpected_2_0 = to!(wchar[][][])(expected_2_0);
196     dchar[][][] dcharExpected_2_0 = to!(dchar[][][])(expected_2_0);
197     dstring[][] dstringExpected_2_0 = to!(dstring[][])(expected_2_0);
198 
199     auto charIFR  = new InputFieldReordering!char(fields_2_0);
200     auto wcharIFR = new InputFieldReordering!wchar(fields_2_0);
201     auto dcharIFR = new InputFieldReordering!dchar(fields_2_0);
202 
203     foreach (lineIndex, line; inputLines)
204     {
205         charIFR.initNewLine;
206         wcharIFR.initNewLine;
207         dcharIFR.initNewLine;
208 
209         foreach (fieldIndex, fieldValue; line)
210         {
211             charIFR.processNextField(fieldIndex, to!(char[])(fieldValue));
212             wcharIFR.processNextField(fieldIndex, to!(wchar[])(fieldValue));
213             dcharIFR.processNextField(fieldIndex, to!(dchar[])(fieldValue));
214 
215             assert ((fieldIndex >= 2) == charIFR.allFieldsFilled);
216             assert ((fieldIndex >= 2) == wcharIFR.allFieldsFilled);
217             assert ((fieldIndex >= 2) == dcharIFR.allFieldsFilled);
218         }
219         assert(charIFR.allFieldsFilled);
220         assert(wcharIFR.allFieldsFilled);
221         assert(dcharIFR.allFieldsFilled);
222 
223         assert(charIFR.outputFields == charExpected_2_0[lineIndex]);
224         assert(wcharIFR.outputFields == wcharExpected_2_0[lineIndex]);
225         assert(dcharIFR.outputFields == dcharExpected_2_0[lineIndex]);
226     }
227 }
228 
229 /* Test of partial line support. */
230 unittest
231 {
232     import std.conv : to;
233 
234     auto inputLines = [["r1f0", "r1f1", "r1f2",   "r1f3"],
235                        ["r2f0", "abc",  "ÀBCßßZ", "ghi"],
236                        ["r3f0", "123",  "456",    "789"]];
237 
238     size_t[] fields_2_0 = [2, 0];
239 
240     // The expected states of the output field while each line and field are processed.
241     auto expectedBylineByfield_2_0 =
242         [
243             [["", "r1f0"], ["", "r1f0"], ["r1f2", "r1f0"],   ["r1f2", "r1f0"]],
244             [["", "r2f0"], ["", "r2f0"], ["ÀBCßßZ", "r2f0"], ["ÀBCßßZ", "r2f0"]],
245             [["", "r3f0"], ["", "r3f0"], ["456", "r3f0"],    ["456", "r3f0"]],
246         ];
247 
248     char[][][][]  charExpectedBylineByfield_2_0 = to!(char[][][][])(expectedBylineByfield_2_0);
249 
250     auto charIFR  = new InputFieldReordering!(char, EnablePartialLines.yes)(fields_2_0);
251 
252     foreach (lineIndex, line; inputLines)
253     {
254         charIFR.initNewLine;
255         foreach (fieldIndex, fieldValue; line)
256         {
257             charIFR.processNextField(fieldIndex, to!(char[])(fieldValue));
258             assert(charIFR.outputFields == charExpectedBylineByfield_2_0[lineIndex][fieldIndex]);
259         }
260     }
261 }
262 
263 /* Field combination tests. */
264 unittest
265 {
266     import std.conv : to;
267     import std.stdio;
268 
269     auto inputLines = [["00", "01", "02", "03"],
270                        ["10", "11", "12", "13"],
271                        ["20", "21", "22", "23"]];
272 
273     size_t[] fields_0 = [0];
274     size_t[] fields_3 = [3];
275     size_t[] fields_01 = [0, 1];
276     size_t[] fields_10 = [1, 0];
277     size_t[] fields_03 = [0, 3];
278     size_t[] fields_30 = [3, 0];
279     size_t[] fields_0123 = [0, 1, 2, 3];
280     size_t[] fields_3210 = [3, 2, 1, 0];
281     size_t[] fields_03001 = [0, 3, 0, 0, 1];
282 
283     auto expected_0 = to!(char[][][])([["00"],
284                                        ["10"],
285                                        ["20"]]);
286 
287     auto expected_3 = to!(char[][][])([["03"],
288                                        ["13"],
289                                        ["23"]]);
290 
291     auto expected_01 = to!(char[][][])([["00", "01"],
292                                         ["10", "11"],
293                                         ["20", "21"]]);
294 
295     auto expected_10 = to!(char[][][])([["01", "00"],
296                                         ["11", "10"],
297                                         ["21", "20"]]);
298 
299     auto expected_03 = to!(char[][][])([["00", "03"],
300                                         ["10", "13"],
301                                         ["20", "23"]]);
302 
303     auto expected_30 = to!(char[][][])([["03", "00"],
304                                         ["13", "10"],
305                                         ["23", "20"]]);
306 
307     auto expected_0123 = to!(char[][][])([["00", "01", "02", "03"],
308                                           ["10", "11", "12", "13"],
309                                           ["20", "21", "22", "23"]]);
310 
311     auto expected_3210 = to!(char[][][])([["03", "02", "01", "00"],
312                                           ["13", "12", "11", "10"],
313                                           ["23", "22", "21", "20"]]);
314 
315     auto expected_03001 = to!(char[][][])([["00", "03", "00", "00", "01"],
316                                            ["10", "13", "10", "10", "11"],
317                                            ["20", "23", "20", "20", "21"]]);
318 
319     auto ifr_0 = new InputFieldReordering!char(fields_0);
320     auto ifr_3 = new InputFieldReordering!char(fields_3);
321     auto ifr_01 = new InputFieldReordering!char(fields_01);
322     auto ifr_10 = new InputFieldReordering!char(fields_10);
323     auto ifr_03 = new InputFieldReordering!char(fields_03);
324     auto ifr_30 = new InputFieldReordering!char(fields_30);
325     auto ifr_0123 = new InputFieldReordering!char(fields_0123);
326     auto ifr_3210 = new InputFieldReordering!char(fields_3210);
327     auto ifr_03001 = new InputFieldReordering!char(fields_03001);
328 
329     foreach (lineIndex, line; inputLines)
330     {
331         ifr_0.initNewLine;
332         ifr_3.initNewLine;
333         ifr_01.initNewLine;
334         ifr_10.initNewLine;
335         ifr_03.initNewLine;
336         ifr_30.initNewLine;
337         ifr_0123.initNewLine;
338         ifr_3210.initNewLine;
339         ifr_03001.initNewLine;
340 
341         foreach (fieldIndex, fieldValue; line)
342         {
343             ifr_0.processNextField(fieldIndex, to!(char[])(fieldValue));
344             ifr_3.processNextField(fieldIndex, to!(char[])(fieldValue));
345             ifr_01.processNextField(fieldIndex, to!(char[])(fieldValue));
346             ifr_10.processNextField(fieldIndex, to!(char[])(fieldValue));
347             ifr_03.processNextField(fieldIndex, to!(char[])(fieldValue));
348             ifr_30.processNextField(fieldIndex, to!(char[])(fieldValue));
349             ifr_0123.processNextField(fieldIndex, to!(char[])(fieldValue));
350             ifr_3210.processNextField(fieldIndex, to!(char[])(fieldValue));
351             ifr_03001.processNextField(fieldIndex, to!(char[])(fieldValue));
352         }
353 
354         assert(ifr_0.outputFields == expected_0[lineIndex]);
355         assert(ifr_3.outputFields == expected_3[lineIndex]);
356         assert(ifr_01.outputFields == expected_01[lineIndex]);
357         assert(ifr_10.outputFields == expected_10[lineIndex]);
358         assert(ifr_03.outputFields == expected_03[lineIndex]);
359         assert(ifr_30.outputFields == expected_30[lineIndex]);
360         assert(ifr_0123.outputFields == expected_0123[lineIndex]);
361         assert(ifr_3210.outputFields == expected_3210[lineIndex]);
362         assert(ifr_03001.outputFields == expected_03001[lineIndex]);
363     }
364 }
365 
366 /**
367 BufferedOutputRange is a performance enhancement over writing directly to an output
368 stream. It holds a File open for write or an OutputRange. Ouput is accumulated in an
369 internal buffer and written to the output stream as a block.
370 
371 Writing to stdout is a key use case. BufferedOutputRange is often dramatically faster
372 than writing to stdout directly. This is especially noticable for outputs with short
373 lines, as it blocks many writes together in a single write.
374 
375 The internal buffer is written to the output stream after flushSize has been reached.
376 This is checked at newline boundaries, when appendln is called or when put is called
377 with a single newline character. Other writes check maxSize, which is used to avoid
378 runaway buffers.
379 
380 
381 BufferedOutputRange has a put method allowing it to be used a range. It has a number
382 of other methods providing additional control.
383 
384 $(LIST
385     * `this(outputStream [, flushSize, reserveSize, maxSize])` - Constructor. Takes the
386       output stream, e.g. stdout. Other arguments are optional, defaults normally suffice.
387 
388     * `append(stuff)` - Append to the internal buffer.
389 
390     * `appendln(stuff)` - Append to the internal buffer, followed by a newline. The buffer
391       is flushed to the output stream if is has reached flushSize.
392 
393     * `appendln()` - Append a newline to the internal buffer. The buffer is flushed to the
394       output stream if is has reached flushSize.
395 
396     * `joinAppend(inputRange, delim)` - An optimization of `append(inputRange.joiner(delim))`.
397       For reasons that are not clear, joiner is quite slow.
398 
399     * `flushIfFull()` - Flush the internal buffer to the output stream if flushSize has been
400       reached.
401 
402     * `flush()` - Write the internal buffer to the output stream.
403 
404     * `put(stuff)` - Appends to the internal buffer. Acts as `appendln()` if passed a single
405       newline character, '\n' or "\n".
406 )
407 
408 The internal buffer is automatically flushed when the BufferedOutputRange goes out of
409 scope.
410 */
411 
412 import std.stdio : isFileHandle;
413 import std.range : isOutputRange;
414 import std.traits : Unqual;
415 
416 struct BufferedOutputRange(OutputTarget)
417 if (isFileHandle!(Unqual!OutputTarget) || isOutputRange!(Unqual!OutputTarget, char))
418 {
419     import std.range : isOutputRange;
420     import std.array : appender;
421     import std.format : format;
422 
423     /* Identify the output element type. Only supporting char and ubyte for now. */
424     static if (isFileHandle!OutputTarget || isOutputRange!(OutputTarget, char))
425     {
426         alias C = char;
427     }
428     else static if (isOutputRange!(OutputTarget, ubyte))
429     {
430         alias C = ubyte;
431     }
432     else static assert(false);
433 
434     private enum defaultReserveSize = 11264;
435     private enum defaultFlushSize = 10240;
436     private enum defaultMaxSize = 4194304;
437 
438     private OutputTarget _outputTarget;
439     private auto _outputBuffer = appender!(C[]);
440     private immutable size_t _flushSize;
441     private immutable size_t _maxSize;
442 
443     this(OutputTarget outputTarget,
444          size_t flushSize = defaultFlushSize,
445          size_t reserveSize = defaultReserveSize,
446          size_t maxSize = defaultMaxSize)
447     {
448         assert(flushSize <= maxSize);
449 
450         _outputTarget = outputTarget;
451         _flushSize = flushSize;
452         _maxSize = (flushSize <= maxSize) ? maxSize : flushSize;
453         _outputBuffer.reserve(reserveSize);
454     }
455 
456     ~this()
457     {
458         flush();
459     }
460 
461     void flush()
462     {
463         static if (isFileHandle!OutputTarget) _outputTarget.write(_outputBuffer.data);
464         else _outputTarget.put(_outputBuffer.data);
465 
466         _outputBuffer.clear;
467     }
468 
469     bool flushIfFull()
470     {
471         bool isFull = _outputBuffer.data.length >= _flushSize;
472         if (isFull) flush();
473         return isFull;
474     }
475 
476     /* flushIfMaxSize is a safety check to avoid runaway buffer growth. */
477     void flushIfMaxSize()
478     {
479         if (_outputBuffer.data.length >= _maxSize) flush();
480     }
481 
482     private void appendRaw(T)(T stuff)
483     {
484         import std.range : rangePut = put;
485         rangePut(_outputBuffer, stuff);
486     }
487 
488     void append(T)(T stuff)
489     {
490         appendRaw(stuff);
491         flushIfMaxSize();
492     }
493 
494     bool appendln()
495     {
496         appendRaw('\n');
497         return flushIfFull();
498     }
499 
500     bool appendln(T)(T stuff)
501     {
502         appendRaw(stuff);
503         return appendln();
504     }
505 
506     /* joinAppend is an optimization of append(inputRange.joiner(delimiter).
507      * This form is quite a bit faster, 40%+ on some benchmarks.
508      */
509     void joinAppend(InputRange, E)(InputRange inputRange, E delimiter)
510     if (isInputRange!InputRange &&
511         is(ElementType!InputRange : const C[]) &&
512         (is(E : const C[]) || is(E : const C)))
513     {
514         if (!inputRange.empty)
515         {
516             appendRaw(inputRange.front);
517             inputRange.popFront;
518         }
519         foreach (x; inputRange)
520         {
521             appendRaw(delimiter);
522             appendRaw(x);
523         }
524         flushIfMaxSize();
525     }
526 
527     /* Make this an output range. */
528     void put(T)(T stuff)
529     {
530         import std.traits;
531         import std.stdio;
532 
533         static if (isSomeChar!T)
534         {
535             if (stuff == '\n') appendln();
536             else appendRaw(stuff);
537         }
538         else static if (isSomeString!T)
539         {
540             if (stuff == "\n") appendln();
541             else append(stuff);
542         }
543         else append(stuff);
544     }
545 }
546 
547 unittest
548 {
549     import tsv_utils.common.unittest_utils;
550     import std.file : rmdirRecurse, readText;
551     import std.path : buildPath;
552 
553     auto testDir = makeUnittestTempDir("tsv_utils_buffered_output");
554     scope(exit) testDir.rmdirRecurse;
555 
556     import std.algorithm : map, joiner;
557     import std.range : iota;
558     import std.conv : to;
559 
560     /* Basic test. Note that exiting the scope triggers flush. */
561     string filepath1 = buildPath(testDir, "file1.txt");
562     {
563         import std.stdio : File;
564 
565         auto ostream = BufferedOutputRange!File(filepath1.File("w"));
566         ostream.append("file1: ");
567         ostream.append("abc");
568         ostream.append(["def", "ghi", "jkl"]);
569         ostream.appendln(100.to!string);
570         ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" "));
571         ostream.appendln();
572     }
573     assert(filepath1.readText == "file1: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n");
574 
575     /* Test with no reserve and no flush at every line. */
576     string filepath2 = buildPath(testDir, "file2.txt");
577     {
578         import std.stdio : File;
579 
580         auto ostream = BufferedOutputRange!File(filepath2.File("w"), 0, 0);
581         ostream.append("file2: ");
582         ostream.append("abc");
583         ostream.append(["def", "ghi", "jkl"]);
584         ostream.appendln("100");
585         ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" "));
586         ostream.appendln();
587     }
588     assert(filepath2.readText == "file2: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n");
589 
590     /* With a locking text writer. Requires version 2.078.0
591        See: https://issues.dlang.org/show_bug.cgi?id=9661
592      */
593     static if (__VERSION__ >= 2078)
594     {
595         string filepath3 = buildPath(testDir, "file3.txt");
596         {
597             import std.stdio : File;
598 
599             auto ltw = filepath3.File("w").lockingTextWriter;
600             {
601                 auto ostream = BufferedOutputRange!(typeof(ltw))(ltw);
602                 ostream.append("file3: ");
603                 ostream.append("abc");
604                 ostream.append(["def", "ghi", "jkl"]);
605                 ostream.appendln("100");
606                 ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" "));
607                 ostream.appendln();
608             }
609         }
610         assert(filepath3.readText == "file3: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n");
611     }
612 
613     /* With an Appender. */
614     import std.array : appender;
615     auto app1 = appender!(char[]);
616     {
617         auto ostream = BufferedOutputRange!(typeof(app1))(app1);
618         ostream.append("appender1: ");
619         ostream.append("abc");
620         ostream.append(["def", "ghi", "jkl"]);
621         ostream.appendln("100");
622         ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" "));
623         ostream.appendln();
624     }
625     assert(app1.data == "appender1: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n");
626 
627     /* With an Appender, but checking flush boundaries. */
628     auto app2 = appender!(char[]);
629     {
630         auto ostream = BufferedOutputRange!(typeof(app2))(app2, 10, 0); // Flush if 10+
631         bool wasFlushed = false;
632 
633         assert(app2.data == "");
634 
635         ostream.append("12345678"); // Not flushed yet.
636         assert(app2.data == "");
637 
638         wasFlushed = ostream.appendln;  // Nineth char, not flushed yet.
639         assert(!wasFlushed);
640         assert(app2.data == "");
641 
642         wasFlushed = ostream.appendln;  // Tenth char, now flushed.
643         assert(wasFlushed);
644         assert(app2.data == "12345678\n\n");
645 
646         app2.clear;
647         assert(app2.data == "");
648 
649         ostream.append("12345678");
650 
651         wasFlushed = ostream.flushIfFull;
652         assert(!wasFlushed);
653         assert(app2.data == "");
654 
655         ostream.flush;
656         assert(app2.data == "12345678");
657 
658         app2.clear;
659         assert(app2.data == "");
660 
661         ostream.append("123456789012345");
662         assert(app2.data == "");
663     }
664     assert(app2.data == "123456789012345");
665 
666     /* Using joinAppend. */
667     auto app1b = appender!(char[]);
668     {
669         auto ostream = BufferedOutputRange!(typeof(app1b))(app1b);
670         ostream.append("appenderB: ");
671         ostream.joinAppend(["a", "bc", "def"], '-');
672         ostream.append(':');
673         ostream.joinAppend(["g", "hi", "jkl"], '-');
674         ostream.appendln("*100*");
675         ostream.joinAppend(iota(0, 6).map!(x => x.to!string), ' ');
676         ostream.append(' ');
677         ostream.joinAppend(iota(6, 10).map!(x => x.to!string), " ");
678         ostream.appendln();
679     }
680     assert(app1b.data == "appenderB: a-bc-def:g-hi-jkl*100*\n0 1 2 3 4 5 6 7 8 9\n",
681            "app1b.data: |" ~app1b.data ~ "|");
682 
683     /* Operating as an output range. When passed to a function as a ref, exiting
684      * the function does not flush. When passed as a value, it get flushed when
685      * the function returns. Also test both UCFS and non-UFCS styles.
686      */
687 
688     void outputStuffAsRef(T)(ref T range)
689     if (isOutputRange!(T, char))
690     {
691         range.put('1');
692         put(range, "23");
693         range.put('\n');
694         range.put(["5", "67"]);
695         put(range, iota(8, 10).map!(x => x.to!string));
696         put(range, "\n");
697     }
698 
699     void outputStuffAsVal(T)(T range)
700     if (isOutputRange!(T, char))
701     {
702         put(range, '1');
703         range.put("23");
704         put(range, '\n');
705         put(range, ["5", "67"]);
706         range.put(iota(8, 10).map!(x => x.to!string));
707         range.put("\n");
708     }
709 
710     auto app3 = appender!(char[]);
711     {
712         auto ostream = BufferedOutputRange!(typeof(app3))(app3, 12, 0);
713         outputStuffAsRef(ostream);
714         assert(app3.data == "", "app3.data: |" ~app3.data ~ "|");
715         outputStuffAsRef(ostream);
716         assert(app3.data == "123\n56789\n123\n", "app3.data: |" ~app3.data ~ "|");
717     }
718     assert(app3.data == "123\n56789\n123\n56789\n", "app3.data: |" ~app3.data ~ "|");
719 
720     auto app4 = appender!(char[]);
721     {
722         auto ostream = BufferedOutputRange!(typeof(app4))(app4, 12, 0);
723         outputStuffAsVal(ostream);
724         assert(app4.data == "123\n56789\n", "app4.data: |" ~app4.data ~ "|");
725         outputStuffAsVal(ostream);
726         assert(app4.data == "123\n56789\n123\n56789\n", "app4.data: |" ~app4.data ~ "|");
727     }
728     assert(app4.data == "123\n56789\n123\n56789\n", "app4.data: |" ~app4.data ~ "|");
729 
730     /* Test maxSize. */
731     auto app5 = appender!(char[]);
732     {
733         auto ostream = BufferedOutputRange!(typeof(app5))(app5, 5, 0, 10); // maxSize 10
734         assert(app5.data == "");
735 
736         ostream.append("1234567");  // Not flushed yet (no newline).
737         assert(app5.data == "");
738 
739         ostream.append("89012");    // Flushed by maxSize
740         assert(app5.data == "123456789012");
741 
742         ostream.put("1234567");     // Not flushed yet (no newline).
743         assert(app5.data == "123456789012");
744 
745         ostream.put("89012");       // Flushed by maxSize
746         assert(app5.data == "123456789012123456789012");
747 
748         ostream.joinAppend(["ab", "cd"], '-');        // Not flushed yet
749         ostream.joinAppend(["de", "gh", "ij"], '-');  // Flushed by maxSize
750         assert(app5.data == "123456789012123456789012ab-cdde-gh-ij");
751     }
752     assert(app5.data == "123456789012123456789012ab-cdde-gh-ij");
753 }
754 
755 /**
756 joinAppend performs a join operation on an input range, appending the results to
757 an output range.
758 
759 Note: The main uses of joinAppend have been replaced by BufferedOutputRange, which has
760 its own joinAppend method.
761 
762 joinAppend was written as a performance enhancement over using std.algorithm.joiner
763 or std.array.join with writeln. Using joiner with writeln is quite slow, 3-4x slower
764 than std.array.join with writeln. The joiner performance may be due to interaction
765 with writeln, this was not investigated. Using joiner with stdout.lockingTextWriter
766 is better, but still substantially slower than join. Using join works reasonably well,
767 but is allocating memory unnecessarily.
768 
769 Using joinAppend with Appender is a bit faster than join, and allocates less memory.
770 The Appender re-uses the underlying data buffer, saving memory. The example below
771 illustrates. It is a modification of the InputFieldReordering example. The role
772 Appender plus joinAppend are playing is to buffer the output. BufferedOutputRange
773 uses a similar technique to buffer multiple lines.
774 
775 ---
776 int main(string[] args)
777 {
778     import tsvutil;
779     import std.algorithm, std.array, std.range, std.stdio;
780     size_t[] fieldIndicies = [3, 0, 2];
781     auto fieldReordering = new InputFieldReordering!char(fieldIndicies);
782     auto outputBuffer = appender!(char[]);
783     foreach (line; stdin.byLine)
784     {
785         fieldReordering.initNewLine;
786         foreach(fieldIndex, fieldValue; line.splitter('\t').enumerate)
787         {
788             fieldReordering.processNextField(fieldIndex, fieldValue);
789             if (fieldReordering.allFieldsFilled) break;
790         }
791         if (fieldReordering.allFieldsFilled)
792         {
793             outputBuffer.clear;
794             writeln(fieldReordering.outputFields.joinAppend(outputBuffer, ('\t')));
795         }
796         else
797         {
798             writeln("Error: Insufficient number of field on the line.");
799         }
800     }
801     return 0;
802 }
803 ---
804 */
805 OutputRange joinAppend(InputRange, OutputRange, E)
806     (InputRange inputRange, ref OutputRange outputRange, E delimiter)
807 if (isInputRange!InputRange &&
808     (is(ElementType!InputRange : const E[]) &&
809      isOutputRange!(OutputRange, E[]))
810      ||
811     (is(ElementType!InputRange : const E) &&
812      isOutputRange!(OutputRange, E))
813     )
814 {
815     if (!inputRange.empty)
816     {
817         outputRange.put(inputRange.front);
818         inputRange.popFront;
819     }
820     foreach (x; inputRange)
821     {
822         outputRange.put(delimiter);
823         outputRange.put(x);
824     }
825     return outputRange;
826 }
827 
828 @safe unittest
829 {
830     import std.array : appender;
831     import std.algorithm : equal;
832 
833     char[] c1 = ['a', 'b', 'c'];
834     char[] c2 = ['d', 'e', 'f'];
835     char[] c3 = ['g', 'h', 'i'];
836     auto cvec = [c1, c2, c3];
837 
838     auto s1 = "abc";
839     auto s2 = "def";
840     auto s3 = "ghi";
841     auto svec = [s1, s2, s3];
842 
843     auto charAppender = appender!(char[])();
844 
845     assert(cvec.joinAppend(charAppender, '_').data == "abc_def_ghi");
846     assert(equal(cvec, [c1, c2, c3]));
847 
848     charAppender.put('$');
849     assert(svec.joinAppend(charAppender, '|').data == "abc_def_ghi$abc|def|ghi");
850     assert(equal(cvec, [s1, s2, s3]));
851 
852     charAppender.clear;
853     assert(svec.joinAppend(charAppender, '|').data == "abc|def|ghi");
854 
855     auto intAppender = appender!(int[])();
856 
857     auto i1 = [100, 101, 102];
858     auto i2 = [200, 201, 202];
859     auto i3 = [300, 301, 302];
860     auto ivec = [i1, i2, i3];
861 
862     assert(ivec.joinAppend(intAppender, 0).data ==
863            [100, 101, 102, 0, 200, 201, 202, 0, 300, 301, 302]);
864 
865     intAppender.clear;
866     assert(i1.joinAppend(intAppender, 0).data ==
867            [100, 0, 101, 0, 102]);
868     assert(i2.joinAppend(intAppender, 1).data ==
869            [100, 0, 101, 0, 102,
870             200, 1, 201, 1, 202]);
871     assert(i3.joinAppend(intAppender, 2).data ==
872            [100, 0, 101, 0, 102,
873             200, 1, 201, 1, 202,
874             300, 2, 301, 2, 302]);
875 }
876 
877 /**
878 getTsvFieldValue extracts the value of a single field from a delimited text string.
879 
880 This is a convenience function intended for cases when only a single field from an
881 input line is needed. If multiple values are needed, it will be more efficient to
882 work directly with std.algorithm.splitter or the InputFieldReordering class.
883 
884 The input text is split by a delimiter character. The specified field is converted
885 to the desired type and the value returned.
886 
887 An exception is thrown if there are not enough fields on the line or if conversion
888 fails. Conversion is done with std.conv.to, it throws a std.conv.ConvException on
889 failure. If not enough fields, the exception text is generated referencing 1-upped
890 field numbers as would be provided by command line users.
891  */
892 T getTsvFieldValue(T, C)(const C[] line, size_t fieldIndex, C delim) pure @safe
893 if (isSomeChar!C)
894 {
895     import std.algorithm : splitter;
896     import std.conv : to;
897     import std.format : format;
898     import std.range;
899 
900     auto splitLine = line.splitter(delim);
901     size_t atField = 0;
902 
903     while (atField < fieldIndex && !splitLine.empty)
904     {
905         splitLine.popFront;
906         atField++;
907     }
908 
909     T val;
910     if (splitLine.empty)
911     {
912         if (fieldIndex == 0)
913         {
914             /* This is a workaround to a splitter special case - If the input is empty,
915              * the returned split range is empty. This doesn't properly represent a single
916              * column file. More correct mathematically, and for this case, would be a
917              * single value representing an empty string. The input line is a convenient
918              * source of an empty line. Info:
919              *   Bug: https://issues.dlang.org/show_bug.cgi?id=15735
920              *   Pull Request: https://github.com/D-Programming-Language/phobos/pull/4030
921              */
922             assert(line.empty);
923             val = line.to!T;
924         }
925         else
926         {
927             throw new Exception(
928                 format("Not enough fields on line. Number required: %d; Number found: %d",
929                        fieldIndex + 1, atField));
930         }
931     }
932     else
933     {
934         val = splitLine.front.to!T;
935     }
936 
937     return val;
938 }
939 
940 unittest
941 {
942     import std.conv : ConvException, to;
943     import std.exception;
944 
945     /* Common cases. */
946     assert(getTsvFieldValue!double("123", 0, '\t') == 123.0);
947     assert(getTsvFieldValue!double("-10.5", 0, '\t') == -10.5);
948     assert(getTsvFieldValue!size_t("abc|123", 1, '|') == 123);
949     assert(getTsvFieldValue!int("紅\t红\t99", 2, '\t') == 99);
950     assert(getTsvFieldValue!int("紅\t红\t99", 2, '\t') == 99);
951     assert(getTsvFieldValue!string("紅\t红\t99", 2, '\t') == "99");
952     assert(getTsvFieldValue!string("紅\t红\t99", 1, '\t') == "红");
953     assert(getTsvFieldValue!string("紅\t红\t99", 0, '\t') == "紅");
954     assert(getTsvFieldValue!string("红色和绿色\tred and green\t赤と緑\t10.5", 2, '\t') == "赤と緑");
955     assert(getTsvFieldValue!double("红色和绿色\tred and green\t赤と緑\t10.5", 3, '\t') == 10.5);
956 
957     /* The empty field cases. */
958     assert(getTsvFieldValue!string("", 0, '\t') == "");
959     assert(getTsvFieldValue!string("\t", 0, '\t') == "");
960     assert(getTsvFieldValue!string("\t", 1, '\t') == "");
961     assert(getTsvFieldValue!string("", 0, ':') == "");
962     assert(getTsvFieldValue!string(":", 0, ':') == "");
963     assert(getTsvFieldValue!string(":", 1, ':') == "");
964 
965     /* Tests with different data types. */
966     string stringLine = "orange and black\tნარინჯისფერი და შავი\t88.5";
967     char[] charLine = "orange and black\tნარინჯისფერი და შავი\t88.5".to!(char[]);
968     dchar[] dcharLine = stringLine.to!(dchar[]);
969     wchar[] wcharLine = stringLine.to!(wchar[]);
970 
971     assert(getTsvFieldValue!string(stringLine, 0, '\t') == "orange and black");
972     assert(getTsvFieldValue!string(stringLine, 1, '\t') == "ნარინჯისფერი და შავი");
973     assert(getTsvFieldValue!wstring(stringLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring);
974     assert(getTsvFieldValue!double(stringLine, 2, '\t') == 88.5);
975 
976     assert(getTsvFieldValue!string(charLine, 0, '\t') == "orange and black");
977     assert(getTsvFieldValue!string(charLine, 1, '\t') == "ნარინჯისფერი და შავი");
978     assert(getTsvFieldValue!wstring(charLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring);
979     assert(getTsvFieldValue!double(charLine, 2, '\t') == 88.5);
980 
981     assert(getTsvFieldValue!string(dcharLine, 0, '\t') == "orange and black");
982     assert(getTsvFieldValue!string(dcharLine, 1, '\t') == "ნარინჯისფერი და შავი");
983     assert(getTsvFieldValue!wstring(dcharLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring);
984     assert(getTsvFieldValue!double(dcharLine, 2, '\t') == 88.5);
985 
986     assert(getTsvFieldValue!string(wcharLine, 0, '\t') == "orange and black");
987     assert(getTsvFieldValue!string(wcharLine, 1, '\t') == "ნარინჯისფერი და შავი");
988     assert(getTsvFieldValue!wstring(wcharLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring);
989     assert(getTsvFieldValue!double(wcharLine, 2, '\t') == 88.5);
990 
991     /* Conversion errors. */
992     assertThrown!ConvException(getTsvFieldValue!double("", 0, '\t'));
993     assertThrown!ConvException(getTsvFieldValue!double("abc", 0, '|'));
994     assertThrown!ConvException(getTsvFieldValue!size_t("-1", 0, '|'));
995     assertThrown!ConvException(getTsvFieldValue!size_t("a23|23.4", 1, '|'));
996     assertThrown!ConvException(getTsvFieldValue!double("23.5|def", 1, '|'));
997 
998     /* Not enough field errors. These should throw, but not a ConvException.*/
999     assertThrown(assertNotThrown!ConvException(getTsvFieldValue!double("", 1, '\t')));
1000     assertThrown(assertNotThrown!ConvException(getTsvFieldValue!double("abc", 1, '\t')));
1001     assertThrown(assertNotThrown!ConvException(getTsvFieldValue!double("abc\tdef", 2, '\t')));
1002 }
1003 
1004 /**
1005 Field-lists - A field-list is a string entered on the command line identifying one or more
1006 field numbers. They are used by the majority of the tsv utility applications. There are
1007 two helper functions, makeFieldListOptionHandler and parseFieldList. Most applications
1008 will use makeFieldListOptionHandler, it creates a delegate that can be passed to
1009 std.getopt to process the command option. Actual processing of the option text is done by
1010 parseFieldList. It can be called directly when the text of the option value contains more
1011 than just the field number.
1012 
1013 Syntax and behavior:
1014 
1015 A 'field-list' is a list of numeric field numbers entered on the command line. Fields are
1016 1-upped integers representing locations in an input line, in the traditional meaning of
1017 Unix command line tools. Fields can be entered as single numbers or a range. Multiple
1018 entries are separated by commas. Some examples (with 'fields' as the command line option):
1019 
1020    --fields 3                 // Single field
1021    --fields 4,1               // Two fields
1022    --fields 3-9               // A range, fields 3 to 9 inclusive
1023    --fields 1,2,7-34,11       // A mix of ranges and fields
1024    --fields 15-5,3-1          // Two ranges in reverse order.
1025 
1026 Incomplete ranges are not supported, for example, '6-'. Zero is disallowed as a field
1027 value by default, but can be enabled to support the notion of zero as representing the
1028 entire line. However, zero cannot be part of a range. Field numbers are one-based by
1029 default, but can be converted to zero-based. If conversion to zero-based is enabled, field
1030 number zero must be disallowed or a signed integer type specified for the returned range.
1031 
1032 An error is thrown if an invalid field specification is encountered. Error text is
1033 intended for display. Error conditions include:
1034   - Empty fields list
1035   - Empty value, e.g. Two consequtive commas, a trailing comma, or a leading comma
1036   - String that does not parse as a valid integer
1037   - Negative integers, or zero if zero is disallowed.
1038   - An incomplete range
1039   - Zero used as part of a range.
1040 
1041 No other behaviors are enforced. Repeated values are accepted. If zero is allowed, other
1042 field numbers can be entered as well. Additional restrictions need to be applied by the
1043 caller.
1044 
1045 Notes:
1046   - The data type determines the max field number that can be entered. Enabling conversion
1047     to zero restricts to the signed version of the data type.
1048   - Use 'import std.typecons : Yes, No' to use the convertToZeroBasedIndex and
1049     allowFieldNumZero template parameters.
1050 */
1051 
1052 /** [Yes|No].convertToZeroBasedIndex parameter controls whether field numbers are
1053  *  converted to zero-based indices by makeFieldListOptionHander and parseFieldList.
1054  */
1055 alias ConvertToZeroBasedIndex = Flag!"convertToZeroBasedIndex";
1056 
1057 /** [Yes|No].allowFieldNumZero parameter controls whether zero is a valid field. This is
1058  *  used by makeFieldListOptionHander and parseFieldList.
1059  */
1060 alias AllowFieldNumZero = Flag!"allowFieldNumZero";
1061 
1062 alias OptionHandlerDelegate = void delegate(string option, string value);
1063 
1064 /**
1065 makeFieldListOptionHandler creates a std.getopt option hander for processing field lists
1066 entered on the command line. A field list is as defined by parseFieldList.
1067 */
1068 OptionHandlerDelegate makeFieldListOptionHandler(
1069                                                  T,
1070                                                  ConvertToZeroBasedIndex convertToZero = No.convertToZeroBasedIndex,
1071                                                  AllowFieldNumZero allowZero = No.allowFieldNumZero)
1072     (ref T[] fieldsArray)
1073 if (isIntegral!T && (!allowZero || !convertToZero || !isUnsigned!T))
1074 {
1075     void fieldListOptionHandler(ref T[] fieldArray, string option, string value)
1076     {
1077         import std.algorithm : each;
1078         try value.parseFieldList!(T, convertToZero, allowZero).each!(x => fieldArray ~= x);
1079         catch (Exception exc)
1080         {
1081             import std.format : format;
1082             exc.msg = format("[--%s] %s", option, exc.msg);
1083             throw exc;
1084         }
1085     }
1086 
1087     return (option, value) => fieldListOptionHandler(fieldsArray, option, value);
1088 }
1089 
1090 unittest
1091 {
1092     import std.exception : assertThrown, assertNotThrown;
1093     import std.getopt;
1094 
1095     {
1096         size_t[] fields;
1097         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1098         getopt(args, "f|fields", fields.makeFieldListOptionHandler);
1099         assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]);
1100     }
1101     {
1102         size_t[] fields;
1103         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1104         getopt(args,
1105                "f|fields", fields.makeFieldListOptionHandler!(size_t, Yes.convertToZeroBasedIndex));
1106         assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]);
1107     }
1108     {
1109         size_t[] fields;
1110         auto args = ["program", "-f", "0"];
1111         getopt(args,
1112                "f|fields", fields.makeFieldListOptionHandler!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1113         assert(fields == [0]);
1114     }
1115     {
1116         size_t[] fields;
1117         auto args = ["program", "-f", "0", "-f", "1,0", "-f", "0,1"];
1118         getopt(args,
1119                "f|fields", fields.makeFieldListOptionHandler!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1120         assert(fields == [0, 1, 0, 0, 1]);
1121     }
1122     {
1123         size_t[] ints;
1124         size_t[] fields;
1125         auto args = ["program", "--ints", "1,2,3", "--fields", "1", "--ints", "4,5,6", "--fields", "2,4,7-9,23-21"];
1126         std.getopt.arraySep = ",";
1127         getopt(args,
1128                "i|ints", "Built-in list of integers.", &ints,
1129                "f|fields", "Field-list style integers.", fields.makeFieldListOptionHandler);
1130         assert(ints == [1, 2, 3, 4, 5, 6]);
1131         assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]);
1132     }
1133 
1134     /* Basic cases involved unsinged types smaller than size_t. */
1135     {
1136         uint[] fields;
1137         auto args = ["program", "-f", "0", "-f", "1,0", "-f", "0,1", "-f", "55-58"];
1138         getopt(args,
1139                "f|fields", fields.makeFieldListOptionHandler!(uint, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1140         assert(fields == [0, 1, 0, 0, 1, 55, 56, 57, 58]);
1141     }
1142     {
1143         ushort[] fields;
1144         auto args = ["program", "-f", "0", "-f", "1,0", "-f", "0,1", "-f", "55-58"];
1145         getopt(args,
1146                "f|fields", fields.makeFieldListOptionHandler!(ushort, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1147         assert(fields == [0, 1, 0, 0, 1, 55, 56, 57, 58]);
1148     }
1149 
1150     /* Basic cases involving unsigned types. */
1151     {
1152         long[] fields;
1153         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1154         getopt(args, "f|fields", fields.makeFieldListOptionHandler);
1155         assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]);
1156     }
1157     {
1158         long[] fields;
1159         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1160         getopt(args,
1161                "f|fields", fields.makeFieldListOptionHandler!(long, Yes.convertToZeroBasedIndex));
1162         assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]);
1163     }
1164     {
1165         long[] fields;
1166         auto args = ["program", "-f", "0"];
1167         getopt(args,
1168                "f|fields", fields.makeFieldListOptionHandler!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1169         assert(fields == [-1]);
1170     }
1171     {
1172         int[] fields;
1173         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1174         getopt(args, "f|fields", fields.makeFieldListOptionHandler);
1175         assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]);
1176     }
1177     {
1178         int[] fields;
1179         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1180         getopt(args,
1181                "f|fields", fields.makeFieldListOptionHandler!(int, Yes.convertToZeroBasedIndex));
1182         assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]);
1183     }
1184     {
1185         int[] fields;
1186         auto args = ["program", "-f", "0"];
1187         getopt(args,
1188                "f|fields", fields.makeFieldListOptionHandler!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1189         assert(fields == [-1]);
1190     }
1191     {
1192         short[] fields;
1193         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1194         getopt(args, "f|fields", fields.makeFieldListOptionHandler);
1195         assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]);
1196     }
1197     {
1198         short[] fields;
1199         auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"];
1200         getopt(args,
1201                "f|fields", fields.makeFieldListOptionHandler!(short, Yes.convertToZeroBasedIndex));
1202         assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]);
1203     }
1204     {
1205         short[] fields;
1206         auto args = ["program", "-f", "0"];
1207         getopt(args,
1208                "f|fields", fields.makeFieldListOptionHandler!(short, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1209         assert(fields == [-1]);
1210     }
1211 
1212     {
1213         /* Error cases. */
1214         size_t[] fields;
1215         auto args = ["program", "-f", "0"];
1216         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1217 
1218         args = ["program", "-f", "-1"];
1219         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1220 
1221         args = ["program", "-f", "--fields", "1"];
1222         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1223 
1224         args = ["program", "-f", "a"];
1225         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1226 
1227         args = ["program", "-f", "1.5"];
1228         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1229 
1230         args = ["program", "-f", "2-"];
1231         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1232 
1233         args = ["program", "-f", "3,5,-7"];
1234         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1235 
1236         args = ["program", "-f", "3,5,"];
1237         assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler));
1238 
1239         args = ["program", "-f", "-1"];
1240         assertThrown(getopt(args,
1241                             "f|fields", fields.makeFieldListOptionHandler!(
1242                                 size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)));
1243     }
1244 }
1245 
1246 /**
1247 parseFieldList lazily generates a range of fields numbers from a 'field-list' string.
1248 */
1249 auto parseFieldList(T = size_t,
1250                     ConvertToZeroBasedIndex convertToZero = No.convertToZeroBasedIndex,
1251                     AllowFieldNumZero allowZero = No.allowFieldNumZero)
1252     (string fieldList, char delim = ',')
1253 if (isIntegral!T && (!allowZero || !convertToZero || !isUnsigned!T))
1254 {
1255     import std.algorithm : splitter;
1256 
1257     auto _splitFieldList = fieldList.splitter(delim);
1258     auto _currFieldParse =
1259         (_splitFieldList.empty ? "" : _splitFieldList.front)
1260         .parseFieldRange!(T, convertToZero, allowZero);
1261 
1262     if (!_splitFieldList.empty) _splitFieldList.popFront;
1263 
1264     struct Result
1265     {
1266         @property bool empty() { return _currFieldParse.empty; }
1267 
1268         @property T front()
1269         {
1270             import std.conv : to;
1271 
1272             assert(!empty, "Attempting to fetch the front of an empty field-list.");
1273             assert(!_currFieldParse.empty, "Internal error. Call to front with an empty _currFieldParse.");
1274 
1275             return _currFieldParse.front.to!T;
1276         }
1277 
1278         void popFront()
1279         {
1280             assert(!empty, "Attempting to popFront an empty field-list.");
1281 
1282             _currFieldParse.popFront;
1283             if (_currFieldParse.empty && !_splitFieldList.empty)
1284             {
1285                 _currFieldParse = _splitFieldList.front.parseFieldRange!(T, convertToZero, allowZero);
1286                 _splitFieldList.popFront;
1287             }
1288         }
1289     }
1290 
1291     return Result();
1292 }
1293 
1294 unittest
1295 {
1296     import std.algorithm : each, equal;
1297     import std.exception : assertThrown, assertNotThrown;
1298 
1299     /* Basic tests. */
1300     assert("1".parseFieldList.equal([1]));
1301     assert("1,2".parseFieldList.equal([1, 2]));
1302     assert("1,2,3".parseFieldList.equal([1, 2, 3]));
1303     assert("1-2".parseFieldList.equal([1, 2]));
1304     assert("1-2,6-4".parseFieldList.equal([1, 2, 6, 5, 4]));
1305     assert("1-2,1,1-2,2,2-1".parseFieldList.equal([1, 2, 1, 1, 2, 2, 2, 1]));
1306     assert("1-2,5".parseFieldList!size_t.equal([1, 2, 5]));
1307 
1308     /* Signed Int tests */
1309     assert("1".parseFieldList!int.equal([1]));
1310     assert("1,2,3".parseFieldList!int.equal([1, 2, 3]));
1311     assert("1-2".parseFieldList!int.equal([1, 2]));
1312     assert("1-2,6-4".parseFieldList!int.equal([1, 2, 6, 5, 4]));
1313     assert("1-2,5".parseFieldList!int.equal([1, 2, 5]));
1314 
1315     /* Convert to zero tests */
1316     assert("1".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0]));
1317     assert("1,2,3".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1, 2]));
1318     assert("1-2".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1]));
1319     assert("1-2,6-4".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1, 5, 4, 3]));
1320     assert("1-2,5".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1, 4]));
1321 
1322     assert("1".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0]));
1323     assert("1,2,3".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1, 2]));
1324     assert("1-2".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1]));
1325     assert("1-2,6-4".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1, 5, 4, 3]));
1326     assert("1-2,5".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1, 4]));
1327 
1328     /* Allow zero tests. */
1329     assert("0".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1330     assert("1,0,3".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 0, 3]));
1331     assert("1-2,5".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 2, 5]));
1332     assert("0".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1333     assert("1,0,3".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 0, 3]));
1334     assert("1-2,5".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 2, 5]));
1335     assert("0".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1]));
1336     assert("1,0,3".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0, -1, 2]));
1337     assert("1-2,5".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0, 1, 4]));
1338 
1339     /* Error cases. */
1340     assertThrown("".parseFieldList.each);
1341     assertThrown(" ".parseFieldList.each);
1342     assertThrown(",".parseFieldList.each);
1343     assertThrown("5 6".parseFieldList.each);
1344     assertThrown(",7".parseFieldList.each);
1345     assertThrown("8,".parseFieldList.each);
1346     assertThrown("8,9,".parseFieldList.each);
1347     assertThrown("10,,11".parseFieldList.each);
1348     assertThrown("".parseFieldList!(long, Yes.convertToZeroBasedIndex).each);
1349     assertThrown("1,2-3,".parseFieldList!(long, Yes.convertToZeroBasedIndex).each);
1350     assertThrown("2-,4".parseFieldList!(long, Yes.convertToZeroBasedIndex).each);
1351     assertThrown("1,2,3,,4".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1352     assertThrown(",7".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1353     assertThrown("8,".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1354     assertThrown("10,0,,11".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1355     assertThrown("8,9,".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1356 
1357     assertThrown("0".parseFieldList.each);
1358     assertThrown("1,0,3".parseFieldList.each);
1359     assertThrown("0".parseFieldList!(int, Yes.convertToZeroBasedIndex, No.allowFieldNumZero).each);
1360     assertThrown("1,0,3".parseFieldList!(int, Yes.convertToZeroBasedIndex, No.allowFieldNumZero).each);
1361     assertThrown("0-2,6-0".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1362     assertThrown("0-2,6-0".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1363     assertThrown("0-2,6-0".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each);
1364 }
1365 
1366 /* parseFieldRange parses a single number or number range. E.g. '5' or '5-8'. These are
1367  * the values in a field-list separated by a comma or other delimiter. It returns a range
1368  * that iterates over all the values in the range.
1369  */
1370 private auto parseFieldRange(T = size_t,
1371                              ConvertToZeroBasedIndex convertToZero = No.convertToZeroBasedIndex,
1372                              AllowFieldNumZero allowZero = No.allowFieldNumZero)
1373     (string fieldRange)
1374 if (isIntegral!T && (!allowZero || !convertToZero || !isUnsigned!T))
1375 {
1376     import std.algorithm : findSplit;
1377     import std.conv : to;
1378     import std.format : format;
1379     import std.range : iota;
1380     import std.traits : Signed;
1381 
1382     /* Pick the largest compatible integral type for the IOTA range. This must be the
1383      * signed type if convertToZero is true, as a reverse order range may end at -1.
1384      */
1385     static if (convertToZero) alias S = Signed!T;
1386     else alias S = T;
1387 
1388     if (fieldRange.length == 0) throw new Exception("Empty field number.");
1389 
1390     auto rangeSplit = findSplit(fieldRange, "-");
1391 
1392     if (!rangeSplit[1].empty && (rangeSplit[0].empty || rangeSplit[2].empty))
1393     {
1394         // Range starts or ends with a dash.
1395         throw new Exception(format("Incomplete ranges are not supported: '%s'", fieldRange));
1396     }
1397 
1398     S start = rangeSplit[0].to!S;
1399     S last = rangeSplit[1].empty ? start : rangeSplit[2].to!S;
1400     Signed!T increment = (start <= last) ? 1 : -1;
1401 
1402     static if (allowZero)
1403     {
1404         if (start == 0 && !rangeSplit[1].empty)
1405         {
1406             throw new Exception(format("Zero cannot be used as part of a range: '%s'", fieldRange));
1407         }
1408     }
1409 
1410     static if (allowZero)
1411     {
1412         if (start < 0 || last < 0)
1413         {
1414             throw new Exception(format("Field numbers must be non-negative integers: '%d'",
1415                                        (start < 0) ? start : last));
1416         }
1417     }
1418     else
1419     {
1420         if (start < 1 || last < 1)
1421         {
1422             throw new Exception(format("Field numbers must be greater than zero: '%d'",
1423                                        (start < 1) ? start : last));
1424         }
1425     }
1426 
1427     static if (convertToZero)
1428     {
1429         start--;
1430         last--;
1431     }
1432 
1433     return iota(start, last + increment, increment);
1434 }
1435 
1436 unittest // parseFieldRange
1437 {
1438     import std.algorithm : equal;
1439     import std.exception : assertThrown, assertNotThrown;
1440 
1441     /* Basic cases */
1442     assert(parseFieldRange("1").equal([1]));
1443     assert("2".parseFieldRange.equal([2]));
1444     assert("3-4".parseFieldRange.equal([3, 4]));
1445     assert("3-5".parseFieldRange.equal([3, 4, 5]));
1446     assert("4-3".parseFieldRange.equal([4, 3]));
1447     assert("10-1".parseFieldRange.equal([10,  9, 8, 7, 6, 5, 4, 3, 2, 1]));
1448 
1449     /* Convert to zero-based indices */
1450     assert(parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)("1").equal([0]));
1451     assert("2".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([1]));
1452     assert("3-4".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([2, 3]));
1453     assert("3-5".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([2, 3, 4]));
1454     assert("4-3".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([3, 2]));
1455     assert("10-1".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]));
1456 
1457     /* Allow zero. */
1458     assert("0".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1459     assert(parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)("1").equal([1]));
1460     assert("3-4".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([3, 4]));
1461     assert("10-1".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([10,  9, 8, 7, 6, 5, 4, 3, 2, 1]));
1462 
1463     /* Allow zero, convert to zero-based index. */
1464     assert("0".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1]));
1465     assert(parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)("1").equal([0]));
1466     assert("3-4".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([2, 3]));
1467     assert("10-1".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]));
1468 
1469     /* Alternate integer types. */
1470     assert("2".parseFieldRange!uint.equal([2]));
1471     assert("3-5".parseFieldRange!uint.equal([3, 4, 5]));
1472     assert("10-1".parseFieldRange!uint.equal([10,  9, 8, 7, 6, 5, 4, 3, 2, 1]));
1473     assert("2".parseFieldRange!int.equal([2]));
1474     assert("3-5".parseFieldRange!int.equal([3, 4, 5]));
1475     assert("10-1".parseFieldRange!int.equal([10,  9, 8, 7, 6, 5, 4, 3, 2, 1]));
1476     assert("2".parseFieldRange!ushort.equal([2]));
1477     assert("3-5".parseFieldRange!ushort.equal([3, 4, 5]));
1478     assert("10-1".parseFieldRange!ushort.equal([10,  9, 8, 7, 6, 5, 4, 3, 2, 1]));
1479     assert("2".parseFieldRange!short.equal([2]));
1480     assert("3-5".parseFieldRange!short.equal([3, 4, 5]));
1481     assert("10-1".parseFieldRange!short.equal([10,  9, 8, 7, 6, 5, 4, 3, 2, 1]));
1482 
1483     assert("0".parseFieldRange!(long, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1484     assert("0".parseFieldRange!(uint, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1485     assert("0".parseFieldRange!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1486     assert("0".parseFieldRange!(ushort, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1487     assert("0".parseFieldRange!(short, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0]));
1488     assert("0".parseFieldRange!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1]));
1489     assert("0".parseFieldRange!(short, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1]));
1490 
1491     /* Max field value cases. */
1492     assert("65535".parseFieldRange!ushort.equal([65535]));   // ushort max
1493     assert("65533-65535".parseFieldRange!ushort.equal([65533, 65534, 65535]));
1494     assert("32767".parseFieldRange!short.equal([32767]));    // short max
1495     assert("32765-32767".parseFieldRange!short.equal([32765, 32766, 32767]));
1496     assert("32767".parseFieldRange!(short, Yes.convertToZeroBasedIndex).equal([32766]));
1497 
1498     /* Error cases. */
1499     assertThrown("".parseFieldRange);
1500     assertThrown(" ".parseFieldRange);
1501     assertThrown("-".parseFieldRange);
1502     assertThrown(" -".parseFieldRange);
1503     assertThrown("- ".parseFieldRange);
1504     assertThrown("1-".parseFieldRange);
1505     assertThrown("-2".parseFieldRange);
1506     assertThrown("-1".parseFieldRange);
1507     assertThrown("1.0".parseFieldRange);
1508     assertThrown("0".parseFieldRange);
1509     assertThrown("0-3".parseFieldRange);
1510     assertThrown("-2-4".parseFieldRange);
1511     assertThrown("2--4".parseFieldRange);
1512     assertThrown("2-".parseFieldRange);
1513     assertThrown("a".parseFieldRange);
1514     assertThrown("0x3".parseFieldRange);
1515     assertThrown("3U".parseFieldRange);
1516     assertThrown("1_000".parseFieldRange);
1517     assertThrown(".".parseFieldRange);
1518 
1519     assertThrown("".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1520     assertThrown(" ".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1521     assertThrown("-".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1522     assertThrown("1-".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1523     assertThrown("-2".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1524     assertThrown("-1".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1525     assertThrown("0".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1526     assertThrown("0-3".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1527     assertThrown("-2-4".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1528     assertThrown("2--4".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex));
1529 
1530     assertThrown("".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1531     assertThrown(" ".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1532     assertThrown("-".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1533     assertThrown("1-".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1534     assertThrown("-2".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1535     assertThrown("-1".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1536     assertThrown("0-3".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1537     assertThrown("-2-4".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1538 
1539     assertThrown("".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1540     assertThrown(" ".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1541     assertThrown("-".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1542     assertThrown("1-".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1543     assertThrown("-2".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1544     assertThrown("-1".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1545     assertThrown("0-3".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1546     assertThrown("-2-4".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero));
1547 
1548     /* Value out of range cases. */
1549     assertThrown("65536".parseFieldRange!ushort);   // One more than ushort max.
1550     assertThrown("65535-65536".parseFieldRange!ushort);
1551     assertThrown("32768".parseFieldRange!short);    // One more than short max.
1552     assertThrown("32765-32768".parseFieldRange!short);
1553     // Convert to zero limits signed range.
1554     assertThrown("32768".parseFieldRange!(ushort, Yes.convertToZeroBasedIndex));
1555     assert("32767".parseFieldRange!(ushort, Yes.convertToZeroBasedIndex).equal([32766]));
1556 }
1557 
1558 /** [Yes|No.newlineWasRemoved] is a template parameter to throwIfWindowsNewlineOnUnix.
1559  *  A Yes value indicates the Unix newline was already removed, as might be done via
1560  *  std.File.byLine or similar mechanism.
1561  */
1562 alias NewlineWasRemoved = Flag!"newlineWasRemoved";
1563 
1564 /**
1565 throwIfWindowsLineNewlineOnUnix is used to throw an exception if a Windows/DOS
1566 line ending is found on a build compiled for a Unix platform. This is used by
1567 the TSV Utilities to detect Window/DOS line endings and terminate processing
1568 with an error message to the user.
1569  */
1570 void throwIfWindowsNewlineOnUnix
1571     (NewlineWasRemoved nlWasRemoved = Yes.newlineWasRemoved)
1572     (const char[] line, const char[] filename, size_t lineNum)
1573 {
1574     version(Posix)
1575     {
1576         static if (nlWasRemoved)
1577         {
1578             bool hasWindowsLineEnding = line.length != 0 && line[$ - 1] == '\r';
1579         }
1580         else
1581         {
1582             bool hasWindowsLineEnding =
1583                 line.length > 1 &&
1584                 line[$ - 2] == '\r' &&
1585                 line[$ - 1] == '\n';
1586         }
1587 
1588         if (hasWindowsLineEnding)
1589         {
1590             import std.format;
1591             throw new Exception(
1592                 format("Windows/DOS line ending found. Convert file to Unix newlines before processing (e.g. 'dos2unix').\n  File: %s, Line: %s",
1593                        (filename == "-") ? "Standard Input" : filename, lineNum));
1594         }
1595     }
1596 }
1597 
1598 unittest
1599 {
1600     /* Note: Currently only building on Posix. Need to add non-Posix test cases
1601      * if Windows builds are ever done.
1602      */
1603     version(Posix)
1604     {
1605         import std.exception;
1606 
1607         assertNotThrown(throwIfWindowsNewlineOnUnix("", "afile.tsv", 1));
1608         assertNotThrown(throwIfWindowsNewlineOnUnix("a", "afile.tsv", 2));
1609         assertNotThrown(throwIfWindowsNewlineOnUnix("ab", "afile.tsv", 3));
1610         assertNotThrown(throwIfWindowsNewlineOnUnix("abc", "afile.tsv", 4));
1611 
1612         assertThrown(throwIfWindowsNewlineOnUnix("\r", "afile.tsv", 1));
1613         assertThrown(throwIfWindowsNewlineOnUnix("a\r", "afile.tsv", 2));
1614         assertThrown(throwIfWindowsNewlineOnUnix("ab\r", "afile.tsv", 3));
1615         assertThrown(throwIfWindowsNewlineOnUnix("abc\r", "afile.tsv", 4));
1616 
1617         assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("\n", "afile.tsv", 1));
1618         assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("a\n", "afile.tsv", 2));
1619         assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("ab\n", "afile.tsv", 3));
1620         assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("abc\n", "afile.tsv", 4));
1621 
1622         assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("\r\n", "afile.tsv", 5));
1623         assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("a\r\n", "afile.tsv", 6));
1624         assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("ab\r\n", "afile.tsv", 7));
1625         assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("abc\r\n", "afile.tsv", 8));
1626 
1627         /* Standard Input formatting. */
1628         import std.algorithm : endsWith;
1629         bool exceptionCaught = false;
1630 
1631         try (throwIfWindowsNewlineOnUnix("\r", "-", 99));
1632         catch (Exception e)
1633         {
1634             assert(e.msg.endsWith("File: Standard Input, Line: 99"));
1635             exceptionCaught = true;
1636         }
1637         finally
1638         {
1639             assert(exceptionCaught);
1640             exceptionCaught = false;
1641         }
1642 
1643         try (throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("\r\n", "-", 99));
1644         catch (Exception e)
1645         {
1646             assert(e.msg.endsWith("File: Standard Input, Line: 99"));
1647             exceptionCaught = true;
1648         }
1649         finally
1650         {
1651             assert(exceptionCaught);
1652             exceptionCaught = false;
1653         }
1654     }
1655 }