Skip to content

Instantly share code, notes, and snippets.

@hadrianw
Last active November 13, 2022 21:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hadrianw/5cd275d5838814d813adc12978dce9be to your computer and use it in GitHub Desktop.
Save hadrianw/5cd275d5838814d813adc12978dce9be to your computer and use it in GitHub Desktop.
Make a C file executable with those few lines.
#if 0
set -e; [ "$0" -nt "$0.bin" ] &&
gcc -Wall -Wextra -pedantic -std=c99 "$0" -o "$0.bin"
exec "$0.bin" "$@"
#endif
#include <stdio.h>
int
main(int argc, char *argv[]) {
int ch = getchar();
printf("Hello %c world!\n", ch);
return 0;
}
@hadrianw
Copy link
Author

hadrianw commented Mar 23, 2017

$ chmod +x shebang.c
$ echo C | ./shebang.c
Hello C world!
$

If your sh supports exec -a you could do a bit nicer:

- exec "$0.bin" "$@"
+ exec -a "$0" "$0.bin" "$@"

@noscript
Copy link

noscript commented Mar 24, 2017

To avoid rebuilding every time:

-{ echo "#line 1 \"$0\""; cat "$0"; } |
-gcc -xc -Wall -Wextra -pedantic -std=c99 - -o "$0.bin"
+printf '%%.bin: %%\n\tgcc -xc -Wall -Wextra -pedantic -std=c99 $^ -o $@' | make "$0.bin" -s -f -

@hadrianw
Copy link
Author

Thanks for suggestion. However I updated it to check if file is newer in shell if it needs rebuilding without calling make.

@stefanos82
Copy link

What if I wanted to erase $0.bin after execution? I don't mind rebuilding it anew on every re-run.

@hadrianw
Copy link
Author

@stefanos82 I think there is no way to make it elegant, but something like this would work:

#if 0
set -e
gcc -Wall -Wextra -pedantic -std=c99 "$0" -o "$0.bin"
trap 'rm -f "$0.bin"' EXIT
"$0.bin" "$@"
exit $?
#endif

The problem is, that we need to leave a shell process running for the clean up. Because of that we can't use exec and we must exit ourselves. But thanks to set -e we could just as well give exit 0 there, as in case of errors we would still get a correct exit code from our program.

This way is a bit less nice, also because the PID of our process is different from the PID of the command. We could think about putting the binary in /tmp and use first mktemp so it could also work from a read-only place like read-only mount or a different user. But I don't like it, because the name (argv[0]) will be cryptic and we can't deduce from the binary were the sources are and some programs may want to have access to some additional files. A solution to that would be exec -a which gives ability to set argv[0] explicitly. However /bin/sh is often dash and not bash (like on Debian) so it is not supported.

Other way that makes it possible to use exec that I thought about is more hacky, but maybe there is a way to make it less racy:

#if 0
set -e
gcc -Wall -Wextra -pedantic -std=c99 "$0" -o "$0.bin"
{ sleep 1; rm -f "$0.bin"; } &
exec "$0.bin" "$@"
#endif

Shell runs a background process that will wait just a bit and then remove the binary. If the parent process will get to exec before this one second will run out everything is good. Because the kernel keeps a file as long as there is a file handle opened to it. In this case a running program keeps a handle. It may not be accessible from the file system, but everything works as intended. I'm not sure what could be done to avoid this racy one second sleep. On the one hand it can be too long, because in less than a second the program could already be done and we wait with removal longer than neccessary. On the other hand it can be too short in a busy system or a slow file system. It would be better for it to wait only as long as neccessary. I wonder if there is a way in shell to have an opened file descriptor with close-on-exec flag set and then the clean-up subprocess would attempt to read from it - then it should break on exec and we would be in the clear.

Or rather open a file from parent with exec 3<> /tmp/tmpfile then in the clean-up subprocess instead of sleep put read <&3, but I did not test it out too much. Probably should work, but then you should replace /tmp/tmpfile with proper mktemp. But all this will look bad.

Not much tested:

#if 0
set -e
gcc -Wall -Wextra -pedantic -std=c99 "$0" -o "$0.bin"
tmp=$(mktemp)
exec 3<> "$tmp"
{ read <&3; rm -f "$0.bin" "$tmp"; } &
exec "$0.bin" "$@"
#endif

@stefanos82
Copy link

Yeah, it makes sense as you describe the whole behavior.

Basically I thought of TCC's and Dlang's -run flags; what I know for sure is that DMD would generate an executable at /tmp, run it, and then delete it immediately.

Of course as you have said yourself, the workarounds you have just shared (thank you by the way) are quite hacky and a bit verbose to say the least, therefore I should stick with your original preprocessor technique.

Thank you for your thorough explanation, I was reminded how script behavior works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment