Repository: Viatorus/quom

Issue:
Issues when used in conjunctions with CMake
I have a [C++ project](https://github.com/Macr0Nerd/eztplib) that uses CMake. The paths are provided by CMake, meaning they aren't wholly relative. This causes quom to not work with this. Is there a way to specify a source directory to always look for files in? All of my files are organized in a tree structure so that if I could specify "look from x directory" quom would find everything it needs.

Discussion:

Patch:
diff --git a/README.md b/README.md
index a993850..db84fbb 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,8 @@ optional arguments:
   --include_guard format, -g format
                         Regex format of the include guard. Default: None
   --trim, -t            Reduce continuous line breaks to one. Default: True
+  --include_directory INCLUDE_DIRECTORY, -I INCLUDE_DIRECTORY
+                        Add include directories for header files.
 ```
 
 ## Simple example
diff --git a/examples/include_directory/README.md b/examples/include_directory/README.md
new file mode 100644
index 0000000..57599b5
--- /dev/null
+++ b/examples/include_directory/README.md
@@ -0,0 +1,3 @@
+```
+quom libfoobar/foobar.hpp foobar_gen.hpp -s "~> stitch <~" -g FOOBAR_.+_HPP -I include/
+```
diff --git a/examples/include_directory/foobar_gen.hpp b/examples/include_directory/foobar_gen.hpp
new file mode 100644
index 0000000..63c83dc
--- /dev/null
+++ b/examples/include_directory/foobar_gen.hpp
@@ -0,0 +1,54 @@
+#pragma once
+
+#ifndef FOOBAR_HPP
+#define FOOBAR_HPP
+
+class Base {
+public:
+    Base();
+};
+
+class Foo : Base {
+public:
+    Abc();
+};
+
+#define none
+
+class Def : Base {
+public:
+    Def();
+};
+
+void start() {
+    Abc abc;
+    Def def;
+}
+
+#endif // FOOBAR_HPP
+
+#ifdef FOOBAR_MAIN
+
+#include <iostream>
+
+/* hello */
+Base::Base() {
+    std::cout << "Base constructed" << std::endl;
+}
+
+Abc::Abc() {
+}
+
+void call();
+
+Def::Def() {
+	call();
+}
+
+#include <iostream>
+
+void call() {
+    std::cout << "called" << std::endl;
+}
+
+#endif /* FOOBAR_MAIN */
diff --git a/examples/include_directory/include/libfoobar/bar.cpp b/examples/include_directory/include/libfoobar/bar.cpp
new file mode 100644
index 0000000..02b0beb
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/bar.cpp
@@ -0,0 +1,7 @@
+#include "bar.hpp"
+
+#include "util.hpp"
+
+Def::Def() {
+	call();
+}
\ No newline at end of file
diff --git a/examples/include_directory/include/libfoobar/bar.hpp b/examples/include_directory/include/libfoobar/bar.hpp
new file mode 100644
index 0000000..d6981b1
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/bar.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#ifndef /*aha*/ FOOBAR_BAR_HPP
+#define FOOBAR_BAR_HPP
+
+#include "libfoobar/base.hpp"
+
+class Def : Base {
+public:
+    Def();
+};
+
+#endif //FOOBAR_BAR_HPP
diff --git a/examples/include_directory/include/libfoobar/base.cpp b/examples/include_directory/include/libfoobar/base.cpp
new file mode 100644
index 0000000..8e0cb0a
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/base.cpp
@@ -0,0 +1,8 @@
+#include "base.hpp"
+
+#include <iostream>
+
+/* hello */
+Base::Base() {
+    std::cout << "Base constructed" << std::endl;
+}
diff --git a/examples/include_directory/include/libfoobar/base.hpp b/examples/include_directory/include/libfoobar/base.hpp
new file mode 100644
index 0000000..8da5916
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/base.hpp
@@ -0,0 +1,11 @@
+#pragma once
+
+#ifndef FOOBAR_BASE_HPP
+#define FOOBAR_BASE_HPP
+
+class Base {
+public:
+    Base();
+};
+
+#endif // FOOBAR_BASE_HPP
diff --git a/examples/include_directory/include/libfoobar/foo.cpp b/examples/include_directory/include/libfoobar/foo.cpp
new file mode 100644
index 0000000..9b0746c
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/foo.cpp
@@ -0,0 +1,4 @@
+#include "foo.hpp"
+
+Abc::Abc() {
+}
\ No newline at end of file
diff --git a/examples/include_directory/include/libfoobar/foo.hpp b/examples/include_directory/include/libfoobar/foo.hpp
new file mode 100644
index 0000000..f57d882
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/foo.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "libfoobar/base.hpp"
+
+#/*123*/ifndef FOOBAR_FOO_HPP
+#define /*123*/ FOOBAR_FOO_HPP
+
+class Foo : Base {
+public:
+    Abc();
+};
+
+# /*123*/ endif /* FOOBAR_FOO_HPP */
+#define none
diff --git a/examples/include_directory/include/libfoobar/foobar.hpp b/examples/include_directory/include/libfoobar/foobar.hpp
new file mode 100644
index 0000000..c8f9914
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/foobar.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#ifndef FOOBAR_HPP
+#define FOOBAR_HPP
+
+#include "libfoobar/foo.hpp"
+#include "libfoobar/bar.hpp"
+
+void start() {
+    Abc abc;
+    Def def;
+}
+
+#endif // FOOBAR_HPP
+
+#ifdef FOOBAR_MAIN
+
+// ~> stitch <~
+
+#endif /* FOOBAR_MAIN */
diff --git a/examples/include_directory/include/libfoobar/util.cpp b/examples/include_directory/include/libfoobar/util.cpp
new file mode 100644
index 0000000..94612f5
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/util.cpp
@@ -0,0 +1,7 @@
+#include "util.hpp"
+
+#include <iostream>
+
+void call() {
+    std::cout << "called" << std::endl;
+}
\ No newline at end of file
diff --git a/examples/include_directory/include/libfoobar/util.hpp b/examples/include_directory/include/libfoobar/util.hpp
new file mode 100644
index 0000000..7a6dcf8
--- /dev/null
+++ b/examples/include_directory/include/libfoobar/util.hpp
@@ -0,0 +1,8 @@
+#pragma once
+
+#ifndef /*aha*/ FOOBAR_UTIL_HPP
+#define FOOBAR_UTIL_HPP
+
+void call();
+
+#endif // FOOBAR_UTIL_HPP
diff --git a/quom/__main__.py b/quom/__main__.py
index 6140dd8..d717853 100644
--- a/quom/__main__.py
+++ b/quom/__main__.py
@@ -1,5 +1,6 @@
 import argparse
 import sys
+import traceback
 from pathlib import Path
 
 from .quom import Quom
@@ -17,15 +18,13 @@ def main():
                         help='Regex format of the include guard. Default: %(default)s')
     parser.add_argument('--trim', '-t', action='store_true', default=True,
                         help='Reduce continuous line breaks to one. Default: %(default)s')
+    parser.add_argument('--include_directory', '-I', type=Path, action='append', default=[],
+                        help='Add include directories for header files.')
 
     args = parser.parse_args()
 
-    try:
-        with args.output_path.open('w+') as file:
-            Quom(args.input_path, file, args.stitch, args.include_guard, args.trim)
-    except Exception as e:
-        print('Error: {}'.format(e), file=sys.stderr)
-        return 1
+    with args.output_path.open('w+') as file:
+        Quom(args.input_path, file, args.stitch, args.include_guard, args.trim, args.include_directory)
 
 
 if __name__ == '__main__':
diff --git a/quom/quom.py b/quom/quom.py
index 5bdf7f0..20bc83e 100644
--- a/quom/quom.py
+++ b/quom/quom.py
@@ -28,25 +28,27 @@ def contains_only_whitespace_and_comment_tokens(tokens: List[Token]):
 
 class Quom:
     def __init__(self, src_file_path: Union[Path, str], dst: TextIO, stitch_format: str = None,
-                 include_guard_format: str = None, trim: bool = True):
+                 include_guard_format: str = None, trim: bool = True,
+                 include_directories: List[Union[Path, str]] = None):
         self.__dst = dst
         self.__stitch_format = stitch_format
         self.__include_guard_format = re.compile('^{}$'.format(include_guard_format)) if include_guard_format else None
         self.__trim = trim
+        self.__include_directories = [Path(x) for x in include_directories] if include_directories else []
 
         self.__processed_files = set()
         self.__source_files = Queue()
-        self.__cont_lb = CONTINUOUS_BREAK_REACHED
+        self.__cont_lb = CONTINUOUS_LINE_BREAK_START
         self.__prev_token = EmptyToken()
 
-        self.__process_file(Path(src_file_path), False, True)
+        self.__process_file(Path(), src_file_path, False, True)
 
         if not self.__source_files.empty():
             if stitch_format is not None:
                 raise QuomError('Couldn\'t stitch source files. The stitch location "{}" was not found.'
                                 .format(stitch_format))
             while not self.__source_files.empty():
-                self.__process_file(self.__source_files.get(), True)
+                self.__process_file(Path(), self.__source_files.get(), True)
             # Write last token.
             self.__write_token(self.__prev_token, True)
         elif self.__cont_lb == CONTINUOUS_LINE_BREAK_START or not isinstance(self.__prev_token,
@@ -54,18 +56,29 @@ class Quom:
             # Write last token, if not a continuous line break.
             self.__write_token(self.__prev_token, True)
 
-    def __process_file(self, file_path: Path, is_source_file: bool, is_main_header=False):
+    def __process_file(self, relative_path: Path, include_path: Path, is_source_file: bool,
+                       is_main_header=False):
+        # First check if file exists relative.
+        file_path = relative_path / include_path
+        if file_path.exists():
+            with file_path.open() as file:
+                tokens = tokenize(file.read())
+        else:
+            # Otherwise search in include directories.
+            for include_directory in self.__include_directories:
+                file_path = include_directory / include_path
+                if file_path.exists():
+                    with file_path.open() as file:
+                        tokens = tokenize(file.read())
+                    break
+            else:
+                raise QuomError('Include not found: \'{}\''.format(include_path))
+
         # Skip already processed files.
         if file_path in self.__processed_files:
             return
         self.__processed_files.add(file_path)
 
-        try:
-            with file_path.open() as file:
-                tokens = tokenize(file.read())
-        except FileNotFoundError:
-            raise QuomError('File not found: \'{}\''.format(file_path))
-
         for token in tokens:
             # Find local includes.
             token = self.__scan_for_include(file_path, token, is_source_file)
@@ -131,7 +144,7 @@ class Quom:
         if not isinstance(token, PreprocessorIncludeToken) or not token.is_local_include:
             return token
 
-        self.__process_file(file_path.parent / str(token.path), is_source_file)
+        self.__process_file(file_path.parent, Path(str(token.path)), is_source_file)
         # Take include tokens line break token if any.
         token = token.preprocessor_tokens[-2]
         if isinstance(token, LinebreakWhitespaceToken):
@@ -144,7 +157,7 @@ class Quom:
             return False
 
         while not self.__source_files.empty():
-            self.__process_file(self.__source_files.get(), True)
+            self.__process_file(Path(), self.__source_files.get(), True)
 
         return True
 


Test patch:
diff --git a/tests/test_quom/test_include_directory.py b/tests/test_quom/test_include_directory.py
new file mode 100644
index 0000000..1d8e572
--- /dev/null
+++ b/tests/test_quom/test_include_directory.py
@@ -0,0 +1,115 @@
+import os
+from io import StringIO
+from pathlib import Path
+
+import pytest
+
+from quom import Quom, QuomError
+
+FILE_MAIN_HPP = """
+int foo = 3;
+
+#include "my_lib/core/core.hpp"
+#include "my_other_lib/bar.hpp"
+"""
+
+FILE_CORE_HPP = """
+#pragma once
+
+#include "my_lib/util/foo.hpp"
+"""
+
+FILE_CORE_CPP = """
+#include "my_lib/core/core.hpp"
+
+int i = 10;
+"""
+
+FILE_FOO_HPP = """
+#pragma once
+
+int foo();
+"""
+
+FILE_FOO_CPP = """
+#include "foo.hpp"
+
+int foo() { return 42; }
+"""
+
+FILE_BAR_HPP = """
+#pragma once
+
+float bar();
+"""
+
+FILE_INFO_HPP = """
+
+float f = 0.42;
+
+"""
+
+FILE_BAR_CPP = """
+#include "bar.hpp"
+
+#include "info.hpp"
+
+float bar() { return f; }
+"""
+
+RESULT = """
+int foo = 3;
+
+int foo();
+
+float bar();
+
+int foo() { return 42; }
+
+int i = 10;
+
+float f = 0.42;
+
+float bar() { return f; }
+"""
+
+
+def test_include_directory(fs):
+    os.makedirs('include/my_lib/core')
+    os.makedirs('include/my_lib/util')
+    os.makedirs('include/my_other_lib/')
+
+    with open('include/my_lib/main.hpp', 'w+') as file:
+        file.write(FILE_MAIN_HPP)
+
+    with open('include/my_lib/core/core.hpp', 'w+') as file:
+        file.write(FILE_CORE_HPP)
+
+    with open('include/my_lib/core/core.cpp', 'w+') as file:
+        file.write(FILE_CORE_CPP)
+
+    with open('include/my_lib/util/foo.hpp', 'w+') as file:
+        file.write(FILE_FOO_HPP)
+
+    with open('include/my_lib/util/foo.cpp', 'w+') as file:
+        file.write(FILE_FOO_CPP)
+
+    with open('include/my_other_lib/bar.hpp', 'w+') as file:
+        file.write(FILE_BAR_HPP)
+
+    with open('include/my_other_lib/bar.cpp', 'w+') as file:
+        file.write(FILE_BAR_CPP)
+
+    with open('include/my_other_lib/info.hpp', 'w+') as file:
+        file.write(FILE_INFO_HPP)
+
+    dst = StringIO()
+
+    with pytest.raises(QuomError):
+        Quom(Path('include/my_lib/main.hpp'), dst)
+
+    dst = StringIO()
+
+    Quom(Path('include/my_lib/main.hpp'), dst, include_directories=['include/'])
+
+    assert dst.getvalue() == RESULT