Shell scripting in C
Recently, one of my shell scripts grew too large. It is probably time to rewrite it in a proper language. If possible I would like to still keep it as a script, so I don’t have to add a compilation step to the installation process of my scripts. Sure, I could use a scripting language like Python or Perl… but what if I wanted to rewrite this script in C?
To be more precise, I want to write a single C file script.c
,
make it executable with chmod +x
, move it to somewhere in my
$PATH
and then be able to run it simply by typing script.c
in
my shell. It turns out that this is actually possible. Let’s see
how right now so we don’t have the time to wonder if it is a wise
thing to do (it does not sound like it).
The trick
The single thing that makes it all possible is that lines starting
with #
are preprocessor directives for C and comments for a UNIX
shell. We can then use the #if
directive for conditional compilation
to “hide” a shell script in our souce file:
#if 0 /* Always false, skipped by the C compiler */
[Your shell script here]
exit 0 # Shell script terminates
#endif
[Your C source code here]
And now we just have to come up with a shell script that compiles
itself - that it, that runs a C compiler on its own source file.
For this we can run the realpath
command on the shell parameter
$0
to obtain the full path to the source file. Then we can compile
this file and save the executable to a temporary file, which we
finally run. The “C Script Hello World” looks like this:
#if 0
bin="$(mktemp)"
cc -o "$bin" "$(realpath $0)"
"$bin"
exit 0
#endif
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
If you want to try this for yourself, you can download script.c instead of copy-pasting.
Caveats
There are few caveats with this approach. First of all, you are running a C compiler every time you launch this “script”. This is obviously less efficient than running a normal shell script.
Secondly, the realpath
command I used above is not standard. I
thought it was in POSIX, but only the C library function with the
same name is, not the command itself. However, it is present in
most Linux distros since around 2012 and OpenBSD since version 7.1
- surprisingly, after I started using each of these operating
systems! The command is also included in FreeBSD (since 4.3) and
MacOS (since 13), and there are probably workarounds to make the
same concept work without it.
Credits
I did not come up with this trick - I read about it in at least three separate occasions from different places. Had I saved any of the links I would add them here.
For completeness, a python version
In case you did not know, making this work with an interpreted language is much simpler. You just need to use a shebang to make the shell use the correct interpreter:
#!/usr/bin/env python3
print("Hello, World!")