generated from parrogo/gomod
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhorzcat.go
174 lines (154 loc) · 4.09 KB
/
horzcat.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// Package horzcat concatenates multiple files in horizontal direction.
// To clarify, consider the following comparison between cat and horzcat of two files:
// $ cat a.dat 1.dat
// a b
// c d
// 1 2
// 3 4
// $ horzcat -s ' ' a.dat 1.dat
// a b 1 2
// c d 3 4
package horzcat
import (
"bufio"
"errors"
"fmt"
"io"
"os"
)
// Options struct groups all options
// accepted by Concat.
//
// Target field contains the io.Writer
// on which to write concateneted lines.
// When it's nil, os.Stdout is used as writer.
//
// Sep field is a string used to separate
// lines from source readers.
//
// Tail field is a string appended
// at the end of every concatenated line.
//
// SameLinesCount field, when set
// to true, requires that every source
// has the exact numer of lines.
// If one or more of the readers has a different
// lines count, an error is returned.
// When the field is false, excess lines from
// one or more reader are still concatened and written
// to output. Sep string is added alone for each of the
// readers that miss one or more lines.
//
// RowHeaderLen could be set to the lenght,
// in bytes, of the row header.
// If > 0, RowHeaderLen bytes will be skipped
// from each line of each source reader except the first.
// When first reader have less lines than the others,
// row header is always kept for the first
// reader that has lines.
type Options struct {
Target io.Writer
Sep string
Tail string
SameLinesCount bool
RowHeaderLen int
}
// Concat read lines from all io.Reader in sources,
// concatenetes line by line horizontally, and finally writes
// concateneted lines to options.Target argument.
// If options.Target is nil, Concat writes to os.Stdout.
// String opt.Sep is added between lines
// String opt.Tail is added at the end of every written line.
func Concat(opt Options, sources ...io.Reader) error {
if len(sources) == 0 {
return errors.New("no source readers provided")
}
var out *bufio.Writer
if opt.Target != nil {
out = bufio.NewWriter(opt.Target)
} else {
out = bufio.NewWriter(os.Stdout)
}
bufreaders := make([]*bufio.Scanner, len(sources))
for idx, source := range sources {
lineScan := bufio.NewScanner(source)
buf := make([]byte, 0, 10*1024*1024)
lineScan.Buffer(buf, 10*1024*1024)
bufreaders[idx] = lineScan
}
lines := make([][]byte, len(sources))
empty := make([]byte, 0)
outWriteErr := func(err error) error {
return fmt.Errorf("cannot write to output: %w", err)
}
for {
allSourcesDone := true
for idx, source := range bufreaders {
if source.Scan() {
allSourcesDone = false
lines[idx] = source.Bytes()
} else {
lines[idx] = empty
}
}
if allSourcesDone {
break
}
firstReaderWritten := false
for _, line := range lines {
if len(line) == 0 {
continue
}
if firstReaderWritten {
_, err := out.WriteString(opt.Sep)
if err != nil {
return outWriteErr(err)
}
if opt.RowHeaderLen > 0 && len(line) >= opt.RowHeaderLen {
// strip column header from readers
// after the first.
line = line[opt.RowHeaderLen:]
}
}
_, err := out.Write(line)
if err != nil {
return outWriteErr(err)
}
firstReaderWritten = true
}
_, err := out.WriteString(opt.Tail + "\n")
if err != nil {
return outWriteErr(err)
}
}
err := out.Flush()
if err != nil {
return fmt.Errorf("cannot flush output: %w", err)
}
for idx, source := range bufreaders {
if err := source.Err(); err != nil {
return fmt.Errorf("error reading from source %d: %w", idx, InputError{err, idx})
}
}
return nil
}
// InputError wraps an error
// in order to include the position
// of failing stream.
type InputError struct {
err error
idx int
}
// Error implements error interface
func (e InputError) Error() string {
return e.err.Error()
}
// Unwrap returns the wrapped error
func (e InputError) Unwrap() error {
return e.err
}
// Convert returns an error that include the
// name of the file that causes the failure
func (e InputError) Convert(filenames []string) error {
return fmt.Errorf("Cannot read file %s: %w", filenames[e.idx], e.err)
}