1#!/bin/sh -e 2 3# Tool to bundle multiple C/C++ source files, inlining any includes. 4# 5# Note: this POSIX-compliant script is many times slower than the original bash 6# implementation (due to the grep calls) but it runs and works everywhere. 7# 8# TODO: ROOTS, FOUND, etc., as arrays (since they fail on paths with spaces) 9# TODO: revert to Bash-only regex (the grep ones being too slow) 10# 11# Author: Carl Woffenden, Numfum GmbH (this script is released under a CC0 license/Public Domain) 12 13# Common file roots 14ROOTS="." 15 16# -x option excluded includes 17XINCS="" 18 19# -k option includes to keep as include directives 20KINCS="" 21 22# Files previously visited 23FOUND="" 24 25# Optional destination file (empty string to write to stdout) 26DESTN="" 27 28# Whether the "#pragma once" directives should be written to the output 29PONCE=0 30 31# Prints the script usage then exits 32usage() { 33 echo "Usage: $0 [-r <path>] [-x <header>] [-k <header>] [-o <outfile>] infile" 34 echo " -r file root search path" 35 echo " -x file to completely exclude from inlining" 36 echo " -k file to exclude from inlining but keep the include directive" 37 echo " -p keep any '#pragma once' directives (removed by default)" 38 echo " -o output file (otherwise stdout)" 39 echo "Example: $0 -r ../my/path - r ../other/path -o out.c in.c" 40 exit 1 41} 42 43# Tests that the grep implementation works as expected (older OSX grep fails) 44test_deps() { 45 if ! echo '#include "foo"' | grep -Eq '^\s*#\s*include\s*".+"'; then 46 echo "Aborting: the grep implementation fails to parse include lines" 47 exit 1 48 fi 49 if ! echo '"foo.h"' | sed -E 's/"([^"]+)"/\1/' | grep -Eq '^foo\.h$'; then 50 echo "Aborting: sed is unavailable or non-functional" 51 exit 1 52 fi 53} 54 55# Tests if list $1 has item $2 (returning zero on a match) 56list_has_item() { 57 if echo "$1" | grep -Eq "(^|\s*)$2(\$|\s*)"; then 58 return 0 59 else 60 return 1 61 fi 62} 63 64# Adds a new line with the supplied arguments to $DESTN (or stdout) 65write_line() { 66 if [ -n "$DESTN" ]; then 67 printf '%s\n' "$@" >> "$DESTN" 68 else 69 printf '%s\n' "$@" 70 fi 71} 72 73log_line() { 74 echo $@ >&2 75} 76 77# Find this file! 78resolve_include() { 79 local srcdir=$1 80 local inc=$2 81 for root in $srcdir $ROOTS; do 82 if [ -f "$root/$inc" ]; then 83 # Try to reduce the file path into a canonical form (so that multiple) 84 # includes of the same file are successfully deduplicated, even if they 85 # are expressed differently. 86 local relpath="$(realpath --relative-to . "$root/$inc" 2>/dev/null)" 87 if [ "$relpath" != "" ]; then # not all realpaths support --relative-to 88 echo "$relpath" 89 return 0 90 fi 91 local relpath="$(realpath "$root/$inc" 2>/dev/null)" 92 if [ "$relpath" != "" ]; then # not all distros have realpath... 93 echo "$relpath" 94 return 0 95 fi 96 # Fallback on Python to reduce the path if the above fails. 97 local relpath=$(python -c "import os,sys; print os.path.relpath(sys.argv[1])" "$root/$inc" 2>/dev/null) 98 if [ "$relpath" != "" ]; then # not all distros have realpath... 99 echo "$relpath" 100 return 0 101 fi 102 # Worst case, fall back to just the root + relative include path. The 103 # problem with this is that it is possible to emit multiple different 104 # resolved paths to the same file, depending on exactly how its included. 105 # Since the main loop below keeps a list of the resolved paths it's 106 # already included, in order to avoid repeated includes, this failure to 107 # produce a canonical/reduced path can lead to multiple inclusions of the 108 # same file. But it seems like the resulting single file library still 109 # works (hurray include guards!), so I guess it's ok. 110 echo "$root/$inc" 111 return 0 112 fi 113 done 114 return 1 115} 116 117# Adds the contents of $1 with any of its includes inlined 118add_file() { 119 local file=$1 120 if [ -n "$file" ]; then 121 log_line "Processing: $file" 122 # Get directory of the current so we can resolve relative includes 123 local srcdir="$(dirname "$file")" 124 # Read the file 125 local line= 126 while IFS= read -r line; do 127 if echo "$line" | grep -Eq '^\s*#\s*include\s*".+"'; then 128 # We have an include directive so strip the (first) file 129 local inc=$(echo "$line" | grep -Eo '".*"' | sed -E 's/"([^"]+)"/\1/' | head -1) 130 local res_inc="$(resolve_include "$srcdir" "$inc")" 131 if list_has_item "$XINCS" "$inc"; then 132 # The file was excluded so error if the source attempts to use it 133 write_line "#error Using excluded file: $inc" 134 log_line "Excluding: $inc" 135 else 136 if ! list_has_item "$FOUND" "$res_inc"; then 137 # The file was not previously encountered 138 FOUND="$FOUND $res_inc" 139 if list_has_item "$KINCS" "$inc"; then 140 # But the include was flagged to keep as included 141 write_line "/**** *NOT* inlining $inc ****/" 142 write_line "$line" 143 log_line "Not Inlining: $inc" 144 else 145 # The file was neither excluded nor seen before so inline it 146 write_line "/**** start inlining $inc ****/" 147 add_file "$res_inc" 148 write_line "/**** ended inlining $inc ****/" 149 fi 150 else 151 write_line "/**** skipping file: $inc ****/" 152 fi 153 fi 154 else 155 # Skip any 'pragma once' directives, otherwise write the source line 156 local write=$PONCE 157 if [ $write -eq 0 ]; then 158 if echo "$line" | grep -Eqv '^\s*#\s*pragma\s*once\s*'; then 159 write=1 160 fi 161 fi 162 if [ $write -ne 0 ]; then 163 write_line "$line" 164 fi 165 fi 166 done < "$file" 167 else 168 write_line "#error Unable to find \"$1\"" 169 log_line "Error: Unable to find: \"$1\"" 170 fi 171} 172 173while getopts ":r:x:k:po:" opts; do 174 case $opts in 175 r) 176 ROOTS="$ROOTS $OPTARG" 177 ;; 178 x) 179 XINCS="$XINCS $OPTARG" 180 ;; 181 k) 182 KINCS="$KINCS $OPTARG" 183 ;; 184 p) 185 PONCE=1 186 ;; 187 o) 188 DESTN="$OPTARG" 189 ;; 190 *) 191 usage 192 ;; 193 esac 194done 195shift $((OPTIND-1)) 196 197if [ -n "$1" ]; then 198 if [ -f "$1" ]; then 199 if [ -n "$DESTN" ]; then 200 printf "" > "$DESTN" 201 fi 202 test_deps 203 add_file "$1" 204 else 205 echo "Input file not found: \"$1\"" 206 exit 1 207 fi 208else 209 usage 210fi 211exit 0 212