11 ms·
Pnut: A C to POSIX shell compiler you can trust
- voidUpdate 2y agoWhen I'm told that "I can trust" something that I feel like I had no reason to distrust, it makes me feel even more suspicious of it
- leni536 2y agohttps://www.smbc-comics.com/comic/2008-09-15 https://www.smbc-comics.com/comic/2008-09-15
- throwaway2037 2y agoYeah, I cringed when I saw that too. It violates an important rule of selling: Never tell the customer "Trust me".
- tzot 2y agoPerhaps you're old enough to remember the Sledge's[1] motto: “Trust me… I know what I'm doing.” HHBS Perusing the pnut site I did not understand either why this is software I can trust. [1] https://www.imdb.com/title/tt0090525/ https://www.imdb.com/title/tt0090525/
- Q-Q3 2y agoHi there! I believe the mention of "trust" is related to the paper Reflections on Trusting Trust by Ken Thompson https://www.cs.cmu.edu/~rdriley/487/papers/Thompson_1984_ReflectionsonTrustingTrust.pdf https://www.cs.cmu.edu/~rdriley/487/papers/Thompson_1984_Ref... Though I do think the tagline used could definitely be improved from a marketing standpoint.
- o11c 2y agoIt's a bad sign when I immediately look at the screenshot and see quoting bugs.
- laurenth 2y agoAuthor here, Because all shell variables in code generated by pnut are numbers, variables never contain whitespace or special characters and don't need to be quoted. We considered quoting all variable expansions as this is generally seen as best practice in shell programming, but thought it hurt readability and decided not to. If you think there are other issues, please let me know!
- cozzyd 2y agoCan finally port systemd to shell to quell the rebellion.
- carapace 2y agoDamned if that isn't the funniest thing I've heard in a long time.
- akoboldfrying 2y agoI was puzzled by the example C function containing pointers. Do I understand correctly that you implement pointers in shell by having a shell variable _0 for the first "byte" of "memory", a shell variable _1 for the second, etc.?
- laurenth 2y agoAuthor here, That's correct! Unlike Bash and other modern shells, the POSIX standard doesn't include arrays or any other data structures. The way we found around this limitation is to use arithmetic expansion and indexed shell variables (that are starting with `_` as you noted) to get random memory access.
- thesnide 2y agoI used almost the same idea, but with files in my https://github.com/steveschnepp/shlibs https://github.com/steveschnepp/shlibs
- osmsucks 2y agoSince I experimented with something similar in the past to mimick multidimensional arrays: depending on the implementation this can absolutely _kill_ performance. IIRC, Dash does a linear lookup of variable names, so when you create tons of variables each lookup starts taking longer and longer.
- n_plus_1_acc 2y agoI hope you're not compiling C to sh for performance reasons.
- osmsucks 2y agoIt's not about performance, it's about viability. If the result is so slow that it's unusable, it doesn't matter how portable it ends up being.
- forrestthewoods 2y agoHrmmm. But why? Quite frankly I think Bash scripting is awful and frequently wish shell scripts were written in a real and debuggable language. For anything non-trivial that is. I feel like I’d rather write C and compile it with Cosmopolitan C to give me a cross-platform binary than this. Neat project. Definitely clever. But it’s headed in the opposite direction from what I’d prefer...
- marcodiego 2y agoMaster Foo once said to a visiting programmer: “There is more Unix-nature in one line of shell script than there is in ten thousand lines of C.” The programmer, who was very proud of his mastery of C, said: “How can this be? C is the language in which the very kernel of Unix is implemented!” Master Foo replied: “That is so. Nevertheless, there is more Unix-nature in one line of shell script than there is in ten thousand lines of C.” The programmer grew distressed. “But through the C language we experience the enlightenment of the Patriarch Ritchie! We become as one with the operating system and the machine, reaping matchless performance!” Master Foo replied: “All that you say is true. But there is still more Unix-nature in one line of shell script than there is in ten thousand lines of C.” The programmer scoffed at Master Foo and rose to depart. But Master Foo nodded to his student Nubi, who wrote a line of shell script on a nearby whiteboard, and said: “Master programmer, consider this pipeline. Implemented in pure C, would it not span ten thousand lines?” The programmer muttered through his beard, contemplating what Nubi had written. Finally he agreed that it was so. “And how many hours would you require to implement and debug that C program?” asked Nubi. “Many,” admitted the visiting programmer. “But only a fool would spend the time to do that when so many more worthy tasks await him.” “And who better understands the Unix-nature?” Master Foo asked. “Is it he who writes the ten thousand lines, or he who, perceiving the emptiness of the task, gains merit by not coding?” Upon hearing this, the programmer was enlightened.
- donatj 2y agoI was going to cite this on reading the parent comment after reading it. Was very glad to see you beat me to it!
- 2y ago
- andrewf 2y agoLooking forward to the point where this can build autoconf. It's great that the generated ./configure script is portable but if I want to make substantial changes to the project I need to find a binary for my machine (and version differences can be quite substantial)
- akdev1l 2y agoThis is going further into the hell that is shell-generated scripts that culminated in the xz-utils attack. We would benefit from steering away from auto-generated scripts. Autoconf included.
- jcranmer 2y ago> Looking forward to the point where this can build autoconf. Autoconf is a perl program that turns (heavily customized) m4 files into shell scripts. How does a C compiler help there?
- andrewf 2y ago> Autoconf is a perl program Oof, did not realize.
- rubicks 2y agoI can't wait to see the shell equivalents for ptrace, setjmp, and dlopen.
- actionfromafar 2y agoDo you really? Maybe then I can also interest you in an exception handler for DOS batch scripts: https://stackoverflow.com/a/55501133/193892 https://stackoverflow.com/a/55501133/193892
- wahern 2y agoThis is very cool, regardless of how serious it was intended to be taken. Before base-64 encoders/decoders became more common as preinstalled commands in the environments I found myself on, I wrote a base64 utility in mostly pure POSIX shell: https://25thandClement.com/~william/2023/base64.sh If this project had existed I might have opted to compile my C-based base-64 encoder and decoder routines, suitably tweaked for pnut's limitations. I say base64.sh is mostly pure not because it relies on shell extensions, but because the only non-builtins it depends on are od(1) or, alternatively, dd(1) to assist with binary I/O. And preferably od(1), as reading certain control characters, like NUL, into a shell variable is especially dubious. The encoder is designed to operate on a stream of decimal encoded bytes. (See decimals_fast for using od to encode stdin to decimals, and decimals_slow for using dd for the same.) It looks like pnut uses `read -r` for reading input. In addition to NULs and related raw byte issues, I was worried about chunking issues (e.g. truncation or errors) on binary data, e.g. no newlines within LINE_BUF bytes. Have you tested binary I/O much? Relatedly, how many different shell implementations have you tested your core scheme with? In addition to bash, dash, and various incarnations of /bin/sh on the BSDs, I also tested base64.sh with Solaris' system shells (ksh88 and ksh93 derivatives), as well as AIX's (ksh88 derivative). AIX had some odd quirks with pipelines even with plain text I/O. (Unfortunately Polar Home is gone, now, so I have no easy way to play with AIX; maybe that's for the better.)
- laurenth 2y agoOne of the example we include is a base64 encoder/decoder: https://github.com/udem-dlteam/pnut/blob/main/examples/compiled/base64.sh It doesn't support NULs as you pointed out, but it's interesting to see similarities between your implementation and the one generated by Pnut. Because we use `read -r`, we haven't tested reading binary files. Fortunately, the shell's `printf` function can emit all 256 characters so Pnut can at least output binary files. This makes it possible for Pnut to have a x86 backend for the use of reproducible builds. Regarding the use of `read`, one constraint we set ourselves when writing Pnut is to not use any external utilities, including those that are specified by the POSIX standard (other than `read` and `printf`). This maximizes portability of the code generated by Pnut and is enough for the reproducible build use case. We're still looking for ways to integrate existing shell code with C. One way this can be done is through the use of the `#include_shell` directive which includes existing shell code in the generated shell script. This makes it possible to call the necessary utilities to read raw bytes without having Pnut itself depends on less portable utilities.
- theamk 2y agoIf you are wondering how it handles C-only functions.. it does not. open(..., O_RDWR | O_EXCL) -> runtime error, "echo "Unknow file mode" ; exit 1" lseek(fd, 1, SEEK_HOLE); -> invalid code (uses undefined _lseek) socket(AF_UNIX, SOCK_STREAM, 0); -> same (uses undefined _socket) looking closer at "cp" and "cat" examples, write() call does not handle errors at all. Forget about partial writes, it does not even return -1 on failures. "Compiler you can Trust", indeed... maybe you can trust it to get all the details wrong?
- PhilipRoman 2y agoImplementation issues aside, while technically it should be possible to seek a file descriptor from shell through a suitable helper program in C, I believe none of the POSIX utilities provide this facility
- oguz-ismail 2y agohead, read, and sed can be used for seeking forward according to POSIX (see the INPUT FILES section here <https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap01.html#tag_18_04 https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V...>). I doubt non-GNU implementations support it though.
- rwmj 2y agohead was used for this purpose in the xz backdoor.
- Someone 2y agoIf it’s in POSIX, chances are the BSDs implement it, too. I think seeking a specific number of bytes and then writing data there will be a problem, though. For seeking n bytes, read nor sed will work; they work with lines. sed is the only one of those that can write, and POSIX doesn’t appear to have the -i option for in-place editing (https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html https://pubs.opengroup.org/onlinepubs/9699919799/utilities/s...) So, I think head for seeking followed by sed (or ed or vi, but sed is the simpler tool, I think) for replacing the first n characters, redirecting to a temp file and then doing a mv is your only option. Advantage will be that writes will be atomic; disadvantage that it will be slow
- kxndnenfn 2y agoThis is quite interesting! Without having dug deeper into it, seeing the human readable output I assume quite different semantics from C? The C to shell transpiler I'm aware of will output unreadable code (elvm using 8cc with sh backend)
- 1vuio0pswjnm7 2y ago"Because Pnut can be distributed as a human-readable shell script (`pnut.sh`), it can serve as the basis for a reproducible build system. With a POSIX compliant shell, `pnut.sh` is sufficiently powerful to compile itself and, with some effort, [TCC](https://bellard.org/tcc/ https://bellard.org/tcc/). Because TCC can be used to bootstrap GCC, this makes it possible to bootstrap a fully featured build toolchain from only human-readable source files and a POSIX shell. Because Pnut doesn't support certain C features used in TCC, Pnut features a native code backend that supports a larger subset of C99. We call this compiler `pnut-exe`, and it can be compiled using `pnut.sh`. This makes it possible to compile `pnut-exe.c` using `pnut.sh`, and then compile TCC, all from a POSIX shell." Anywhere we can see a step-by-step demo of this process. Curious if the authors tried NetBSD or OpenBSD, or using another small C compiler, e.g., pcc. Historically, tcc was problematic for NetBSD and its forks. Not sure about today, but tcc is still in NetBSD pkgsrc WIP which suggests problems remain.
- kazinator 2y agoProblem is: - a shell is required, which has to be built from sources, using a compiler which was also built from sources using a compile binary. That's the real boostrap. - even if you could pick some shell, and compiled it with pnut.exe, the compiled code requires interpretation by an executable shell. - there is no such thing as a "POSIX compliant shell"; that's an abstract category. All this amounts to is a promise that pnut.sh will not generate code that uses non-POSIX features.
- dsp_person 2y agoI use linux-vt-setcolors in my startup, which would be a bit more convenient if it was a shell script instead of C, but it uses ioctl. Trying to compile with this tool fails with "comp_glo_decl: unexpected declaration"
- gojomybeloved 2y agoLove this!
- teo_zero 2y agoJust to be clear, the input must be written in a subset of C, because many constructs are not recognized, like unsigned types, static variables, [] arrays, etc. Is there a plan to remove such limitations?
- blueflow 2y agoThese are restrictions of the target language and there isn't much pnut can do about this.
- fulafel 2y agoSurely unsigned (aka modulo) arithmetic and arrays are expressible in shell script? edit: For reference, someone's take on building out better bash-like array functionality in posix shell: https://github.com/friendly-bits/POSIX-arrays https://github.com/friendly-bits/POSIX-arrays (there's only very rudimentary array support built-in to posix sh, basically working with stuff in $@ using set -- arg1 arg2..)
- lmm 2y agoShell is Turing complete, you could implement anything there with enough effort.
- osmsucks 2y agoI'm writing something similar, but it's based on its own scripting language. The idea of transpiling C sounds appealing but impractical: how do they plan to compile, say, things using mmap, setjmp, pthreads, ...? It would be better to clearly promise only a restricted subset of C.
- vermon 2y agoIf the end goal is portability for C, would Cosmopolitan Libc be a better choice because it supports a lot more features and probably runs faster?
- Y_Y 2y agoI cant run cosmolibc on Android, for example. Then again this converter is somewhat limited and didn't accept any of the IOCCC code I gave it.
- hnlmorg 2y ago> I cant run cosmolibc on Android, for example. You can: https://justine.lol/cosmo3/ https://justine.lol/cosmo3/ > After nearly one year of development, I'm pleased to announce our version 3.0 release of the Cosmopolitan library. [...] we invented a new linker that lets you build fat binaries which can run on these platforms: AMD ... ARM64 https://github.com/jart/cosmopolitan/releases/tag/3.5.3 https://github.com/jart/cosmopolitan/releases/tag/3.5.3 > This release fixes Android support. You can now run LLMs on your phone using Cosmopolitan software like llamafile. See 78d3b86 for further details. Thank you @aj47 (techfren.net) for bug reports and and testing efforts.
- Y_Y 2y agoThanks for the link! My comment was based on cloning master yesterday and trying to build redbean but hitting what looks like https://github.com/jart/cosmopolitan/issues/940 https://github.com/jart/cosmopolitan/issues/940 Indeed it lioks like the commit you mentioned should have fixed the issue with the pointer having too many bits for the weird kernel used on android and some raspis. Fingers crossed that release works. edit: Testing that release on Termux 118, stock Android 14 on a moto g73 5G (XT2237-2): ~/cosmopolitan $ uname -a Linux localhost 5.10.205-android12-9-00027-g4d6c07fc6342-ab11525972 #1 SMP PREEMPT Mon Mar 4 18:49:33 UTC 2024 aarch64 Android ~/cosmopolitan $ /data/data/com.termux/files/home/cosmopolitan/build/bootstrap/cocmd ape error: /data/data/com.termux/files/home/cosmopolitan/build/bootstrap/cocmd: prog mmap failed w/ errno 12
- okaleniuk 2y agoI love things like these because they shake our perception of normal loose. And who said our perception of normal doesn't deserve a good shake? A C to shell compiler might seem impractical, but you know what is even more impractical? Having a separate language for a build system. And yet, here we are. Using Shell, Make or CMake to build a C program is only acceptable because is has always been so. It's a "perceived normality" in the C world. There is no good reason, however, CMake isn't a C library. With build system being a library, we could write, read, and, most importantly, debug build scripts just like any other part of the buildable. We already have includeOS, why not includeMake?
- bregma 2y agoWhy would you need a screwdriver or a glass cutter if you already have a hammer?
- okaleniuk 2y agoWith C, you have the whole toolbox and the toolbox factory.
- defrost 2y agoBoth the tweezers and the bit flipping magnet .. and who would want anything more?
- thechao 2y agoYeah — C would be ok as a build system language if it was easy to: invoke & manage subprocesses; build & manage dynamic dependency graphs; and, easily work with file metadata. Or... work with me: Make does that, well enough.
- okaleniuk 2y agoAnd that's why I'm saying CMake should have been a library. We want the functionality of Make but not necessary the language. And Pnut shows us well that {language != functionality}. For the sake of mental experiment, let's pretend Make is a separate executable, separate process, but with some sort of API. You can manage dynamic dependency graphs by calling its routines from C. Now let's say Make is a dynamic library with all functionality exposed. You can invoke and manage subprocesses using its functions, but now your C program and the Make share a process together. Now let's say Make is a C library. GNU Make is written in C so this is not impossible to imagine. Your C program shares the process, and the names on compilation+linking phase with Make, which is annoying. But you can still work with metadata using Make's facilities. Also now you can use all the tools: debuggers, profilers, static analyzers, dynamic analyzers - you use for the rest of your codebase. We perceive C as a low-level language but, and Pnut shows it well, C is only a set of rules. We can write shell scripts with C rules. Why can't we then write build scripts?
- layer8 2y agoCan you trust that it faithfully reproduces undefined behavior? ;)
- itvision 2y agoInstantly make your C code 200 times slower without any effort!
- actionfromafar 2y agoI think it takes probably some effort, not all C programs will compile on this thing.
- chasil 2y agoIt would actually be interesting to see how much faster dash is than everything else.
- throwaway2037 2y agoWhy is Dash frequently touted as so much faster than Bash? What is different?
- tzot 2y agoIt is much simpler (and therefore less resource-hungry) than bash.
- chasil 2y agoOn rhel9, this is a list of my installed shells. You might notice that dash is smaller than ls (and the rest of the shells). $ ll /bin/bash /bin/dash /bin/ksh93 /bin/ls /bin/mksh -rwxr-xr-x. 1 root root 1389064 May 1 00:59 /bin/bash -rwxr-xr-x. 1 root root 128608 May 9 2023 /bin/dash -rwxr-xr-x. 1 root root 1414912 Apr 9 07:26 /bin/ksh93 -rwxr-xr-x. 1 root root 140920 Apr 8 08:20 /bin/ls -rwxr-xr-x. 1 root root 325208 Jan 9 2022 /bin/mksh $ rpm -qi dash | tail -4 Description : DASH is a POSIX-compliant implementation of /bin/sh that aims to be as small as possible. It does this without sacrificing speed where possible. In fact, it is significantly faster than bash (the GNU Bourne-Again SHell) for most tasks.
- laurenth 2y agoFrom our experience, ksh is generally faster, and dash sits between ksh and bash. One reason is that dash stores variables using a very small hash table with only 37 entries[0] meaning variable access quickly becomes linear as memory usage grows. But even with that, dash is still surprisingly fast -- when compiling `pnut.c` with `pnut.sh`, dash comes in second place: ksh93: 31s dash: 1m06s bash: 1m19s zsh: >15m [0]: https://git.kernel.org/pub/scm/utils/dash/dash.git/tree/src/var.c#n66 https://git.kernel.org/pub/scm/utils/dash/dash.git/tree/src/... EDIT: ksh93, not ksh
- atilaneves 2y agoI'm still figuring out why anyone would want to write a shell script in C. That sounds like torture to me.
- Retr0id 2y agoCan it do wrapping arithmetic? The `sum` example doesn't seem to do wrapping, but signed int overflow is technically UB so I guess they're fine not to. Switching it to `unsigned int` gives me: code.c:1:1 syntax error: unsupported type
- iod 2y agoI am sorry if this comes off to be negative, but with every example provided on the site, when compiled and then fed into ShellCheck¹, generates warnings about non-portable and ambiguous problems with the script. What exactly are we supposed to trust? ¹ https://www.shellcheck.net https://www.shellcheck.net
- laurenth 2y agoIt seems ShellCheck errs on the side of caution when checking arithmetic expansions and some of its recommendations are not relevant in the context they are given. For example, on `cat.sh`, one of the lines that are marked in red is: In examples/compiled/cat.sh line 7: : $((_$__ALLOC = $2)) # Track object size ^-- SC1102 (error): Shells disambiguate $(( differently or not at all. For $(command substitution), add space after $( . For $((arithmetics)), fix parsing errors. ^-----------------^ SC2046 (warning): Quote this to prevent word splitting. ^--------------^ SC2205 (warning): (..) is a subshell. Did you mean [ .. ], a test expression? ^-- SC2283 (error): Remove spaces around = to assign (or use [ ] to compare, or quote '=' if literal). ^-- SC2086 (info): Double quote to prevent globbing and word splitting. It seems to be parsing the arithmetic expansion as a command substitution, which then causes the analyzer to produce errors that aren't relevant. ShellCheck's own documentation[0] mention this in the exceptions section, and the code is generated such that quoting and word splitting are not an issue (because variables never contain whitespace or special characters). It also warns about `let` being undefined in POSIX shell, but `let` is defined in the shell script so it's a false positive that's caused by the use of the `let` keyword specifically. If you think there are other issues or ways to improve Pnut's compatibility with Shellcheck, please let us know! 0: https://www.shellcheck.net/wiki/SC1102 https://www.shellcheck.net/wiki/SC1102
- metadat 2y agoAlso see this related submission from May, 2024: Amber: Programming language compiled to Bash https://news.ycombinator.com/item?id=40431835 https://news.ycombinator.com/item?id=40431835 (318 comments) --- Pnut doesn't seem to differentiate between `int' and `int*' function parameters. That's weird, and doesn't come across as trustworthy at all! Shouldn't the use of pointers be disallowed instead? int test1(int a, int len) { return a; } int test2(int* a, int len) { return a; } Both compile to the exact same thing: : $((len = a = 0)) _test1() { let a $2; let len $3 : $(($1 = a)) endlet $1 len a } : $((len = a = 0)) _test2() { let a $2; let len $3 : $(($1 = a)) endlet $1 len a } The "runtime library" portion at the bottom of every script is nigh unreadable. Even still, it's a cool concept.
- deleted 2y ago[deleted]
- JoshTriplett 2y agoSeveral times I've found myself wishing for the reverse: a shell-to-binary compiler or JIT.
- deleted 2y ago[deleted]
- kazinator 2y agoThis is not useful if it doesn't call external libraries. Even POSIX standard ones. Chokes on: #include <glob.h> int main() // must be (); (void) results in syntax error. { glob_t gb; // syntax error here glob("abc", 0, NULL, &gb); return 0; } Nobody needs entirely self-contained C programs with no libraries to be turned into shell scripts; Unix people switch to C when there is a library function they need to call for which there no command in /bin or /usr/bin. If I reduce it to: #include <glob.h> int main() { glob("abc", 0, NULL, 0); return 0; } it "compiles" into something with a main function like: _main() { defstr __str_0 "abc" _glob __ $__str_0 0 $_NULL 0 : $(($1 = 0)) } but what good is that without a definition of _glob.
- yencabulator 2y agoIt seems to have practically no error checking. Try compiling int why(int unused) { wat_why_does_this_compile; no_error_checking(); }