-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathargs.ts
More file actions
173 lines (158 loc) · 5.25 KB
/
Copy pathargs.ts
File metadata and controls
173 lines (158 loc) · 5.25 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
/**
* @module cli/args
*
* Command-line argument parsing and input resolution.
*
* The CLI accepts a positional path or explicit flags (`-t`, `-f`, `-d`).
* `resolveInputArgs` normalises these into a single internal representation
* and auto-detects files vs directories when a bare path is given.
*/
import { existsSync, statSync } from "node:fs"
import { assertDirectory, readTextFile } from "../fs/paths.js"
import type { Encoding } from "../types.js"
/** Parsed CLI flags (before input resolution). */
export interface Args {
text?: string
path?: string
file?: string
dir?: string
output?: string
encoding?: string
/** Max chars per part, or `0` to disable splitting (`--no-split` / `-s 0`). */
split?: number
password?: string
/** Force compress or decompress instead of auto-detecting from input. */
mode?: "compress" | "decompress"
}
/** Reject multiple simultaneous input sources. */
function assertSingleInput(args: Args) {
const sources = [args.text, args.path, args.file, args.dir].filter((value) => value !== undefined)
if (sources.length > 1) {
throw new Error("Multiple inputs specified. Pass one path, or use -t, -f, or -d.")
}
}
/**
* Parse `process.argv` tail into an {@link Args} object.
*
* Uses a simple sequential scan (not a general-purpose parser library)
* because the flag set is small and fixed.
*/
export function parseArgs(argv: string[]): Args {
const args: Args = {}
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
if (arg === "-t" || arg === "--text") {
args.text = argv[++i]
} else if (arg === "-f" || arg === "--file") {
args.file = argv[++i]
} else if (arg === "-d" || arg === "--dir") {
args.dir = argv[++i]
} else if (arg === "-o" || arg === "--output") {
args.output = argv[++i]
} else if (arg === "-e" || arg === "--encoding") {
args.encoding = argv[++i]
} else if (arg === "-s" || arg === "--split") {
const value = argv[++i]
const split = Number(value)
if (!value || !Number.isInteger(split) || split < 0) {
throw new Error(
`Invalid -s/--split "${value}". Use 0 to disable splitting, or a positive integer character count.`,
)
}
args.split = split
} else if (arg === "--no-split") {
args.split = 0
} else if (arg === "-p" || arg === "--password") {
const value = argv[++i]
if (!value) {
throw new Error("Missing value for -p/--password.")
}
args.password = value
} else if (arg === "-C" || arg === "--compress") {
args.mode = "compress"
} else if (arg === "-D" || arg === "--decompress") {
args.mode = "decompress"
} else if (!arg.startsWith("-")) {
if (args.path !== undefined || args.text !== undefined || args.file || args.dir) {
throw new Error("Multiple inputs specified. Pass one path, or use -t, -f, or -d.")
}
args.path = arg
}
}
return args
}
/**
* Normalise and validate input paths after parsing.
*
* Mutates `args` in place: e.g. a directory passed as `-f` becomes `args.dir`.
*/
export function resolveInputArgs(args: Args, command: "compress" | "decompress"): void {
assertSingleInput(args)
if (args.text !== undefined) return
const path = args.file ?? args.dir ?? args.path
if (!path) {
throw new Error("No input provided. Pass a path, or use -t <text>.")
}
if (args.dir) {
assertDirectory(args.dir)
return
}
if (args.file) {
if (existsSync(args.file)) {
const stat = statSync(args.file)
if (stat.isDirectory()) {
if (command === "decompress") {
throw new Error(
`"${args.file}" is a directory. Pass the compressed .txt file, not a decompressed output folder.`,
)
}
args.dir = args.file
args.file = undefined
}
}
return
}
if (!existsSync(path)) {
if (command === "compress") {
args.text = path
args.path = undefined
return
}
throw new Error(`Input not found: ${path}`)
}
const stat = statSync(path)
if (stat.isDirectory()) {
if (command === "decompress") {
throw new Error(
`"${path}" is a directory. Pass the compressed .txt file, not a decompressed output folder.`,
)
}
args.dir = path
args.path = undefined
return
}
if (stat.isFile()) {
args.file = path
args.path = undefined
return
}
throw new Error(`Cannot read "${path}": not a regular file or directory.`)
}
/** Read compress input from resolved args (file or inline text). */
export function readInput(args: Args): string {
if (args.file) return readTextFile(args.file, "compress")
if (args.text !== undefined) return args.text
throw new Error("No input provided. Pass a path, or use -t <text>.")
}
/** Parse `-e` encoding flag into the library's {@link Encoding} type. */
export function resolveEncoding(args: Args): Encoding {
if (!args.encoding) return 64
if (args.encoding === "64") return 64
if (args.encoding === "85") return 85
throw new Error(`Invalid -e/--encoding "${args.encoding}". Use 64 or 85.`)
}
/** Parse `-e` when set, otherwise `undefined` for auto-detection. */
export function resolveEncodingOptional(args: Args): Encoding | undefined {
if (!args.encoding) return undefined
return resolveEncoding(args)
}