-
Notifications
You must be signed in to change notification settings - Fork 1
/
main.oak
236 lines (214 loc) · 5.65 KB
/
main.oak
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
// rush lets you work on many files at once
{
println: println
default: default
map: map
each: each
take: take
last: last
slice: slice
stdin: stdin
append: append
filter: filter
compact: compact
rindexOf: rindexOf
contains?: contains?
} := import('std')
{
lower: lower
trim: trim
split: split
endsWith?: endsWith?
} := import('str')
fs := import('fs')
cli := import('cli')
fmt := import('fmt')
sort := import('sort')
path := import('path')
debug := import('debug')
datetime := import('datetime')
Version := '1.0'
Cli := with cli.parseArgv() if {
args().1 |> default('') |> endsWith?('main.oak') -> args()
_ -> ['oak', 'rush.oak'] |> append(args() |> slice(1))
}
if Cli.opts.version != ? -> {
fmt.printf('Rush v{{0}}', Version)
exit(0)
}
if Cli.opts.help != ? | Cli.opts.h != ? -> {
println('Rush lets you work on many files at once.
Usage
rush [cmd] [src-files] [rename-spec] [options]
Options
--[h]elp Show this help message
--stdin Receive source files list from STDIN
--[d]ry-run Print all operations, but do not run them
--[v]erbose Print all executed commands as they are run
--version Print version information and exit
--[f]orce Overwrite any conflicting files
--debug Print CLI arguments for debugging then exit
Template parameters
path Full absolute path of the file
e.g. "/home/me/image.jpg"
dir Directory of the file
e.g. "/home/me"
fullname Entire file name, including extension
e.g. "image.jpg"
name File name, without the extension
e.g. "image"
ext Extension of the file (string after last \'.\', or
empty string if there is no \'.\')
e.g. "jpg"
i Integer index of this file when the file list is
sorted lexicographically
e.g. "10"
mtime Last-modified time of the file as a UNIX timestamp
e.g. "1651654423"
mdate Last-modified date of the file as an ISO date string
e.g. "2022-05-04"
len Size of the file in bytes
e.g. "4094"
Examples
Change extensions
rush mv *.jpeg \'{{name}}.jpg\'
Rename all PDF files to numbers
rush mv *.pdf \'{{i}}.pdf\'
Add a prefix "secret" to all files, but don\'t delete originals
rush cp * \'secret-{{fullname}}\'
Add last-modified times to photos and put them in folders by dates
rush mv * \'{{mdate}}/{{name}}-{{mtime}}.{{ext}}\'
Convert all PNGs to JPGs using ImageMagick "convert"
rush convert *.png \'{{name}}.jpg\'
')
exit(0)
}
if Cli.verb = ? -> {
println('No command provided.')
exit(1)
}
// the input (source) files are normally all CLI arguments except the last, but
// may be overridden with the --stdin flag.
srcPaths := if {
Cli.opts.stdin -> stdin() |> split('\n') |> filter(fn(s) s != '')
len(Cli.args) > 0 -> Cli.args |> take(len(Cli.args) - 1)
_ -> {
println('No source files provided.')
exit(1)
}
}
// the rename spec is always the last CLI argument
destSpec := Cli.args |> last()
if destSpec = ? -> {
println('No rename spec provided.')
exit(1)
}
// sanity check the rename spec
//
// If the rename spec is not an Oak template string, the user has probably made
// a mistake, and we need to abort.
if destSpec |> fmt.format({}) = destSpec -> {
fmt.printf('Constant rename spec "{{0}}".', destSpec)
exit(1)
}
if Cli.opts.debug != ? -> {
debug.println({
exe: Cli.exe
main: Cli.main
opts: Cli.opts
src: srcPaths
dest: destSpec
})
exit(0)
}
Mappings := srcPaths |> sort.sort() |> with map() fn(src, i) {
srcPath := path.resolve(src)
[srcDir, srcFile] := path.cut(srcPath)
[srcName, srcExt] := if extDotIdx := srcFile |> rindexOf('.') {
-1 -> [srcFile, '']
_ -> [
srcFile |> slice(0, extDotIdx)
srcFile |> slice(extDotIdx + 1)
]
}
stat := fs.statFile(srcPath)
if stat = ? -> {
fmt.printf('Could not stat "{{0}}".', srcPath)
exit(1)
}
dest := destSpec |> fmt.format({
path: srcPath
dir: srcDir
fullname: srcFile
name: srcName
ext: srcExt
i: i
mtime: stat.mod
mdate: datetime.format(stat.mod) |> take(10)
len: stat.len
})
{
src: src
dest: dest
}
}
// For any files whose destination file already exists, ask the user if it
// should be overwritten. If yes, mark it as an overwrite to log later. If no,
// remove it from the mappings list.
Overwrites := []
Force? := Cli.opts.f != ? | Cli.opts.force != ?
fn askOverwrite?(dest) {
print(fmt.format('"{{0}}" already exists. Overwrite? [y/N] ', dest))
evt := input()
if evt.type {
:error -> {
println('Could not read input.')
exit(1)
}
_ -> {
response := evt.data |> trim() |> lower()
response = 'y' | response = 'yes'
}
}
}
Mappings |> with each() fn(mapping, i) {
{ src: src, dest: dest } := mapping
stat := fs.statFile(dest)
if stat != ? -> {
if {
Force?, askOverwrite?(dest) -> Overwrites << dest
_ -> {
fmt.printf('Skipping "{{0}}".', src)
Mappings.(i) := ?
}
}
}
}
Mappings <- Mappings |> compact()
Cmd := Cli.verb
Mappings |> with each() fn(mapping) {
cmdline := fmt.format(
'{{0}} {{1}} {{2}}'
Cmd, mapping.src, mapping.dest
) + if Overwrites |> contains?(mapping.dest) {
true -> ' (overwritten)'
_ -> ''
}
if {
Cli.opts.d, Cli.opts.'dry-run' -> println(cmdline)
_ -> {
if Cli.opts.verbose = true | Cli.opts.v = true -> println(cmdline)
evt := exec(Cmd, [mapping.src, mapping.dest], '')
if evt.type = :error -> {
fmt.printf('Could not {{0}} "{{1}}" to "{{2}}".', Cmd, mapping.src, mapping.dest)
println('[error] ' + evt.message)
exit(1)
}
if evt.status != 0 -> {
fmt.printf('Could not {{0}} "{{1}}" to "{{2}}".', Cmd, mapping.src, mapping.dest)
println(evt.stdout + evt.stderr)
exit(1)
}
}
}
}