cssgrep 1.0.0 → 1.1.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/CHANGELOG.md +39 -0
- package/README.md +19 -0
- package/completions/_cssgrep +6 -0
- package/completions/cssgrep.bash +4 -3
- package/completions/cssgrep.fish +6 -0
- package/index.js +93 -19
- package/man/cssgrep.1 +41 -2
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.1.0] - 2026-07-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `-H`/`--with-filename` and `--no-filename` to force the `file:` prefix on or off.
|
|
14
|
+
- `-s`/`--no-messages` to suppress errors for unreadable or missing files.
|
|
15
|
+
- `--include`/`--exclude` file globs with brace alternation (`{a,b,c}`).
|
|
16
|
+
`--exclude` is an alias of `--ignore`; `--include` replaces the `--ext` filter.
|
|
17
|
+
- `--max-depth <n>` to cap `-r` recursion depth.
|
|
18
|
+
- Automatic skipping of binary files (NUL byte / control-byte heuristic), with a
|
|
19
|
+
note on stderr suppressible by `-s`/`-q`.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- CI now runs on Linux, macOS and Windows across Node 20 and 22.
|
|
23
|
+
- Releases publish to npm with provenance and smoke-test the built binary.
|
|
24
|
+
|
|
25
|
+
## [1.0.0] - 2026-06-29
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Initial release: match a CSS selector against HTML and print each hit
|
|
29
|
+
grep-style, with byte-accurate `line:col` via `-n` (works on minified HTML).
|
|
30
|
+
- Recursive search: `-r`/`--recursive`, `--ext`, `-i`/`--ignore`, `--ignore-file`.
|
|
31
|
+
- Print modes: default line, `-p`/`--print`, `--attr`, `--text`, `--json`.
|
|
32
|
+
- `--parent <n>` structural context; line context `-A`/`-B`/`-C`.
|
|
33
|
+
- Limits `-m`/`--max-count`, `-M`/`--max-total`; aggregates `-c`, `-l`/`-L`, `-q`.
|
|
34
|
+
- `-0`/`--null`, `--color`, `-w`/`--max-width`, `-V`/`--version`.
|
|
35
|
+
- Standalone binaries (Bun, Node SEA), shell completions, and a man page.
|
|
36
|
+
|
|
37
|
+
[Unreleased]: https://github.com/msbatarce/cssgrep/compare/v1.1.0...HEAD
|
|
38
|
+
[1.1.0]: https://github.com/msbatarce/cssgrep/compare/v1.0.0...v1.1.0
|
|
39
|
+
[1.0.0]: https://github.com/msbatarce/cssgrep/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# cssgrep
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/cssgrep)
|
|
4
|
+
[](https://github.com/msbatarce/cssgrep/actions/workflows/test.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
Search HTML by CSS selector and print each match, grep-style — add `-n` to
|
|
4
8
|
report it as `file:line:col`.
|
|
5
9
|
|
|
@@ -73,8 +77,11 @@ locator appears only with `-n`:
|
|
|
73
77
|
| Flag | Description |
|
|
74
78
|
|------|-------------|
|
|
75
79
|
| `-r`, `--recursive` | Recurse into directory arguments (defaults to `.` if none given). |
|
|
80
|
+
| `--max-depth <n>` | Limit `-r` recursion depth (`1` = the given directory only, no subdirectories). |
|
|
76
81
|
| `--ext <list>` | Extensions to scan with `-r` (default `html,htm`). Value attaches with `=`: `--ext htm` or `--ext=htm`. |
|
|
82
|
+
| `--include <glob>` | Only search files whose name/path matches `<glob>` while recursing (repeatable). Replaces `--ext` — the two can't be combined. e.g. `--include '*.{html,htm,xhtml}'`. |
|
|
77
83
|
| `-i`, `--ignore <glob>` | Skip files/dirs matching `<glob>` while recursing (repeatable). `node_modules`, `*.min.html`, `build/` (dir-only), or path globs like `src/vendor/**`. |
|
|
84
|
+
| `--exclude <glob>` | Alias for `--ignore` (grep's name for the same thing). |
|
|
78
85
|
| `--ignore-file <path>` | Load ignore globs from a file, one per line (`#` comments and blank lines ignored) — like a `.gitignore`. |
|
|
79
86
|
| `-n`, `--line-number` | Prefix each match with its `line:col` locator. Mutually exclusive with `-c` and `-p`. |
|
|
80
87
|
| `-p`, `--print` | Pretty-print the matched node's HTML, re-indented from scratch (works on minified input). No `line:col` locator is shown. |
|
|
@@ -92,7 +99,10 @@ locator appears only with `-n`:
|
|
|
92
99
|
| `-l`, `--files-with-matches` | Print only the names of files that contain a match. |
|
|
93
100
|
| `-L`, `--files-without-match` | Print only the names of files with no match. |
|
|
94
101
|
| `-q`, `--quiet` | Print nothing; exit `0` on the first match, `1` if none. Stops early. |
|
|
102
|
+
| `-s`, `--no-messages` | Suppress error messages for unreadable or missing files (handy with `-r`). |
|
|
95
103
|
| `-0`, `--null` | Output a NUL after each file name instead of `:` (or, with `-l`/`-L`, instead of the newline) — pipe to `xargs -0`. |
|
|
104
|
+
| `-H`, `--with-filename` | Always print the `file:` prefix, even for a single file or stdin. |
|
|
105
|
+
| `--no-filename` | Never print the `file:` prefix, even when searching multiple files. |
|
|
96
106
|
| `--color[=<when>]` | Colorize output: `auto` (default — color only when stdout is a terminal), `always`, or `never`. A bare `--color` means `always`. |
|
|
97
107
|
| `-h`, `--help` | Show help. |
|
|
98
108
|
| `-V`, `--version` | Print the version and exit. |
|
|
@@ -100,6 +110,15 @@ locator appears only with `-n`:
|
|
|
100
110
|
Boolean short flags can be combined into one token (`-rn` is `-r -n`), and a
|
|
101
111
|
value-taking flag may close such a cluster (`-rnw100`).
|
|
102
112
|
|
|
113
|
+
Globs for `--include`/`--ignore`/`--exclude` support `*` (within a path
|
|
114
|
+
segment), `**` (across `/`), `?` (one non-slash char), and brace alternation
|
|
115
|
+
like `*.{html,htm}`. A trailing `/` matches directories only; a pattern with a
|
|
116
|
+
`/` matches against the path, otherwise the basename — gitignore-flavored.
|
|
117
|
+
|
|
118
|
+
Binary files are detected (a NUL byte or a high ratio of control bytes in the
|
|
119
|
+
first 8 KB) and skipped with a note on stderr — parsing them as HTML is never
|
|
120
|
+
useful. Suppress the note with `-s` (or `-q`).
|
|
121
|
+
|
|
103
122
|
When coloring is on, the matched node is highlighted within its line (grep's
|
|
104
123
|
bold-red); the `file:` prefix and `line:col` locator get their own colors
|
|
105
124
|
(magenta and green, like grep). Plain `-p` prints no color (the whole block is
|
package/completions/_cssgrep
CHANGED
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
_cssgrep() {
|
|
7
7
|
_arguments -s -S \
|
|
8
8
|
'(-r --recursive)'{-r,--recursive}'[recurse into directory arguments]' \
|
|
9
|
+
'--max-depth[limit -r recursion depth (1 = given dir only)]:depth:' \
|
|
9
10
|
'--ext[extensions to scan with -r]:extensions (comma-separated):' \
|
|
11
|
+
'*--include[only search files matching glob (replaces --ext)]:glob:' \
|
|
10
12
|
'*'{-i,--ignore}'[skip files/dirs matching glob while recursing]:glob:' \
|
|
13
|
+
'*--exclude[alias for --ignore]:glob:' \
|
|
11
14
|
'--ignore-file[read ignore globs from a file]:file:_files' \
|
|
12
15
|
'(-n --line-number)'{-n,--line-number}'[prefix each match with line:col]' \
|
|
13
16
|
'(-p --print)'{-p,--print}'[pretty-print the matched node HTML]' \
|
|
@@ -25,7 +28,10 @@ _cssgrep() {
|
|
|
25
28
|
'(-l --files-with-matches)'{-l,--files-with-matches}'[print only names of files with a match]' \
|
|
26
29
|
'(-L --files-without-match)'{-L,--files-without-match}'[print only names of files without a match]' \
|
|
27
30
|
'(-q --quiet)'{-q,--quiet}'[print nothing; exit on first match]' \
|
|
31
|
+
'(-s --no-messages)'{-s,--no-messages}'[suppress errors for unreadable/missing files]' \
|
|
28
32
|
'(-0 --null)'{-0,--null}'[separate the file name with a NUL byte]' \
|
|
33
|
+
'(--no-filename)'{-H,--with-filename}'[always print the file name prefix]' \
|
|
34
|
+
'(-H --with-filename)--no-filename[never print the file name prefix]' \
|
|
29
35
|
'--color=-[colorize output]::when:(auto always never)' \
|
|
30
36
|
'(-h --help)'{-h,--help}'[show help and exit]' \
|
|
31
37
|
'(-V --version)'{-V,--version}'[show version and exit]' \
|
package/completions/cssgrep.bash
CHANGED
|
@@ -6,11 +6,12 @@ _cssgrep() {
|
|
|
6
6
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
|
7
7
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
|
8
8
|
|
|
9
|
-
opts="-r --recursive --ext -i --ignore --ignore-file -n --line-number \
|
|
9
|
+
opts="-r --recursive --max-depth --ext --include -i --ignore --exclude --ignore-file -n --line-number \
|
|
10
10
|
-p --print --attr --text --json --parent -w --max-width \
|
|
11
11
|
-A --after-context -B --before-context -C --context \
|
|
12
12
|
-m --max-count -M --max-total -c --count \
|
|
13
|
-
-l --files-with-matches -L --files-without-match -q --quiet -0 --null \
|
|
13
|
+
-l --files-with-matches -L --files-without-match -q --quiet -s --no-messages -0 --null \
|
|
14
|
+
-H --with-filename --no-filename \
|
|
14
15
|
--color -h --help -V --version"
|
|
15
16
|
|
|
16
17
|
case "$prev" in
|
|
@@ -22,7 +23,7 @@ _cssgrep() {
|
|
|
22
23
|
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
23
24
|
return 0
|
|
24
25
|
;;
|
|
25
|
-
--ext|-i|--ignore|--attr|--parent|-w|--max-width|-A|--after-context|\
|
|
26
|
+
--max-depth|--ext|--include|-i|--ignore|--exclude|--attr|--parent|-w|--max-width|-A|--after-context|\
|
|
26
27
|
-B|--before-context|-C|--context|-m|--max-count|-M|--max-total)
|
|
27
28
|
# value-taking flag with no enumerable completion; offer nothing
|
|
28
29
|
return 0
|
package/completions/cssgrep.fish
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
# Install: place this file in ~/.config/fish/completions/ (named cssgrep.fish).
|
|
3
3
|
|
|
4
4
|
complete -c cssgrep -s r -l recursive -d 'Recurse into directory arguments'
|
|
5
|
+
complete -c cssgrep -l max-depth -x -d 'Limit -r recursion depth (1 = given dir only)'
|
|
5
6
|
complete -c cssgrep -l ext -x -d 'Extensions to scan with -r (comma-separated)'
|
|
7
|
+
complete -c cssgrep -l include -x -d 'Only search files matching glob (replaces --ext)'
|
|
6
8
|
complete -c cssgrep -s i -l ignore -x -d 'Skip files/dirs matching glob while recursing'
|
|
9
|
+
complete -c cssgrep -l exclude -x -d 'Alias for --ignore'
|
|
7
10
|
complete -c cssgrep -l ignore-file -r -F -d 'Read ignore globs from a file'
|
|
8
11
|
complete -c cssgrep -s n -l line-number -d 'Prefix each match with line:col'
|
|
9
12
|
complete -c cssgrep -s p -l print -d 'Pretty-print the matched node HTML'
|
|
@@ -21,7 +24,10 @@ complete -c cssgrep -s c -l count -d 'Print only a count of matches'
|
|
|
21
24
|
complete -c cssgrep -s l -l files-with-matches -d 'Print only names of files with a match'
|
|
22
25
|
complete -c cssgrep -s L -l files-without-match -d 'Print only names of files without a match'
|
|
23
26
|
complete -c cssgrep -s q -l quiet -d 'Print nothing; exit on first match'
|
|
27
|
+
complete -c cssgrep -s s -l no-messages -d 'Suppress errors for unreadable/missing files'
|
|
24
28
|
complete -c cssgrep -s 0 -l null -d 'Separate the file name with a NUL byte'
|
|
29
|
+
complete -c cssgrep -s H -l with-filename -d 'Always print the file name prefix'
|
|
30
|
+
complete -c cssgrep -l no-filename -d 'Never print the file name prefix'
|
|
25
31
|
complete -c cssgrep -l color -x -a 'auto always never' -d 'Colorize output'
|
|
26
32
|
complete -c cssgrep -s h -l help -d 'Show help and exit'
|
|
27
33
|
complete -c cssgrep -s V -l version -d 'Show version and exit'
|
package/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const { html: beautify } = require('js-beautify');
|
|
|
12
12
|
// package.json, so it survives compilation into a standalone binary (Bun
|
|
13
13
|
// --compile / Node SEA), where package.json won't sit next to the executable.
|
|
14
14
|
// Keep in sync with package.json on release.
|
|
15
|
-
const VERSION = '1.
|
|
15
|
+
const VERSION = '1.1.0';
|
|
16
16
|
|
|
17
17
|
const USAGE = `cssgrep - search HTML by CSS selector, grep-style.
|
|
18
18
|
|
|
@@ -29,8 +29,11 @@ Output (one line per match):
|
|
|
29
29
|
|
|
30
30
|
Options:
|
|
31
31
|
-r, --recursive Recurse into directory arguments.
|
|
32
|
+
--max-depth <n> Limit -r recursion depth (1 = the given dir only).
|
|
32
33
|
--ext <list> Comma-separated extensions for -r (default: html,htm).
|
|
34
|
+
--include <glob> Only search files matching <glob> (replaces --ext; repeatable).
|
|
33
35
|
-i, --ignore <glob> Skip files/dirs matching <glob> when recursing (repeatable).
|
|
36
|
+
--exclude <glob> Alias for --ignore.
|
|
34
37
|
--ignore-file <path> Read ignore globs from <path> (one per line, # comments).
|
|
35
38
|
-n, --line-number Prefix each match with its line:col (excludes -c, -p).
|
|
36
39
|
-p, --print Pretty-print the matched node's HTML above its location.
|
|
@@ -48,7 +51,10 @@ Options:
|
|
|
48
51
|
-l, --files-with-matches Print only the names of files that have a match.
|
|
49
52
|
-L, --files-without-match Print only the names of files with no match.
|
|
50
53
|
-q, --quiet Print nothing; exit 0 on first match, 1 if none.
|
|
54
|
+
-s, --no-messages Suppress error messages for unreadable/missing files.
|
|
51
55
|
-0, --null Separate the file name with a NUL byte (for xargs -0).
|
|
56
|
+
-H, --with-filename Always print the file name prefix (even for one file).
|
|
57
|
+
--no-filename Never print the file name prefix (even for many files).
|
|
52
58
|
--color[=<when>] Colorize output: auto (default), always or never.
|
|
53
59
|
-h, --help Show this help.
|
|
54
60
|
-V, --version Show version and exit.
|
|
@@ -57,6 +63,9 @@ Short flags combine (-rn) and a value attaches to its flag (-w100) or follows it
|
|
|
57
63
|
(-w 100); a value-taking flag may close a cluster (-rnw100). Long options take a
|
|
58
64
|
value with = or as the next word (--max-width=100, --ext htm).
|
|
59
65
|
|
|
66
|
+
Globs (--include/--ignore/--exclude) support *, ** (crosses /), ?, and brace
|
|
67
|
+
alternation like *.{html,htm}.
|
|
68
|
+
|
|
60
69
|
Exit status: 0 if any match was found, 1 if none, 2 on error.`;
|
|
61
70
|
|
|
62
71
|
function fail(msg) {
|
|
@@ -84,6 +93,8 @@ function parseArgs(argv) {
|
|
|
84
93
|
paths: [],
|
|
85
94
|
recursive: false,
|
|
86
95
|
exts: ['html', 'htm'],
|
|
96
|
+
extGiven: false,
|
|
97
|
+
maxDepth: 0,
|
|
87
98
|
lineNumber: false,
|
|
88
99
|
print: false,
|
|
89
100
|
maxWidth: 0,
|
|
@@ -101,9 +112,14 @@ function parseArgs(argv) {
|
|
|
101
112
|
before: 0,
|
|
102
113
|
after: 0,
|
|
103
114
|
ignore: [],
|
|
115
|
+
include: [],
|
|
116
|
+
withFilename: false,
|
|
117
|
+
noFilename: false,
|
|
118
|
+
noMessages: false,
|
|
104
119
|
color: 'auto',
|
|
105
120
|
};
|
|
106
121
|
const setExts = v => {
|
|
122
|
+
opts.extGiven = true;
|
|
107
123
|
opts.exts = (v || '').split(',').map(s => s.trim().replace(/^\./, '')).filter(Boolean);
|
|
108
124
|
};
|
|
109
125
|
const boundedInt = (v, what, min) => {
|
|
@@ -115,11 +131,13 @@ function parseArgs(argv) {
|
|
|
115
131
|
const setMaxWidth = v => { opts.maxWidth = positiveInt(v, '--max-width'); };
|
|
116
132
|
const setMaxCount = v => { opts.maxCount = positiveInt(v, '--max-count'); };
|
|
117
133
|
const setMaxTotal = v => { opts.maxTotal = positiveInt(v, '--max-total'); };
|
|
134
|
+
const setMaxDepth = v => { opts.maxDepth = positiveInt(v, '--max-depth'); };
|
|
118
135
|
const setParent = v => { opts.parent = positiveInt(v, '--parent'); };
|
|
119
136
|
const setAfter = v => { opts.after = boundedInt(v, '--after-context', 0); };
|
|
120
137
|
const setBefore = v => { opts.before = boundedInt(v, '--before-context', 0); };
|
|
121
138
|
const setContext = v => { opts.after = opts.before = boundedInt(v, '--context', 0); };
|
|
122
139
|
const addIgnore = v => { const c = compileIgnore(v); if (c) opts.ignore.push(c); };
|
|
140
|
+
const addInclude = v => { const c = compileIgnore(v); if (c) opts.include.push(c); };
|
|
123
141
|
const addIgnoreFile = v => {
|
|
124
142
|
let content;
|
|
125
143
|
try { content = fs.readFileSync(v, 'utf8'); }
|
|
@@ -152,12 +170,17 @@ function parseArgs(argv) {
|
|
|
152
170
|
case '--files-without-match': opts.filesWithoutMatch = true; break;
|
|
153
171
|
case '--quiet': case '--silent': opts.quiet = true; break;
|
|
154
172
|
case '--null': opts.nul = true; break;
|
|
173
|
+
case '--with-filename': opts.withFilename = true; break;
|
|
174
|
+
case '--no-filename': opts.noFilename = true; break;
|
|
175
|
+
case '--no-messages': opts.noMessages = true; break;
|
|
155
176
|
case '--ext': setExts(value()); break;
|
|
156
|
-
case '--ignore': addIgnore(value()); break;
|
|
177
|
+
case '--ignore': case '--exclude': addIgnore(value()); break;
|
|
157
178
|
case '--ignore-file': addIgnoreFile(value()); break;
|
|
179
|
+
case '--include': addInclude(value()); break;
|
|
158
180
|
case '--max-width': setMaxWidth(value()); break;
|
|
159
181
|
case '--max-count': setMaxCount(value()); break;
|
|
160
182
|
case '--max-total': setMaxTotal(value()); break;
|
|
183
|
+
case '--max-depth': setMaxDepth(value()); break;
|
|
161
184
|
case '--attr': opts.attr = value(); break;
|
|
162
185
|
case '--text': opts.text = true; break;
|
|
163
186
|
case '--json': opts.json = true; break;
|
|
@@ -193,6 +216,8 @@ function parseArgs(argv) {
|
|
|
193
216
|
case 'L': opts.filesWithoutMatch = true; break;
|
|
194
217
|
case 'q': opts.quiet = true; break;
|
|
195
218
|
case '0': case 'Z': opts.nul = true; break;
|
|
219
|
+
case 'H': opts.withFilename = true; break;
|
|
220
|
+
case 's': opts.noMessages = true; break;
|
|
196
221
|
default: fail(`unknown option: -${ch}`);
|
|
197
222
|
}
|
|
198
223
|
}
|
|
@@ -206,6 +231,10 @@ function parseArgs(argv) {
|
|
|
206
231
|
const aggregates = [opts.count, opts.filesWithMatches, opts.filesWithoutMatch, opts.quiet]
|
|
207
232
|
.filter(Boolean).length;
|
|
208
233
|
if (aggregates > 1) fail('only one of -c, -l, -L, -q may be given');
|
|
234
|
+
// Filename prefix can be forced on or off, but not both (grep -H / -h).
|
|
235
|
+
if (opts.withFilename && opts.noFilename) fail('-H cannot be combined with --no-filename');
|
|
236
|
+
// --include fully replaces the extension filter, so the two can't coexist.
|
|
237
|
+
if (opts.extGiven && opts.include.length) fail('--ext and --include cannot be combined');
|
|
209
238
|
// Per-match print modes choose what is printed for each match; only one.
|
|
210
239
|
const printModes = [opts.print, opts.attr != null, opts.text, opts.json].filter(Boolean).length;
|
|
211
240
|
if (printModes > 1) fail('only one of -p, --attr, --text, --json may be given');
|
|
@@ -594,9 +623,11 @@ function searchSource(src, name, showLabel, opts, out, limit = Infinity) {
|
|
|
594
623
|
}
|
|
595
624
|
|
|
596
625
|
// Translate a glob to a regex body. `*` matches within a path segment, `**`
|
|
597
|
-
// across segments, `?` a single non-slash char
|
|
626
|
+
// across segments, `?` a single non-slash char, `{a,b,c}` alternation; every
|
|
627
|
+
// other char is literal.
|
|
598
628
|
function globToRegex(glob) {
|
|
599
629
|
let re = '';
|
|
630
|
+
let depth = 0; // open `{` alternation groups
|
|
600
631
|
for (let i = 0; i < glob.length; i++) {
|
|
601
632
|
const c = glob[i];
|
|
602
633
|
if (c === '*') {
|
|
@@ -609,12 +640,21 @@ function globToRegex(glob) {
|
|
|
609
640
|
}
|
|
610
641
|
} else if (c === '?') {
|
|
611
642
|
re += '[^/]';
|
|
612
|
-
} else if (
|
|
643
|
+
} else if (c === '{') {
|
|
644
|
+
re += '(';
|
|
645
|
+
depth++;
|
|
646
|
+
} else if (c === '}' && depth > 0) {
|
|
647
|
+
re += ')';
|
|
648
|
+
depth--;
|
|
649
|
+
} else if (c === ',' && depth > 0) {
|
|
650
|
+
re += '|';
|
|
651
|
+
} else if (/[.+^$()|[\]\\]/.test(c)) { // note: { } handled above
|
|
613
652
|
re += '\\' + c;
|
|
614
653
|
} else {
|
|
615
654
|
re += c;
|
|
616
655
|
}
|
|
617
656
|
}
|
|
657
|
+
while (depth-- > 0) re += ')'; // close any unbalanced `{`
|
|
618
658
|
return re;
|
|
619
659
|
}
|
|
620
660
|
|
|
@@ -631,7 +671,9 @@ function compileIgnore(pattern) {
|
|
|
631
671
|
return { re, dirOnly, hasSlash };
|
|
632
672
|
}
|
|
633
673
|
|
|
634
|
-
|
|
674
|
+
// True if name/path matches any compiled glob matcher (shared by --ignore/
|
|
675
|
+
// --exclude and --include).
|
|
676
|
+
function matchesAny(name, full, isDir, matchers) {
|
|
635
677
|
for (const m of matchers) {
|
|
636
678
|
if (m.dirOnly && !isDir) continue;
|
|
637
679
|
if (m.re.test(m.hasSlash ? full : name)) return true;
|
|
@@ -639,23 +681,29 @@ function isIgnored(name, full, isDir, matchers) {
|
|
|
639
681
|
return false;
|
|
640
682
|
}
|
|
641
683
|
|
|
642
|
-
function* walk(dir, opts) {
|
|
684
|
+
function* walk(dir, opts, depth = 1) {
|
|
643
685
|
let entries;
|
|
644
686
|
try {
|
|
645
687
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
646
688
|
} catch (e) {
|
|
647
|
-
process.stderr.write(`cssgrep: ${dir}: ${e.code || e.message}\n`);
|
|
689
|
+
if (!opts.noMessages) process.stderr.write(`cssgrep: ${dir}: ${e.code || e.message}\n`);
|
|
648
690
|
return;
|
|
649
691
|
}
|
|
650
692
|
for (const e of entries) {
|
|
651
693
|
const full = path.join(dir, e.name);
|
|
652
694
|
const isDir = e.isDirectory();
|
|
653
|
-
if (opts.ignore.length &&
|
|
695
|
+
if (opts.ignore.length && matchesAny(e.name, full, isDir, opts.ignore)) continue;
|
|
654
696
|
if (isDir) {
|
|
655
|
-
|
|
697
|
+
// --max-depth caps how far we descend; depth 1 = the target's children.
|
|
698
|
+
if (!opts.maxDepth || depth < opts.maxDepth) yield* walk(full, opts, depth + 1);
|
|
656
699
|
} else if (e.isFile()) {
|
|
657
|
-
|
|
658
|
-
if (opts.
|
|
700
|
+
// --include replaces the extension filter; otherwise filter by --ext.
|
|
701
|
+
if (opts.include.length) {
|
|
702
|
+
if (matchesAny(e.name, full, false, opts.include)) yield full;
|
|
703
|
+
} else {
|
|
704
|
+
const ext = path.extname(e.name).slice(1).toLowerCase();
|
|
705
|
+
if (opts.exts.includes(ext)) yield full;
|
|
706
|
+
}
|
|
659
707
|
}
|
|
660
708
|
}
|
|
661
709
|
}
|
|
@@ -664,6 +712,23 @@ function readStdin() {
|
|
|
664
712
|
return fs.readFileSync(0, 'utf8');
|
|
665
713
|
}
|
|
666
714
|
|
|
715
|
+
// Heuristic binary-file detector (grep/git style). A NUL byte in the first 8 KB
|
|
716
|
+
// is a decisive binary signal; failing that, a high ratio of non-text control
|
|
717
|
+
// bytes flags binary content that happens to lack NULs. HTML is text, so binary
|
|
718
|
+
// files are never worth parsing and are skipped. (stat() can't tell us this —
|
|
719
|
+
// only the bytes can.)
|
|
720
|
+
const BINARY_SNIFF_BYTES = 8192;
|
|
721
|
+
function looksBinary(buf) {
|
|
722
|
+
const n = Math.min(buf.length, BINARY_SNIFF_BYTES);
|
|
723
|
+
let suspicious = 0;
|
|
724
|
+
for (let i = 0; i < n; i++) {
|
|
725
|
+
const b = buf[i];
|
|
726
|
+
if (b === 0) return true; // NUL: definitely binary
|
|
727
|
+
if (b < 9 || (b > 13 && b < 32)) suspicious++; // control char (not \t\n\v\f\r)
|
|
728
|
+
}
|
|
729
|
+
return n > 0 && suspicious / n > 0.3;
|
|
730
|
+
}
|
|
731
|
+
|
|
667
732
|
function main() {
|
|
668
733
|
const opts = parseArgs(process.argv.slice(2));
|
|
669
734
|
resolveSelectorAndPaths(opts);
|
|
@@ -676,7 +741,7 @@ function main() {
|
|
|
676
741
|
const targets = opts.paths.length ? opts.paths : ['.'];
|
|
677
742
|
for (const p of targets) {
|
|
678
743
|
const st = fs.statSync(p, { throwIfNoEntry: false });
|
|
679
|
-
if (!st) { process.stderr.write(`cssgrep: ${p}: no such file or directory\n`); continue; }
|
|
744
|
+
if (!st) { if (!opts.noMessages) process.stderr.write(`cssgrep: ${p}: no such file or directory\n`); continue; }
|
|
680
745
|
if (st.isDirectory()) files.push(...walk(p, opts));
|
|
681
746
|
else files.push(p);
|
|
682
747
|
}
|
|
@@ -686,8 +751,11 @@ function main() {
|
|
|
686
751
|
useStdin = true;
|
|
687
752
|
}
|
|
688
753
|
|
|
689
|
-
// A label (file prefix) is shown when searching more than one file
|
|
690
|
-
|
|
754
|
+
// A label (file prefix) is shown when searching more than one file; -H forces
|
|
755
|
+
// it on (even for one file or stdin) and --no-filename forces it off.
|
|
756
|
+
const showLabel = opts.withFilename ? true
|
|
757
|
+
: opts.noFilename ? false
|
|
758
|
+
: (!useStdin && files.length > 1);
|
|
691
759
|
|
|
692
760
|
const out = [];
|
|
693
761
|
let total = 0;
|
|
@@ -697,18 +765,24 @@ function main() {
|
|
|
697
765
|
|
|
698
766
|
try {
|
|
699
767
|
if (useStdin) {
|
|
700
|
-
total += searchSource(readStdin(), '(standard input)',
|
|
768
|
+
total += searchSource(readStdin(), '(standard input)', showLabel, opts, out, room());
|
|
701
769
|
} else {
|
|
702
770
|
for (const f of files) {
|
|
703
771
|
if (room() <= 0) break; // -M: global budget exhausted
|
|
704
|
-
let
|
|
772
|
+
let buf;
|
|
705
773
|
try {
|
|
706
|
-
|
|
774
|
+
buf = fs.readFileSync(f); // Buffer: sniff before decoding
|
|
707
775
|
} catch (e) {
|
|
708
|
-
process.stderr.write(`cssgrep: ${f}: ${e.code || e.message}\n`);
|
|
776
|
+
if (!opts.noMessages) process.stderr.write(`cssgrep: ${f}: ${e.code || e.message}\n`);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (looksBinary(buf)) {
|
|
780
|
+
if (!opts.noMessages && !opts.quiet) {
|
|
781
|
+
process.stderr.write(`cssgrep: ${f}: binary file (skipped)\n`);
|
|
782
|
+
}
|
|
709
783
|
continue;
|
|
710
784
|
}
|
|
711
|
-
total += searchSource(
|
|
785
|
+
total += searchSource(buf.toString('utf8'), f, showLabel, opts, out, room());
|
|
712
786
|
if (opts.quiet && total > 0) break; // -q: first match decides the status
|
|
713
787
|
}
|
|
714
788
|
}
|
package/man/cssgrep.1
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.\" Man page for cssgrep. Keep the OPTIONS section in sync with the USAGE
|
|
2
2
|
.\" string in index.js.
|
|
3
|
-
.TH CSSGREP 1 "2026-
|
|
3
|
+
.TH CSSGREP 1 "2026-07-02" "cssgrep 1.1.0" "User Commands"
|
|
4
4
|
.SH NAME
|
|
5
5
|
cssgrep \- search HTML by CSS selector, grep-style
|
|
6
6
|
.SH SYNOPSIS
|
|
@@ -33,7 +33,13 @@ Input is read from the
|
|
|
33
33
|
.I file
|
|
34
34
|
arguments, recursively from directories with
|
|
35
35
|
.BR \-r ,
|
|
36
|
-
or from standard input when no path is given.
|
|
36
|
+
or from standard input when no path is given. Binary files (a NUL byte or a
|
|
37
|
+
high ratio of control bytes in the first 8 KB) are skipped with a note on
|
|
38
|
+
standard error, suppressible with
|
|
39
|
+
.B \-s
|
|
40
|
+
or
|
|
41
|
+
.BR \-q .
|
|
42
|
+
One line is printed per match.
|
|
37
43
|
As with
|
|
38
44
|
.BR grep ,
|
|
39
45
|
the matched line is printed on its own; a
|
|
@@ -48,17 +54,35 @@ locator appears only with
|
|
|
48
54
|
Recurse into directory arguments (defaults to the current directory if none
|
|
49
55
|
are given).
|
|
50
56
|
.TP
|
|
57
|
+
.BI \-\-max\-depth " n"
|
|
58
|
+
Limit
|
|
59
|
+
.B \-r
|
|
60
|
+
recursion depth.
|
|
61
|
+
.I 1
|
|
62
|
+
means the given directory only (no subdirectories).
|
|
63
|
+
.TP
|
|
51
64
|
.BI \-\-ext " list"
|
|
52
65
|
Comma-separated extensions to scan with
|
|
53
66
|
.B \-r
|
|
54
67
|
(default:
|
|
55
68
|
.IR html,htm ).
|
|
56
69
|
.TP
|
|
70
|
+
.BI \-\-include " glob"
|
|
71
|
+
Only search files whose name/path matches
|
|
72
|
+
.I glob
|
|
73
|
+
while recursing (repeatable). Replaces
|
|
74
|
+
.BR \-\-ext ;
|
|
75
|
+
the two cannot be combined.
|
|
76
|
+
.TP
|
|
57
77
|
.BR \-i ", " \-\-ignore " \fIglob\fR"
|
|
58
78
|
Skip files/dirs matching
|
|
59
79
|
.I glob
|
|
60
80
|
while recursing (repeatable).
|
|
61
81
|
.TP
|
|
82
|
+
.BI \-\-exclude " glob"
|
|
83
|
+
Alias for
|
|
84
|
+
.BR \-\-ignore .
|
|
85
|
+
.TP
|
|
62
86
|
.BI \-\-ignore\-file " path"
|
|
63
87
|
Read ignore globs from
|
|
64
88
|
.IR path ,
|
|
@@ -139,10 +163,19 @@ Print only the names of files with no match.
|
|
|
139
163
|
.BR \-q ", " \-\-quiet
|
|
140
164
|
Print nothing; exit 0 on the first match, 1 if none.
|
|
141
165
|
.TP
|
|
166
|
+
.BR \-s ", " \-\-no\-messages
|
|
167
|
+
Suppress error messages for unreadable or missing files.
|
|
168
|
+
.TP
|
|
142
169
|
.BR \-0 ", " \-\-null
|
|
143
170
|
Separate the file name with a NUL byte (for
|
|
144
171
|
.BR "xargs \-0" ).
|
|
145
172
|
.TP
|
|
173
|
+
.BR \-H ", " \-\-with\-filename
|
|
174
|
+
Always print the file name prefix, even for a single file or standard input.
|
|
175
|
+
.TP
|
|
176
|
+
.B \-\-no\-filename
|
|
177
|
+
Never print the file name prefix, even when searching multiple files.
|
|
178
|
+
.TP
|
|
146
179
|
.BR \-\-color [ =\fIwhen\fR ]
|
|
147
180
|
Colorize output:
|
|
148
181
|
.I auto
|
|
@@ -169,6 +202,12 @@ Long options take a value with
|
|
|
169
202
|
.B =
|
|
170
203
|
or as the next word
|
|
171
204
|
.RB ( \-\-max\-width=100 ", " "\-\-ext htm" ).
|
|
205
|
+
.PP
|
|
206
|
+
Globs for
|
|
207
|
+
.BR \-\-include ", " \-\-ignore ", and " \-\-exclude
|
|
208
|
+
support
|
|
209
|
+
.BR * " (within a path segment), " ** " (across " / ),
|
|
210
|
+
.BR ? " (one non-slash char), and brace alternation like " *.{html,htm} .
|
|
172
211
|
.SH EXIT STATUS
|
|
173
212
|
.TP
|
|
174
213
|
.B 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cssgrep",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Search HTML by CSS selector and print matches grep-style (file:line:col with -n).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"index.js",
|
|
12
12
|
"README.md",
|
|
13
|
+
"CHANGELOG.md",
|
|
13
14
|
"LICENSE",
|
|
14
15
|
"man/cssgrep.1",
|
|
15
16
|
"completions/"
|