Skip to content

Instantly share code, notes, and snippets.

@gerwin3
Last active January 23, 2021 16:37
Show Gist options
  • Save gerwin3/4bc6be2cebd253025aea6d166279ab86 to your computer and use it in GitHub Desktop.
Save gerwin3/4bc6be2cebd253025aea6d166279ab86 to your computer and use it in GitHub Desktop.
Generate skeleton project for C++17 project

Automated creation of C++ project structure

From time to time I find myself manually creating a directory structure for my new C++ project. Usually, you will start by defining the program architecture, then continue to create header and source files for each of the classes, add include guards, add namespaces and much more. Why do this from scratch every time?

This little script takes a "tree" file as input, and then generates the entire source tree for you! It includes:

  • CMakeLists.txt
    • With settings for C++17
    • Correct source_group structure
    • Seperation between executable and lib
  • The entire directory tree with header and source files for each class
    • Header files include include guards
    • Header and source files have correct namespace
    • All classes will have a default constructor
    • Automatic inheritance of other classes in specified (with necessary includes!)
    • Deleted copy constructors if specified
  • A main.cpp file that initializes one instance of each class to prove it compiles!

To try this out, copy over the codegen.py and tree files to the same directory.

  1. Run python3 -m codegen tree dst and wait for done!.
  2. Go to the destination directory: cd dst.
  3. Try and compile! mkdir build && cd build && cmake .. && make && ./my-project
  4. You should see Successfully tested program structure..

Now edit the tree file yourself and see the results!

import sys
import os
MIN_CMAKE_VERSION = "3.10"
LF = '\n'
FIRST_NAMESPACE_IS_DEFAULT = True
TPL_CMAKELISTS = \
"""cmake_minimum_required(VERSION {#MIN_CMAKE_VERSION})
project({#PROJECT} VERSION {#VERSION})
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
file(GLOB_RECURSE SOURCE_LIST "src/*.h" "src/*.cpp")
add_library({#PROJECT}-lib "${SOURCE_LIST}")
foreach(SOURCE_ABSOLUTE IN LISTS SOURCE_LIST)
file(RELATIVE_PATH SOURCE "${CMAKE_CURRENT_SOURCE_DIR}" "${SOURCE_ABSOLUTE}")
get_filename_component(SOURCE_PATH "${SOURCE}" PATH)
string(REPLACE "/" "\\\\" SOURCE_DIR "${SOURCE_PATH}")
source_group("${SOURCE_DIR}" FILES "${SOURCE}")
endforeach()
add_executable({#PROJECT} src/main.cpp)
target_link_libraries({#PROJECT} PRIVATE {#PROJECT}-lib)
"""
TPL_MAIN = \
"""#include <iostream>
{#INCLUDE_ALL_HEADERS}
{#USE_DEFAULT_NAMESPACE}
int main (int argc, char* argv[]) {
{#CONSTRUCT_ALL_CLASSES}
std::cout << "Successfully tested program structure." << std::endl;
return 0;
}
"""
TPL_HEADER = \
"""#ifndef {#DIRECTIVE}
#define {#DIRECTIVE}
{#INCLUDES}
namespace {#NAMESPACE} {
class {#NAME}{#INHERIT} {
public:
{#NAME}();{#PROP_NO_COPY}
};
}
#endif // {#DIRECTIVE}
"""
TPL_SOURCE = \
"""#include "{#NAME}.h"
namespace {#NAMESPACE} {
{#NAME}::{#NAME}() {}
}
"""
PROP_NO_COPY = """
{#NAME}({#NAME}&) = delete;
{#NAME}& operator=({#NAME}&) = delete;"""
def parse_tree_lines(ls):
lineno = 0
path = []
clevel = -1
def _determine_type(l):
symbol = l.strip()[0]
if symbol == '-':
return 'namespace'
elif symbol == '*':
return 'class'
elif symbol == '#':
return 'project'
else:
raise ValueError(f'syntax error on line {lineno}: expected one of "-", "*" but found "{symbol}"')
def _determine_level(l):
white = 0
for c in l:
if c != ' ':
break
else:
white += 1
else:
raise ValueError(f'syntax error on line {lineno}: did not find anything besides whitespace')
if white % 2 != 0:
raise ValueError(f'syntax error on line {lineno}: unexpected indent size {white}, indent must be divisible by 2')
return int(white / 2)
def _deconstruct_identifier(l):
identifier = l.strip()[1:].strip()
parts = [p.strip() for p in identifier.split(' ')]
stage = 'name'
name, props, base = '', [], None
for part in parts:
defer_set_stage = None
if part.startswith('('):
stage = 'props'
part = part[1:]
if part.endswith(')'):
defer_set_stage = None
part = part[:-1]
if part == ':':
stage = 'base'
continue
if stage == 'name':
name += part
elif stage == 'props':
props.append(part)
elif stage == 'base':
base = part
stage = None
if defer_set_stage:
stage = defer_set_stage
return name.strip(), props, base
project = 'untitled'
version = '0.1'
items = []
for line in ls:
kind = _determine_type(line)
level = _determine_level(line)
name, props, base = _deconstruct_identifier(line)
item = None
if kind == 'project':
project = name
if len(props) == 1:
version = props[0]
elif len(props) > 1:
raise ValueError(f'semantic error on line {lineno}: expected version but found multiple tokens')
else:
if level == clevel:
pass
elif level < clevel:
fall = clevel - level
path = path[:-fall]
elif level == clevel + 1:
pass
else:
raise ValueError(f'semantic error on line {lineno}: level inconsistent between subsequent lines from {clevel} to {level}')
if kind == 'namespace':
path = path + [name]
elif kind == 'class':
item = (name, props, base, path)
items.append(item)
lineno += 1
clevel = level
return project, version, items
def print_items(items):
for item in items:
name, props, base, path = item
pathfmt = '::'.join(path)
print(f' * {pathfmt}::{name}')
if base:
print(f' - inherits from {base}')
for prop in props:
print(f' - {prop}')
def generate_cmake_lists(root_dir, project, version):
cmake_lists_file = os.path.join(root_dir, 'CMakeLists.txt')
def _do_replacements(tpl):
return tpl \
.replace("{#PROJECT}", project) \
.replace("{#VERSION}", version) \
.replace("{#MIN_CMAKE_VERSION}", MIN_CMAKE_VERSION)
with open(cmake_lists_file, 'w') as f:
f.write(_do_replacements(TPL_CMAKELISTS))
def generate_main(root_dir, project, items):
root_src_dir = os.path.join(root_dir, 'src')
all_header_files = []
construct_all_classes = []
default_namespace = None
for name, _, __, path in items:
path_relative = path[1:] if FIRST_NAMESPACE_IS_DEFAULT else path
header_file_path = os.path.join(*(path_relative), name + '.h')
if FIRST_NAMESPACE_IS_DEFAULT and len(path) == 1:
class_invocation = f' {path[0]}::{name} {name}{{}};'
else:
class_invocation = ' ' + '::'.join(path_relative + [name]) + f' {name}{{}};'
all_header_files.append(header_file_path)
construct_all_classes.append(class_invocation)
if FIRST_NAMESPACE_IS_DEFAULT:
if default_namespace is None:
default_namespace = path[0]
else:
if default_namespace != path[0]:
raise ValueError('semantic error: FIRST_NAMESPACE_IS_DEFAULT is set but there is no single top-level namespace')
def _do_replacements(tpl):
return tpl \
.replace("{#INCLUDE_ALL_HEADERS}", '\n'.join([f'#include "{h}"' for h in all_header_files])) \
.replace("{#CONSTRUCT_ALL_CLASSES}", '\n'.join(construct_all_classes)) \
.replace("{#USE_DEFAULT_NAMESPACE}", f'\nusing namespace {default_namespace};\n' if FIRST_NAMESPACE_IS_DEFAULT else '')
with open(os.path.join(root_src_dir, 'main.cpp'), 'w') as f:
f.write(_do_replacements(TPL_MAIN))
def generate_code(root_dir, name, props, base, path):
root_src_dir = os.path.join(root_dir, 'src')
base_path = os.path.join(root_src_dir, *(path[1:] if FIRST_NAMESPACE_IS_DEFAULT else path))
os.makedirs(base_path, exist_ok=True)
namespace = '::'.join(path)
directive = '_'.join([*path, name]).upper()
inherit = ''
includes = ''
if base:
inherit = f' : public {base}'
includes = f'\n#include "{base}.h"\n'
prop_no_copy = ''
if 'nocopy' in props:
prop_no_copy = PROP_NO_COPY
def _do_replacements(tpl):
return tpl \
.replace("{#PROP_NO_COPY}", prop_no_copy) \
.replace("{#INCLUDES}", includes) \
.replace("{#DIRECTIVE}", directive) \
.replace("{#NAMESPACE}", namespace) \
.replace("{#INHERIT}", inherit) \
.replace("{#NAME}", name)
def _write_header(fn):
with open(fn, 'w') as f:
f.write(_do_replacements(TPL_HEADER))
def _write_source(fn):
with open(fn, 'w') as f:
f.write(_do_replacements(TPL_SOURCE))
_write_header(os.path.join(base_path, f'{name}.h'))
_write_source(os.path.join(base_path, f'{name}.cpp'))
def run(tree_file, root_dir):
print('reading tree...')
tree_lines = [l.rstrip() for l in open(tree_file).readlines()]
print('parsing tree...')
project, version, items = parse_tree_lines(tree_lines)
print('parsed tree:')
print('project name:', project)
print_items(items)
def _find_items_in_path(p):
return [x for x in items if x[3] == p]
print('generating code...')
for item in items:
name = item[1]
base = item[2]
if base:
names_in_same_path = [x[0] for x in _find_items_in_path(item[3])]
if base not in names_in_same_path:
raise ValueError(f'semantic error: can only inherit from class in same path for now ({name} : {base})')
generate_code(root_dir, *item)
generate_main(root_dir, project, items)
generate_cmake_lists(root_dir, project, version)
print('done!')
if __name__ == "__main__":
if len(sys.argv) != 3:
print('usage: python3 -m codegen <tree-file> <target-dir>')
exit(1)
tree_file = sys.argv[1]
root_dir = sys.argv[2]
if not os.path.isfile(tree_file):
print('no such file: ' + tree_file)
exit(1)
try:
run(tree_file, root_dir)
except ValueError as e:
print(str(e))
exit(1)
# my-project (1.2)
- TodoApp
- Data
* Database (nocopy)
* Item
* TodoItem : Item
- Ui
* Window
* ListWindow : Window
* TaskWindow : Window
- Components
* Tickbox
* Textbox
- Cloud
* Server (nocopy)
* Connection
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment