Pnut compiles a C subset into shell scripts you can actually read
🥜 A Self-Compiling C Transpiler Targeting Human-Readable POSIX Shell
At a glance
- What is it?
- Pnut is a self-compiling C transpiler that turns a reasonably large subset of C99 into human-readable POSIX shell, and it exists to bootstrap a compiler from readable source. It suits someone who wants portable shell without writing shell, and it is a subset, not all of C.
- Who is it for?
- Pnut is worth adopting if your goal is a portable artifact that a human can audit, or if you are interested in bootstrapping a compiler from source that anyone can read, because the generated shell is deliberately human-readable rather than obfuscated, and because the bootstrap chain from a four-function C compiler through pnut-sh and pnut-exe to TCC is documented step by step.
- Can I use it commercially?
- Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The output is meant to be read, not just executed
Pnut compiles a reasonably large subset of C99 to human-readable POSIX shell scripts, and the stated purpose is to generate portable shell scripts without writing shell code. The emphasis on human-readable output is the whole design, not a side effect. The features list says there is no new language to learn, C code in and shell code out, and that the human-readable shell script is easy to read and understand. That matters for a specific reason. A shell script produced by a transpiler is usually a wall of quoted arithmetic, and nobody can review it. A Pnut script is structured like the C it came from, so a reviewer can follow it. The repository also hosts a web version at pnut.sh, and the `examples/compiled` directory holds many compiled examples, which is a useful way to see what the generated code actually looks like before committing to the approach. The project description in the repository metadata calls it a self-compiling C transpiler targeting human-readable POSIX shell, and the licence is BSD-2-Clause.
Two distributions: a shell script and a native compiler
Pnut ships in two forms and the difference is the interesting part. The first is `pnut-sh.sh`, a shell script, and the second is `pnut`, a compiler that produces executable code. Installing both is one command:
> git clone https://github.com/udem-dlteam/pnut.git
> cd pnut
> sudo make install DESTDIR=/usr/localThat installs both `pnut-sh.sh` and `pnut` into `/usr/local/bin/`. There is also a native code backend that generates executable code, currently for x86 Linux and MacOS, called `pnut-exe`, and it has its own install target along with a shell version `pnut-exe.sh`:
> sudo make install-pnut-exe DESTDIR=/usr/localPregenerated shell scripts are also published on the GitHub releases page, so you do not have to build anything to try the tool. The releases list shows pnut-1.1 from 2026-03-23 and pnut-sh-1.0 from 2025-11-18, and the repository has a Dockerfile for containerised builds.
Compiling a C file is one command and three steps
The compiler takes a C file path as input and writes the POSIX shell code to stdout, which means the shell is a filter rather than a destination. The documented example compiles fib.c to a shell script, makes it executable and runs it:
> pnut-sh.sh examples/fib.c > fib.sh # Compile fib.c to a shell script
> chmod +x fib.sh # Make the shell script executable
> ./fib.sh # Run the shell scriptThe redirect is doing the work of the output parameter, and it is why the tool fits into a shell pipeline without a flag. The examples directory makes the range visible. There are trivial cases like `empty.c` and `hello.c`, algorithmic ones like `fib.c`, `reverse.c`, `print-reverse.c` and `sum-array.c`, and ones that exercise the runtime library, including `cat.c`, `cp.c`, `wc.c`, `wc-stdin.c` and `sha256sum.c` with a stdin variant. There is also `all-chars.c` and `winterpi.c`, which suggests the corners get attention.
The unsupported C constructs are the ones shell cannot express, and the preprocessor fills the rest
The documentation is direct about the limits, and the reason given is that certain C constructs do not map nicely to POSIX shell. There is no support for floating point numbers and unsigned integers. `goto` and `switch` fallthrough are not supported. The address-of operator on local variables is not supported, because a shell variable has no address you can take. Arrays and structures cannot be stack-allocated or passed by value, and arrays of aggregate types, meaning arrays of structures or arrays of arrays, are not supported. Function pointers and indirect calls are not supported either. Read as a list, this is the set of C features that require a memory model POSIX shell does not have. The practical consequence for an adopter is that a C program which uses floating point arithmetic or passes a struct by value is not a Pnut program, and no amount of configuration changes that. The list is short enough to check by hand before you start, which is the right way to evaluate this tool. Two features turn a toy subset into something you can write real scripts in. The first is the preprocessor, which supports `#include`, `#ifdef`, `#if`, `#define MACRO ...` and function-like `#define MACRO_F(x) ...`, so a Pnut program has conditional compilation and macros rather than being a single flat translation. The second is a runtime library that includes file I/O and dynamic memory allocations, which is what lets a compiled program open a file, read it and free it. The third mechanism is the mixing feature, and it is the one that makes Pnut scripts pleasant to work with. The `#include "file.sh"` directive can be used to include shell code in the generated shell script, which makes it possible to call system utilities from C code, or to use shell scripts generated by Pnut as a library. The documentation points at `examples/select-file.c` and `examples/posix-utils.sh` for how to use this, and the `examples/prepare.sh` file is another piece of the same idea.
The bootstrap chain is what the project is really for
The second stated use is bootstrapping a compiler written in C with an executable version that is still human readable, and this is where the design pays off. Because Pnut can be distributed as a human-readable shell script, it can serve as the basis of a reproducible build system. Along with a POSIX compliant shell, `pnut-sh.sh` is sufficient to bootstrap `pnut-exe`, which can then be used to bootstrap TCC. Because TCC can be used to compile GCC, this makes it possible to bootstrap a fully featured build toolchain from only human-readable source files and a POSIX shell. The chain to TCC is documented in four steps. You compile `pnut-exe.c` to `pnut-exe.sh` using `pnut-sh.sh`, which is a shell script that turns C code into machine code. You then compile `pnut-exe.c` to `pnut-exe` using `pnut-exe.sh`, producing an executable that is much faster. You compile the `kit/bintools.c` using `pnut-exe` to produce the binary utilities used for bootstrapping, and finally you compile TCC with `pnut-exe` and then recompile it with TCC a few times to get the final `tcc` executable.
Bootstrapping from a four-function compiler and verifying the result
The project goes one step further back than a normal compiler, and the verification is the part that makes it credible. There are make targets that compile `pnut-exe`, `pnut-sh.sh` and `pnut-awk.awk` with c4, which is C in four functions, and its companion preprocessor `cpp.c`, both taken from the `kit/bootstrap-C4` submodule. The detail that matters is what happens afterwards: those targets verify that the result is identical to the one obtained with the system C compiler. So the claim is not that the bootstrapped compiler is good, it is that the bootstrapped compiler is provably the same artifact the system compiler produces. The Makefile lists the full set of bootstrap targets, including variants that go through the shell, through awk, and through annotations. The `pnut-awk` variant is a separate implementation of the compiler in awk, which is a remarkable thing to have exist at all and a reminder that this project takes minimalism seriously. The `kit/README.md` file has the details on bootstrapping TCC from `pnut-sh.sh` and c4.
Annotated output turns the shell script back into its own source
The annotation feature is what closes the loop. `pnut-sh` can include C code annotations in the generated shell scripts, using the `ANNOTATE_C_CODE=1` makefile option, and the annotations correspond to the original C source code with the lines inside inactive `#if` and `#ifdef` blocks removed, and with each top-level shell declaration prefixed with its corresponding C code as a comment. The documentation calls this especially interesting when applied to `pnut-sh.sh` itself, because it turns it into a quine-like program: from the annotated shell script, the original C source can be extracted and recompiled to obtain the original `pnut-sh.sh` shell script, closing the loop and confirming that the generated shell script matches the embedded C code. The three commands to do it are a make invocation with the annotation option, then an extraction step that runs the generated script with a `-C` flag, then a recompile. The other compilation options change the generated output in other ways: `SH_COMPACT_RT=1` uses a compact runtime library with reduced I/O performance, `SH_FAST=1` generates shell code using `set` for local variables, and `MINIMAL=1` includes only the set of features required to bootstrap pnut.
Where a shell transpiler stops being the right answer
Two limits are worth naming. The first is the subset itself, and it is not a small restriction for the kind of code people actually want to port. Floating point, unsigned integers, structs by value, function pointers and stack-allocated arrays are all ordinary C, and a program that uses any of them is out. Writing a shell script that avoids them is a different exercise from writing the same program in C, so the promise of not learning a new language comes with a constraint that makes the source a subset of C rather than C. The second is performance. The generated code runs in a shell, and the documentation is candid that certain shells are faster than others, recommending `ksh`, `dash` or `bash` for faster executions, and noting that `zsh` is supported but tends to be slower on large programs. The `SH_COMPACT_RT=1` option trades I/O performance for a smaller runtime, which is the same trade in miniature. The alternative to weigh against is simply writing POSIX shell directly, which has the advantage that the output is exactly what you would have written, and the disadvantage that shell is unpleasant for anything with arithmetic or data structures. A C program of the supported subset is a reasonable middle ground for the many scripts that are mostly control flow and string handling. For a program that manipulates floating point numbers, Pnut is the wrong tool, and no build option changes that.
Editorial conclusion
Pnut is worth adopting if your goal is a portable artifact that a human can audit, or if you are interested in bootstrapping a compiler from source that anyone can read, because the generated shell is deliberately human-readable rather than obfuscated, and because the bootstrap chain from a four-function C compiler through pnut-sh and pnut-exe to TCC is documented step by step. It is a poor fit if you need general C99, since floating point, unsigned integers, stack-allocated arrays and structures, function pointers, aggregate arrays, `goto` and `switch` fallthrough are all listed as unsupported. It is also a poor fit as a general shell-script generator, because the C subset does not map naturally onto shell and the preprocessor and runtime library are the parts you would otherwise take from a shell toolkit. Before you rely on it, check which of your constructs fall outside the supported subset, and note that the last push to the repository was on 2026-10-15 with the most recent release pnut-1.1 dated 2026-03-23.
Frequently asked questions
What is Pnut and what does it produce?
Pnut compiles a reasonably large subset of C99 to human-readable POSIX shell scripts. It can be used to generate portable shell scripts without writing shell code, and it is also a way to bootstrap a compiler written in C with an executable version that is still human readable, for reproducible builds.
How do I install Pnut?
Clone the repository, change into the pnut directory and run sudo make install DESTDIR=/usr/local, which installs both pnut-sh.sh and pnut into /usr/local/bin/. The native code backend pnut-exe and its shell version pnut-exe.sh have their own target, sudo make install-pnut-exe DESTDIR=/usr/local, and pregenerated shell scripts are published on the GitHub releases page.
Which C features does Pnut not support?
The documentation lists the constructs that do not map nicely to POSIX shell: no floating point numbers and no unsigned integers, no goto and no switch fallthrough, no address-of operator on local variables, arrays and structures cannot be stack-allocated or passed by value, function pointers and indirect calls are not supported, and arrays of aggregate types are not supported.
How does Pnut bootstrap the TCC compiler?
In four steps: compile pnut-exe.c to pnut-exe.sh using pnut-sh.sh, then compile pnut-exe.c to the pnut-exe executable using pnut-exe.sh, then compile kit/bintools.c with pnut-exe to produce the bootstrapping binary utilities, and finally compile TCC with pnut-exe and recompile it with TCC a few times to get the final tcc executable.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/udem-dlteam-pnut)Was this useful?