file_query_text 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl
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.
- file_query_text/cli.py +30 -3
- file_query_text/grammar.py +8 -7
- file_query_text/main.py +64 -3
- {file_query_text-0.1.1.dist-info → file_query_text-0.1.2.dist-info}/METADATA +5 -1
- file_query_text-0.1.2.dist-info/RECORD +9 -0
- file_query_text-0.1.1.dist-info/RECORD +0 -9
- {file_query_text-0.1.1.dist-info → file_query_text-0.1.2.dist-info}/WHEEL +0 -0
- {file_query_text-0.1.1.dist-info → file_query_text-0.1.2.dist-info}/entry_points.txt +0 -0
- {file_query_text-0.1.1.dist-info → file_query_text-0.1.2.dist-info}/top_level.txt +0 -0
file_query_text/cli.py
CHANGED
@@ -4,7 +4,7 @@ import sys
|
|
4
4
|
import argparse
|
5
5
|
from pathlib import Path
|
6
6
|
# Fix imports to work when installed as a package
|
7
|
-
from file_query_text.main import parse_query, QueryVisitor, execute_query
|
7
|
+
from file_query_text.main import parse_query, QueryVisitor, execute_query, get_file_attributes
|
8
8
|
|
9
9
|
def main():
|
10
10
|
parser = argparse.ArgumentParser(description="SQL-like queries for your filesystem")
|
@@ -15,6 +15,16 @@ def main():
|
|
15
15
|
action="store_true",
|
16
16
|
help="Display content of the matching files"
|
17
17
|
)
|
18
|
+
parser.add_argument(
|
19
|
+
"--debug", "-d",
|
20
|
+
action="store_true",
|
21
|
+
help="Debug mode: print all file attributes regardless of query"
|
22
|
+
)
|
23
|
+
parser.add_argument(
|
24
|
+
"--no-gitignore", "-g",
|
25
|
+
action="store_true",
|
26
|
+
help="Don't respect .gitignore files (show ignored files)"
|
27
|
+
)
|
18
28
|
args = parser.parse_args()
|
19
29
|
|
20
30
|
# Get current working directory for the query
|
@@ -40,7 +50,12 @@ def main():
|
|
40
50
|
if parsed:
|
41
51
|
visitor = QueryVisitor()
|
42
52
|
visitor.visit(parsed)
|
43
|
-
results = execute_query(
|
53
|
+
results = execute_query(
|
54
|
+
visitor.select,
|
55
|
+
visitor.from_dirs,
|
56
|
+
visitor.where,
|
57
|
+
respect_gitignore=not args.no_gitignore
|
58
|
+
)
|
44
59
|
|
45
60
|
# Display results
|
46
61
|
if not results:
|
@@ -48,8 +63,20 @@ def main():
|
|
48
63
|
return
|
49
64
|
|
50
65
|
print(f"Found {len(results)} matching files:")
|
66
|
+
|
67
|
+
# In debug mode, print headers
|
68
|
+
if args.debug:
|
69
|
+
print(f"{'EXTENSION':<15} {'NAME':<30} {'SIZE':<10} {'PATH'}")
|
70
|
+
print(f"{'-'*15} {'-'*30} {'-'*10} {'-'*30}")
|
71
|
+
|
51
72
|
for file_path in results:
|
52
|
-
|
73
|
+
if args.debug:
|
74
|
+
# Print all attributes in debug mode
|
75
|
+
attrs = get_file_attributes(file_path)
|
76
|
+
print(f"{attrs['extension']:<15} {attrs['name']:<30} {attrs['size']:<10} {attrs['relative_path']}")
|
77
|
+
else:
|
78
|
+
# Standard output
|
79
|
+
print(file_path)
|
53
80
|
|
54
81
|
# Optionally display file contents
|
55
82
|
if args.show_content:
|
file_query_text/grammar.py
CHANGED
@@ -17,15 +17,16 @@ from pyparsing import (
|
|
17
17
|
c_style_comment,
|
18
18
|
nums,
|
19
19
|
pyparsing_common,
|
20
|
+
CaselessKeyword,
|
20
21
|
)
|
21
22
|
|
22
23
|
# Define keywords
|
23
|
-
SELECT = Suppress(
|
24
|
-
FROM = Suppress(
|
25
|
-
WHERE = Suppress(
|
26
|
-
AND =
|
27
|
-
OR =
|
28
|
-
NOT =
|
24
|
+
SELECT = Suppress(CaselessKeyword("SELECT"))
|
25
|
+
FROM = Suppress(CaselessKeyword("FROM"))
|
26
|
+
WHERE = Suppress(CaselessKeyword("WHERE"))
|
27
|
+
AND = CaselessKeyword("AND")
|
28
|
+
OR = CaselessKeyword("OR")
|
29
|
+
NOT = CaselessKeyword("NOT")
|
29
30
|
|
30
31
|
# Define identifiers and literals
|
31
32
|
IDENTIFIER = Word(alphas + "_")
|
@@ -35,7 +36,7 @@ NUMERIC_LITERAL = pyparsing_common.integer
|
|
35
36
|
DIRECTORY_LIST = Group(delimitedList(STRING_LITERAL))
|
36
37
|
|
37
38
|
# Define comparison operators
|
38
|
-
COMPARISON_OP = oneOf("== != < <= > >=")
|
39
|
+
COMPARISON_OP = oneOf("= == != <> < <= > >=")
|
39
40
|
ATTRIBUTE = IDENTIFIER + Suppress("=") + STRING_LITERAL
|
40
41
|
|
41
42
|
# Define basic condition with support for both string and numeric literals
|
file_query_text/main.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import os
|
2
2
|
import sys
|
3
3
|
from file_query_text.grammar import query # Import the fixed grammar
|
4
|
+
import gitignore_parser
|
5
|
+
import os.path
|
4
6
|
|
5
7
|
|
6
8
|
def parse_query(query_str):
|
@@ -29,14 +31,51 @@ class QueryVisitor:
|
|
29
31
|
self.from_dirs = parsed_query.get("from_dirs", [])
|
30
32
|
self.where = parsed_query.get("where", None)
|
31
33
|
|
32
|
-
def execute_query(select, from_dirs, where_conditions):
|
34
|
+
def execute_query(select, from_dirs, where_conditions, respect_gitignore=True):
|
33
35
|
matched_files = []
|
36
|
+
|
37
|
+
# Set up gitignore matching if requested
|
38
|
+
gitignore_matchers = {}
|
39
|
+
|
34
40
|
for directory in from_dirs:
|
35
41
|
if not os.path.exists(directory):
|
36
42
|
continue
|
43
|
+
|
44
|
+
# Initialize gitignore matcher for this directory
|
45
|
+
if respect_gitignore:
|
46
|
+
# Find the repository root (where .git directory is)
|
47
|
+
repo_root = directory
|
48
|
+
while repo_root != os.path.dirname(repo_root): # Stop at filesystem root
|
49
|
+
if os.path.exists(os.path.join(repo_root, '.git')):
|
50
|
+
break
|
51
|
+
repo_root = os.path.dirname(repo_root)
|
52
|
+
|
53
|
+
# If we found a git repo, check for .gitignore
|
54
|
+
gitignore_path = os.path.join(repo_root, '.gitignore')
|
55
|
+
if os.path.exists(gitignore_path):
|
56
|
+
if repo_root not in gitignore_matchers:
|
57
|
+
try:
|
58
|
+
gitignore_matchers[repo_root] = gitignore_parser.parse_gitignore(gitignore_path)
|
59
|
+
except Exception as e:
|
60
|
+
print(f"Warning: Error parsing .gitignore at {gitignore_path}: {e}")
|
61
|
+
|
37
62
|
for root, _, files in os.walk(directory):
|
38
63
|
for filename in files:
|
39
64
|
file_path = os.path.join(root, filename)
|
65
|
+
|
66
|
+
# Check if file is ignored by gitignore rules
|
67
|
+
if respect_gitignore:
|
68
|
+
# Find which repo this file belongs to
|
69
|
+
file_repo_root = None
|
70
|
+
for repo_root in gitignore_matchers:
|
71
|
+
if file_path.startswith(repo_root):
|
72
|
+
file_repo_root = repo_root
|
73
|
+
break
|
74
|
+
|
75
|
+
# Skip if file is ignored by gitignore
|
76
|
+
if file_repo_root and gitignore_matchers[file_repo_root](file_path):
|
77
|
+
continue
|
78
|
+
|
40
79
|
if evaluate_conditions(file_path, where_conditions):
|
41
80
|
matched_files.append(file_path)
|
42
81
|
return matched_files
|
@@ -52,6 +91,8 @@ def evaluate_conditions(file_path, condition):
|
|
52
91
|
return os.path.basename(file_path)
|
53
92
|
if attr_name == "size":
|
54
93
|
return os.path.getsize(file_path)
|
94
|
+
if attr_name == "path":
|
95
|
+
return file_path
|
55
96
|
# Add more attributes as needed
|
56
97
|
return None
|
57
98
|
|
@@ -69,8 +110,8 @@ def evaluate_conditions(file_path, condition):
|
|
69
110
|
op = expr[1]
|
70
111
|
val = expr[2].strip("'") if isinstance(expr[2], str) else expr[2] # Remove quotes if string
|
71
112
|
|
72
|
-
if op == "==": return str(attr_val) == val
|
73
|
-
if op == "!=": return str(attr_val) != val
|
113
|
+
if op == "=" or op == "==": return str(attr_val) == val
|
114
|
+
if op == "!=" or op == "<>": return str(attr_val) != val
|
74
115
|
if op == "<": return attr_val is not None and int(attr_val) < int(val)
|
75
116
|
if op == "<=": return attr_val is not None and int(attr_val) <= int(val)
|
76
117
|
if op == ">": return attr_val is not None and int(attr_val) > int(val)
|
@@ -90,6 +131,26 @@ def evaluate_conditions(file_path, condition):
|
|
90
131
|
|
91
132
|
return eval_expr(condition.asList())
|
92
133
|
|
134
|
+
# Function to get all attributes for a file
|
135
|
+
def get_file_attributes(file_path):
|
136
|
+
"""Get all available attributes for a file."""
|
137
|
+
cwd = os.getcwd()
|
138
|
+
try:
|
139
|
+
# Make path relative to current directory
|
140
|
+
rel_path = os.path.relpath(file_path, cwd)
|
141
|
+
except ValueError:
|
142
|
+
# Handle case when on different drives (Windows)
|
143
|
+
rel_path = file_path
|
144
|
+
|
145
|
+
attributes = {
|
146
|
+
"extension": os.path.splitext(file_path)[1][1:],
|
147
|
+
"name": os.path.basename(file_path),
|
148
|
+
"size": os.path.getsize(file_path),
|
149
|
+
"path": file_path,
|
150
|
+
"relative_path": rel_path,
|
151
|
+
}
|
152
|
+
return attributes
|
153
|
+
|
93
154
|
# Example usage
|
94
155
|
if __name__ == "__main__":
|
95
156
|
# Get project root directory for demonstration
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: file_query_text
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.2
|
4
4
|
Summary: SQL-like interface for querying files in your filesystem
|
5
5
|
Author-email: nik <42a11b@nikdav.is>
|
6
6
|
License-Expression: MIT
|
@@ -90,6 +90,7 @@ SELECT * FROM 'directory_path' WHERE condition
|
|
90
90
|
- `extension`: File extension (without the dot)
|
91
91
|
- `name`: Filename with extension
|
92
92
|
- `size`: File size in bytes
|
93
|
+
- `path`: Full file path
|
93
94
|
|
94
95
|
### Operators
|
95
96
|
|
@@ -107,4 +108,7 @@ fq "NOT name == 'main.py'"
|
|
107
108
|
|
108
109
|
# Find all large image files
|
109
110
|
fq "SELECT * FROM '.' WHERE (extension == 'jpg' OR extension == 'png') AND size > 500000"
|
111
|
+
|
112
|
+
# Find files with 'config' in their path
|
113
|
+
fq "path == '.*config.*'"
|
110
114
|
```
|
@@ -0,0 +1,9 @@
|
|
1
|
+
file_query_text/__init__.py,sha256=WXdvguZ706HG7MXS13Yb8e7VC5UFYyBzuExXbwBOTak,87
|
2
|
+
file_query_text/cli.py,sha256=z1e16emuXsgsJP298UKbezhtIVMx9NZQ6v7FNHAahoo,3306
|
3
|
+
file_query_text/grammar.py,sha256=XanSi9VCKuIQNlOMXRBZbeWxaJj2UhMJPDIYKgw4lEQ,1655
|
4
|
+
file_query_text/main.py,sha256=PVUQ6BZsvN2eEUhORjWr2aNh761evNieOaXTs_RDlB4,6377
|
5
|
+
file_query_text-0.1.2.dist-info/METADATA,sha256=AYCc9743EJ0bwqpBKy5qfhDX0uR_htYHFh5fQL6wylY,2440
|
6
|
+
file_query_text-0.1.2.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
7
|
+
file_query_text-0.1.2.dist-info/entry_points.txt,sha256=rNFYWzvcIsUZkGNsc_E_B5HyYRnqqdj_u8_IeQpw1wo,48
|
8
|
+
file_query_text-0.1.2.dist-info/top_level.txt,sha256=o1FzSvLa6kSV61b7RLHWRhEezc96m05YwIKqjuWUSxU,16
|
9
|
+
file_query_text-0.1.2.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
file_query_text/__init__.py,sha256=WXdvguZ706HG7MXS13Yb8e7VC5UFYyBzuExXbwBOTak,87
|
2
|
-
file_query_text/cli.py,sha256=_0ahAdVoDCRBhAcIkcgKM6ZqA0YlqmAyF9i-s2GQRjk,2365
|
3
|
-
file_query_text/grammar.py,sha256=lhw2pUq83IMga2cppA0r9RbebioPB7a65Az5f4Lheso,1572
|
4
|
-
file_query_text/main.py,sha256=qMuY5YZ2TXrAUyfXaQfPkQOJZawpRE86NIre5Pz88Tk,4011
|
5
|
-
file_query_text-0.1.1.dist-info/METADATA,sha256=0VTvghCuE7IIixRJdPVw1Y0Y5ijX8YerDMVpUlf_5UE,2347
|
6
|
-
file_query_text-0.1.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
7
|
-
file_query_text-0.1.1.dist-info/entry_points.txt,sha256=rNFYWzvcIsUZkGNsc_E_B5HyYRnqqdj_u8_IeQpw1wo,48
|
8
|
-
file_query_text-0.1.1.dist-info/top_level.txt,sha256=o1FzSvLa6kSV61b7RLHWRhEezc96m05YwIKqjuWUSxU,16
|
9
|
-
file_query_text-0.1.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|