Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/bin.ts
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import { foregroundChild } from 'foreground-child'
import { existsSync } from 'fs'
import { jack } from 'jackspeak'
import { globStream } from './index.js'
import { version } from '../package.json'
import { globStream } from './index.js'

const j = jack({
usage: 'glob [options] [<pattern> [<pattern> ...]]'
usage: 'glob [options] [<pattern> [<pattern> ...]]',
})
.description(
`
Expand All @@ -24,6 +24,14 @@ const j = jack({
matches as arguments.`,
},
})
.opt({
default: {
short: 'p',
hint: 'pattern',
description: `If no positional arguments are provided, glob will use
this pattern`,
},
})
.flag({
all: {
short: 'A',
Expand Down Expand Up @@ -219,7 +227,10 @@ try {
console.log(j.usage())
process.exit(0)
}
if (positionals.length === 0) throw 'No patterns provided'
if (positionals.length === 0 && !values.default)
throw 'No patterns provided'
if (positionals.length === 0 && values.default)
positionals.push(values.default)
const patterns = values.all
? positionals
: positionals.filter(p => !existsSync(p))
Expand Down
145 changes: 145 additions & 0 deletions tap-snapshots/test/bin.ts.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/* IMPORTANT
* This snapshot file is auto-generated, but designed for humans.
* It should be checked into source control and tracked carefully.
* Re-generate by setting TAP_SNAPSHOT=1 and running tests.
* Make sure to inspect the output below. Do not ignore changes!
*/
'use strict'
exports[`test/bin.ts TAP usage > -h shows usage 1`] = `
Object {
"args": Array [
"-h",
],
"code": 0,
"options": Object {},
"signal": null,
"stderr": "",
"stdout": String(
Usage:
glob [options] [<pattern> [<pattern> ...]]

Glob v10.2.7

Expand the positional glob expression arguments into any matching file system
paths found.

-c<command> --cmd=<command>
Run the command provided, passing the glob expression
matches as arguments.

-p<pattern> --default=<pattern>
If no positional arguments are provided, glob will use
this pattern

-A --all By default, the glob cli command will not expand any
arguments that are an exact match to a file on disk.

This prevents double-expanding, in case the shell
expands an argument whose filename is a glob
expression.

For example, if 'app/*.ts' would match 'app/[id].ts',
then on Windows powershell or cmd.exe, 'glob app/*.ts'
will expand to 'app/[id].ts', as expected. However, in
posix shells such as bash or zsh, the shell will first
expand 'app/*.ts' to a list of filenames. Then glob
will look for a file matching 'app/[id].ts' (ie,
'app/i.ts' or 'app/d.ts'), which is unexpected.

Setting '--all' prevents this behavior, causing glob to
treat ALL patterns as glob expressions to be expanded,
even if they are an exact match to a file on disk.

When setting this option, be sure to enquote arguments
so that the shell will not expand them prior to passing
them to the glob command process.

-a --absolute Expand to absolute paths
-d --dot-relative Prepend './' on relative matches
-m --mark Append a / on any directories matched
-x --posix Always resolve to posix style paths, using '/' as the
directory separator, even on Windows. Drive letter
absolute matches on Windows will be expanded to their
full resolved UNC maths, eg instead of 'C:\\\\foo\\\\bar', it
will expand to '//?/C:/foo/bar'.

-f --follow Follow symlinked directories when expanding '**'
-R --realpath Call 'fs.realpath' on all of the results. In the case
of an entry that cannot be resolved, the entry is
omitted. This incurs a slight performance penalty, of
course, because of the added system calls.

-s --stat Call 'fs.lstat' on all entries, whether required or not
to determine if it's a valid match.

-b --match-base Perform a basename-only match if the pattern does not
contain any slash characters. That is, '*.js' would be
treated as equivalent to '**/*.js', matching js files
in all directories.

--dot Allow patterns to match files/directories that start
with '.', even if the pattern does not start with '.'

--nobrace Do not expand {...} patterns
--nocase Perform a case-insensitive match. This defaults to
'true' on macOS and Windows platforms, and false on all
others.

Note: 'nocase' should only be explicitly set when it is
known that the filesystem's case sensitivity differs
from the platform default. If set 'true' on
case-insensitive file systems, then the walk may return
more or less results than expected.

--nodir Do not match directories, only files.

Note: to *only* match directories, append a '/' at the
end of the pattern.

--noext Do not expand extglob patterns, such as '+(a|b)'
--noglobstar Do not expand '**' against multiple path portions. Ie,
treat it as a normal '*' instead.

--windows-path-no-escape
Use '\\\\' as a path separator *only*, and *never* as an
escape character. If set, all '\\\\' characters are
replaced with '/' in the pattern.

-D<n> --max-depth=<n> Maximum depth to traverse from the current working
directory

-C<cwd> --cwd=<cwd> Current working directory to execute/match in
-r<root> --root=<root> A string path resolved against the 'cwd', which is used
as the starting point for absolute patterns that start
with '/' (but not drive letters or UNC paths on
Windows).

Note that this *doesn't* necessarily limit the walk to
the 'root' directory, and doesn't affect the cwd
starting point for non-absolute patterns. A pattern
containing '..' will still be able to traverse out of
the root directory, if it is not an actual root
directory on the filesystem, and any non-absolute
patterns will still be matched in the 'cwd'.

To start absolute and non-absolute patterns in the same
path, you can use '--root=' to set it to the empty
string. However, be aware that on Windows systems, a
pattern like 'x:/*' or '//host/share/*' will *always*
start in the 'x:/' or '//host/share/' directory,
regardless of the --root setting.

--platform=<platform> Defaults to the value of 'process.platform' if
available, or 'linux' if not. Setting --platform=win32
on non-Windows systems may cause strange behavior!

-i<ignore> --ignore=<ignore>
Glob patterns to ignore Can be set multiple times
-v --debug Output a huge amount of noisy debug information about
patterns as they are parsed and used to match files.

-h --help Show this usage information

),
}
`
105 changes: 105 additions & 0 deletions test/bin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { spawn, SpawnOptions } from 'child_process'
import t from 'tap'
import { sep } from 'path'
const bin = require.resolve('../dist/cjs/src/bin.js')

interface Result {
args: string[]
options: SpawnOptions
stdout: string
stderr: string
code: number | null
signal: NodeJS.Signals | null
}
const run = async (args: string[], options = {}) => {
const proc = spawn(
process.execPath,
['--enable-source-maps', bin, ...args],
options
)
const out: Buffer[] = []
const err: Buffer[] = []
proc.stdout.on('data', c => out.push(c))
proc.stderr.on('data', c => err.push(c))
return new Promise<Result>(res => {
proc.on('close', (code, signal) => {
res({
args,
options,
stdout: Buffer.concat(out).toString(),
stderr: Buffer.concat(err).toString(),
code,
signal,
})
})
})
}

t.test('usage', async t => {
t.matchSnapshot(await run(['-h']), '-h shows usage')
const res = await run([])
t.equal(res.code, 1, 'exit with code 1 when no args')
t.match(res.stderr, 'No patterns provided')
t.match(res.stderr, /-h --help +Show this usage information$/m)
const badp = await run(['--platform=glorb'])
t.equal(badp.code, 1, 'exit with code 1 on bad platform arg')
t.match(badp.stderr, 'Invalid value provided for --platform: "glorb"\n')
})

t.test('finds matches for a pattern', async t => {
const cwd = t.testdir({
a: {
'x.y': '',
'x.a': '',
b: {
'z.y': '',
'z.a': '',
},
},
})
const res = await run(['**/*.y'], { cwd })
t.match(res.stdout, `a${sep}x.y\n`)
t.match(res.stdout, `a${sep}b${sep}z.y\n`)

const c = `node -p "process.argv.map(s=>s.toUpperCase())"`
const cmd = await run(['**/*.y', '-c', c], { cwd })
t.match(cmd.stdout, `'a${sep}x.y'`.toUpperCase())
t.match(cmd.stdout, `'a${sep}b${sep}z.y'`.toUpperCase())
})

t.test('prioritizes exact match if exists, unless --all', async t => {
const cwd = t.testdir({
routes: {
'[id].tsx': '',
'i.tsx': '',
'd.tsx': '',
}
})
const res = await run(['routes/[id].tsx'], { cwd })
t.equal(res.stdout, 'routes/[id].tsx\n')

const all = await run(['routes/[id].tsx', '--all'], { cwd })
t.match(all.stdout, 'routes/i.tsx\n')
t.match(all.stdout, 'routes/d.tsx\n')
})

t.test('uses default pattern if none provided', async t => {
const cwd = t.testdir({
a: {
'x.y': '',
'x.a': '',
b: {
'z.y': '',
'z.a': '',
},
},
})

const def = await run(['-p', '**/*.y'], { cwd })
t.match(def.stdout, `a${sep}x.y\n`)
t.match(def.stdout, `a${sep}b${sep}z.y\n`)

const exp = await run(['-p', '**/*.y', '**/*.a'], { cwd })
t.match(exp.stdout, `a${sep}x.a\n`)
t.match(exp.stdout, `a${sep}b${sep}z.a\n`)
})