| 1 | // amalgamate multiple C source files into a single |
| 2 | // C source file. See https://sqlite.org/amalgamation.html |
| 3 | // for a description of file amalgamation. |
| 4 | // |
| 5 | // If an input file is not specified, source is read |
| 6 | // from stdin. |
| 7 | // |
| 8 | // If an output file is not specified, source is output |
| 9 | // to stdout. |
| 10 | |
| 11 | module main |
| 12 | |
| 13 | import flag |
| 14 | import os |
| 15 | import regex |
| 16 | |
| 17 | const app_name = 'amalgamate' |
| 18 | const app_version = '0.0.1' |
| 19 | |
| 20 | // pre-compile the include statement regex |
| 21 | const re = regex.regex_opt(r'^\s*#\s*include\s*"([^"]+)"')! |
| 22 | |
| 23 | struct Config { |
| 24 | mut: |
| 25 | input_files []string |
| 26 | output_file string |
| 27 | search_dirs []string |
| 28 | blacklist []string |
| 29 | } |
| 30 | |
| 31 | struct Context { |
| 32 | config Config |
| 33 | mut: |
| 34 | processed_files []string |
| 35 | } |
| 36 | |
| 37 | fn parse_arguments() Config { |
| 38 | mut cfg := Config{} |
| 39 | |
| 40 | mut parser := flag.new_flag_parser(os.args) |
| 41 | parser.skip_executable() |
| 42 | parser.application(app_name) |
| 43 | parser.version(app_version) |
| 44 | |
| 45 | parser.arguments_description('[file ...]') |
| 46 | |
| 47 | parser.description('combine multiple .c and .h files into one.') |
| 48 | parser.description('') |
| 49 | parser.description('Combine input, coming from either stdin or input files, into one') |
| 50 | parser.description('large file. Include statements are processed and the contents') |
| 51 | parser.description('copied in place. Only #include "file.h" statements cause their') |
| 52 | parser.description('contents to be copied, not #include <file.h> statements. If no') |
| 53 | parser.description('input files are specified, read from stdin.') |
| 54 | |
| 55 | parser.footer('\nAn example showing multiple blacklisted files and multiple search') |
| 56 | parser.footer('directories.') |
| 57 | parser.footer('') |
| 58 | parser.footer(' amalgamate -o output_file.c -b ignore_me.h \\') |
| 59 | parser.footer(' -b ignore_me_2.h -b other/ignore_me.h \\') |
| 60 | parser.footer(' -s relative/search/dir -s /absolute/search/dir \\') |
| 61 | parser.footer(' file1.c file2.c') |
| 62 | parser.footer('') |
| 63 | |
| 64 | cfg.output_file = parser.string('output', `o`, '', 'output file. If not specified,\n' + |
| 65 | flag.space + 'defaults to stdout.\n', val_desc: '<filename>') |
| 66 | |
| 67 | cfg.blacklist = parser.string_multi('blacklist', `b`, |
| 68 | 'blacklist a file name. This prevents\n' + flag.space + |
| 69 | 'the named file from being included.\n' + flag.space + |
| 70 | 'This can be specified more that once.\n', val_desc: '<include_file>') |
| 71 | |
| 72 | cfg.search_dirs = parser.string_multi('search_path', `s`, |
| 73 | 'add a directory to the search path.\n' + flag.space + |
| 74 | 'An include file is searched for in\n' + flag.space + |
| 75 | 'the current working directory and\n' + flag.space + |
| 76 | 'if not found, the directories in this\n' + flag.space + |
| 77 | 'list are searched, in order, until the\n' + flag.space + |
| 78 | 'file is found or the search list is\n' + flag.space + |
| 79 | 'exhausted. This can be specified\n' + flag.space + 'more that once.\n', |
| 80 | val_desc: '<search_dir>' |
| 81 | ) |
| 82 | |
| 83 | cfg.input_files = parser.finalize() or { |
| 84 | // this only reports the first unrecognized argument |
| 85 | eprintln('${err}\n') |
| 86 | eprintln('${parser.usage()}\n') |
| 87 | exit(1) |
| 88 | } |
| 89 | |
| 90 | return cfg |
| 91 | } |
| 92 | |
| 93 | fn main() { |
| 94 | cfg := parse_arguments() |
| 95 | |
| 96 | mut ctx := Context{ |
| 97 | config: cfg |
| 98 | } |
| 99 | |
| 100 | ctx.amalgamate() or { |
| 101 | eprintln('error: ${err}') |
| 102 | exit(1) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | fn (mut c Context) amalgamate() ! { |
| 107 | mut source := '' |
| 108 | |
| 109 | if c.config.input_files.len == 0 { |
| 110 | // source += '/* ########## stdin */\n' |
| 111 | // if there are no input files, read from stdin |
| 112 | local_dir := os.getwd() |
| 113 | source += c.handle_includes(local_dir, os.get_raw_lines_joined())! |
| 114 | // source += '/* ########## stdin end */\n' |
| 115 | } else { |
| 116 | // read each input file, in order, and |
| 117 | // handle all of its includes. |
| 118 | for file in c.config.input_files { |
| 119 | if file in c.config.blacklist { |
| 120 | // skip blacklisted files |
| 121 | continue |
| 122 | } |
| 123 | |
| 124 | found_file := c.find_file(file)! |
| 125 | |
| 126 | if found_file in c.processed_files { |
| 127 | // skip over files already read |
| 128 | continue |
| 129 | } |
| 130 | |
| 131 | // source += '/* ########## ${file} */\n' |
| 132 | c.processed_files << found_file |
| 133 | local_dir := os.dir(found_file) |
| 134 | file_source_code := os.read_file(found_file)! |
| 135 | source += c.handle_includes(local_dir, file_source_code)! |
| 136 | // source += '/* ########## ${file} end */\n' |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | if c.config.output_file == '' { |
| 141 | print(source) |
| 142 | } else { |
| 143 | os.write_file(c.config.output_file, source)! |
| 144 | } |
| 145 | |
| 146 | return |
| 147 | } |
| 148 | |
| 149 | fn (c Context) find_file(file string) !string { |
| 150 | mut full_path := os.real_path(file) |
| 151 | |
| 152 | if os.is_file(full_path) { |
| 153 | return full_path |
| 154 | } |
| 155 | |
| 156 | for dir in c.config.search_dirs { |
| 157 | full_path = os.real_path(os.join_path_single(dir, file)) |
| 158 | |
| 159 | if os.is_file(full_path) { |
| 160 | return full_path |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | return error('file "${file}" not found') |
| 165 | } |
| 166 | |
| 167 | // handle_includes looks for lines that start with #include |
| 168 | // and inserts the lines from the named include file. |
| 169 | // |
| 170 | // The pattern matches file names for local header files, |
| 171 | // not system header files as are denoted by < and >. |
| 172 | fn (mut c Context) handle_includes(local_dir string, input_source string) !string { |
| 173 | source_lines := input_source.split_into_lines() |
| 174 | mut output_lines := []string{} |
| 175 | |
| 176 | for line in source_lines { |
| 177 | start, _ := re.match_string(line) |
| 178 | |
| 179 | if start >= 0 { |
| 180 | file := line[re.groups[0]..re.groups[1]] |
| 181 | mut found_file := '' |
| 182 | |
| 183 | if file in c.config.blacklist { |
| 184 | // leave blacklisted files alone |
| 185 | if file in c.processed_files { |
| 186 | // we don't want a second include |
| 187 | output_lines << '\n' |
| 188 | } else { |
| 189 | output_lines << line |
| 190 | c.processed_files << file |
| 191 | } |
| 192 | continue |
| 193 | } |
| 194 | |
| 195 | if !os.is_abs_path(file) { |
| 196 | found_file = c.find_file(os.join_path_single(local_dir, file)) or { |
| 197 | // keep looking |
| 198 | '' |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | if found_file == '' { |
| 203 | found_file = c.find_file(file)! |
| 204 | } |
| 205 | |
| 206 | if found_file in c.processed_files { |
| 207 | // skip over files already read |
| 208 | continue |
| 209 | } |
| 210 | c.processed_files << found_file |
| 211 | file_source_code := os.read_file(found_file)! |
| 212 | // output_lines << '/* ########## ${file} begin */\n' |
| 213 | output_lines << c.handle_includes(os.dir(found_file), file_source_code)! |
| 214 | // output_lines << '/* ########## ${file} end */\n' |
| 215 | } else { |
| 216 | output_lines << line |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | return output_lines.join_lines() + '\n' |
| 221 | } |
| 222 | |