This commit is contained in:
parent
2a2c13bddf
commit
3765449064
|
@ -67,6 +67,9 @@
|
|||
"xmemory": "cpp",
|
||||
"xstring": "cpp",
|
||||
"xtr1common": "cpp",
|
||||
"xutility": "cpp"
|
||||
"xutility": "cpp",
|
||||
"fstream": "cpp",
|
||||
"iostream": "cpp",
|
||||
"codecvt": "cpp"
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "prodir"
|
||||
version = "0.0.6"
|
||||
description = "A module for analyzing directory structures"
|
||||
version = "0.0.7"
|
||||
description = "A module for analyzing and creating directory structures"
|
||||
scripts = {prodir = "prodir.__main__:main"}
|
||||
dependencies = []
|
||||
|
|
2
setup.py
2
setup.py
|
@ -20,7 +20,7 @@ tree_structurer_module = Extension(
|
|||
|
||||
setup(
|
||||
name='prodir',
|
||||
version='0.0.6',
|
||||
version='0.0.7',
|
||||
description='A module for analyzing directory structures',
|
||||
ext_modules=[tree_structurer_module],
|
||||
packages=find_packages(where="src"),
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
from prodir._tree_structurer import (
|
||||
create_tree_structurer,
|
||||
get_structure,
|
||||
create_structure_from_file,
|
||||
create_structure_from_string
|
||||
create_structure_from_file
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'create_tree_structurer',
|
||||
'get_structure',
|
||||
'create_structure_from_file',
|
||||
'create_structure_from_string'
|
||||
'create_structure_from_file'
|
||||
]
|
|
@ -4,18 +4,17 @@ import argparse
|
|||
from prodir import (
|
||||
create_tree_structurer,
|
||||
get_structure,
|
||||
create_structure_from_file,
|
||||
create_structure_from_string
|
||||
create_structure_from_file
|
||||
)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Directory structure tool: Display and create directory structures'
|
||||
)
|
||||
|
||||
|
||||
# Create subparsers for different commands
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
|
||||
|
||||
# Display command
|
||||
display_parser = subparsers.add_parser('display', help='Display directory structure')
|
||||
display_parser.add_argument(
|
||||
|
@ -29,17 +28,13 @@ def main():
|
|||
action='store_true',
|
||||
help='Show more detailed output'
|
||||
)
|
||||
|
||||
|
||||
# Create command
|
||||
create_parser = subparsers.add_parser('create', help='Create directory structure')
|
||||
create_parser.add_argument(
|
||||
'-f', '--file',
|
||||
'file',
|
||||
help='File containing the directory structure'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'-s', '--string',
|
||||
help='String containing the directory structure'
|
||||
)
|
||||
create_parser.add_argument(
|
||||
'-o', '--output',
|
||||
default=os.getcwd(),
|
||||
|
@ -50,53 +45,40 @@ def main():
|
|||
action='store_true',
|
||||
help='Show more detailed output'
|
||||
)
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
# If no command is specified, default to display
|
||||
if not args.command:
|
||||
args.command = 'display'
|
||||
args.path = os.getcwd()
|
||||
args.verbose = False
|
||||
|
||||
|
||||
try:
|
||||
create_tree_structurer()
|
||||
|
||||
|
||||
if args.command == 'display':
|
||||
if args.verbose:
|
||||
print(f"Analyzing directory: {args.path}")
|
||||
|
||||
|
||||
structure = get_structure(args.path)
|
||||
for line in structure:
|
||||
print(line)
|
||||
|
||||
|
||||
elif args.command == 'create':
|
||||
if not args.file and not args.string:
|
||||
parser.error("Either --file or --string must be specified")
|
||||
|
||||
if args.file and args.string:
|
||||
parser.error("Cannot specify both --file and --string")
|
||||
|
||||
if args.verbose:
|
||||
print(f"Creating directory structure in: {args.output}")
|
||||
|
||||
if args.file:
|
||||
if args.verbose:
|
||||
print(f"Using structure from file: {args.file}")
|
||||
create_structure_from_file(args.file, args.output)
|
||||
|
||||
elif args.string:
|
||||
if args.verbose:
|
||||
print("Creating structure from provided string")
|
||||
create_structure_from_string(args.string, args.output)
|
||||
|
||||
print(f"Using structure from file: {args.file}")
|
||||
|
||||
create_structure_from_file(args.file, args.output)
|
||||
|
||||
if args.verbose:
|
||||
print("Structure created successfully")
|
||||
print("\nResulting structure:")
|
||||
structure = get_structure(args.output)
|
||||
for line in structure:
|
||||
print(line)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "Directory does not exist" in error_msg:
|
||||
|
|
|
@ -66,28 +66,6 @@ static PyObject* create_structure_from_file(PyObject* self, PyObject* args) {
|
|||
}
|
||||
}
|
||||
|
||||
static PyObject* create_structure_from_string(PyObject* self, PyObject* args) {
|
||||
const char* structure_str = nullptr;
|
||||
const char* target_path = "."; // Default to current directory
|
||||
|
||||
if (!PyArg_ParseTuple(args, "s|s", &structure_str, &target_path)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (g_tree_structurer == nullptr) {
|
||||
PyErr_SetString(TreeStructurerError, "TreeStructurer not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try {
|
||||
g_tree_structurer->create_structure_from_string(structure_str, target_path);
|
||||
Py_RETURN_NONE;
|
||||
} catch (const std::exception& e) {
|
||||
PyErr_SetString(TreeStructurerError, e.what());
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static PyMethodDef TreeStructurerMethods[] = {
|
||||
{"create_tree_structurer", create_tree_structurer, METH_NOARGS,
|
||||
"Create a new TreeStructurer instance"},
|
||||
|
@ -95,8 +73,6 @@ static PyMethodDef TreeStructurerMethods[] = {
|
|||
"Get the directory structure for the given path"},
|
||||
{"create_structure_from_file", create_structure_from_file, METH_VARARGS,
|
||||
"Create directory structure from a file containing the structure description"},
|
||||
{"create_structure_from_string", create_structure_from_string, METH_VARARGS,
|
||||
"Create directory structure from a string containing the structure description"},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@ std::string TreeStructurer::get_relative_path(const fs::path& path, const fs::pa
|
|||
return rel.string();
|
||||
}
|
||||
|
||||
|
||||
std::vector<fs::path> TreeStructurer::get_filtered_paths(const fs::path& start) {
|
||||
std::vector<fs::path> paths;
|
||||
fs::directory_options options = fs::directory_options::skip_permission_denied;
|
||||
|
@ -115,95 +116,93 @@ std::vector<fs::path> TreeStructurer::get_filtered_paths(const fs::path& start)
|
|||
return paths;
|
||||
}
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Directory Structure Generation (tree printing)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
std::vector<std::string> TreeStructurer::get_directory_structure(const std::string& startpath) {
|
||||
std::vector<std::string> lines;
|
||||
fs::path start(startpath);
|
||||
if (!fs::exists(start)) {
|
||||
throw std::runtime_error("Path does not exist: " + startpath);
|
||||
std::vector<std::string> result;
|
||||
|
||||
// Normalize the input path by removing ./ or .\ prefix if present
|
||||
std::string normalized_path = startpath;
|
||||
if (startpath.substr(0, 2) == ".\\" || startpath.substr(0, 2) == "./") {
|
||||
normalized_path = startpath.substr(2);
|
||||
}
|
||||
|
||||
fs::path start = normalized_path.empty() ? fs::current_path() : fs::path(normalized_path);
|
||||
|
||||
try {
|
||||
auto paths = get_filtered_paths(start);
|
||||
if (paths.empty()) {
|
||||
throw std::runtime_error("No valid files or directories found in: " + start.string());
|
||||
}
|
||||
std::vector<bool> is_last_at_level(256, false);
|
||||
|
||||
for (size_t i = 1; i < paths.size(); ++i) {
|
||||
const auto& path = paths[i];
|
||||
std::string rel_path = get_relative_path(path, start);
|
||||
|
||||
int level = std::count(rel_path.begin(), rel_path.end(), fs::path::preferred_separator);
|
||||
|
||||
bool is_last = true;
|
||||
for (size_t j = i + 1; j < paths.size(); ++j) {
|
||||
std::string next_rel_path = get_relative_path(paths[j], start);
|
||||
int next_level = std::count(next_rel_path.begin(), next_rel_path.end(), fs::path::preferred_separator);
|
||||
if (next_level == level) {
|
||||
is_last = false;
|
||||
break;
|
||||
}
|
||||
if (next_level < level) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
is_last_at_level[level] = is_last;
|
||||
|
||||
std::string line;
|
||||
for (int j = 0; j < level; ++j) {
|
||||
if (j == level - 1) {
|
||||
line += is_last ? "└── " : "├── ";
|
||||
} else {
|
||||
line += is_last_at_level[j] ? " " : "│ ";
|
||||
}
|
||||
}
|
||||
|
||||
line += path.filename().string();
|
||||
if (fs::is_directory(path)) {
|
||||
line += "/";
|
||||
}
|
||||
result.push_back(line);
|
||||
}
|
||||
} catch (const fs::filesystem_error& e) {
|
||||
throw std::runtime_error("Failed to access directory: " + std::string(e.what()));
|
||||
}
|
||||
|
||||
// Recursive lambda that traverses the filesystem.
|
||||
std::function<void(const fs::path&, const std::string&, bool)> traverse;
|
||||
traverse = [&](const fs::path& path, const std::string& indent, bool isLast) {
|
||||
std::string line;
|
||||
if (indent.empty()) {
|
||||
// For the root we simply output the name (appending a directory marker if needed)
|
||||
line = path.filename().string();
|
||||
} else {
|
||||
// If not at the root, prepend a branch marker.
|
||||
std::string branch = isLast ?
|
||||
(std::string(1, static_cast<char>(TREE_CORNER)) + "── ") :
|
||||
(std::string(1, static_cast<char>(TREE_BRANCH)) + "── ");
|
||||
line = indent + branch + path.filename().string();
|
||||
}
|
||||
if (fs::is_directory(path)) {
|
||||
line.push_back(static_cast<char>(DIRECTORY_MARKER));
|
||||
}
|
||||
lines.push_back(line);
|
||||
|
||||
if (fs::is_directory(path)) {
|
||||
// Collect and sort the children.
|
||||
std::vector<fs::path> children;
|
||||
for (auto& entry : fs::directory_iterator(path)) {
|
||||
std::string filename = entry.path().filename().string();
|
||||
if (entry.is_directory() && should_ignore_dir(filename))
|
||||
continue;
|
||||
if (!entry.is_directory() && should_ignore_file(filename))
|
||||
continue;
|
||||
children.push_back(entry.path());
|
||||
}
|
||||
std::sort(children.begin(), children.end(), [](const fs::path& a,
|
||||
const fs::path& b) {
|
||||
return a.filename().string() < b.filename().string();
|
||||
});
|
||||
|
||||
// Recurse into children.
|
||||
for (size_t i = 0; i < children.size(); i++) {
|
||||
bool last = (i == children.size() - 1);
|
||||
// New indent: if we are not at the root then extend the current indent.
|
||||
std::string newIndent = indent;
|
||||
if (!indent.empty()) {
|
||||
newIndent += isLast ? " " : u8"│ ";
|
||||
}
|
||||
traverse(children[i], newIndent, last);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Start the traversal with an empty indent.
|
||||
traverse(start, "", true);
|
||||
return lines;
|
||||
return result;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Structure Creation from a Tree-like File or String
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void TreeStructurer::create_structure_from_file(const std::string& filepath,
|
||||
const std::string& target_path) {
|
||||
const std::string& target_path) {
|
||||
std::vector<std::string> lines = read_structure_file(filepath);
|
||||
validate_structure(lines);
|
||||
TreeNode root = build_tree_from_lines(lines);
|
||||
create_node(root, fs::path(target_path));
|
||||
}
|
||||
|
||||
void TreeStructurer::create_structure_from_string(const std::string& structure,
|
||||
const std::string& target_path) {
|
||||
std::vector<std::string> lines;
|
||||
std::istringstream iss(structure);
|
||||
std::string line;
|
||||
while (std::getline(iss, line)) {
|
||||
if (!line.empty()) {
|
||||
lines.push_back(line);
|
||||
|
||||
fs::path target(target_path);
|
||||
|
||||
// Check if the root directory name matches the target directory name
|
||||
if (root.name == target.filename().string()) {
|
||||
// If names match, create the children directly in the target directory
|
||||
for (const auto& child : root.children) {
|
||||
create_node(child, target);
|
||||
}
|
||||
} else {
|
||||
// If names don't match, create the full structure including root
|
||||
create_node(root, target);
|
||||
}
|
||||
validate_structure(lines);
|
||||
TreeNode root = build_tree_from_lines(lines);
|
||||
create_node(root, fs::path(target_path));
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
@ -217,55 +216,115 @@ std::string TreeStructurer::create_indent(size_t level) {
|
|||
|
||||
// Determines the "indent level" of a line by scanning it in 4‑character groups.
|
||||
// Recognizes either a blank indent (" " or "│ ") or a branch marker ("├── " or "└── ").
|
||||
|
||||
size_t TreeStructurer::get_indent_level(const std::string& line) {
|
||||
size_t indent = 0;
|
||||
size_t pos = 0;
|
||||
while (pos + 4 <= line.size()) {
|
||||
std::string group = line.substr(pos, 4);
|
||||
if (group == " " || group == u8"│ ") {
|
||||
|
||||
while (pos < line.length()) {
|
||||
if (pos >= line.length()) break;
|
||||
|
||||
// Check for basic space indent (4 spaces)
|
||||
if (pos + 3 < line.length() && line.substr(pos, 4) == " ") {
|
||||
indent++;
|
||||
pos += 4;
|
||||
continue;
|
||||
}
|
||||
if (group == u8"├── " || group == u8"└── ") {
|
||||
|
||||
// Check for │ (vertical line) followed by 3 spaces
|
||||
// Bytes: E2 94 82 20 20 20
|
||||
if (pos + 5 < line.length() &&
|
||||
static_cast<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
static_cast<unsigned char>(line[pos + 2]) == 0x82 &&
|
||||
line[pos + 3] == ' ' &&
|
||||
line[pos + 4] == ' ' &&
|
||||
line[pos + 5] == ' ') {
|
||||
indent++;
|
||||
break;
|
||||
pos += 6;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for ├── or └── (branch or corner followed by dashes and space)
|
||||
// ├ = E2 94 9C
|
||||
// └ = E2 94 94
|
||||
// ─ = E2 94 80
|
||||
if (pos + 8 < line.length() &&
|
||||
static_cast<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
(static_cast<unsigned char>(line[pos + 2]) == 0x9C || // ├
|
||||
static_cast<unsigned char>(line[pos + 2]) == 0x94) && // └
|
||||
static_cast<unsigned char>(line[pos + 3]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 4]) == 0x94 &&
|
||||
static_cast<unsigned char>(line[pos + 5]) == 0x80 && // ─
|
||||
static_cast<unsigned char>(line[pos + 6]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 7]) == 0x94 &&
|
||||
static_cast<unsigned char>(line[pos + 8]) == 0x80 && // ─
|
||||
(pos + 9 >= line.length() || line[pos + 9] == ' ')) {
|
||||
indent++;
|
||||
break; // We've found our indent marker, stop here
|
||||
}
|
||||
|
||||
// If we get here without finding a valid indent pattern, we're done
|
||||
break;
|
||||
}
|
||||
|
||||
return indent;
|
||||
}
|
||||
|
||||
// Parses a single line of the structure (after knowing its indent level) and returns a TreeNode.
|
||||
// The function "consumes" indent groups until the branch marker.
|
||||
TreeStructurer::TreeNode TreeStructurer::parse_structure_line(const std::string& line,
|
||||
size_t indent_level) {
|
||||
TreeStructurer::TreeNode TreeStructurer::parse_structure_line(const std::string& line, size_t indent_level) {
|
||||
size_t pos = 0;
|
||||
size_t groups = 0;
|
||||
while (pos + 4 <= line.size() && groups < indent_level) {
|
||||
std::string group = line.substr(pos, 4);
|
||||
if (group == " " || group == u8"│ ") {
|
||||
size_t current_indent = 0;
|
||||
|
||||
// Skip through indentation patterns
|
||||
while (current_indent < indent_level && pos < line.length()) {
|
||||
// Check for basic space indent
|
||||
if (pos + 3 < line.length() && line.substr(pos, 4) == " ") {
|
||||
pos += 4;
|
||||
groups++;
|
||||
current_indent++;
|
||||
continue;
|
||||
}
|
||||
if (group == u8"├── " || group == u8"└── ") {
|
||||
pos += 4;
|
||||
groups++;
|
||||
|
||||
// Check for │ followed by spaces
|
||||
if (pos + 5 < line.length() &&
|
||||
static_cast<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
static_cast<unsigned char>(line[pos + 2]) == 0x82 &&
|
||||
line[pos + 3] == ' ' &&
|
||||
line[pos + 4] == ' ' &&
|
||||
line[pos + 5] == ' ') {
|
||||
pos += 6;
|
||||
current_indent++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for ├── or └── pattern
|
||||
if (pos + 9 < line.length() &&
|
||||
static_cast<unsigned char>(line[pos]) == 0xE2 &&
|
||||
static_cast<unsigned char>(line[pos + 1]) == 0x94 &&
|
||||
(static_cast<unsigned char>(line[pos + 2]) == 0x9C ||
|
||||
static_cast<unsigned char>(line[pos + 2]) == 0x94)) {
|
||||
pos += 10; // Skip the entire pattern including space
|
||||
current_indent++;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Extract the name (everything after the indentation)
|
||||
std::string name = line.substr(pos);
|
||||
name = sanitize_path(name);
|
||||
|
||||
bool is_file = true;
|
||||
if (!name.empty() && name.back() == static_cast<char>(DIRECTORY_MARKER)) {
|
||||
if (!name.empty() && name.back() == '/') {
|
||||
is_file = false;
|
||||
name.pop_back(); // Remove the directory marker.
|
||||
name.pop_back();
|
||||
}
|
||||
|
||||
return TreeNode{name, is_file, {}};
|
||||
}
|
||||
|
||||
// Builds a tree (with TreeNode nodes) from the vector of structure lines.
|
||||
// The first line is assumed to be the root.
|
||||
TreeStructurer::TreeNode TreeStructurer::build_tree_from_lines(const std::vector<std::string>& lines) {
|
||||
|
@ -350,31 +409,42 @@ void TreeStructurer::create_file(const fs::path& path) {
|
|||
// Reads a structure file into a vector of non-empty lines.
|
||||
std::vector<std::string> TreeStructurer::read_structure_file(const std::string& filepath) {
|
||||
std::vector<std::string> lines;
|
||||
std::ifstream file(filepath);
|
||||
// Open file in binary mode to avoid Windows CRLF conversion
|
||||
std::ifstream file(filepath, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Failed to open file: " + filepath);
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
// Remove carriage return if present (Windows files)
|
||||
if (!line.empty() && line.back() == '\r') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
if (!line.empty()) {
|
||||
lines.push_back(line);
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Checks the structure for obvious mistakes (e.g. a jump in indentation).
|
||||
void TreeStructurer::validate_structure(const std::vector<std::string>& lines) {
|
||||
if (lines.empty()) {
|
||||
throw std::runtime_error("Empty structure provided");
|
||||
}
|
||||
size_t prev_indent = 0;
|
||||
for (const auto& line : lines) {
|
||||
for (size_t i = 0; i < lines.size(); i++) {
|
||||
const auto& line = lines[i];
|
||||
if (line.empty()) continue;
|
||||
size_t indent = get_indent_level(line);
|
||||
std::cout << "Line: \"" << line << "\" Indent level: " << indent << std::endl;
|
||||
|
||||
size_t indent = get_indent_level(line);
|
||||
if (indent > prev_indent + 1) {
|
||||
throw std::runtime_error("Invalid indentation structure in the file");
|
||||
throw std::runtime_error(
|
||||
"Invalid indentation jump at line " + std::to_string(i + 1) +
|
||||
": from level " + std::to_string(prev_indent) +
|
||||
" to " + std::to_string(indent)
|
||||
);
|
||||
}
|
||||
prev_indent = indent;
|
||||
}
|
||||
|
|
|
@ -11,9 +11,7 @@ public:
|
|||
TreeStructurer() = default;
|
||||
|
||||
std::vector<std::string> get_directory_structure(const std::string& startpath = std::filesystem::current_path().string());
|
||||
|
||||
void create_structure_from_file(const std::string& filepath, const std::string& target_path = std::filesystem::current_path().string());
|
||||
void create_structure_from_string(const std::string& structure, const std::string& target_path = std::filesystem::current_path().string());
|
||||
|
||||
private:
|
||||
struct TreeNode {
|
||||
|
|
Loading…
Reference in New Issue