mediasnacks 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.zsh/completions/_mediasnacks +19 -27
- package/README.md +29 -6
- package/package.json +1 -1
- package/src/avif.js +6 -7
- package/src/dropdups.js +6 -8
- package/src/hev1tohvc1.js +6 -6
- package/src/moov2front.js +6 -6
- package/src/resize.js +6 -7
- package/src/sqcrop.js +6 -7
- package/src/utils/args-with-globs.js +61 -0
- package/src/utils/args-with-globs.test.js +153 -0
- package/src/utils/fs-utils.js +2 -13
- package/src/utils/fs-utils.test.js +4 -4
|
@@ -11,42 +11,34 @@ _mediasnacks_commands=(
|
|
|
11
11
|
'hev1tohvc1:Fixes video thumbnails not rendering in macOS Finder'
|
|
12
12
|
'framediff:ffplay with a filter for diffing adjacent frames'
|
|
13
13
|
'vdiff:Plays a video with the difference of two videos'
|
|
14
|
-
'vconcat:Concatenates videos'
|
|
15
|
-
'dlaudio: yt-dlp best audio'
|
|
16
|
-
'dlvideo: yt-dlp best video'
|
|
14
|
+
'vconcat:Concatenates videos'
|
|
15
|
+
'dlaudio: yt-dlp best audio'
|
|
16
|
+
'dlvideo: yt-dlp best video'
|
|
17
17
|
'unemoji:Removes emojis from filenames'
|
|
18
18
|
'rmcover:Removes cover art'
|
|
19
|
-
'curltime:Measures request response timings'
|
|
20
|
-
'gif:Video to GIF'
|
|
21
|
-
'vcut:Split video in two parts by time'
|
|
19
|
+
'curltime:Measures request response timings'
|
|
20
|
+
'gif:Video to GIF'
|
|
21
|
+
'vcut:Split video in two parts by time'
|
|
22
22
|
'flattendir:Moves unique files to the top dir and deletes empty dirs'
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
fi
|
|
25
|
+
if (( CURRENT == 2 )); then
|
|
26
|
+
_describe -t commands 'mediasnacks commands' _mediasnacks_commands
|
|
27
|
+
return
|
|
28
|
+
fi
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
compdef _mediasnacks mediasnacks
|
|
30
|
+
local cmd="$words[2]"
|
|
31
|
+
case "$cmd" in
|
|
32
|
+
avif|resize|sqcrop|moov2front|dropdups|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|vcut|flattendir)
|
|
33
|
+
_files
|
|
34
|
+
;;
|
|
35
|
+
qdir)
|
|
36
|
+
_files -/
|
|
37
|
+
;;
|
|
38
|
+
esac
|
|
43
39
|
|
|
44
40
|
|
|
45
41
|
# INSTALL in: ~/.zshrc
|
|
46
42
|
#fpath=(~/.zsh/completions $fpath)
|
|
47
43
|
#zmodload zsh/complist
|
|
48
44
|
#autoload -U compinit && compinit
|
|
49
|
-
|
|
50
|
-
#function mediasnacks() {
|
|
51
|
-
# $HOME/work/mediasnacks/src/./cli.js "$@"
|
|
52
|
-
#}
|
package/README.md
CHANGED
|
@@ -18,23 +18,46 @@ Commands:
|
|
|
18
18
|
- `dropdups` Removes duplicate frames in a video
|
|
19
19
|
- `seqcheck` Finds missing sequence number
|
|
20
20
|
- `qdir` Sequentially runs all *.sh files in a folder
|
|
21
|
-
- `hev1tohvc1`: Fixes video thumbnails not rendering in macOS Finder
|
|
21
|
+
- `hev1tohvc1`: Fixes video thumbnails not rendering in macOS Finder
|
|
22
22
|
|
|
23
23
|
- `framediff`: Plays a video of adjacent frames diff
|
|
24
24
|
- `vdiff`: Plays a video with the difference of two videos
|
|
25
25
|
- `vconcat`: Concatenates videos
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
- `dlaudio`: yt-dlp best audio
|
|
28
28
|
- `dlvideo`: yt-dlp best video
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
- `unemoji`: Removes emojis from filenames
|
|
31
|
-
- `rmcover`: Removes cover art
|
|
32
|
-
|
|
31
|
+
- `rmcover`: Removes cover art
|
|
32
|
+
|
|
33
33
|
- `curltime`: Measures request response timings
|
|
34
34
|
- `gif`: Video to GIF
|
|
35
35
|
- `vcut`: Split video in two parts at a given time
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
- `flattendir`: Moves unique files to the top dir and deletes empty dirs
|
|
38
|
+
|
|
39
|
+
### Glob Patterns and Literal Filenames
|
|
40
|
+
|
|
41
|
+
Most commands accept glob patterns (like `*.png` or `file[234].png`) to match multiple files. By default, these patterns are expanded by Node.js to match existing files.
|
|
42
|
+
|
|
43
|
+
To treat arguments as literal filenames instead of glob patterns, use the `--` (double dash) separator:
|
|
44
|
+
|
|
45
|
+
```shell
|
|
46
|
+
# This expands the pattern to match files: file2.png, file3.png, file4.png
|
|
47
|
+
npx mediasnacks avif file[234].png
|
|
48
|
+
|
|
49
|
+
# This treats the argument as a literal filename: "file[234].png"
|
|
50
|
+
npx mediasnacks avif -- file[234].png
|
|
51
|
+
|
|
52
|
+
# You can mix both: expand first pattern, treat second as literal
|
|
53
|
+
npx mediasnacks avif file2.png -- file[234].png
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This follows the standard POSIX convention where `--` stops option/pattern processing. Arguments after `--` are treated exactly as written, which is useful when:
|
|
57
|
+
- Filenames contain special glob characters like `[`, `]`, `*`, or `?`
|
|
58
|
+
- You want to pass a pattern to be processed later
|
|
59
|
+
- Working with filenames that haven't been created yet
|
|
60
|
+
|
|
38
61
|
<br/>
|
|
39
62
|
|
|
40
63
|
### Converting Images to AVIF
|
package/package.json
CHANGED
package/src/avif.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
import { parseArgs } from 'node:util'
|
|
5
4
|
|
|
6
|
-
import { replaceExt, lstat
|
|
5
|
+
import { replaceExt, lstat } from './utils/fs-utils.js'
|
|
6
|
+
import { parseArgsWithGlobs } from './utils/args-with-globs.js'
|
|
7
7
|
import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
8
8
|
|
|
9
9
|
|
|
@@ -17,13 +17,12 @@ Converts images to AVIF.
|
|
|
17
17
|
async function main() {
|
|
18
18
|
await assertUserHasFFmpeg()
|
|
19
19
|
|
|
20
|
-
const { values,
|
|
20
|
+
const { values, files } = await parseArgsWithGlobs({
|
|
21
21
|
options: {
|
|
22
22
|
'output-dir': { type: 'string', default: '' },
|
|
23
23
|
overwrite: { short: 'y', type: 'boolean', default: false },
|
|
24
24
|
help: { short: 'h', type: 'boolean', default: false },
|
|
25
|
-
}
|
|
26
|
-
allowPositionals: true
|
|
25
|
+
}
|
|
27
26
|
})
|
|
28
27
|
|
|
29
28
|
if (values.help) {
|
|
@@ -31,11 +30,11 @@ async function main() {
|
|
|
31
30
|
process.exit(0)
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
if (!
|
|
33
|
+
if (!files.length)
|
|
35
34
|
throw new Error('No images specified. See npx mediasnacks avif --help')
|
|
36
35
|
|
|
37
36
|
console.log('AVIF…')
|
|
38
|
-
for (const file of
|
|
37
|
+
for (const file of files)
|
|
39
38
|
await toAvif({
|
|
40
39
|
file,
|
|
41
40
|
outFile: join(values['output-dir'], replaceExt(file, 'avif')),
|
package/src/dropdups.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
3
|
import { resolve, parse, format } from 'node:path'
|
|
5
4
|
|
|
6
|
-
import {
|
|
5
|
+
import { parseArgsWithGlobs } from './utils/args-with-globs.js'
|
|
7
6
|
import { ffmpeg, assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
|
|
8
7
|
|
|
9
8
|
|
|
@@ -34,13 +33,12 @@ Options:
|
|
|
34
33
|
|
|
35
34
|
async function main() {
|
|
36
35
|
await assertUserHasFFmpeg()
|
|
37
|
-
|
|
38
|
-
const { values,
|
|
36
|
+
|
|
37
|
+
const { values, files } = await parseArgsWithGlobs({
|
|
39
38
|
options: {
|
|
40
39
|
'bad-frame-number': { short: 'n', type: 'string', default: '' },
|
|
41
40
|
help: { short: 'h', type: 'boolean', default: false },
|
|
42
|
-
}
|
|
43
|
-
allowPositionals: true
|
|
41
|
+
}
|
|
44
42
|
})
|
|
45
43
|
|
|
46
44
|
if (values.help) {
|
|
@@ -48,7 +46,7 @@ async function main() {
|
|
|
48
46
|
process.exit(0)
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
if (!
|
|
49
|
+
if (!files.length)
|
|
52
50
|
throw new Error('No video specified. See npx mediasnacks dropdups --help')
|
|
53
51
|
|
|
54
52
|
let nBadFrame = values['bad-frame-number']
|
|
@@ -56,7 +54,7 @@ async function main() {
|
|
|
56
54
|
throw new Error('Invalid --bad-frame-number. It must be a positive integer.')
|
|
57
55
|
|
|
58
56
|
console.log('Dropping Duplicate Frames…')
|
|
59
|
-
for (const file of
|
|
57
|
+
for (const file of files)
|
|
60
58
|
await drop(resolve(file), nBadFrame)
|
|
61
59
|
}
|
|
62
60
|
|
package/src/hev1tohvc1.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { uniqueFilenameFor, overwrite, globAll } from './utils/fs-utils.js'
|
|
3
|
+
import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
|
|
4
|
+
import { parseArgsWithGlobs } from './utils/args-with-globs.js'
|
|
6
5
|
import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
7
6
|
|
|
8
7
|
|
|
@@ -18,12 +17,13 @@ by changing the container’s sample entry code from HEV1 to HVC1.
|
|
|
18
17
|
async function main() {
|
|
19
18
|
await assertUserHasFFmpeg()
|
|
20
19
|
|
|
21
|
-
const {
|
|
22
|
-
|
|
20
|
+
const { files } = await parseArgsWithGlobs({})
|
|
21
|
+
|
|
22
|
+
if (!files.length)
|
|
23
23
|
throw new Error(USAGE)
|
|
24
24
|
|
|
25
25
|
console.log('HEV1 to HVC1…')
|
|
26
|
-
for (const file of
|
|
26
|
+
for (const file of files)
|
|
27
27
|
await toHvc1(file)
|
|
28
28
|
}
|
|
29
29
|
|
package/src/moov2front.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { parseArgs } from 'node:util'
|
|
4
|
-
|
|
5
3
|
import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
6
|
-
import { uniqueFilenameFor, overwrite
|
|
4
|
+
import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
|
|
5
|
+
import { parseArgsWithGlobs } from './utils/args-with-globs.js'
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
const USAGE = `
|
|
@@ -17,12 +16,13 @@ Files are overwritten.
|
|
|
17
16
|
async function main() {
|
|
18
17
|
await assertUserHasFFmpeg()
|
|
19
18
|
|
|
20
|
-
const {
|
|
21
|
-
|
|
19
|
+
const { files } = await parseArgsWithGlobs({})
|
|
20
|
+
|
|
21
|
+
if (!files.length)
|
|
22
22
|
throw new Error(USAGE)
|
|
23
23
|
|
|
24
24
|
console.log('Optimizing video for progressive download…')
|
|
25
|
-
for (const file of
|
|
25
|
+
for (const file of files)
|
|
26
26
|
await moov2front(file)
|
|
27
27
|
}
|
|
28
28
|
|
package/src/resize.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { rename } from 'node:fs/promises'
|
|
5
|
-
import { parseArgs } from 'node:util'
|
|
6
5
|
|
|
7
|
-
import { isFile, uniqueFilenameFor
|
|
6
|
+
import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
|
|
7
|
+
import { parseArgsWithGlobs } from './utils/args-with-globs.js'
|
|
8
8
|
import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
9
9
|
|
|
10
10
|
|
|
@@ -30,15 +30,14 @@ Details:
|
|
|
30
30
|
async function main() {
|
|
31
31
|
await assertUserHasFFmpeg()
|
|
32
32
|
|
|
33
|
-
const { values,
|
|
33
|
+
const { values, files } = await parseArgsWithGlobs({
|
|
34
34
|
options: {
|
|
35
35
|
width: { type: 'string', default: '-2' },
|
|
36
36
|
height: { type: 'string', default: '-2' },
|
|
37
37
|
'output-dir': { type: 'string', default: '' },
|
|
38
38
|
overwrite: { short: 'y', type: 'boolean', default: false },
|
|
39
39
|
help: { short: 'h', type: 'boolean', default: false },
|
|
40
|
-
}
|
|
41
|
-
allowPositionals: true
|
|
40
|
+
}
|
|
42
41
|
})
|
|
43
42
|
|
|
44
43
|
if (values.help) {
|
|
@@ -52,12 +51,12 @@ async function main() {
|
|
|
52
51
|
if (width <= 0 && height <= 0)
|
|
53
52
|
throw new Error('--width or --height need to be greater than 0')
|
|
54
53
|
|
|
55
|
-
if (!
|
|
54
|
+
if (!files.length)
|
|
56
55
|
throw new Error('No video files specified')
|
|
57
56
|
|
|
58
57
|
|
|
59
58
|
console.log('Resizing…')
|
|
60
|
-
for (const file of
|
|
59
|
+
for (const file of files)
|
|
61
60
|
await resize({
|
|
62
61
|
file,
|
|
63
62
|
outFile: join(values['output-dir'], file),
|
package/src/sqcrop.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
import { parseArgs } from 'node:util'
|
|
5
4
|
|
|
6
5
|
import { rename } from 'node:fs/promises'
|
|
7
6
|
import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
8
|
-
import { replaceExt, lstat,
|
|
7
|
+
import { replaceExt, lstat, uniqueFilenameFor } from './utils/fs-utils.js'
|
|
8
|
+
import { parseArgsWithGlobs } from './utils/args-with-globs.js'
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
const USAGE = `
|
|
@@ -18,13 +18,12 @@ Square crops images
|
|
|
18
18
|
async function main() {
|
|
19
19
|
await assertUserHasFFmpeg()
|
|
20
20
|
|
|
21
|
-
const { values,
|
|
21
|
+
const { values, files } = await parseArgsWithGlobs({
|
|
22
22
|
options: {
|
|
23
23
|
'output-dir': { type: 'string', default: '' },
|
|
24
24
|
overwrite: { short: 'y', type: 'boolean', default: false },
|
|
25
25
|
help: { short: 'h', type: 'boolean', default: false },
|
|
26
|
-
}
|
|
27
|
-
allowPositionals: true
|
|
26
|
+
}
|
|
28
27
|
})
|
|
29
28
|
|
|
30
29
|
if (values.help) {
|
|
@@ -32,11 +31,11 @@ async function main() {
|
|
|
32
31
|
process.exit(0)
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
if (!
|
|
34
|
+
if (!files.length)
|
|
36
35
|
throw new Error('No images specified. See npx mediasnacks sqcrop --help')
|
|
37
36
|
|
|
38
37
|
console.log('Cropping…')
|
|
39
|
-
for (const file of
|
|
38
|
+
for (const file of files)
|
|
40
39
|
await sqcrop({
|
|
41
40
|
file,
|
|
42
41
|
outFile: join(values['output-dir'], file),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { promisify } from 'node:util'
|
|
2
|
+
import { parseArgs } from 'node:util'
|
|
3
|
+
import { glob as _glob } from 'node:fs'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const glob = promisify(_glob)
|
|
7
|
+
|
|
8
|
+
export async function parseArgsWithGlobs(config) {
|
|
9
|
+
const { values, positionals, tokens } = parseArgs({
|
|
10
|
+
allowPositionals: true,
|
|
11
|
+
...config,
|
|
12
|
+
tokens: true
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const files = await globAll(positionals, tokens)
|
|
16
|
+
|
|
17
|
+
return { values, positionals, tokens, files }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function globAll(arr, tokens) {
|
|
21
|
+
const terminatorIndex = findTerminatorIndex(tokens)
|
|
22
|
+
const set = new Set()
|
|
23
|
+
|
|
24
|
+
if (terminatorIndex >= 0) {
|
|
25
|
+
// Glob arguments before the terminator
|
|
26
|
+
const beforeTerminator = arr.slice(0, terminatorIndex)
|
|
27
|
+
const afterTerminator = arr.slice(terminatorIndex)
|
|
28
|
+
|
|
29
|
+
for (const g of beforeTerminator)
|
|
30
|
+
for (const file of await glob(g))
|
|
31
|
+
set.add(file)
|
|
32
|
+
|
|
33
|
+
// Add arguments after terminator as literal strings
|
|
34
|
+
for (const literal of afterTerminator)
|
|
35
|
+
set.add(literal)
|
|
36
|
+
} else {
|
|
37
|
+
// No terminator, glob everything (current behavior)
|
|
38
|
+
for (const g of arr)
|
|
39
|
+
for (const file of await glob(g))
|
|
40
|
+
set.add(file)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return Array.from(set)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function findTerminatorIndex(tokens) {
|
|
47
|
+
if (!tokens) return -1
|
|
48
|
+
for (const token of tokens) {
|
|
49
|
+
if (token.kind === 'option-terminator') {
|
|
50
|
+
// Find the position in the positionals array
|
|
51
|
+
// The terminator itself is not in positionals, so we count preceding positionals
|
|
52
|
+
let positionalCount = 0
|
|
53
|
+
for (const t of tokens) {
|
|
54
|
+
if (t === token) break
|
|
55
|
+
if (t.kind === 'positional') positionalCount++
|
|
56
|
+
}
|
|
57
|
+
return positionalCount
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return -1
|
|
61
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { equal, deepEqual } from 'node:assert/strict'
|
|
2
|
+
import test, { describe, before, after } from 'node:test'
|
|
3
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { globAll, parseArgsWithGlobs } from './args-with-globs.js'
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
describe('globAll with token terminator', () => {
|
|
10
|
+
let testDir
|
|
11
|
+
const testFiles = ['test-file2.png', 'test-file3.png', 'test-file4.png', 'other-file.png']
|
|
12
|
+
|
|
13
|
+
before(async () => {
|
|
14
|
+
testDir = await mkdtemp(join(tmpdir(), 'glob-test-'))
|
|
15
|
+
for (const file of testFiles) {
|
|
16
|
+
await writeFile(join(testDir, file), '')
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
after(async () => {
|
|
21
|
+
await rm(testDir, { recursive: true, force: true })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('globs patterns when no terminator present', async () => {
|
|
25
|
+
const pattern = join(testDir, 'test-file[234].png')
|
|
26
|
+
const result = await globAll([pattern])
|
|
27
|
+
const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
|
|
28
|
+
deepEqual(result.sort(), expected)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('globs patterns before terminator and keeps literals after', async () => {
|
|
32
|
+
const pattern = join(testDir, 'test-file[234].png')
|
|
33
|
+
const literal = 'my-literal-file[234].png'
|
|
34
|
+
|
|
35
|
+
// Simulate tokens from parseArgs with terminator
|
|
36
|
+
const tokens = [
|
|
37
|
+
{ kind: 'positional', index: 0, value: pattern },
|
|
38
|
+
{ kind: 'option-terminator', index: 1 },
|
|
39
|
+
{ kind: 'positional', index: 1, value: literal }
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const result = await globAll([pattern, literal], tokens)
|
|
43
|
+
const expected = [
|
|
44
|
+
...testFiles.slice(0, 3).map(f => join(testDir, f)),
|
|
45
|
+
literal
|
|
46
|
+
].sort()
|
|
47
|
+
deepEqual(result.sort(), expected)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('keeps all arguments literal when terminator is first', async () => {
|
|
51
|
+
const literal1 = 'literal-file[234].png'
|
|
52
|
+
const literal2 = 'another-file[567].png'
|
|
53
|
+
|
|
54
|
+
// Simulate tokens where terminator comes before all positionals
|
|
55
|
+
const tokens = [
|
|
56
|
+
{ kind: 'option-terminator', index: 0 },
|
|
57
|
+
{ kind: 'positional', index: 0, value: literal1 },
|
|
58
|
+
{ kind: 'positional', index: 1, value: literal2 }
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
const result = await globAll([literal1, literal2], tokens)
|
|
62
|
+
deepEqual(result.sort(), [literal1, literal2].sort())
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('handles no terminator with tokens (backwards compat)', async () => {
|
|
66
|
+
const pattern = join(testDir, 'test-file[234].png')
|
|
67
|
+
|
|
68
|
+
// Tokens without terminator
|
|
69
|
+
const tokens = [
|
|
70
|
+
{ kind: 'positional', index: 0, value: pattern }
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
const result = await globAll([pattern], tokens)
|
|
74
|
+
const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
|
|
75
|
+
deepEqual(result.sort(), expected)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('handles undefined tokens (backwards compat)', async () => {
|
|
79
|
+
const pattern = join(testDir, 'test-file[234].png')
|
|
80
|
+
const result = await globAll([pattern], undefined)
|
|
81
|
+
const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
|
|
82
|
+
deepEqual(result.sort(), expected)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('handles empty arrays', async () => {
|
|
86
|
+
const result = await globAll([])
|
|
87
|
+
deepEqual(result, [])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('deduplicates results', async () => {
|
|
91
|
+
const pattern = join(testDir, 'test-file2.png')
|
|
92
|
+
const result = await globAll([pattern, pattern])
|
|
93
|
+
deepEqual(result, [join(testDir, 'test-file2.png')])
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('parseArgsWithGlobs', () => {
|
|
98
|
+
let testDir
|
|
99
|
+
const testFiles = ['file1.png', 'file2.png', 'file3.png']
|
|
100
|
+
|
|
101
|
+
before(async () => {
|
|
102
|
+
testDir = await mkdtemp(join(tmpdir(), 'parse-args-test-'))
|
|
103
|
+
for (const file of testFiles) {
|
|
104
|
+
await writeFile(join(testDir, file), '')
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
after(async () => {
|
|
109
|
+
await rm(testDir, { recursive: true, force: true })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('parses args and globs files in one call', async () => {
|
|
113
|
+
const pattern = join(testDir, 'file[12].png')
|
|
114
|
+
const { values, files } = await parseArgsWithGlobs({
|
|
115
|
+
args: ['--output-dir', '/tmp', pattern],
|
|
116
|
+
options: {
|
|
117
|
+
'output-dir': { type: 'string' }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
equal(values['output-dir'], '/tmp')
|
|
122
|
+
deepEqual(files.sort(), [
|
|
123
|
+
join(testDir, 'file1.png'),
|
|
124
|
+
join(testDir, 'file2.png')
|
|
125
|
+
].sort())
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('respects verbatim token in parseArgsWithGlobs', async () => {
|
|
129
|
+
const pattern = join(testDir, 'file[12].png')
|
|
130
|
+
const literal = 'literal-file[99].png'
|
|
131
|
+
const { files } = await parseArgsWithGlobs({
|
|
132
|
+
args: [pattern, '--', literal]
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
deepEqual(files.sort(), [
|
|
136
|
+
join(testDir, 'file1.png'),
|
|
137
|
+
join(testDir, 'file2.png'),
|
|
138
|
+
literal
|
|
139
|
+
].sort())
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('returns empty files array when no positionals', async () => {
|
|
143
|
+
const { files, values } = await parseArgsWithGlobs({
|
|
144
|
+
args: ['--flag'],
|
|
145
|
+
options: {
|
|
146
|
+
flag: { type: 'boolean' }
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
equal(values.flag, true)
|
|
151
|
+
deepEqual(files, [])
|
|
152
|
+
})
|
|
153
|
+
})
|
package/src/utils/fs-utils.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import { promisify } from 'node:util'
|
|
2
1
|
import { randomUUID } from 'node:crypto'
|
|
3
2
|
import { unlink, rename } from 'node:fs/promises'
|
|
4
3
|
import { dirname, extname, join } from 'node:path'
|
|
5
|
-
import { lstatSync
|
|
4
|
+
import { lstatSync } from 'node:fs'
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
const glob = promisify(_glob)
|
|
9
|
-
|
|
10
|
-
export async function globAll(arr) {
|
|
11
|
-
const set = new Set()
|
|
12
|
-
for (const g of arr)
|
|
13
|
-
for (const file of await glob(g))
|
|
14
|
-
set.add(file)
|
|
15
|
-
return Array.from(set)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
7
|
export const lstat = f => lstatSync(f, { throwIfNoEntry: false })
|
|
19
8
|
export const isFile = path => lstat(path)?.isFile()
|
|
20
9
|
|
|
@@ -26,7 +15,7 @@ export const replaceExt = (f, ext) => {
|
|
|
26
15
|
return parts.join('.')
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
export const uniqueFilenameFor = file =>
|
|
18
|
+
export const uniqueFilenameFor = file =>
|
|
30
19
|
join(dirname(file), randomUUID() + extname(file))
|
|
31
20
|
|
|
32
21
|
|
|
@@ -4,18 +4,18 @@ import { replaceExt } from './fs-utils.js'
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
describe('replaceExt', () => {
|
|
7
|
-
test('replaces a simple extension', () =>
|
|
7
|
+
test('replaces a simple extension', () =>
|
|
8
8
|
equal(replaceExt('file.txt', 'md'), 'file.md'))
|
|
9
9
|
|
|
10
|
-
test('replaces a multi-part extension', () =>
|
|
10
|
+
test('replaces a multi-part extension', () =>
|
|
11
11
|
equal(replaceExt('archive.tar.gz', 'zip'), 'archive.tar.zip'))
|
|
12
12
|
|
|
13
|
-
test('adds extension when none exists', () =>
|
|
13
|
+
test('adds extension when none exists', () =>
|
|
14
14
|
equal(replaceExt('README', 'md'), 'README.md'))
|
|
15
15
|
|
|
16
16
|
test('handles empty filename', () =>
|
|
17
17
|
equal(replaceExt('', 'ext'), '.ext'))
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
test('handles dot-files', () =>
|
|
20
20
|
equal(replaceExt('.env', 'txt'), '.env.txt'))
|
|
21
21
|
})
|