pseek 3.6.2__tar.gz → 3.7.1__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pseek
3
- Version: 3.6.2
3
+ Version: 3.7.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -51,6 +51,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
51
51
  * [Fuzzy](#fuzzy-search)
52
52
  * [Fuzzy Similarity Threshold](#fuzzy-similarity-threshold)
53
53
  * [Full Path](#full-path)
54
+ * [Line](#line)
54
55
  * [Expression Queries](#expression-queries)
55
56
  * [Operators](#supported-operators)
56
57
  * [AND](#and)
@@ -63,6 +64,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
63
64
  * [Whole Word](#whole-word)
64
65
  * [Fuzzy](#fuzzy)
65
66
  * [Full Path](#full-path-1)
67
+ * [Line](#line-1)
66
68
  * [Combined Prefixes](#combined-prefixes)
67
69
  * [Automatic Filtering](#automatic-filtering)
68
70
  * [Manual Filtering](#manual-filtering)
@@ -215,6 +217,7 @@ psk --exclude build
215
217
  | `--expr` | Enable boolean query expressions. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"`. Prefixes: `r=regex`, `c=case-sensitive`, `w=whole-word`, `f=fuzzy` |
216
218
  | `--context` | Show N context lines before and after matches, or use N:M to specify before:after |
217
219
  | `--full-path` | By default, query is only matched against the filename or directory name (this applies only when searching for files or directories, not file contents). Using this flag, query is matched against the full path (and, if `--absolute-path` flag is enabled, against the absolute path) |
220
+ | `--line` | Match lines where the entire line participates in a match |
218
221
  | `--depth` | Limit directory traversal to given depth range. By default, there is no limit on search depth |
219
222
  | `--timeout` | Stop the search after the specified number of seconds |
220
223
  | `--fuzzy` | Enable fuzzy search (Highlighting and counting matches are disabled in this mode if `--word` is not enabled to prevent the program from slowing down). except when `--expr` is enabled, in which case you can make it fuzzy by putting `f` before term: `f"foo"` |
@@ -270,6 +273,8 @@ psk backup --directory
270
273
  psk TODO --content
271
274
  ```
272
275
 
276
+ > **Note:** When searching through file contents, binary files are automatically ignored, and this behavior cannot be changed. If null bytes are present within the first 8 KiB of a file, it is identified as a binary file (The entire file is not examined, in order to increase speed).
277
+
273
278
  ### Search Everywhere
274
279
 
275
280
  ```bash
@@ -399,6 +404,39 @@ With `--full-path`, you can match `fd/f` against the entry `fd/file.txt` inside
399
404
 
400
405
  > **Note:** For ordinary queries without `--regex`, backslashes are treated literally, so Windows paths can be searched without additional escaping. When using `--expr`, quoted terms use JSON-style escaping; escape backslashes accordingly. For example, `"\\"` represents a single literal backslash, while `"\\n"` represents the literal sequence `\n`.
401
406
 
407
+ ### Line
408
+
409
+ Match the entire line or selected path instead of matching a pattern anywhere within it.
410
+
411
+ ```bash
412
+ # Match lines exactly equal to "TODO"
413
+ psk TODO --line
414
+
415
+ # Match only lines containing exactly "foo" or "bar"
416
+ psk 'foo|bar' --regex --line
417
+
418
+ # Match files whose names are exactly "cli.py"
419
+ psk "cli.py" --file --line
420
+ ```
421
+
422
+ > **Note:** If both options are specified, `--line` takes precedence over `--word`. In this case, instead of checking individual word boundaries, the entire line or selected path is matched.
423
+
424
+ By default, Pseek matches a file's or directory's **name**. Use `--full-path` to match against its full path instead.
425
+
426
+ ```bash
427
+ # Match files named "cli.py"
428
+ psk "cli.py" --file --line
429
+
430
+ # Match the full relative path "pseek/cli.py"
431
+ psk "pseek/cli.py" --file --full-path --line
432
+
433
+ # Match the full absolute path
434
+ psk "/home/python/cli/pseek/utils.py" \
435
+ --file --full-path --absolute-path --line
436
+ ```
437
+
438
+ > **Note:** Leading and trailing whitespace is significant. `--line` does not trim the target before matching. Use an appropriate regular expression, such as `\s*TODO\s*` with `--line`, if surrounding whitespace should be ignored.
439
+
402
440
  ## Expression Queries
403
441
 
404
442
  Expression mode enables logical search expressions.
@@ -453,9 +491,7 @@ Each term can have its own search mode.
453
491
 
454
492
  #### Regex
455
493
 
456
- ```text
457
- r"pattern"
458
- ```
494
+ Usage: `r"pattern"`
459
495
 
460
496
  Example:
461
497
 
@@ -465,9 +501,7 @@ psk 'r"error\d+"' --expr
465
501
 
466
502
  #### Case Sensitive
467
503
 
468
- ```text
469
- c"text"
470
- ```
504
+ Usage: `c"text"`
471
505
 
472
506
  Example:
473
507
 
@@ -477,9 +511,7 @@ psk 'c"Error"' --expr
477
511
 
478
512
  #### Whole Word
479
513
 
480
- ```text
481
- w"text"
482
- ```
514
+ Usage: `w"text"`
483
515
 
484
516
  Example:
485
517
 
@@ -489,9 +521,7 @@ psk 'w"cat"' --expr
489
521
 
490
522
  #### Fuzzy
491
523
 
492
- ```text
493
- f"text"
494
- ```
524
+ Usage: `f"text"`
495
525
 
496
526
  Example:
497
527
 
@@ -501,9 +531,7 @@ psk 'f"apple"' --expr
501
531
 
502
532
  #### Full Path
503
533
 
504
- ```text
505
- p"text"
506
- ```
534
+ Usage: `p"text"`
507
535
 
508
536
  Example:
509
537
 
@@ -511,6 +539,16 @@ Example:
511
539
  psk 'p"lib"' --expr
512
540
  ```
513
541
 
542
+ #### Line
543
+
544
+ Usage: `l"text"`
545
+
546
+ Example:
547
+
548
+ ```bash
549
+ psk 'l"foo"' --expr
550
+ ```
551
+
514
552
  #### Combined Prefixes
515
553
 
516
554
  Prefixes can be combined.
@@ -538,9 +576,7 @@ Meaning:
538
576
 
539
577
  simultaneously.
540
578
 
541
- Allowed modes: `r`, `c`, `w`, `f`, `p`, `rp`, `pc`, `wp`, `fp`, `rc`, `cw`, `cf`, `wf`, `cwf`, `rcp`, `cwp`, `cfp`, `wfp`, `cwfp`
542
-
543
- and their permutations.
579
+ All modes are permitted, except for those where regex and word, or regex and fuzzy, are used simultaneously.
544
580
 
545
581
  > **Note:** Whole word matching and regex matching cannot be used at the same time, because we can use `\b` in regex to enable whole word matching: `r"\btext\b"`
546
582
 
@@ -723,12 +759,12 @@ Controls how deep the search goes inside directories.
723
759
  A depth of `0` means that only base path's immediate contents are searched; subdirectories are not entered.
724
760
 
725
761
  ```text
726
- project/ ← base directory
727
- ├── file.txt ← depth 0
728
- ├── folder1/ ← depth 0
729
- │ ├── file.txt ← depth 1
730
- │ └── folder2/ ← depth 1
731
- │ └── file.txt ← depth 2
762
+ project/ <- base directory
763
+ ├── file.txt <- depth 0
764
+ ├── folder1/ <- depth 0
765
+ │ ├── file.txt <- depth 1
766
+ │ └── folder2/ <- depth 1
767
+ │ └── file.txt <- depth 2
732
768
  ```
733
769
 
734
770
  `--depth` can be specified multiple times to search non-contiguous depth ranges:
@@ -931,7 +967,7 @@ If the search exceeds the limit, it will be terminated. Results found before the
931
967
  Display a summary of the search, including result counts, scanned paths, archive information, and search time.
932
968
 
933
969
  ```bash
934
- pseek config --stats
970
+ pseek config --archive --stats
935
971
  ```
936
972
 
937
973
  Example output:
@@ -940,7 +976,7 @@ Example output:
940
976
  Statistics
941
977
  ────────────────
942
978
  Results
943
- Files: 12
979
+ Files matched: 12
944
980
  Directories matched: 3
945
981
  Files with matched content: 8
946
982
  Lines matched: 24
@@ -23,6 +23,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
23
23
  * [Fuzzy](#fuzzy-search)
24
24
  * [Fuzzy Similarity Threshold](#fuzzy-similarity-threshold)
25
25
  * [Full Path](#full-path)
26
+ * [Line](#line)
26
27
  * [Expression Queries](#expression-queries)
27
28
  * [Operators](#supported-operators)
28
29
  * [AND](#and)
@@ -35,6 +36,7 @@ Fast and powerful command-line search tool for finding files, directories, and t
35
36
  * [Whole Word](#whole-word)
36
37
  * [Fuzzy](#fuzzy)
37
38
  * [Full Path](#full-path-1)
39
+ * [Line](#line-1)
38
40
  * [Combined Prefixes](#combined-prefixes)
39
41
  * [Automatic Filtering](#automatic-filtering)
40
42
  * [Manual Filtering](#manual-filtering)
@@ -187,6 +189,7 @@ psk --exclude build
187
189
  | `--expr` | Enable boolean query expressions. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"`. Prefixes: `r=regex`, `c=case-sensitive`, `w=whole-word`, `f=fuzzy` |
188
190
  | `--context` | Show N context lines before and after matches, or use N:M to specify before:after |
189
191
  | `--full-path` | By default, query is only matched against the filename or directory name (this applies only when searching for files or directories, not file contents). Using this flag, query is matched against the full path (and, if `--absolute-path` flag is enabled, against the absolute path) |
192
+ | `--line` | Match lines where the entire line participates in a match |
190
193
  | `--depth` | Limit directory traversal to given depth range. By default, there is no limit on search depth |
191
194
  | `--timeout` | Stop the search after the specified number of seconds |
192
195
  | `--fuzzy` | Enable fuzzy search (Highlighting and counting matches are disabled in this mode if `--word` is not enabled to prevent the program from slowing down). except when `--expr` is enabled, in which case you can make it fuzzy by putting `f` before term: `f"foo"` |
@@ -242,6 +245,8 @@ psk backup --directory
242
245
  psk TODO --content
243
246
  ```
244
247
 
248
+ > **Note:** When searching through file contents, binary files are automatically ignored, and this behavior cannot be changed. If null bytes are present within the first 8 KiB of a file, it is identified as a binary file (The entire file is not examined, in order to increase speed).
249
+
245
250
  ### Search Everywhere
246
251
 
247
252
  ```bash
@@ -371,6 +376,39 @@ With `--full-path`, you can match `fd/f` against the entry `fd/file.txt` inside
371
376
 
372
377
  > **Note:** For ordinary queries without `--regex`, backslashes are treated literally, so Windows paths can be searched without additional escaping. When using `--expr`, quoted terms use JSON-style escaping; escape backslashes accordingly. For example, `"\\"` represents a single literal backslash, while `"\\n"` represents the literal sequence `\n`.
373
378
 
379
+ ### Line
380
+
381
+ Match the entire line or selected path instead of matching a pattern anywhere within it.
382
+
383
+ ```bash
384
+ # Match lines exactly equal to "TODO"
385
+ psk TODO --line
386
+
387
+ # Match only lines containing exactly "foo" or "bar"
388
+ psk 'foo|bar' --regex --line
389
+
390
+ # Match files whose names are exactly "cli.py"
391
+ psk "cli.py" --file --line
392
+ ```
393
+
394
+ > **Note:** If both options are specified, `--line` takes precedence over `--word`. In this case, instead of checking individual word boundaries, the entire line or selected path is matched.
395
+
396
+ By default, Pseek matches a file's or directory's **name**. Use `--full-path` to match against its full path instead.
397
+
398
+ ```bash
399
+ # Match files named "cli.py"
400
+ psk "cli.py" --file --line
401
+
402
+ # Match the full relative path "pseek/cli.py"
403
+ psk "pseek/cli.py" --file --full-path --line
404
+
405
+ # Match the full absolute path
406
+ psk "/home/python/cli/pseek/utils.py" \
407
+ --file --full-path --absolute-path --line
408
+ ```
409
+
410
+ > **Note:** Leading and trailing whitespace is significant. `--line` does not trim the target before matching. Use an appropriate regular expression, such as `\s*TODO\s*` with `--line`, if surrounding whitespace should be ignored.
411
+
374
412
  ## Expression Queries
375
413
 
376
414
  Expression mode enables logical search expressions.
@@ -425,9 +463,7 @@ Each term can have its own search mode.
425
463
 
426
464
  #### Regex
427
465
 
428
- ```text
429
- r"pattern"
430
- ```
466
+ Usage: `r"pattern"`
431
467
 
432
468
  Example:
433
469
 
@@ -437,9 +473,7 @@ psk 'r"error\d+"' --expr
437
473
 
438
474
  #### Case Sensitive
439
475
 
440
- ```text
441
- c"text"
442
- ```
476
+ Usage: `c"text"`
443
477
 
444
478
  Example:
445
479
 
@@ -449,9 +483,7 @@ psk 'c"Error"' --expr
449
483
 
450
484
  #### Whole Word
451
485
 
452
- ```text
453
- w"text"
454
- ```
486
+ Usage: `w"text"`
455
487
 
456
488
  Example:
457
489
 
@@ -461,9 +493,7 @@ psk 'w"cat"' --expr
461
493
 
462
494
  #### Fuzzy
463
495
 
464
- ```text
465
- f"text"
466
- ```
496
+ Usage: `f"text"`
467
497
 
468
498
  Example:
469
499
 
@@ -473,9 +503,7 @@ psk 'f"apple"' --expr
473
503
 
474
504
  #### Full Path
475
505
 
476
- ```text
477
- p"text"
478
- ```
506
+ Usage: `p"text"`
479
507
 
480
508
  Example:
481
509
 
@@ -483,6 +511,16 @@ Example:
483
511
  psk 'p"lib"' --expr
484
512
  ```
485
513
 
514
+ #### Line
515
+
516
+ Usage: `l"text"`
517
+
518
+ Example:
519
+
520
+ ```bash
521
+ psk 'l"foo"' --expr
522
+ ```
523
+
486
524
  #### Combined Prefixes
487
525
 
488
526
  Prefixes can be combined.
@@ -510,9 +548,7 @@ Meaning:
510
548
 
511
549
  simultaneously.
512
550
 
513
- Allowed modes: `r`, `c`, `w`, `f`, `p`, `rp`, `pc`, `wp`, `fp`, `rc`, `cw`, `cf`, `wf`, `cwf`, `rcp`, `cwp`, `cfp`, `wfp`, `cwfp`
514
-
515
- and their permutations.
551
+ All modes are permitted, except for those where regex and word, or regex and fuzzy, are used simultaneously.
516
552
 
517
553
  > **Note:** Whole word matching and regex matching cannot be used at the same time, because we can use `\b` in regex to enable whole word matching: `r"\btext\b"`
518
554
 
@@ -695,12 +731,12 @@ Controls how deep the search goes inside directories.
695
731
  A depth of `0` means that only base path's immediate contents are searched; subdirectories are not entered.
696
732
 
697
733
  ```text
698
- project/ ← base directory
699
- ├── file.txt ← depth 0
700
- ├── folder1/ ← depth 0
701
- │ ├── file.txt ← depth 1
702
- │ └── folder2/ ← depth 1
703
- │ └── file.txt ← depth 2
734
+ project/ <- base directory
735
+ ├── file.txt <- depth 0
736
+ ├── folder1/ <- depth 0
737
+ │ ├── file.txt <- depth 1
738
+ │ └── folder2/ <- depth 1
739
+ │ └── file.txt <- depth 2
704
740
  ```
705
741
 
706
742
  `--depth` can be specified multiple times to search non-contiguous depth ranges:
@@ -903,7 +939,7 @@ If the search exceeds the limit, it will be terminated. Results found before the
903
939
  Display a summary of the search, including result counts, scanned paths, archive information, and search time.
904
940
 
905
941
  ```bash
906
- pseek config --stats
942
+ pseek config --archive --stats
907
943
  ```
908
944
 
909
945
  Example output:
@@ -912,7 +948,7 @@ Example output:
912
948
  Statistics
913
949
  ────────────────
914
950
  Results
915
- Files: 12
951
+ Files matched: 12
916
952
  Directories matched: 3
917
953
  Files with matched content: 8
918
954
  Lines matched: 24
@@ -49,7 +49,7 @@ def build_highlight(text, matches):
49
49
  return ''.join(parts)
50
50
 
51
51
 
52
- def echo(results: dict, context: bool):
52
+ def echo(results: dict, context: tuple[int, int]):
53
53
  """Display results with a specific format and color scheme"""
54
54
  for match_type, datas in results.items():
55
55
  if datas:
@@ -258,7 +258,7 @@ def search_with_timeout(config):
258
258
  time.sleep(0.0005)
259
259
 
260
260
 
261
- def check_rar_backend(archive_enabled: bool, tool_path: str, backend: str):
261
+ def check_rar_backend(archive_enabled: bool, tool_path: str | None, backend: str):
262
262
  """Check for the existence of rar backend or save and set it for rarfile"""
263
263
 
264
264
  # Save backend path for later executions
@@ -347,6 +347,8 @@ def check_rar_backend(archive_enabled: bool, tool_path: str, backend: str):
347
347
  '(this applies only when searching for files or directories, not file contents). '
348
348
  'Using this flag, query is matched against the full path '
349
349
  '(and, if --absolute-path flag is enabled, against the absolute path).')
350
+ @click.option('--line', is_flag=True,
351
+ help='Match lines where the entire line participates in a match.')
350
352
  @click.option('-D', '--depth', multiple=True, metavar='MIN:MAX',
351
353
  help='Limit directory traversal to given depth range. '
352
354
  'By default, there is no limit on search depth.')
@@ -440,10 +442,10 @@ def search(**kwargs):
440
442
 
441
443
  check_rar_backend(config.archive, config.rar_backend, config.query)
442
444
 
443
- if not config.expr and config.fuzzy:
445
+ if not config.expr and config.fuzzy and not config.line:
444
446
  if not config.word:
445
447
  click.secho(
446
- "Warning: Fuzzy substring highlighting and counting matches are disabled to improve performance.",
448
+ "Warning: To improve performance, highlighting of matches is disabled.",
447
449
  fg="yellow"
448
450
  )
449
451
  elif config.word and " " in config.query:
@@ -26,7 +26,8 @@ class TermNode(ExprNode):
26
26
  case_sensitive: bool,
27
27
  fuzzy: bool,
28
28
  fuzzy_level: int | None,
29
- full_path: bool
29
+ full_path: bool,
30
+ whole_line: bool
30
31
  ):
31
32
  self.raw_term = term
32
33
  self.term_lower = term.lower()
@@ -36,6 +37,7 @@ class TermNode(ExprNode):
36
37
  self.fuzzy = fuzzy
37
38
  self.fuzzy_level = fuzzy_level
38
39
  self.full_path = full_path
40
+ self.whole_line = whole_line
39
41
 
40
42
  if not fuzzy:
41
43
  flags = 0 if case_sensitive else re.IGNORECASE # Adjust case sensitivity
@@ -44,7 +46,7 @@ class TermNode(ExprNode):
44
46
  if not regex:
45
47
  term = re.escape(term) # Escape if not regex
46
48
  # Apply whole-word matching only if not using regex (or if desired behavior is defined)
47
- if whole_word:
49
+ if whole_word and not whole_line:
48
50
  term = r'\b' + term + r'\b'
49
51
 
50
52
  self.pattern = compile_regex(term, flags) # Precompile the regex pattern for performance
@@ -60,20 +62,29 @@ class TermNode(ExprNode):
60
62
  if not self.fuzzy:
61
63
  # Use substring search for more speed
62
64
  if not self.regex and not self.whole_word:
65
+ if self.whole_line:
66
+ return term == text_cmp
63
67
  return term in text_cmp
64
68
 
65
- return bool(self.pattern.search(text))
69
+ return bool(self.pattern.search(text)) \
70
+ if not self.whole_line \
71
+ else bool(self.pattern.fullmatch(text))
72
+
73
+ if self.whole_line:
74
+ score = fuzz.ratio(term, text_cmp)
75
+ return score >= self.fuzzy_level
66
76
 
67
77
  if self.whole_word:
68
78
  words = re.findall(r'\w+', text_cmp)
69
79
  return any(fuzz.ratio(term, word) >= self.fuzzy_level for word in words)
80
+
81
+ # Use the correct method depending on the len of str to increase accuracy and avoid illogical matching
82
+ if len(text_cmp) > len(term):
83
+ score = fuzz.partial_ratio(term, text_cmp)
70
84
  else:
71
- # Use the correct method depending on the len of str to increase accuracy and avoid illogical matching
72
- if len(text_cmp) > len(term):
73
- score = fuzz.partial_ratio(term, text_cmp)
74
- else:
75
- score = fuzz.ratio(term, text_cmp)
76
- return score >= self.fuzzy_level
85
+ score = fuzz.ratio(term, text_cmp)
86
+
87
+ return score >= self.fuzzy_level
77
88
 
78
89
  def get_binary_pattern(self):
79
90
  """
@@ -129,25 +140,8 @@ class OrNode(ExprNode):
129
140
  return self.left.evaluate(text) or self.right.evaluate(text)
130
141
 
131
142
 
132
- def gvp() -> str:
133
- """Generate all possible states and permutations of input prefixes
134
-
135
- Returns:
136
- str: prefixes=("f", "r", "rf") -> result="f|r|rf|fr"
137
- """
138
-
139
- prefixes = {'r','c','w','f','p','rp','pc','wp','fp','rc','cw',
140
- 'cf','wf','cwf','rcp','cwp','cfp','wfp','cwfp'}
141
- valid_prefixes = set()
142
-
143
- for p in prefixes:
144
- valid_prefixes |= {''.join(pr) for pr in permutations(p)}
145
-
146
- return '|'.join(valid_prefixes)
147
-
148
-
149
143
  # Lark grammar for parsing logical expressions
150
- query_grammar = rf"""
144
+ query_grammar = r"""
151
145
  ?start: expr
152
146
 
153
147
  ?expr: or_expr
@@ -165,7 +159,7 @@ query_grammar = rf"""
165
159
  | ESCAPED_STRING -> string
166
160
  | "(" expr ")"
167
161
 
168
- PREFIXED_STRING: /({gvp()})"([^"\\]|\\.)*"/
162
+ PREFIXED_STRING: /[rcwfpl]+"([^"\\]|\\.)*"/
169
163
 
170
164
  %import common.ESCAPED_STRING
171
165
  %import common.WS
@@ -204,6 +198,7 @@ class TreeToExpr(Transformer):
204
198
  False,
205
199
  False,
206
200
  None,
201
+ False,
207
202
  False
208
203
  )
209
204
 
@@ -213,6 +208,14 @@ class TreeToExpr(Transformer):
213
208
 
214
209
  quote_index = text.index('"')
215
210
  prefix = text[:quote_index]
211
+
212
+ if len(prefix) != len(set(prefix)):
213
+ raise ValueError(f'Duplicate prefix flag: {prefix}')
214
+ if 'r' in prefix and 'f' in prefix:
215
+ raise ValueError('Regex and fuzzy prefixes cannot be combined.')
216
+ if 'r' in prefix and 'w' in prefix:
217
+ raise ValueError('Regex and whole-word prefixes cannot be combined. To use whole-word matching in regex, use \\b: r"\\btext\\b"')
218
+
216
219
  quoted = text[quote_index:]
217
220
 
218
221
  is_regex = 'r' in prefix
@@ -225,7 +228,8 @@ class TreeToExpr(Transformer):
225
228
  case_sensitive='c' in prefix,
226
229
  fuzzy='f' in prefix,
227
230
  fuzzy_level=self.fuzzy_level,
228
- full_path='p' in prefix
231
+ full_path='p' in prefix,
232
+ whole_line='l' in prefix
229
233
  )
230
234
 
231
235
  def and_expr(self, args):
@@ -238,7 +242,7 @@ class TreeToExpr(Transformer):
238
242
  return NotNode(args[0])
239
243
 
240
244
 
241
- def parse_query_expression(config) -> ExprNode:
245
+ def parse_query_expression(config) -> ExprNode | None:
242
246
  """
243
247
  Function to parse the query and return expression tree.
244
248
  If expr is False, treat the whole query as a single term.
@@ -255,7 +259,8 @@ def parse_query_expression(config) -> ExprNode:
255
259
  config.case_sensitive,
256
260
  config.fuzzy,
257
261
  config.fuzzy_level,
258
- config.full_path
262
+ config.full_path,
263
+ config.line
259
264
  )
260
265
 
261
266
  # Otherwise, parse using Lark
@@ -276,7 +281,7 @@ def find_term_matches(node: TermNode, text: str | Path, num: int) -> list[tuple[
276
281
 
277
282
  if node.fuzzy:
278
283
  # Fuzzy partial matching currently has no span extraction
279
- if not node.whole_word:
284
+ if not node.whole_word and not node.whole_line:
280
285
  return []
281
286
 
282
287
  text_cmp = (
@@ -288,6 +293,11 @@ def find_term_matches(node: TermNode, text: str | Path, num: int) -> list[tuple[
288
293
  else node.term_lower
289
294
  )
290
295
 
296
+ if node.whole_line:
297
+ if fuzz.ratio(term, text_cmp) >= node.fuzzy_level:
298
+ return [(num, len(text) + num)]
299
+ return []
300
+
291
301
  matches = []
292
302
 
293
303
  for match in re.finditer(r'\w+', text_cmp):
@@ -301,6 +311,9 @@ def find_term_matches(node: TermNode, text: str | Path, num: int) -> list[tuple[
301
311
 
302
312
  return matches
303
313
 
314
+ if node.whole_line and node.pattern.fullmatch(text) is not None:
315
+ return [(num, len(text) + num)]
316
+
304
317
  return [
305
318
  (match.start() + num, match.end() + num)
306
319
  for match in node.pattern.finditer(text)
@@ -7,6 +7,8 @@ from itertools import islice
7
7
  from pathlib import Path
8
8
  from threading import RLock
9
9
 
10
+ import click
11
+
10
12
  from pseek import _pignore
11
13
 
12
14
  from .archive import ARCHIVE_EXTS, extract_names_from_archive, extract_text_from_archive
@@ -378,6 +380,20 @@ def is_symlink_valid(entry):
378
380
  return True
379
381
 
380
382
 
383
+ def needs_ignore_matcher(config) -> bool:
384
+ # In cases where all paths must be searched and
385
+ # there are no filtering rules,
386
+ # there is no need to create matcher.
387
+ return (
388
+ not config.hidden
389
+ or not config.no_git_ignore
390
+ or not config.no_ignore_dot
391
+ or not config.no_ignore_exclude
392
+ or not config.no_ignore_global
393
+ or bool(config.glob)
394
+ )
395
+
396
+
381
397
  def walk_logic(path: Path, config, depth: int, matcher, ancestors: set):
382
398
  """Recursively walk a directory while pruning excluded subtrees"""
383
399
 
@@ -411,18 +427,27 @@ def walk_logic(path: Path, config, depth: int, matcher, ancestors: set):
411
427
  continue
412
428
  is_dir = False
413
429
 
414
- # Ignore rules / hidden filtering / glob logic
415
- absolute_entry = (
416
- Path(os.path.abspath(entry_path))
417
- if not config.absolute_path else entry_path
418
- ).as_posix()
419
- ignored, whitelisted, should_descend = (
420
- matcher.match_path(
421
- absolute_entry,
422
- # We don't need target of symlink; symlink itself is always a file
423
- is_dir if not entry.is_symlink() else False
424
- )
425
- )
430
+ if matcher is None:
431
+ ignored = False
432
+ whitelisted = False
433
+ should_descend = is_dir
434
+ else:
435
+ # Ignore rules / hidden filtering / glob logic
436
+
437
+ absolute_entry = (
438
+ Path(os.path.abspath(entry_path))
439
+ if not config.absolute_path
440
+ else entry_path
441
+ ).as_posix()
442
+
443
+ try:
444
+ ignored, whitelisted, should_descend = matcher.match_path(
445
+ absolute_entry,
446
+ # We don't need target of symlink; symlink itself is always a file
447
+ is_dir if not entry.is_symlink() else False,
448
+ )
449
+ except RuntimeError:
450
+ continue
426
451
 
427
452
  if ignored:
428
453
  continue
@@ -433,7 +458,12 @@ def walk_logic(path: Path, config, depth: int, matcher, ancestors: set):
433
458
  # This flag is used in seek func to indicate that this dir
434
459
  # doesn't need to be displayed, even though it must still
435
460
  # be included in metric calculation.
436
- metric_only = is_dir and matcher.has_positive_globs and not whitelisted
461
+ metric_only = (
462
+ is_dir
463
+ and matcher is not None
464
+ and matcher.has_positive_globs
465
+ and not whitelisted
466
+ )
437
467
 
438
468
  # A symlink may be a valid filename-search candidate even
439
469
  # when its target is not followed or cannot be resolved.
@@ -462,6 +492,7 @@ def walk(config):
462
492
  """Include paths define traversal roots, avoiding unnecessary traversal
463
493
  of unrelated parts of the tree"""
464
494
  roots = config.include or {config.path}
495
+ needs_matcher = needs_ignore_matcher(config)
465
496
 
466
497
  for root in roots:
467
498
  depth = len(root.relative_to(config.path).parts) - 1
@@ -473,18 +504,27 @@ def walk(config):
473
504
  yield root.resolve(), False
474
505
 
475
506
  if root.is_dir() and any(depth < d[1] for d in config.depth):
476
- matcher = _pignore.IgnoreMatcher(
477
- Path(os.path.abspath(root)).as_posix(),
478
- git_ignore=not config.no_git_ignore,
479
- ignore=not config.no_ignore_dot,
480
- git_exclude=not config.no_ignore_exclude,
481
- git_global=not config.no_ignore_global,
482
- parents=not config.no_ignore_parent,
483
- require_git=not config.no_require_git,
484
- hidden=not config.hidden,
485
- globs=list(config.glob) or None,
486
- glob_root=config.path.as_posix()
487
- )
507
+ matcher = None
508
+
509
+ if needs_matcher:
510
+ try:
511
+ matcher = _pignore.IgnoreMatcher(
512
+ Path(os.path.abspath(root)).as_posix(),
513
+ git_ignore=not config.no_git_ignore,
514
+ ignore=not config.no_ignore_dot,
515
+ git_exclude=not config.no_ignore_exclude,
516
+ git_global=not config.no_ignore_global,
517
+ parents=not config.no_ignore_parent,
518
+ require_git=not config.no_require_git,
519
+ hidden=not config.hidden,
520
+ globs=list(config.glob) or None,
521
+ glob_root=config.path.as_posix()
522
+ )
523
+ except ValueError as e:
524
+ raise click.BadParameter(e, param_hint='--glob')
525
+ except RuntimeError as e:
526
+ click.secho(e, fg='red')
527
+ raise click.exceptions.Exit(1)
488
528
 
489
529
  yield from walk_logic(root, config, depth, matcher, set())
490
530
 
@@ -133,6 +133,7 @@ class SearchConfig:
133
133
  case_sensitive: bool
134
134
  regex: bool
135
135
  word: bool
136
+ line: bool
136
137
  expr: bool
137
138
  depth: list[tuple]
138
139
  timeout: int | None
@@ -24,7 +24,7 @@ def compile_regex(txt, flags=0) -> re.Pattern | None:
24
24
  raise click.exceptions.Exit(1)
25
25
 
26
26
 
27
- def get_path_suffix(path: Path | str, is_dir: bool | None = None) -> str:
27
+ def get_path_suffix(path: Path | str, is_dir: bool | None = None) -> str | None:
28
28
  """ If multiple file suffixes are valid, return them, otherwise return only the last suffix """
29
29
  if isinstance(path, str):
30
30
  path = Path(path)
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "pseek"
7
- version = "3.6.2"
7
+ version = "3.7.1"
8
8
  description = "Pseek is a Python CLI toolkit to search files, folders, and text"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes