Merge pull request 'add_creation' (#9) from add_creation into develop
Gitea Actions For Tree-Structurer / Explore-Gitea-Actions (push) Has been cancelled Details

Reviewed-on: #9
This commit is contained in:
Falko Victor Habel 2025-02-09 11:12:34 +00:00
commit e0eb17512c
8 changed files with 466 additions and 41 deletions

View File

@ -67,6 +67,9 @@
"xmemory": "cpp",
"xstring": "cpp",
"xtr1common": "cpp",
"xutility": "cpp"
"xutility": "cpp",
"fstream": "cpp",
"iostream": "cpp",
"codecvt": "cpp"
}
}

View File

@ -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 = []

View File

@ -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"),

View File

@ -1 +1,11 @@
from prodir._tree_structurer import create_tree_structurer, get_structure
from prodir._tree_structurer import (
create_tree_structurer,
get_structure,
create_structure_from_file
)
__all__ = [
'create_tree_structurer',
'get_structure',
'create_structure_from_file'
]

View File

@ -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()

View File

@ -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<std::string> 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;
}

View File

@ -1,6 +1,12 @@
#include "tree_structurer.hpp"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <functional>
#include <iostream>
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<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;
@ -76,7 +81,6 @@ std::vector<fs::path> 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<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> result;
@ -171,4 +180,283 @@ std::vector<std::string> TreeStructurer::get_directory_structure(const std::stri
}
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) {
std::vector<std::string> 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 4character 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<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++;
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) {
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<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;
}
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<std::string>& 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<TreeNode*> 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 lines last character is a directory marker.
bool TreeStructurer::is_directory_marker(const std::string& line) {
return (!line.empty() && line.back() == static_cast<char>(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<std::string> TreeStructurer::read_structure_file(const std::string& filepath) {
std::vector<std::string> 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<std::string>& 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;
}

View File

@ -2,16 +2,44 @@
#include <string>
#include <vector>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <algorithm>
class TreeStructurer {
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());
private:
struct TreeNode {
std::string name;
bool is_file;
std::vector<TreeNode> 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<std::filesystem::path> get_filtered_paths(const std::filesystem::path& start);
};
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<std::string>& 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<std::string> read_structure_file(const std::string& filepath);
void validate_structure(const std::vector<std::string>& 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'';
};