combine.sh 6.2 KB

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