aboutsummaryrefslogtreecommitdiff
path: root/src/blog/2024-09-20-c-scripting/c-scripting.md
blob: 49ea366fb40b56f42a4101993ea762de86951a7b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# Shell scripting in C

Recently, one of my [shell scripts](https://git.tronto.net/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](./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.

*Edit 2025-06-09: as a reader points out, `realpath` is included
in the most recent POSIX standard,
[POSIX.1-2024](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/realpath.html),
so it should be more widely available in the future.*

## 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](https://en.wikipedia.org/wiki/Shebang_(Unix)) to make
the shell use the correct interpreter:

```
#!/usr/bin/env python3

print("Hello, World!")
```

Generated with cgit - Back to sebastiano.tronto.net