file_query_text 0.1.0__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/__init__.py +3 -0
- file_query_text/cli.py +66 -0
- file_query_text/grammar.py +63 -0
- file_query_text/main.py +110 -0
- file_query_text-0.1.0.dist-info/METADATA +110 -0
- file_query_text-0.1.0.dist-info/RECORD +9 -0
- file_query_text-0.1.0.dist-info/WHEEL +5 -0
- file_query_text-0.1.0.dist-info/entry_points.txt +2 -0
- file_query_text-0.1.0.dist-info/top_level.txt +1 -0
file_query_text/cli.py
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import os
|
3
|
+
import sys
|
4
|
+
import argparse
|
5
|
+
from pathlib import Path
|
6
|
+
# Fix imports to work when installed as a package
|
7
|
+
from file_query.main import parse_query, QueryVisitor, execute_query
|
8
|
+
|
9
|
+
def main():
|
10
|
+
parser = argparse.ArgumentParser(description="SQL-like queries for your filesystem")
|
11
|
+
parser.add_argument("query", nargs="?", default="",
|
12
|
+
help="SQL query for finding files (default: lists all files in current directory)")
|
13
|
+
parser.add_argument(
|
14
|
+
"--show-content", "-c",
|
15
|
+
action="store_true",
|
16
|
+
help="Display content of the matching files"
|
17
|
+
)
|
18
|
+
args = parser.parse_args()
|
19
|
+
|
20
|
+
# Get current working directory for the query
|
21
|
+
cwd = os.getcwd()
|
22
|
+
|
23
|
+
# Handle different query formats:
|
24
|
+
# 1. Full SQL format: "SELECT * FROM 'path' WHERE condition"
|
25
|
+
# 2. Simple condition: "extension == 'py'"
|
26
|
+
# 3. Simple path only: "SELECT * FROM 'path'"
|
27
|
+
# 4. Empty query: return all files in current directory
|
28
|
+
if not args.query.strip():
|
29
|
+
# Empty query - list all files in current directory
|
30
|
+
query_str = f"SELECT * FROM '{cwd}'"
|
31
|
+
elif args.query.strip().upper().startswith("SELECT"):
|
32
|
+
# Full SQL format or SELECT * FROM without WHERE - use as is
|
33
|
+
query_str = args.query
|
34
|
+
else:
|
35
|
+
# Simple condition format - assume it's a WHERE condition
|
36
|
+
query_str = f"SELECT * FROM '{cwd}' WHERE {args.query}"
|
37
|
+
|
38
|
+
# Parse and execute the query
|
39
|
+
parsed = parse_query(query_str)
|
40
|
+
if parsed:
|
41
|
+
visitor = QueryVisitor()
|
42
|
+
visitor.visit(parsed)
|
43
|
+
results = execute_query(visitor.select, visitor.from_dirs, visitor.where)
|
44
|
+
|
45
|
+
# Display results
|
46
|
+
if not results:
|
47
|
+
print("No matching files found.")
|
48
|
+
return
|
49
|
+
|
50
|
+
print(f"Found {len(results)} matching files:")
|
51
|
+
for file_path in results:
|
52
|
+
print(file_path)
|
53
|
+
|
54
|
+
# Optionally display file contents
|
55
|
+
if args.show_content:
|
56
|
+
try:
|
57
|
+
with open(file_path, 'r') as f:
|
58
|
+
content = f.read()
|
59
|
+
print("\n--- File Content ---")
|
60
|
+
print(content)
|
61
|
+
print("--- End Content ---\n")
|
62
|
+
except Exception as e:
|
63
|
+
print(f"Error reading file: {e}")
|
64
|
+
|
65
|
+
if __name__ == "__main__":
|
66
|
+
main()
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from pyparsing import (
|
2
|
+
Word,
|
3
|
+
alphas,
|
4
|
+
alphanums,
|
5
|
+
QuotedString,
|
6
|
+
delimitedList,
|
7
|
+
Optional,
|
8
|
+
Group,
|
9
|
+
Suppress,
|
10
|
+
ZeroOrMore,
|
11
|
+
oneOf,
|
12
|
+
Forward,
|
13
|
+
Literal,
|
14
|
+
OneOrMore,
|
15
|
+
infixNotation,
|
16
|
+
opAssoc,
|
17
|
+
c_style_comment,
|
18
|
+
nums,
|
19
|
+
pyparsing_common,
|
20
|
+
)
|
21
|
+
|
22
|
+
# Define keywords
|
23
|
+
SELECT = Suppress(Word("SELECT"))
|
24
|
+
FROM = Suppress(Word("FROM"))
|
25
|
+
WHERE = Suppress(Word("WHERE"))
|
26
|
+
AND = Literal("AND")
|
27
|
+
OR = Literal("OR")
|
28
|
+
NOT = Literal("NOT")
|
29
|
+
|
30
|
+
# Define identifiers and literals
|
31
|
+
IDENTIFIER = Word(alphas + "_")
|
32
|
+
STRING_LITERAL = QuotedString("'", unquoteResults=True)
|
33
|
+
# Use pyparsing_common for numeric literals
|
34
|
+
NUMERIC_LITERAL = pyparsing_common.integer
|
35
|
+
DIRECTORY_LIST = Group(delimitedList(STRING_LITERAL))
|
36
|
+
|
37
|
+
# Define comparison operators
|
38
|
+
COMPARISON_OP = oneOf("== != < <= > >=")
|
39
|
+
ATTRIBUTE = IDENTIFIER + Suppress("=") + STRING_LITERAL
|
40
|
+
|
41
|
+
# Define basic condition with support for both string and numeric literals
|
42
|
+
VALUE = STRING_LITERAL | NUMERIC_LITERAL
|
43
|
+
basic_condition = Group(IDENTIFIER + COMPARISON_OP + VALUE)
|
44
|
+
|
45
|
+
# Define logical expressions using infixNotation for better handling of AND and OR
|
46
|
+
condition_expr = Forward()
|
47
|
+
condition_expr <<= infixNotation(
|
48
|
+
basic_condition,
|
49
|
+
[
|
50
|
+
(NOT, 1, opAssoc.RIGHT),
|
51
|
+
(AND, 2, opAssoc.LEFT),
|
52
|
+
(OR, 2, opAssoc.LEFT),
|
53
|
+
],
|
54
|
+
)
|
55
|
+
|
56
|
+
# Define the full query structure
|
57
|
+
query = (
|
58
|
+
SELECT
|
59
|
+
+ (Literal("*") | Group(OneOrMore(IDENTIFIER))).setResultsName("select")
|
60
|
+
+ FROM
|
61
|
+
+ DIRECTORY_LIST.setResultsName("from_dirs")
|
62
|
+
+ Optional(WHERE + condition_expr.setResultsName("where"))
|
63
|
+
)
|
file_query_text/main.py
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
import os
|
2
|
+
import sys
|
3
|
+
from file_query_text.grammar import query # Import the fixed grammar
|
4
|
+
|
5
|
+
|
6
|
+
def parse_query(query_str):
|
7
|
+
try:
|
8
|
+
# Increase recursion limit temporarily to handle complex queries
|
9
|
+
old_limit = sys.getrecursionlimit()
|
10
|
+
sys.setrecursionlimit(2000)
|
11
|
+
|
12
|
+
parsed = query.parseString(query_str, parseAll=True)
|
13
|
+
|
14
|
+
# Restore original recursion limit
|
15
|
+
sys.setrecursionlimit(old_limit)
|
16
|
+
return parsed
|
17
|
+
except Exception as e:
|
18
|
+
print(f"Parse error: {e}")
|
19
|
+
return None
|
20
|
+
|
21
|
+
class QueryVisitor:
|
22
|
+
def __init__(self):
|
23
|
+
self.select = []
|
24
|
+
self.from_dirs = []
|
25
|
+
self.where = None
|
26
|
+
|
27
|
+
def visit(self, parsed_query):
|
28
|
+
self.select = parsed_query.get("select", ["*"])
|
29
|
+
self.from_dirs = parsed_query.get("from_dirs", [])
|
30
|
+
self.where = parsed_query.get("where", None)
|
31
|
+
|
32
|
+
def execute_query(select, from_dirs, where_conditions):
|
33
|
+
matched_files = []
|
34
|
+
for directory in from_dirs:
|
35
|
+
if not os.path.exists(directory):
|
36
|
+
continue
|
37
|
+
for root, _, files in os.walk(directory):
|
38
|
+
for filename in files:
|
39
|
+
file_path = os.path.join(root, filename)
|
40
|
+
if evaluate_conditions(file_path, where_conditions):
|
41
|
+
matched_files.append(file_path)
|
42
|
+
return matched_files
|
43
|
+
|
44
|
+
def evaluate_conditions(file_path, condition):
|
45
|
+
if not condition:
|
46
|
+
return True
|
47
|
+
|
48
|
+
def get_file_attr(attr_name):
|
49
|
+
if attr_name == "extension":
|
50
|
+
return os.path.splitext(file_path)[1][1:]
|
51
|
+
if attr_name == "name":
|
52
|
+
return os.path.basename(file_path)
|
53
|
+
if attr_name == "size":
|
54
|
+
return os.path.getsize(file_path)
|
55
|
+
# Add more attributes as needed
|
56
|
+
return None
|
57
|
+
|
58
|
+
# Evaluation function for expressions
|
59
|
+
def eval_expr(expr):
|
60
|
+
if not isinstance(expr, list):
|
61
|
+
return expr # For simple terms like 'AND', 'OR'
|
62
|
+
|
63
|
+
if len(expr) == 3:
|
64
|
+
# Handle three types of expressions:
|
65
|
+
|
66
|
+
# 1. Basic condition: [attr, op, value]
|
67
|
+
if isinstance(expr[0], str) and isinstance(expr[1], str):
|
68
|
+
attr_val = get_file_attr(expr[0])
|
69
|
+
op = expr[1]
|
70
|
+
val = expr[2].strip("'") if isinstance(expr[2], str) else expr[2] # Remove quotes if string
|
71
|
+
|
72
|
+
if op == "==": return str(attr_val) == val
|
73
|
+
if op == "!=": return str(attr_val) != val
|
74
|
+
if op == "<": return attr_val is not None and int(attr_val) < int(val)
|
75
|
+
if op == "<=": return attr_val is not None and int(attr_val) <= int(val)
|
76
|
+
if op == ">": return attr_val is not None and int(attr_val) > int(val)
|
77
|
+
if op == ">=": return attr_val is not None and int(attr_val) >= int(val)
|
78
|
+
|
79
|
+
# 2. Logical operations from infixNotation: [left, op, right]
|
80
|
+
elif expr[1] == "AND":
|
81
|
+
return eval_expr(expr[0]) and eval_expr(expr[2])
|
82
|
+
elif expr[1] == "OR":
|
83
|
+
return eval_expr(expr[0]) or eval_expr(expr[2])
|
84
|
+
|
85
|
+
# 3. NOT operation: ['NOT', expr]
|
86
|
+
elif len(expr) == 2 and expr[0] == "NOT":
|
87
|
+
return not eval_expr(expr[1])
|
88
|
+
|
89
|
+
return False
|
90
|
+
|
91
|
+
return eval_expr(condition.asList())
|
92
|
+
|
93
|
+
# Example usage
|
94
|
+
if __name__ == "__main__":
|
95
|
+
# Get project root directory for demonstration
|
96
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
97
|
+
project_root = os.path.dirname(current_dir)
|
98
|
+
src_dir = os.path.join(project_root, "src")
|
99
|
+
tests_dir = os.path.join(project_root, "tests")
|
100
|
+
query_str = f"SELECT * FROM '{src_dir}', '{tests_dir}' WHERE extension == 'py'"
|
101
|
+
parsed = parse_query(query_str)
|
102
|
+
if parsed:
|
103
|
+
visitor = QueryVisitor()
|
104
|
+
visitor.visit(parsed)
|
105
|
+
results = execute_query(visitor.select, visitor.from_dirs, visitor.where)
|
106
|
+
print("Matching files:")
|
107
|
+
for file in results:
|
108
|
+
# Skip files in .venv directory
|
109
|
+
if ".venv" not in file:
|
110
|
+
print(file)
|
@@ -0,0 +1,110 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: file_query_text
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: SQL-like interface for querying files in your filesystem
|
5
|
+
Author-email: nik <42a11b@nikdav.is>
|
6
|
+
License-Expression: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/nikdavis/file_query_text
|
8
|
+
Project-URL: Bug Tracker, https://github.com/nikdavis/file_query_text/issues
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
10
|
+
Classifier: Operating System :: OS Independent
|
11
|
+
Requires-Python: >=3.12
|
12
|
+
Description-Content-Type: text/markdown
|
13
|
+
Requires-Dist: pyparsing>=3.2.3
|
14
|
+
Provides-Extra: dev
|
15
|
+
Requires-Dist: pytest>=8.3.5; extra == "dev"
|
16
|
+
|
17
|
+
# File Query
|
18
|
+
|
19
|
+
A SQL-like interface for querying files in your filesystem.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
```bash
|
24
|
+
# Clone the repository
|
25
|
+
git clone https://github.com/yourusername/file-query.git
|
26
|
+
cd file-query
|
27
|
+
|
28
|
+
# Install with pip
|
29
|
+
pip install -e .
|
30
|
+
|
31
|
+
# Or use UV
|
32
|
+
uv run python -m src.cli "your query"
|
33
|
+
|
34
|
+
# Install as a permanent tool with UV
|
35
|
+
uv tool install .
|
36
|
+
# This will install the 'fq' command
|
37
|
+
```
|
38
|
+
|
39
|
+
## Usage
|
40
|
+
|
41
|
+
### Command Line
|
42
|
+
|
43
|
+
The quickest way to run file-query is with UV:
|
44
|
+
|
45
|
+
```bash
|
46
|
+
uv run python -m src.cli "your query here"
|
47
|
+
```
|
48
|
+
|
49
|
+
After installation, you can use the shorthand command:
|
50
|
+
|
51
|
+
```bash
|
52
|
+
fq "your query here"
|
53
|
+
```
|
54
|
+
|
55
|
+
#### Basic Usage
|
56
|
+
|
57
|
+
```bash
|
58
|
+
# Find all Python files
|
59
|
+
fq "extension == 'py'"
|
60
|
+
|
61
|
+
# Find all text files and show their content
|
62
|
+
fq "extension == 'txt'" --show-content
|
63
|
+
```
|
64
|
+
|
65
|
+
#### Advanced Queries
|
66
|
+
|
67
|
+
File Query supports full SQL-like syntax:
|
68
|
+
|
69
|
+
```bash
|
70
|
+
# Find all Python files in the src directory
|
71
|
+
fq "SELECT * FROM 'src' WHERE extension == 'py'"
|
72
|
+
|
73
|
+
# Find all files larger than 100KB
|
74
|
+
fq "SELECT * FROM '.' WHERE size > 102400"
|
75
|
+
|
76
|
+
# Complex conditions
|
77
|
+
fq "SELECT * FROM '.' WHERE (extension == 'pdf' AND size > 1000000) OR (extension == 'txt' AND NOT name == 'README.txt')"
|
78
|
+
```
|
79
|
+
|
80
|
+
## Query Syntax
|
81
|
+
|
82
|
+
File Query uses a SQL-like syntax:
|
83
|
+
|
84
|
+
```sql
|
85
|
+
SELECT * FROM 'directory_path' WHERE condition
|
86
|
+
```
|
87
|
+
|
88
|
+
### Available Attributes
|
89
|
+
|
90
|
+
- `extension`: File extension (without the dot)
|
91
|
+
- `name`: Filename with extension
|
92
|
+
- `size`: File size in bytes
|
93
|
+
|
94
|
+
### Operators
|
95
|
+
|
96
|
+
- Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=`
|
97
|
+
- Logical: `AND`, `OR`, `NOT`
|
98
|
+
|
99
|
+
## Examples
|
100
|
+
|
101
|
+
```bash
|
102
|
+
# Find all PDF files
|
103
|
+
fq "extension == 'pdf'"
|
104
|
+
|
105
|
+
# Find all files not named "main.py"
|
106
|
+
fq "NOT name == 'main.py'"
|
107
|
+
|
108
|
+
# Find all large image files
|
109
|
+
fq "SELECT * FROM '.' WHERE (extension == 'jpg' OR extension == 'png') AND size > 500000"
|
110
|
+
```
|
@@ -0,0 +1,9 @@
|
|
1
|
+
file_query_text/__init__.py,sha256=WXdvguZ706HG7MXS13Yb8e7VC5UFYyBzuExXbwBOTak,87
|
2
|
+
file_query_text/cli.py,sha256=W4Lf9WC4hYIW6DpL57JMcLXIfyTr2v8X-Y-WjPjoN1s,2360
|
3
|
+
file_query_text/grammar.py,sha256=lhw2pUq83IMga2cppA0r9RbebioPB7a65Az5f4Lheso,1572
|
4
|
+
file_query_text/main.py,sha256=qMuY5YZ2TXrAUyfXaQfPkQOJZawpRE86NIre5Pz88Tk,4011
|
5
|
+
file_query_text-0.1.0.dist-info/METADATA,sha256=TjD_kUagIoxHH9lPV0qVDOoYH9ZlkmmpwEv5yXuaD1c,2347
|
6
|
+
file_query_text-0.1.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
7
|
+
file_query_text-0.1.0.dist-info/entry_points.txt,sha256=rNFYWzvcIsUZkGNsc_E_B5HyYRnqqdj_u8_IeQpw1wo,48
|
8
|
+
file_query_text-0.1.0.dist-info/top_level.txt,sha256=o1FzSvLa6kSV61b7RLHWRhEezc96m05YwIKqjuWUSxU,16
|
9
|
+
file_query_text-0.1.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
file_query_text
|