pseek 2.5.0__tar.gz → 2.5.2__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: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: Pseek is a Python library to search files, folders, and text
5
5
  Author-email: Arian <ariannasiri86@gmail.com>
6
6
  License-Expression: MIT
@@ -44,7 +44,9 @@ pip install pseek
44
44
  ```sh
45
45
  git clone https://github.com/ArianN8610/pysearch.git
46
46
  cd pysearch
47
- pip install click==8.1.8 lark==1.2.2
47
+ python -m venv venv
48
+ [Activate venv]
49
+ pip install click==8.1.8 lark==1.2.2 py7zr==1.0.0 rarfile==4.2 rapidfuzz==3.13.0
48
50
  ```
49
51
 
50
52
  ## Usage
@@ -82,32 +84,33 @@ pseek "error\d+" --regex
82
84
 
83
85
  ## Command Options
84
86
 
85
- | Option | Description |
86
- |--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
87
- | `--path` | Base directory to search in (default: current directory `.`) |
88
- | `--file` | Search only in file names |
89
- | `--directory` | Search only in directory names |
90
- | `--content` | Search inside file contents |
91
- | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
92
- | `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
93
- | `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
94
- | `--include`, `--exclude` | Limit search results to specific set of directories or files |
95
- | `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
96
- | `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
97
- | `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, and case-sensitive features, you can use the prefixes `r`, `w`, and `c` before terms. Allowed modes: `r`, `w`, `c`, `wc`, `cw`, `rc`, `cr`. Examples: `r"foo.*bar"`, `wc"Foo"`, `cr".*Foo"`, ...) |
98
- | `--timeout` | To stop the search after a specified period of time (Seconds) |
99
- | `--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) |
100
- | `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
101
- | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
102
- | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
103
- | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
104
- | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
105
- | `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
106
- | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
107
- | `--full-path` | Display full path of files and directories |
108
- | `--no-content` | Only display files path for content search |
87
+ | Option | Description |
88
+ |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
89
+ | `--path` | Base directory to search in (default: current directory `.`) |
90
+ | `--file` | Search only in file names |
91
+ | `--directory` | Search only in directory names |
92
+ | `--content` | Search inside file contents |
93
+ | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
94
+ | `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
95
+ | `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
96
+ | `--include`, `--exclude` | Limit search results to specific set of directories or files |
97
+ | `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
98
+ | `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
99
+ | `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, case-sensitive, and fuzzy features, you can use the prefixes `r`, `w`, `c`, and `f` before terms. Allowed modes: `r`, `c`, `w`, `f`, `rc`, `cr`, `cw`, `wc`, `cf`, `fc`, `wf`, `fw`, `cwf`, `cfw`, `wcf`, `wfc`, `fcw`, `fwc`. Examples: `r"foo.*bar"`, `wcf"Aple"`, `cr".*Foo"`, ...) |
100
+ | `--timeout` | To stop the search after a specified period of time (Seconds) |
101
+ | `--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"` |
102
+ | `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
103
+ | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
104
+ | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
105
+ | `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
106
+ | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
107
+ | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
108
+ | `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
109
+ | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
110
+ | `--full-path` | Display full path of files and directories |
111
+ | `--no-content` | Only display files path for content search |
109
112
 
110
113
  ## Requirements
111
114
 
112
115
  * Python 3.6 or higher
113
- * `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
116
+ * `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
@@ -27,7 +27,9 @@ pip install pseek
27
27
  ```sh
28
28
  git clone https://github.com/ArianN8610/pysearch.git
29
29
  cd pysearch
30
- pip install click==8.1.8 lark==1.2.2
30
+ python -m venv venv
31
+ [Activate venv]
32
+ pip install click==8.1.8 lark==1.2.2 py7zr==1.0.0 rarfile==4.2 rapidfuzz==3.13.0
31
33
  ```
32
34
 
33
35
  ## Usage
@@ -65,32 +67,33 @@ pseek "error\d+" --regex
65
67
 
66
68
  ## Command Options
67
69
 
68
- | Option | Description |
69
- |--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
70
- | `--path` | Base directory to search in (default: current directory `.`) |
71
- | `--file` | Search only in file names |
72
- | `--directory` | Search only in directory names |
73
- | `--content` | Search inside file contents |
74
- | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
75
- | `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
76
- | `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
77
- | `--include`, `--exclude` | Limit search results to specific set of directories or files |
78
- | `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
79
- | `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
80
- | `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, and case-sensitive features, you can use the prefixes `r`, `w`, and `c` before terms. Allowed modes: `r`, `w`, `c`, `wc`, `cw`, `rc`, `cr`. Examples: `r"foo.*bar"`, `wc"Foo"`, `cr".*Foo"`, ...) |
81
- | `--timeout` | To stop the search after a specified period of time (Seconds) |
82
- | `--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) |
83
- | `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
84
- | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
85
- | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
86
- | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
87
- | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
88
- | `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
89
- | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
90
- | `--full-path` | Display full path of files and directories |
91
- | `--no-content` | Only display files path for content search |
70
+ | Option | Description |
71
+ |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
72
+ | `--path` | Base directory to search in (default: current directory `.`) |
73
+ | `--file` | Search only in file names |
74
+ | `--directory` | Search only in directory names |
75
+ | `--content` | Search inside file contents |
76
+ | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
77
+ | `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
78
+ | `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
79
+ | `--include`, `--exclude` | Limit search results to specific set of directories or files |
80
+ | `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
81
+ | `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
82
+ | `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, case-sensitive, and fuzzy features, you can use the prefixes `r`, `w`, `c`, and `f` before terms. Allowed modes: `r`, `c`, `w`, `f`, `rc`, `cr`, `cw`, `wc`, `cf`, `fc`, `wf`, `fw`, `cwf`, `cfw`, `wcf`, `wfc`, `fcw`, `fwc`. Examples: `r"foo.*bar"`, `wcf"Aple"`, `cr".*Foo"`, ...) |
83
+ | `--timeout` | To stop the search after a specified period of time (Seconds) |
84
+ | `--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"` |
85
+ | `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
86
+ | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
87
+ | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
88
+ | `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
89
+ | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
90
+ | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
91
+ | `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
92
+ | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
93
+ | `--full-path` | Display full path of files and directories |
94
+ | `--no-content` | Only display files path for content search |
92
95
 
93
96
  ## Requirements
94
97
 
95
98
  * Python 3.6 or higher
96
- * `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
99
+ * `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
@@ -46,12 +46,15 @@ def run_search_process(file, directory, content, search_instance):
46
46
  'in which case you can make it match whole word by putting w before term: w"foo")')
47
47
  @click.option('--expr', is_flag=True,
48
48
  help='Enable to write conditions in the query. Example: r"foo.*bar" and ("bar" or "baz") and not "qux" '
49
- '(To use regex, word, and case-sensitive features, '
50
- 'you can use the prefixes r, w, and c before terms. Allowed modes: r, w, c, wc, cw, rc, cr. '
51
- 'Examples: r"foo.*bar", wc"Foo", cr".*Foo", ...)')
49
+ '(To use regex, word, case-sensitive, and fuzzy features, '
50
+ 'you can use the prefixes r, w, c, and f before terms. Allowed modes: '
51
+ 'r, c, w, f, rc, cr, cw, wc, cf, fc, wf, fw, cwf, cfw, wcf, wfc, fcw, fwc. '
52
+ 'Examples: r"foo.*bar", wcf"Aple", cr".*Foo", ...)')
52
53
  @click.option('--timeout', type=click.INT,
53
54
  help='To stop the search after a specified period of time (Seconds)')
54
- @click.option('--fuzzy', is_flag=True, help='Enable fuzzy search (approximate matching).')
55
+ @click.option('--fuzzy', is_flag=True, help='Enable fuzzy search (approximate matching). '
56
+ 'except when --expr is enabled, '
57
+ 'in which case you can make it fuzzy by putting f before term: f"foo"')
55
58
  @click.option('--fuzzy-level', type=click.IntRange(0, 99), default=80, show_default=True,
56
59
  help='Similarity threshold from 0 to 99 for fuzzy search.')
57
60
  # Extension filters
@@ -74,6 +77,8 @@ def run_search_process(file, directory, content, search_instance):
74
77
  # Archive options
75
78
  @click.option('--archive', is_flag=True,
76
79
  help='Enable search within archive files (e.g. zip, rar, 7z, gz, bz2, xz, tar, tar.gz, tar.bz2, tar.xz)')
80
+ @click.option('--depth', type=click.IntRange(min=0), show_default=True,
81
+ help='Maximum archive depth to recurse into (e.g. 2 means only 2 levels).')
77
82
  @click.option('--arc-ext', multiple=True, type=click.STRING,
78
83
  help='Include files with these extensions inside archive files. Example: --arc-ext py --arc-ext js')
79
84
  @click.option('--arc-ee', multiple=True, type=click.STRING,
@@ -91,8 +96,8 @@ def run_search_process(file, directory, content, search_instance):
91
96
  @click.option('--full-path', is_flag=True, help='Display full paths for results.')
92
97
  @click.option('--no-content', is_flag=True, help='Only display files path for content search.')
93
98
  def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex, include, exclude,
94
- re_include, re_exclude, word, expr, timeout, fuzzy, fuzzy_level, max_size, min_size, archive, arc_ext,
95
- arc_ee, arc_inc, arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
99
+ re_include, re_exclude, word, expr, timeout, fuzzy, fuzzy_level, max_size, min_size, archive, depth,
100
+ arc_ext, arc_ee, arc_inc, arc_exc, arc_max, arc_min, rarfb, full_path, no_content):
96
101
  """Search for files, directories, and file content based on the query."""
97
102
 
98
103
  check_rar_backend(archive, rarfb, query)
@@ -105,7 +110,7 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
105
110
  fg="yellow"
106
111
  )
107
112
  )
108
- else:
113
+ elif word and " " in query:
109
114
  click.echo(
110
115
  click.style(
111
116
  'Warning: When using "--fuzzy" and "--word", it is better to have the query be a word and '
@@ -137,6 +142,7 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
137
142
  max_size=max_size,
138
143
  min_size=min_size,
139
144
  archive=archive,
145
+ depth=depth,
140
146
  arc_ext=arc_ext,
141
147
  arc_ee=arc_ee,
142
148
  arc_inc=arc_inc,
@@ -123,7 +123,7 @@ query_grammar = r"""
123
123
  | ESCAPED_STRING -> string
124
124
  | "(" expr ")"
125
125
 
126
- PREFIXED_STRING: /(r|c|w|rc|cr|cw|wc)"([^"\\]|\\.)*"/
126
+ PREFIXED_STRING: /(r|c|w|f|rc|cr|cw|wc|cf|fc|wf|fw|cwf|cfw|wcf|wfc|fcw|fwc)"([^"\\]|\\.)*"/
127
127
 
128
128
  %import common.ESCAPED_STRING
129
129
  %import common.WS
@@ -133,9 +133,8 @@ PREFIXED_STRING: /(r|c|w|rc|cr|cw|wc)"([^"\\]|\\.)*"/
133
133
 
134
134
  class TreeToExpr(Transformer):
135
135
  """Transform parsed tree into expression tree (ExprNode subclasses)"""
136
- def __init__(self, fuzzy, fuzzy_level):
136
+ def __init__(self, fuzzy_level):
137
137
  super().__init__()
138
- self.fuzzy = fuzzy
139
138
  self.fuzzy_level = fuzzy_level
140
139
 
141
140
  def string(self, s):
@@ -146,8 +145,8 @@ class TreeToExpr(Transformer):
146
145
  False,
147
146
  False,
148
147
  False,
149
- self.fuzzy,
150
- self.fuzzy_level
148
+ False,
149
+ None
151
150
  )
152
151
 
153
152
  def prefixed_string(self, s):
@@ -160,7 +159,7 @@ class TreeToExpr(Transformer):
160
159
  regex='r' in prefix,
161
160
  whole_word='w' in prefix,
162
161
  case_sensitive='c' in prefix,
163
- fuzzy=self.fuzzy,
162
+ fuzzy='f' in prefix,
164
163
  fuzzy_level=self.fuzzy_level
165
164
  )
166
165
 
@@ -187,7 +186,7 @@ def parse_query_expression(query: str, expr, regex, whole_word, case_sensitive,
187
186
  parser = Lark(query_grammar, parser="lalr")
188
187
  try:
189
188
  tree = parser.parse(query)
190
- return TreeToExpr(fuzzy, fuzzy_level).transform(tree)
189
+ return TreeToExpr(fuzzy_level).transform(tree)
191
190
  except Exception as e:
192
191
  click.echo(click.style("Query parser error:\n\n", fg='red') + str(e))
193
192
  sys.exit(1)
@@ -223,10 +222,6 @@ def highlight_text(expr: ExprNode, text: str, fuzzy: bool) -> str:
223
222
  elif isinstance(node, NotNode):
224
223
  collect_matches(node.child)
225
224
 
226
- # If fuzzy is enabled but whole_word is False, return plain text
227
- if fuzzy and isinstance(expr, TermNode) and not expr.whole_word:
228
- return text
229
-
230
225
  collect_matches(expr)
231
226
 
232
227
  # Sort and merge overlapping matches (for example, if one match was inside another match)
@@ -1,7 +1,7 @@
1
1
  import mmap
2
2
  import click
3
3
  from pathlib import Path
4
- from .utils import compile_regex, get_archive_path_size, try_decode
4
+ from .utils import compile_regex, get_archive_path_size, try_decode, get_path_suffix
5
5
  from .parser import parse_query_expression, TermNode, highlight_text
6
6
  from concurrent.futures import ThreadPoolExecutor
7
7
  import zipfile
@@ -11,9 +11,10 @@ import gzip
11
11
  import bz2
12
12
  import lzma
13
13
  import rarfile
14
+ import io
14
15
 
15
16
  # Archive extensions that are allowed
16
- ARCHIVE_EXTS = ('zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz', 'tar.gz', 'tar.bz2', 'tar.xz')
17
+ ARCHIVE_EXTS = ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz', 'gz', 'bz2', 'xz')
17
18
 
18
19
  # Extensions that are not suitable for content search (binary, media, etc.)
19
20
  EXCLUDED_EXTENSIONS = (
@@ -30,7 +31,7 @@ EXCLUDED_EXTENSIONS = (
30
31
 
31
32
  class Search:
32
33
  def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, re_include,
33
- re_exclude, whole_word, expr, fuzzy, fuzzy_level, max_size, min_size, archive, arc_ext, arc_ee,
34
+ re_exclude, whole_word, expr, fuzzy, fuzzy_level, max_size, min_size, archive, depth, arc_ext, arc_ee,
34
35
  arc_inc, arc_exc, arc_max, arc_min, full_path, no_content):
35
36
  """Initialize search parameters"""
36
37
  self.base_path = Path(base_path)
@@ -50,6 +51,7 @@ class Search:
50
51
  self.max_size = max_size
51
52
  self.min_size = min_size
52
53
  self.archive = archive
54
+ self.depth = depth
53
55
  self.arc_ext = set(arc_ext)
54
56
  self.arc_ee = set(arc_ee) | {''} if arc_ee else set()
55
57
  self.arc_inc = {Path(p) for p in arc_inc}
@@ -71,10 +73,10 @@ class Search:
71
73
  # If path is inaccessible, skip it.
72
74
  return True
73
75
 
74
- file_ext = ''.join(p_resolved.suffixes)[1:].lower()
76
+ file_ext = get_path_suffix(p_resolved)
75
77
 
76
78
  # Ignore some filters for archive files when archive is enabled
77
- if (not self.archive or not file_ext in ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz')) and \
79
+ if (not self.archive or not file_ext in ARCHIVE_EXTS[:-3]) and \
78
80
  ((search_type in ('file', 'content') and not p_resolved.is_file())
79
81
  or (search_type == 'directory' and not p_resolved.is_dir())):
80
82
  return True
@@ -96,11 +98,10 @@ class Search:
96
98
 
97
99
  return False
98
100
 
99
-
100
101
  def archive_should_skip(self, path_info: Path, search_type: str, is_file: bool, is_dir: bool, p_size: float):
101
102
  """Check whether the file/directory inside archive files should be skipped based on various filters"""
102
103
 
103
- file_ext = ''.join(path_info.suffixes)[1:].lower()
104
+ file_ext = get_path_suffix(path_info)
104
105
 
105
106
  if (search_type in ('file', 'content') and not is_file) \
106
107
  or (search_type == 'directory' and not is_dir):
@@ -122,38 +123,57 @@ class Search:
122
123
 
123
124
  return False
124
125
 
125
- def extract_names_from_archive(self, file_path: Path, search_type: str):
126
- """Extract files and directories name from archive files to search"""
126
+ def extract_names_from_archive(self, file_path: Path, search_type: str,
127
+ file_bytes: bytes = None, parent_label: str = ''):
128
+ """
129
+ Recursively extract files and directories name from archive files.
130
+ Supports nested archives like a.zip::b.7z::c.txt.
131
+
132
+ Parameters:
133
+ file_path (Path): the archive file path
134
+ search_type (str): search type ( file / directory )
135
+ file_bytes (bytes | None): optional byte data if already read (for recursion)
136
+ parent_label (str): string for nested archive tracking like a.zip::b.7z::file.txt
137
+
138
+ Yields:
139
+ (str, Path): tuple of parent label and file or directory name
140
+ """
127
141
 
128
- file_ext = ''.join(file_path.suffixes)[1:].lower()
142
+ file_ext = get_path_suffix(file_path)
143
+ label_prefix = (parent_label + str(file_path) + '::') if parent_label else '::'
129
144
 
130
145
  try:
131
- if file_ext == 'zip':
132
- with zipfile.ZipFile(file_path) as zf:
133
- for info in zf.infolist():
146
+ # Decide the stream source: from disk or memory
147
+ file_stream = io.BytesIO(file_bytes) if file_bytes is not None else open(file_path, 'rb')
148
+
149
+ # Handle ZIP and RAR archives
150
+ if file_ext in ('zip', 'rar'):
151
+ opener = {'zip': zipfile.ZipFile, 'rar': rarfile.RarFile}[file_ext]
152
+ with opener(file_stream) as f:
153
+ for info in f.infolist():
134
154
  name = Path(info.filename)
135
155
  if not self.archive_should_skip(
136
156
  name,
137
157
  search_type,
138
158
  not info.is_dir(),
139
159
  info.is_dir(),
140
- get_archive_path_size(info, 'zip')
160
+ get_archive_path_size(info, file_ext)
141
161
  ):
142
- yield name
143
- elif file_ext == 'rar':
144
- with rarfile.RarFile(file_path) as rf:
145
- for info in rf.infolist():
146
- name = Path(info.filename)
147
- if not self.archive_should_skip(
162
+ yield label_prefix, name
163
+
164
+ # At each recursion, subtract 1 from depth if it's set
165
+ new_depth = None if self.depth is None else self.depth - 1
166
+ # Check if this is a nested archive
167
+ if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
168
+ yield from self.extract_names_from_archive(
148
169
  name,
149
170
  search_type,
150
- not info.is_dir(),
151
- info.is_dir(),
152
- get_archive_path_size(info, 'rar')
153
- ):
154
- yield name
171
+ f.read(info),
172
+ label_prefix
173
+ )
174
+ # Handle 7Z archives
155
175
  elif file_ext == '7z':
156
- with py7zr.SevenZipFile(file_path, mode='r') as z:
176
+ with py7zr.SevenZipFile(file_stream, mode='r') as z:
157
177
  for info in z.list():
158
178
  name = Path(info.filename)
159
179
  if not self.archive_should_skip(
@@ -163,18 +183,31 @@ class Search:
163
183
  info.is_directory,
164
184
  get_archive_path_size(info, '7z')
165
185
  ):
166
- yield name
186
+ yield label_prefix, name
187
+
188
+ new_depth = None if self.depth is None else self.depth - 1
189
+ if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
190
+ file_data = z.read([info.filename]).get(info.filename)
191
+ if file_data is None:
192
+ continue
193
+
194
+ yield from self.extract_names_from_archive(
195
+ name,
196
+ search_type,
197
+ file_data.read(),
198
+ label_prefix
199
+ )
200
+ # Handle TAR and compressed TAR formats
167
201
  elif file_ext in ('tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
168
202
  # Specify the mode based on the file ext to open it
169
- mode = 'r'
170
- if file_ext == 'tar.gz':
171
- mode = 'r:gz'
172
- elif file_ext == 'tar.bz2':
173
- mode = 'r:bz2'
174
- elif file_ext == 'tar.xz':
175
- mode = 'r:xz'
176
-
177
- with tarfile.open(file_path, mode) as tf:
203
+ mode = {
204
+ 'tar': 'r',
205
+ 'tar.gz': 'r:gz',
206
+ 'tar.bz2': 'r:bz2',
207
+ 'tar.xz': 'r:xz'
208
+ }[file_ext]
209
+
210
+ with tarfile.open(fileobj=file_stream, mode=mode) as tf:
178
211
  for member in tf.getmembers():
179
212
  name = Path(member.name)
180
213
  if not self.archive_should_skip(
@@ -184,120 +217,139 @@ class Search:
184
217
  member.isdir(),
185
218
  get_archive_path_size(member, file_ext)
186
219
  ):
187
- yield name
220
+ yield label_prefix, name
221
+
222
+ new_depth = None if self.depth is None else self.depth - 1
223
+ if get_path_suffix(name) in ARCHIVE_EXTS[:-3] and (new_depth is None or new_depth >= 0):
224
+ f = tf.extractfile(member)
225
+ if f is None:
226
+ continue
227
+
228
+ yield from self.extract_names_from_archive(
229
+ name,
230
+ search_type,
231
+ f.read(),
232
+ label_prefix
233
+ )
188
234
  except Exception:
189
235
  return # silently skip invalid archives
190
236
 
191
- def extract_text_from_archive(self, file_path: Path):
237
+ def extract_text_from_archive(self, file_path: Path, file_bytes: bytes = None, parent_label: str = ''):
192
238
  """
193
- Generator yielding (filename, content_text) from archive files.
194
- Supports zip, rar, tar, gz, bz2, xz, 7z.
239
+ Recursively extract (path_label, text_content) from any archive file.
240
+ Supports nested archives like a.zip::b.7z::c.txt.
241
+
242
+ Parameters:
243
+ file_path (Path): the archive file path
244
+ file_bytes (bytes | None): optional byte data if already read (for recursion)
245
+ parent_label (str): string for nested archive tracking like a.zip::b.7z::file.txt
246
+
247
+ Yields:
248
+ (str, str): tuple of full virtual path and decoded content text
195
249
  """
196
250
 
197
- file_ext = ''.join(file_path.suffixes)[1:].lower()
251
+ file_ext = get_path_suffix(file_path)
252
+ label_prefix = (parent_label + str(file_path) + '::') if parent_label else '::'
198
253
 
199
254
  try:
200
- if file_ext == 'zip':
201
- with zipfile.ZipFile(file_path) as zf:
202
- for info in zf.infolist():
203
- file_name = Path(info.filename)
204
- if self.archive_should_skip(
205
- file_name,
206
- 'content',
207
- not info.is_dir(),
208
- info.is_dir(),
209
- get_archive_path_size(info, 'zip')
210
- ):
211
- continue
212
-
213
- with zf.open(info) as f:
214
- data = f.read()
215
- text = try_decode(data)
216
- if text is not None:
217
- yield info.filename, text
218
- elif file_ext == 'rar':
219
- with rarfile.RarFile(file_path) as rf:
220
- for info in rf.infolist():
255
+ # Decide the stream source: from disk or memory
256
+ file_stream = io.BytesIO(file_bytes) if file_bytes is not None else open(file_path, 'rb')
257
+
258
+ # Handle ZIP and RAR archives
259
+ if file_ext in ('zip', 'rar'):
260
+ opener = {'zip': zipfile.ZipFile, 'rar': rarfile.RarFile}[file_ext]
261
+ with opener(file_stream) as f:
262
+ for info in f.infolist():
221
263
  file_name = Path(info.filename)
222
- if self.archive_should_skip(
223
- file_name,
224
- 'content',
225
- not info.is_dir(),
226
- info.is_dir(),
227
- get_archive_path_size(info, 'rar')
228
- ):
229
- continue
264
+ data = f.read(info)
265
+ # At each recursion, subtract 1 from depth if it's set
266
+ new_depth = None if self.depth is None else self.depth - 1
267
+
268
+ # Check if this is a nested archive
269
+ if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
270
+ yield from self.extract_text_from_archive(file_name, data, label_prefix)
271
+ else:
272
+ if self.archive_should_skip(
273
+ file_name,
274
+ 'content',
275
+ not info.is_dir(),
276
+ info.is_dir(),
277
+ get_archive_path_size(info, file_ext)
278
+ ):
279
+ continue
230
280
 
231
- with rf.open(info) as f:
232
- data = f.read()
233
281
  text = try_decode(data)
234
282
  if text is not None:
235
- yield info.filename, text
283
+ yield label_prefix + str(file_name), text
284
+ # Handle 7Z archives
236
285
  elif file_ext == '7z':
237
- with py7zr.SevenZipFile(file_path, mode='r') as archive:
286
+ with py7zr.SevenZipFile(file_stream, mode='r') as archive:
238
287
  for info in archive.list():
239
- file_name = Path(info.filename)
240
- if self.archive_should_skip(
241
- file_name,
242
- 'content',
243
- not info.is_directory,
244
- info.is_directory,
245
- get_archive_path_size(info, '7z')
246
- ):
288
+ file_data = archive.read([info.filename]).get(info.filename)
289
+ if file_data is None:
247
290
  continue
248
291
 
249
- data = archive.read([info.filename])
250
- filedata = data.get(info.filename)
251
- if filedata is None:
252
- continue
253
- text = try_decode(filedata.read())
254
- if text is not None:
255
- yield info.filename, text
292
+ file_name = Path(info.filename)
293
+ data = file_data.read()
294
+ new_depth = None if self.depth is None else self.depth - 1
295
+
296
+ if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
297
+ yield from self.extract_text_from_archive(file_name, data, label_prefix)
298
+ else:
299
+ if self.archive_should_skip(
300
+ file_name,
301
+ 'content',
302
+ not info.is_directory,
303
+ info.is_directory,
304
+ get_archive_path_size(info, '7z')
305
+ ):
306
+ continue
307
+
308
+ text = try_decode(data)
309
+ if text is not None:
310
+ yield label_prefix + str(file_name), text
311
+ # Handle TAR and compressed TAR formats
256
312
  elif file_ext in ('tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
257
- mode = 'r'
258
- if file_ext == 'tar.gz':
259
- mode = 'r:gz'
260
- elif file_ext == 'tar.bz2':
261
- mode = 'r:bz2'
262
- elif file_ext == 'tar.xz':
263
- mode = 'r:xz'
264
- with tarfile.open(file_path, mode) as tf:
313
+ mode = {
314
+ 'tar': 'r',
315
+ 'tar.gz': 'r:gz',
316
+ 'tar.bz2': 'r:bz2',
317
+ 'tar.xz': 'r:xz'
318
+ }[file_ext]
319
+
320
+ with tarfile.open(fileobj=file_stream, mode=mode) as tf:
265
321
  for member in tf.getmembers():
266
- file_name = Path(member.name)
267
- if self.archive_should_skip(
268
- file_name,
269
- 'content',
270
- member.isfile(),
271
- member.isdir(),
272
- get_archive_path_size(member, file_ext)
273
- ):
274
- continue
275
-
276
322
  f = tf.extractfile(member)
277
323
  if f is None:
278
324
  continue
325
+
326
+ file_name = Path(member.name)
279
327
  data = f.read()
280
- text = try_decode(data)
281
- if text is not None:
282
- yield member.name, text
283
- elif file_ext == 'gz':
284
- with gzip.open(file_path, 'rb') as f:
285
- data = f.read()
286
- text = try_decode(data)
287
- if text is not None:
288
- yield file_path.name, text
289
- elif file_ext == 'bz2':
290
- with bz2.open(file_path, 'rb') as f:
291
- data = f.read()
292
- text = try_decode(data)
293
- if text is not None:
294
- yield file_path.name, text
295
- elif file_ext == 'xz':
296
- with lzma.open(file_path, 'rb') as f:
328
+ new_depth = None if self.depth is None else self.depth - 1
329
+
330
+ if get_path_suffix(file_name) in ARCHIVE_EXTS and (new_depth is None or new_depth >= 0):
331
+ yield from self.extract_text_from_archive(file_name, data, label_prefix)
332
+ else:
333
+ if self.archive_should_skip(
334
+ file_name,
335
+ 'content',
336
+ member.isfile(),
337
+ member.isdir(),
338
+ get_archive_path_size(member, file_ext)
339
+ ):
340
+ continue
341
+
342
+ text = try_decode(data)
343
+ if text is not None:
344
+ yield label_prefix + str(file_name), text
345
+ # Handle single compressed files like .gz, .bz2, .xz
346
+ elif file_ext in ARCHIVE_EXTS[-3:]:
347
+ opener = {'gz': gzip.open, 'bz2': bz2.open, 'xz': lzma.open}[file_ext]
348
+ with opener(file_stream, 'rb') as f:
297
349
  data = f.read()
298
350
  text = try_decode(data)
299
351
  if text is not None:
300
- yield file_path.name, text
352
+ yield file_ext, text
301
353
  except Exception:
302
354
  return
303
355
 
@@ -319,7 +371,7 @@ class Search:
319
371
 
320
372
  # Choose parent path based on full_path flag
321
373
  p_parent = p_resolved.parent if self.full_path else p.parent
322
- p_ext = ''.join(p_resolved.suffixes)[1:].lower()
374
+ p_ext = get_path_suffix(p_resolved)
323
375
 
324
376
  if pattern.evaluate(p.name) and not (search_type == 'directory' and p_resolved.is_file()):
325
377
  # Highlight matched query in the name
@@ -327,11 +379,11 @@ class Search:
327
379
  matches.append(f'{p_parent}\\{highlighted_name}')
328
380
 
329
381
  # Search for files and directories name inside archive files if archive is active
330
- if self.archive and p_ext in ('zip', 'rar', '7z', 'tar', 'tar.gz', 'tar.bz2', 'tar.xz'):
331
- for name in self.extract_names_from_archive(p_resolved, search_type):
382
+ if self.archive and p_ext in ARCHIVE_EXTS[:-3]:
383
+ for label, name in self.extract_names_from_archive(p_resolved, search_type):
332
384
  if pattern.evaluate(name.name):
333
385
  highlighted_name = highlight_text(pattern, name.name, self.fuzzy)
334
- matches.append(f'{p_parent}\\{p.name}::{name.parent}\\{highlighted_name}')
386
+ matches.append(f'{p_parent}\\{p.name}{label}{name.parent}\\{highlighted_name}')
335
387
  else: # content search
336
388
  # Use dictionary: key: file path (colored), value: list of line matches
337
389
  matches = {} if not self.no_content else set()
@@ -355,13 +407,14 @@ class Search:
355
407
  file_label = str(file_path.resolve()) if self.full_path else str(file_path)
356
408
 
357
409
  # First, check if the file is an archive, extract it from the archive and perform a search
358
- if self.archive and any(str(file_path).endswith(ext) for ext in ARCHIVE_EXTS):
410
+ if self.archive and get_path_suffix(file_path) in ARCHIVE_EXTS:
359
411
  for fname, content in self.extract_text_from_archive(file_path):
360
412
  if not pattern.evaluate(content) and not self.expr:
361
413
  continue
362
414
 
363
- # Change file_label for archive files
364
- file_label += '::' + fname.replace('/', '\\')
415
+ # Change file_label for archive files (zip, rar, 7z, tar)
416
+ if fname not in ARCHIVE_EXTS[-3:]:
417
+ file_label += fname
365
418
 
366
419
  if self.no_content and not self.expr:
367
420
  matches.add(click.style(file_label, fg='cyan'))
@@ -85,3 +85,7 @@ def check_rar_backend(archive_enabled: bool, tool_path: str, backend: str):
85
85
  rarfile.UNAR_TOOL = tool
86
86
  elif b == '7z':
87
87
  rarfile.SEVENZIP_TOOL = tool
88
+
89
+
90
+ def get_path_suffix(path: Path) -> str:
91
+ return ''.join(path.suffixes)[1:].lower()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pseek
3
- Version: 2.5.0
3
+ Version: 2.5.2
4
4
  Summary: Pseek is a Python library to search files, folders, and text
5
5
  Author-email: Arian <ariannasiri86@gmail.com>
6
6
  License-Expression: MIT
@@ -44,7 +44,9 @@ pip install pseek
44
44
  ```sh
45
45
  git clone https://github.com/ArianN8610/pysearch.git
46
46
  cd pysearch
47
- pip install click==8.1.8 lark==1.2.2
47
+ python -m venv venv
48
+ [Activate venv]
49
+ pip install click==8.1.8 lark==1.2.2 py7zr==1.0.0 rarfile==4.2 rapidfuzz==3.13.0
48
50
  ```
49
51
 
50
52
  ## Usage
@@ -82,32 +84,33 @@ pseek "error\d+" --regex
82
84
 
83
85
  ## Command Options
84
86
 
85
- | Option | Description |
86
- |--------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
87
- | `--path` | Base directory to search in (default: current directory `.`) |
88
- | `--file` | Search only in file names |
89
- | `--directory` | Search only in directory names |
90
- | `--content` | Search inside file contents |
91
- | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
92
- | `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
93
- | `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
94
- | `--include`, `--exclude` | Limit search results to specific set of directories or files |
95
- | `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
96
- | `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
97
- | `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, and case-sensitive features, you can use the prefixes `r`, `w`, and `c` before terms. Allowed modes: `r`, `w`, `c`, `wc`, `cw`, `rc`, `cr`. Examples: `r"foo.*bar"`, `wc"Foo"`, `cr".*Foo"`, ...) |
98
- | `--timeout` | To stop the search after a specified period of time (Seconds) |
99
- | `--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) |
100
- | `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
101
- | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
102
- | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
103
- | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
104
- | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
105
- | `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
106
- | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
107
- | `--full-path` | Display full path of files and directories |
108
- | `--no-content` | Only display files path for content search |
87
+ | Option | Description |
88
+ |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
89
+ | `--path` | Base directory to search in (default: current directory `.`) |
90
+ | `--file` | Search only in file names |
91
+ | `--directory` | Search only in directory names |
92
+ | `--content` | Search inside file contents |
93
+ | `--ext`, `--exclude-ext` | Filter by file extension (e.g., `txt`, `log`) |
94
+ | `--case-sensitive` | Make the search case-sensitive (except when `--expr` is enabled, in which case you can make it case sensitive by putting `c` before term: `c"foo"`) |
95
+ | `--regex` | Use regular expressions to search (except when `--expr` is enabled, in which case you can make it regex by putting `r` before term: `r"foo"`) |
96
+ | `--include`, `--exclude` | Limit search results to specific set of directories or files |
97
+ | `--re-include`, `--re-exclude` | Limit search results to specific directories or files with regex |
98
+ | `--word` | Match the whole word only (except when `--expr` is enabled, in which case you can make it match whole word by putting `w` before term: `w"foo"`) |
99
+ | `--expr` | Enable to write conditions in the query. Example: `r"foo.*bar" and ("bar" or "baz") and not "qux"` (To use regex, word, case-sensitive, and fuzzy features, you can use the prefixes `r`, `w`, `c`, and `f` before terms. Allowed modes: `r`, `c`, `w`, `f`, `rc`, `cr`, `cw`, `wc`, `cf`, `fc`, `wf`, `fw`, `cwf`, `cfw`, `wcf`, `wfc`, `fcw`, `fwc`. Examples: `r"foo.*bar"`, `wcf"Aple"`, `cr".*Foo"`, ...) |
100
+ | `--timeout` | To stop the search after a specified period of time (Seconds) |
101
+ | `--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"` |
102
+ | `--fuzzy_level` | Similarity threshold from 0 to 99 for fuzzy search (default: `80`) |
103
+ | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
104
+ | `--archive` | Enable search within archive files (e.g. `zip`, `rar`, `7z`, `gz`, `bz2`, `xz`, `tar`, `tar.gz`, `tar.bz2`, `tar.xz`) |
105
+ | `--depth` | Maximum archive depth to recurse into (e.g. 2 means only 2 levels) |
106
+ | `--arc-ext`, `--arc-ee` | Filter by file extension inside archive files |
107
+ | `--arc-inc`, `--arc-exc` | Limit search results to specific set of directories or files inside archive files |
108
+ | `--arc-max`, `--arc-min` | Specify maximum and minimum sizes for files inside archive files (It doesn't work for directories because their size is zero in archive files) |
109
+ | `--rarfb` | Path to RAR backend tool (e.g. UnRAR.exe, ...) |
110
+ | `--full-path` | Display full path of files and directories |
111
+ | `--no-content` | Only display files path for content search |
109
112
 
110
113
  ## Requirements
111
114
 
112
115
  * Python 3.6 or higher
113
- * `unrar`, `bsdtar`, `unar` or `7zip` for the rarfile library to support searching inside `.rar` files (optional)
116
+ * `unrar`, `bsdtar`, `unar` or `7zip` for the [rarfile](https://pypi.org/project/rarfile/) library to support searching inside `.rar` files (optional)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pseek"
7
- version = "2.5.0"
7
+ version = "2.5.2"
8
8
  description = "Pseek is a Python library 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