diff --git a/.vscode/settings.json b/.vscode/settings.json index 1c29706..5dc5c07 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -67,6 +67,9 @@ "xmemory": "cpp", "xstring": "cpp", "xtr1common": "cpp", - "xutility": "cpp" + "xutility": "cpp", + "fstream": "cpp", + "iostream": "cpp", + "codecvt": "cpp" } } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6a57252..4f0aa46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [] diff --git a/setup.py b/setup.py index 79f7bc5..152e1bb 100644 --- a/setup.py +++ b/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"), diff --git a/src/prodir/__init__.py b/src/prodir/__init__.py index 520a304..84442b3 100644 --- a/src/prodir/__init__.py +++ b/src/prodir/__init__.py @@ -1 +1,11 @@ -from prodir._tree_structurer import create_tree_structurer, get_structure \ No newline at end of file +from prodir._tree_structurer import ( + create_tree_structurer, + get_structure, + create_structure_from_file +) + +__all__ = [ + 'create_tree_structurer', + 'get_structure', + 'create_structure_from_file' +] \ No newline at end of file diff --git a/src/prodir/__main__.py b/src/prodir/__main__.py index 1967b82..d1cc593 100644 --- a/src/prodir/__main__.py +++ b/src/prodir/__main__.py @@ -1,36 +1,102 @@ import os import sys import argparse -from prodir import create_tree_structurer, get_structure +from prodir import ( + create_tree_structurer, + get_structure, + create_structure_from_file +) def main(): - parser = argparse.ArgumentParser(description='Display directory structure in a tree-like format') - parser.add_argument('path', nargs='?', default=os.getcwd(), - help='Path to analyze (default: current directory)') - parser.add_argument('-v', '--verbose', action='store_true', - help='Show more detailed output') + 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( + 'path', + nargs='?', + default=os.getcwd(), + help='Path to analyze (default: current directory)' + ) + display_parser.add_argument( + '-v', '--verbose', + action='store_true', + help='Show more detailed output' + ) + + # Create command + create_parser = subparsers.add_parser('create', help='Create directory structure') + create_parser.add_argument( + 'file', + help='File containing the directory structure' + ) + create_parser.add_argument( + '-o', '--output', + default=os.getcwd(), + help='Output directory (default: current directory)' + ) + create_parser.add_argument( + '-v', '--verbose', + 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: - if args.verbose: - print(f"Analyzing directory: {args.path}") - create_tree_structurer() - structure = get_structure(args.path) - - for line in structure: - print(line) - + + 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 args.verbose: + print(f"Creating directory structure in: {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: - print(f"Error: The specified directory does not exist: {args.path}", file=sys.stderr) + print(f"Error: The specified directory does not exist: {args.path if args.command == 'display' else args.output}", + file=sys.stderr) elif "Directory is empty" in error_msg: - print(f"Error: The specified directory is empty: {args.path}", file=sys.stderr) + print(f"Error: The specified directory is empty: {args.path if args.command == 'display' else args.output}", + file=sys.stderr) elif "Path is not a directory" in error_msg: - print(f"Error: The specified path is not a directory: {args.path}", file=sys.stderr) + print(f"Error: The specified path is not a directory: {args.path if args.command == 'display' else args.output}", + file=sys.stderr) + elif "Unable to open structure file" in error_msg: + print(f"Error: Unable to open structure file: {args.file}", file=sys.stderr) + elif "Invalid structure format" in error_msg: + print(f"Error: Invalid structure format in {'file' if args.file else 'string'}", file=sys.stderr) else: print(f"Error: {error_msg}", file=sys.stderr) sys.exit(1) + if __name__ == "__main__": main() \ No newline at end of file diff --git a/src/prodir/cpp/bindings.cpp b/src/prodir/cpp/bindings.cpp index 6aa031a..affbede 100644 --- a/src/prodir/cpp/bindings.cpp +++ b/src/prodir/cpp/bindings.cpp @@ -18,19 +18,26 @@ static PyObject* get_structure(PyObject* self, PyObject* args) { if (!PyArg_ParseTuple(args, "|s", &path)) { return NULL; } - if (g_tree_structurer == nullptr) { PyErr_SetString(TreeStructurerError, "TreeStructurer not initialized"); return NULL; } - + try { std::string path_str = path ? path : ""; std::vector structure = g_tree_structurer->get_directory_structure(path_str); PyObject* list = PyList_New(structure.size()); + if (!list) { + return NULL; + } for (size_t i = 0; i < structure.size(); i++) { - PyList_SET_ITEM(list, i, PyUnicode_FromString(structure[i].c_str())); - } + PyObject* str = PyUnicode_FromString(structure[i].c_str()); + if (!str) { + Py_DECREF(list); + return NULL; + } + PyList_SET_ITEM(list, i, str); + } return list; } catch (const std::exception& e) { PyErr_SetString(TreeStructurerError, e.what()); @@ -38,27 +45,50 @@ static PyObject* get_structure(PyObject* self, PyObject* args) { } } +static PyObject* create_structure_from_file(PyObject* self, PyObject* args) { + const char* structure_file = nullptr; + const char* target_path = "."; // Default to current directory + if (!PyArg_ParseTuple(args, "s|s", &structure_file, &target_path)) { + return NULL; + } + + if (g_tree_structurer == nullptr) { + PyErr_SetString(TreeStructurerError, "TreeStructurer not initialized"); + return NULL; + } + + try { + g_tree_structurer->create_structure_from_file(structure_file, 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_tree_structurer", create_tree_structurer, METH_NOARGS, "Create a new TreeStructurer instance"}, {"get_structure", get_structure, METH_VARARGS, "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"}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef tree_structurer_module = { PyModuleDef_HEAD_INIT, - "_tree_structurer", // Changed module name to match Python import - "Module for analyzing directory structures", + "_tree_structurer", + "Module for analyzing and creating directory structures", -1, TreeStructurerMethods }; -PyMODINIT_FUNC PyInit__tree_structurer(void) { // Changed function name to match module name +PyMODINIT_FUNC PyInit__tree_structurer(void) { PyObject* m = PyModule_Create(&tree_structurer_module); if (m == NULL) return NULL; - + TreeStructurerError = PyErr_NewException("tree_structurer.error", NULL, NULL); Py_XINCREF(TreeStructurerError); if (PyModule_AddObject(m, "error", TreeStructurerError) < 0) { @@ -67,6 +97,6 @@ PyMODINIT_FUNC PyInit__tree_structurer(void) { // Changed function name to matc Py_DECREF(m); return NULL; } - + return m; } \ No newline at end of file diff --git a/src/prodir/cpp/tree_structurer.cpp b/src/prodir/cpp/tree_structurer.cpp index 2ef3be2..2302cce 100644 --- a/src/prodir/cpp/tree_structurer.cpp +++ b/src/prodir/cpp/tree_structurer.cpp @@ -1,6 +1,12 @@ #include "tree_structurer.hpp" + #include #include +#include +#include +#include +#include +#include namespace fs = std::filesystem; @@ -10,9 +16,9 @@ bool TreeStructurer::should_ignore_dir(const std::string& dirname) { ".git", ".idea", ".vscode", "__pycache__", "**pycache**" }; - // Check for __main__.py or __init__.py files + // Allow these Python files even if their names start with underscores. if (!dirname.empty() && (dirname == "__main__.py" || dirname == "__init__.py")) { - return false; // Do not ignore these files + return false; } if (std::find(ignore_list.begin(), ignore_list.end(), dirname) != ignore_list.end()) { @@ -37,9 +43,8 @@ bool TreeStructurer::should_ignore_file(const std::string& filename) { ".o", ".obj", ".a", ".lib" }; - // Check for __main__.py or __init__.py files if (!filename.empty() && (filename == "__main__.py" || filename == "__init__.py")) { - return false; // Do not ignore these files + return false; } if (!filename.empty() && (filename[0] == '.' || filename[0] == '_')) { @@ -55,12 +60,12 @@ bool TreeStructurer::should_ignore_file(const std::string& filename) { return false; } -// Add the missing get_relative_path implementation std::string TreeStructurer::get_relative_path(const fs::path& path, const fs::path& base) { fs::path rel = fs::relative(path, base); return rel.string(); } + std::vector TreeStructurer::get_filtered_paths(const fs::path& start) { std::vector paths; fs::directory_options options = fs::directory_options::skip_permission_denied; @@ -76,7 +81,6 @@ std::vector TreeStructurer::get_filtered_paths(const fs::path& start) paths.push_back(start); - // Check if directory is empty bool is_empty = fs::directory_iterator(start) == fs::directory_iterator(); if (is_empty) { throw std::runtime_error("Directory is empty: " + start.string()); @@ -112,6 +116,11 @@ std::vector TreeStructurer::get_filtered_paths(const fs::path& start) return paths; } + +// ----------------------------------------------------------------------------- +// Directory Structure Generation (tree printing) +// ----------------------------------------------------------------------------- + std::vector TreeStructurer::get_directory_structure(const std::string& startpath) { std::vector result; @@ -171,4 +180,283 @@ std::vector TreeStructurer::get_directory_structure(const std::stri } return result; -} \ No newline at end of file +} + +// ----------------------------------------------------------------------------- +// Structure Creation from a Tree-like File or String +// ----------------------------------------------------------------------------- +void TreeStructurer::create_structure_from_file(const std::string& filepath, + const std::string& target_path) { + std::vector lines = read_structure_file(filepath); + validate_structure(lines); + TreeNode root = build_tree_from_lines(lines); + + 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); + } +} + +// ----------------------------------------------------------------------------- +// Private Helper Functions (no duplicates) +// ----------------------------------------------------------------------------- + +// Returns a string consisting of (level * 4) spaces. +std::string TreeStructurer::create_indent(size_t level) { + return std::string(level * 4, ' '); +} + +// 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 < 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; + } + + // Check for │ (vertical line) followed by 3 spaces + // Bytes: E2 94 82 20 20 20 + if (pos + 5 < line.length() && + static_cast(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + static_cast(line[pos + 2]) == 0x82 && + line[pos + 3] == ' ' && + line[pos + 4] == ' ' && + line[pos + 5] == ' ') { + indent++; + 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(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + (static_cast(line[pos + 2]) == 0x9C || // ├ + static_cast(line[pos + 2]) == 0x94) && // └ + static_cast(line[pos + 3]) == 0xE2 && + static_cast(line[pos + 4]) == 0x94 && + static_cast(line[pos + 5]) == 0x80 && // ─ + static_cast(line[pos + 6]) == 0xE2 && + static_cast(line[pos + 7]) == 0x94 && + static_cast(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) { + size_t pos = 0; + 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; + current_indent++; + continue; + } + + // Check for │ followed by spaces + if (pos + 5 < line.length() && + static_cast(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + static_cast(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(line[pos]) == 0xE2 && + static_cast(line[pos + 1]) == 0x94 && + (static_cast(line[pos + 2]) == 0x9C || + static_cast(line[pos + 2]) == 0x94)) { + pos += 10; // Skip the entire pattern including space + current_indent++; + 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() == '/') { + is_file = false; + 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& lines) { + if (lines.empty()) { + throw std::runtime_error("Empty structure provided"); + } + // Process the first line as the root. + TreeNode root = parse_structure_line(lines[0], 0); + if (root.is_file) { + throw std::runtime_error("Root must be a directory"); + } + std::vector stack; + stack.push_back(&root); + + // Process each subsequent line. + for (size_t i = 1; i < lines.size(); ++i) { + size_t indent = get_indent_level(lines[i]); + TreeNode node = parse_structure_line(lines[i], indent); + if (indent > stack.size()) { + throw std::runtime_error("Invalid indentation structure in the file"); + } + while (stack.size() > indent) { + stack.pop_back(); + } + if (stack.empty()) { + throw std::runtime_error("Invalid indentation structure in the file"); + } + stack.back()->children.push_back(node); + if (!node.is_file) { + // Push a pointer to the newly added child. + stack.push_back(&stack.back()->children.back()); + } + } + return root; +} + +// Recursively creates directories and files on disk according to the tree. +void TreeStructurer::create_node(const TreeNode& node, const fs::path& current_path) { + fs::path new_path = current_path / node.name; + try { + if (node.is_file) { + // Ensure the parent directory exists. + fs::path parent = new_path.parent_path(); + if (!fs::exists(parent)) { + fs::create_directories(parent); + } + create_file(new_path); + } else { + create_directory(new_path); + for (const auto& child : node.children) { + create_node(child, new_path); + } + } + } catch (const fs::filesystem_error& e) { + throw std::runtime_error("Failed to create path '" + new_path.string() + "': " + e.what()); + } +} + +// Returns true if the given line’s last character is a directory marker. +bool TreeStructurer::is_directory_marker(const std::string& line) { + return (!line.empty() && line.back() == static_cast(DIRECTORY_MARKER)); +} + +// Creates a directory (and any necessary parent directories). +void TreeStructurer::create_directory(const fs::path& path) { + if (!fs::exists(path)) { + fs::create_directories(path); + } +} + +// Creates an empty file. +void TreeStructurer::create_file(const fs::path& path) { + if (!fs::exists(path)) { + std::ofstream ofs(path); + if (!ofs.is_open()) { + throw std::runtime_error("Failed to create file: " + path.string()); + } + ofs.close(); + } +} + +// Reads a structure file into a vector of non-empty lines. +std::vector TreeStructurer::read_structure_file(const std::string& filepath) { + std::vector lines; + // 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& lines) { + if (lines.empty()) { + throw std::runtime_error("Empty structure provided"); + } + size_t prev_indent = 0; + 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); + if (indent > prev_indent + 1) { + 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; + } +} + +// Removes any disallowed characters from a node name (here we allow printable ASCII and '/'). +std::string TreeStructurer::sanitize_path(const std::string& path) { + std::string result; + for (char c : path) { + if ((c >= 32 && c <= 126) || c == '/') { + result.push_back(c); + } + } + return result; +} diff --git a/src/prodir/cpp/tree_structurer.hpp b/src/prodir/cpp/tree_structurer.hpp index 06c51be..d1b800e 100644 --- a/src/prodir/cpp/tree_structurer.hpp +++ b/src/prodir/cpp/tree_structurer.hpp @@ -2,16 +2,44 @@ #include #include #include +#include +#include +#include class TreeStructurer { public: TreeStructurer() = default; + std::vector 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()); private: + struct TreeNode { + std::string name; + bool is_file; + std::vector children; + }; + bool should_ignore_dir(const std::string& dirname); bool should_ignore_file(const std::string& filename); - std::string create_indent(int level); + std::string create_indent(size_t level); std::string get_relative_path(const std::filesystem::path& path, const std::filesystem::path& base); std::vector get_filtered_paths(const std::filesystem::path& start); -}; \ No newline at end of file + size_t get_indent_level(const std::string& line); + TreeNode parse_structure_line(const std::string& line, size_t indent_level); + TreeNode build_tree_from_lines(const std::vector& lines); + void create_node(const TreeNode& node, const std::filesystem::path& current_path); + bool is_directory_marker(const std::string& line); + void create_directory(const std::filesystem::path& path); + void create_file(const std::filesystem::path& path); + std::vector read_structure_file(const std::string& filepath); + void validate_structure(const std::vector& lines); + std::string sanitize_path(const std::string& path); + + // Update the constants to use wide characters + static const wchar_t DIRECTORY_MARKER = L'/'; + static const wchar_t TREE_PIPE = L'│'; + static const wchar_t TREE_BRANCH = L'├'; + static const wchar_t TREE_CORNER = L'└'; + static const wchar_t TREE_DASH = L'─'; +};