-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.go
More file actions
340 lines (315 loc) · 8.16 KB
/
Copy pathmain.go
File metadata and controls
340 lines (315 loc) · 8.16 KB
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
)
// Config holds the application's configuration flags
type Config struct {
QuietMode bool
DryRun bool
TrimWhitespace bool
IgnoreCase bool
IgnoreBlank bool
ShowCounts bool
InputFilename string
OutputFilename string
BackupSuffix string
DoBackup bool
}
// Stats holds runtime statistics
type Stats struct {
LinesRead int
DuplicatesFound int
BlankLinesSkipped int
NewLinesOutput int // To stdout or file
LinesWritten int // Specifically to file
}
// normalizeLine applies configured normalization (trimming, case)
func normalizeLine(line string, cfg *Config) string {
if cfg.TrimWhitespace {
line = strings.TrimSpace(line)
}
if cfg.IgnoreCase {
line = strings.ToLower(line)
}
return line
}
// backupFile creates a backup of the source file if needed
func backupFile(filename, suffix string) error {
if _, err := os.Stat(filename); err != nil {
// If file doesn't exist, no need to backup
if errors.Is(err, os.ErrNotExist) {
return nil
}
// Other stat error
return fmt.Errorf(
"could not stat file for backup %q: %w",
filename,
err,
)
}
backupName := filename + suffix
// Simple approach: copy content. Rename could be faster but riskier on failure.
sourceFile, err := os.Open(filename)
if err != nil {
return fmt.Errorf(
"failed to open source file for backup %q: %w",
filename,
err,
)
}
defer sourceFile.Close()
destFile, err := os.Create(backupName)
if err != nil {
return fmt.Errorf(
"failed to create backup file %q: %w",
backupName,
err,
)
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return fmt.Errorf(
"failed to copy content to backup file %q: %w",
backupName,
err,
)
}
fmt.Fprintf(os.Stderr, "Backed up %q to %q\n", filename, backupName)
return nil
}
func main() {
cfg := Config{}
stats := Stats{}
// --- Configuration Flags ---
flag.BoolVar(
&cfg.QuietMode,
"q",
false,
"Quiet mode (no stdout output except errors)",
)
flag.BoolVar(
&cfg.DryRun,
"d",
false,
"Dry run (don't write to output file)",
)
flag.BoolVar(
&cfg.TrimWhitespace,
"t",
false,
"Trim leading/trailing whitespace before comparison",
)
flag.BoolVar(&cfg.IgnoreCase, "i", false, "Ignore case during comparison")
flag.BoolVar(&cfg.IgnoreBlank, "B", false, "Ignore blank lines from stdin")
flag.BoolVar(
&cfg.ShowCounts,
"c",
false,
"Show counts of lines processed at the end (to stderr)",
)
flag.StringVar(
&cfg.OutputFilename,
"o",
"",
"Output file to append unique lines (default: use input file)",
)
// Backup flag needs custom handling because of optional value
backupFlag := flag.String(
"backup",
"",
"Create backup of input file (if also output file) with optional SUFFIX (default: .bak)",
)
flag.Usage = func() {
fmt.Fprintf(
os.Stderr,
"Usage: %s [options] [input_filename]\n\n",
os.Args[0],
)
fmt.Fprintf(
os.Stderr,
"Appends unique lines from stdin to input_filename (or -o file).\n",
)
fmt.Fprintf(
os.Stderr,
"Reads existing lines from input_filename to check for uniqueness.\n\nOptions:\n",
)
flag.PrintDefaults()
}
flag.Parse()
// Handle backup flag presence and optional value
if *backupFlag != "" {
cfg.DoBackup = true
cfg.BackupSuffix = *backupFlag
} else {
// Check if the flag was set without a value (e.g., --backup)
// This is a bit hacky, relies on inspecting os.Args
for _, arg := range os.Args[1:] {
if arg == "--backup" || arg == "-backup" { // Check common forms
cfg.DoBackup = true
cfg.BackupSuffix = ".bak" // Default suffix
break
}
}
}
if flag.NArg() > 1 {
fmt.Fprintf(os.Stderr, "Error: Too many filename arguments.\n")
flag.Usage()
os.Exit(1)
}
cfg.InputFilename = flag.Arg(0)
// Determine the actual target file for writing
targetFilename := cfg.OutputFilename
if targetFilename == "" {
targetFilename = cfg.InputFilename // Default to writing back to input file
}
// --- Handle Backup ---
// Backup the input file *only* if we intend to write back to it and backup is requested.
if cfg.DoBackup && cfg.InputFilename != "" &&
targetFilename == cfg.InputFilename {
if err := backupFile(cfg.InputFilename, cfg.BackupSuffix); err != nil {
fmt.Fprintf(os.Stderr, "Error creating backup: %v\n", err)
os.Exit(1)
}
}
// --- Read Existing Lines (from InputFilename) ---
existingLines := make(map[string]bool)
if cfg.InputFilename != "" {
file, err := os.Open(cfg.InputFilename)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
// Report errors other than "file not found"
fmt.Fprintf(
os.Stderr,
"Warning: could not open input file %q for reading: %v\n",
cfg.InputFilename,
err,
)
}
// Continue, existingLines will be empty
} else {
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
normalized := normalizeLine(scanner.Text(), &cfg)
// Don't add empty normalized lines to the existing set if IgnoreBlank is true,
// otherwise blank lines in the file would prevent adding blank lines from stdin.
if normalized != "" || !cfg.IgnoreBlank {
existingLines[normalized] = true
}
}
if err := scanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading input file %q: %v\n", cfg.InputFilename, err)
// Decide whether to exit or continue with a potentially incomplete set
// os.Exit(1)
}
}
}
// --- Setup Output Writer ---
var outputFile *os.File
var outputWriter *bufio.Writer
var err error
// Only open for writing if not dryRun AND a target file is specified
if !cfg.DryRun && targetFilename != "" {
// Use os.O_CREATE so it works even if -o specifies a new file
outputFile, err = os.OpenFile(
targetFilename,
os.O_APPEND|os.O_WRONLY|os.O_CREATE,
0644,
)
if err != nil {
fmt.Fprintf(
os.Stderr,
"Error: failed to open output file %q for writing: %v\n",
targetFilename,
err,
)
os.Exit(1)
}
defer outputFile.Close()
outputWriter = bufio.NewWriter(outputFile)
defer outputWriter.Flush() // Ensure buffer is flushed on exit
}
// --- Process Stdin ---
stdinScanner := bufio.NewScanner(os.Stdin)
for stdinScanner.Scan() {
stats.LinesRead++
originalLine := stdinScanner.Text()
normalizedLine := normalizeLine(originalLine, &cfg)
// Handle blank lines from stdin
if cfg.IgnoreBlank && normalizedLine == "" {
stats.BlankLinesSkipped++
continue
}
// Check for duplicates
if existingLines[normalizedLine] {
stats.DuplicatesFound++
continue // Skip duplicate
}
// Mark as seen (handles duplicates within stdin itself)
existingLines[normalizedLine] = true
stats.NewLinesOutput++ // Counts lines intended for output (stdout or file)
// Output to stdout if not quiet
if !cfg.QuietMode {
fmt.Println(originalLine) // Print the original line
}
// Append to file if writer is configured
if outputWriter != nil { // Implies !DryRun and targetFilename != "" and OpenFile succeeded
_, err := fmt.Fprintln(
outputWriter,
originalLine,
) // Write the original line
if err != nil {
fmt.Fprintf(
os.Stderr,
"Error writing to output file %q: %v\n",
targetFilename,
err,
)
// Consider exiting or just reporting
// os.Exit(1)
} else {
stats.LinesWritten++
}
}
}
if err := stdinScanner.Err(); err != nil {
fmt.Fprintf(os.Stderr, "Error reading standard input: %v\n", err)
os.Exit(1)
}
// --- Report Counts ---
if cfg.ShowCounts {
fmt.Fprintf(os.Stderr, "--- Statistics ---\n")
fmt.Fprintf(os.Stderr, "Lines read from stdin: %d\n", stats.LinesRead)
if cfg.IgnoreBlank {
fmt.Fprintf(
os.Stderr,
"Blank lines skipped: %d\n",
stats.BlankLinesSkipped,
)
}
fmt.Fprintf(
os.Stderr,
"Duplicate lines found: %d\n",
stats.DuplicatesFound,
)
if cfg.DryRun {
fmt.Fprintf(
os.Stderr,
"New unique lines (dry run): %d\n",
stats.NewLinesOutput,
)
} else {
fmt.Fprintf(os.Stderr, "New unique lines output: %d\n", stats.NewLinesOutput)
if targetFilename != "" {
fmt.Fprintf(os.Stderr, "Lines appended to file: %d\n", stats.LinesWritten)
}
}
}
}