Preprocess C and C++ Safely with x86_64-linux-gnu-cpp-13
By the end of this guide you will be able to turn a C or C++ source file into inspectable preprocessor output, control its macros and include paths, and produce a Make dependency file. The examples target Ubuntu's x86_64-linux-gnu-cpp-13, provided by GCC 13.3.0 packages. Allow about 10 minutes if the source file already exists.
The route
Jump straight to the step you need, or tick off Done means at the end.
Before you start
You need a shell, the installed command, and a C or C++ source file that you are allowed to read. Check the version and executable location first:
x86_64-linux-gnu-cpp-13 --version
command -v x86_64-linux-gnu-cpp-13
On the target system this reports Ubuntu's GCC 13.3.0 build and normally resolves to /usr/bin/x86_64-linux-gnu-cpp-13. The unversioned x86_64-linux-gnu-cpp alias reports the same installed GCC build here.
Checkpoint
Continue only when the command exists and its version is the one you intend to document or reproduce.
1. Expand a source file to standard output
Create a small input in a disposable directory, or substitute a real source path. This example uses a temporary file and sends the result to standard output, so it changes no project files:
work=/tmp/cpp-guide
mkdir -p "$work"
printf '%s\n' '#define LIMIT 3' 'int values[LIMIT];' > "$work/sample.c"
x86_64-linux-gnu-cpp-13 -P "$work/sample.c"
The output contains the declaration with LIMIT expanded to 3. The -P option suppresses preprocessor linemarkers, which makes output easier to read when another tool expects source-like text.
Without -P, CPP normally emits linemarkers so a compiler can retain the original file and line locations. Do not treat preprocessed output as a replacement source file unless the next tool expects it.
Checkpoint
Rerun the command without -P if you need to diagnose where a generated line came from.
2. Select behaviour with macros and headers
Use -D to define a macro before processing and -U to cancel a previous definition. Options are applied in the order given, so a later -U can remove a definition made by an earlier -D. Keep shell quoting around definitions that contain spaces or shell metacharacters.
printf '%s\n' '#if FEATURE_LEVEL >= 2' 'const char *mode = "new";' '#else' 'const char *mode = "old";' '#endif' > "$work/feature.c"
x86_64-linux-gnu-cpp-13 -P -DFEATURE_LEVEL=2 "$work/feature.c"
Expected output is:
const char *mode = "new";
For project headers, -I adds a directory to the include search path for both quoted and angle-bracket includes. -iquote applies only to quoted includes. The quoted form searches the current file's directory first, then quote directories, then -I directories and system directories. This order is a common source of accidental header shadowing.
mkdir -p "$work/include"
printf '%s\n' '#define MESSAGE "from local header"' > "$work/include/settings.h"
printf '%s\n' '#include "settings.h"' 'const char *message = MESSAGE;' > "$work/header.c"
x86_64-linux-gnu-cpp-13 -P -I "$work/include" "$work/header.c"
Expected output is:
const char *message = "from local header";
Safety boundary
Do not use -I casually to replace vendor or system headers. The manpage recommends -isystem for vendor-supplied system headers, because it preserves system-header treatment and avoids surprising include ordering.
3. Save output without overwriting the input
Use -o to write preprocessed output to a separate file. The command accepts standard input or standard output as -, and omitted input or output is treated similarly. Make the destination explicit when scripting so a misplaced argument cannot silently replace an important file.
x86_64-linux-gnu-cpp-13 -P -I "$work/include" "$work/header.c" -o "$work/header.i"
test -s "$work/header.i" && sed -n '1,5p' "$work/header.i"
There is no elevated-privilege step here. If the destination is owned by another user, fix the path or permissions rather than reaching for sudo. Warning: the cleanup command below permanently removes files under the named temporary directory. Check the path before running it: rm -rf /tmp/cpp-guide. Use the actual disposable directory if you used the commands above.
4. Generate Make dependencies
For build automation, -MMD produces dependencies for user headers while excluding system headers. -MF chooses the dependency file. Use -MT when the Make target is not the default object name.
x86_64-linux-gnu-cpp-13 -MMD -MF "$work/header.d" -MT 'build/header.o' -I "$work/include" "$work/header.c"
sed -n '1,5p' "$work/header.d"
The dependency rule names build/header.o, the source file and settings.h. If you need system headers included too, use -MD instead. If a generated header is not present yet, -MG can add it to dependency output without raising an error, but use that only when the build has a real generation step.
Do not confuse these modes: -M and -MM emit dependency rules instead of normal preprocessed output. -MF keeps those rules out of the normal output stream. A dependency file is build metadata, not compilable C.
5. Investigate a failing include or macro
Use -H to print each header as it is read, with indentation showing nesting. Use -dM to list the macros defined during processing, including predefined macros. These are diagnostic views, so capture them separately when another tool consumes standard output.
x86_64-linux-gnu-cpp-13 -H -P -I "$work/include" "$work/header.c" > "$work/expanded.txt" 2> "$work/headers.txt"
sed -n '1,5p' "$work/headers.txt"
x86_64-linux-gnu-cpp-13 -dM - < /dev/null | grep -E '^#define (__GNUC__|__x86_64__)'
If an undefined identifier is evaluated in an #if, CPP treats it as zero. Add -Wundef when that default could hide a typo. For strict standard diagnostics, select the relevant -std=c90, -std=c99, -std=c11 or -std=c17 mode and add -pedantic when required. The exact language standard belongs to the project, not to a generic troubleshooting command.
Common traps
- CPP is for C, C++ and Objective-C lexical input. It can damage formats such as Makefiles by removing significant tabs; use a real text processor for general text.
- Do not group multi-letter options.
-dMis not equivalent to-d -M. - Do not assume
-Cis harmless. Preserving comments can make comments act as tokens and change how directives are recognised. - Do not edit system headers to repair an include problem. Inspect the search order with
-H, then correct-I,-iquoteor the build configuration.
Done means
- The installed GCC 13.3.0 preprocessor version is confirmed.
- A disposable source file expands to the expected output.
- Macro and include-path choices are explicit and verified.
- Preprocessed output and Make dependencies are written to separate files.
- Header and macro diagnostics can be captured without contaminating build output.