9 ms·
Write a Shell in C
- jarym 6y agoHere's a one liner: printf("a Shell");
- glouwbug 6y agoPrefer puts, as the newline is included
- userbinator 6y agoOne of the odd inconsistencies that I've never found a good answer to is why puts writes a string and a newline, but fputs doesn't write a newline. Likewise, and more problematic, is gets vs fgets --- the latter has a buffer length parameter, while the former doesn't and thus always leave the possibility of overflow. On the other hand, fprintf and printf, fscanf and scanf are consistent.
- glouwbug 6y agoNo one is perfect
- cogburnd02 6y agoExcept for Bruce Schneier. https://www.schneierfacts.com/ https://www.schneierfacts.com/
- blackrock 6y agoAnd Chuck Norris.
- veltas 6y agoCredit to author for editing and maintaining the article, fixing bugs others have found.
- itsmemattchung 6y agoAlso recommend CMU's "shell lab": http://csapp.cs.cmu.edu/3e/labs.html http://csapp.cs.cmu.edu/3e/labs.html When I was new to systems programming, that lab — along with all the other labs part of "Computing Systems from a programmer's perspective, especially the binary bomb — helped me better understand lower level concepts such as debugging assembly, fork-exec, etc.
- einpoklum 6y agoWhy write it in C rather than in modern C++? Toolchain is pretty similar, often more performant, you protect yourself against memory leaks, dangling file descriptors as such (yes, without needing a garbage collector [1]), and some decent libraries to help you out with what you're not writing yourself. (Potential answer: Bigger executable if you're not careful. In some cases that could be a consideration. [2]) -- [1] : https://stackoverflow.com/a/48046118/1593077 https://stackoverflow.com/a/48046118/1593077 [2] : https://softwareengineering.stackexchange.com/q/246167/63497 https://softwareengineering.stackexchange.com/q/246167/63497
- userbinator 6y agoYou can write a C compiler relatively easily. In fact there have been numerous articles submitted to HN about someone who did. I don't think anyone has done the same with C++, because of its immense complexity. and some decent libraries to help you out with what you're not writing yourself. If you're writing a shell you are probably already somewhere on the "bootstrap pilgrimage". Using something someone else has written is the exact opposite of what you want to do in that case. (Why not just use a shell that someone else has written then...?)
- kwdc 6y agoSome might say that as many people started writing c++ compilers as those of C but that the C++ crowd just haven't finished them. Perhaps another decade? I don't envy anyone writing such a compiler. Its possible but I didn't start one. Seemed like less fun and more like red tape. The pilgrimage thing is real. Done it myself. The journey from even boot to bash is real and the benefits of awareness it brings definitely appear in strange places. I went even further before the "boot" stage.
- soheil 6y agobufsize += LSH_RL_BUFSIZE; Shouldn't that be: bufsize += LSH_RL_BUFSIZE * sizeof(char);
- ChrisRR 6y agoProbably but a char is often 1 byte so it doesn't make a difference
- soheil 6y agoSure but he does it the first time
- amelius 6y agoI think the multiplication should be here: buffer = realloc(buffer, bufsize * sizeof(char)); This makes the code symmetric wrt. the malloc() call. And better allows the code to be rewritten when moving to e.g. a wide-character type.
- messe 6y ago> often 1 byte char is always 1-byte according to the C standard. (Actually, I think it's more like the smallest addressable unit is 1-char, but it's been I while since I've read the spec)
- vlovich123 6y agoIt is now. There were more esoteric platforms for which it was defined as 2 bytes (eg csr8675 and the Kalimba DSP coprocessor was even 24-bit char, short and ints). Until recently C only defined that char was the smallest unit and had size 1. I think as of C11 that may have changed. It definitely did in C++ because basically any platform that mattered defined char as 1 byte and any platforms that didn’t also didn’t really care about portability of existing software that much as the porting is generally more involved anyway (eg I ported a crypto library to Kalimba and it was challenging to get the bit math correct but eventually I did).
- ufo 6y agoToday I learned that getline is technically part of POSIX now. Does anyone know which libcs implement it, in addition to glibc?
- LukeShu 6y agogetline was originally a GNU extension since at least 1991 (for inclusion in glibc 0.4, at least). It became part of POSIX-2008. It appeared in FreeBSD 8.0 (2009). From FreeBSD it made its way in to macOS 10.7 ("Lion", 2011). It appeared in NetBSD 6.0 (2012). It made its way from NetBSD to OpenBSD 5.2 (2012).
- guerrilla 6y agoDid you have that memorized or is there somewhere you looked that up?
- LukeShu 6y agoI checked the man pages and git history! The Linux man-pages project[1] tends to do a pretty good job of telling you when different libcs got a given function, or compatibility concerns between different libcs. BSD man pages tend to just tell you where they copied their implementation from. But unfortunately the Linux man-pages getline(3) page only told me that "Both getline() and getdelim() were originally GNU extensions. They were standardized in POSIX.1-2008.". (I guess that saved me from searching my POSIX PDFs to determine the POSIX version. While I was a student and could legally aquire such things for free I hunted down every version of POSIX or SUS prior to 2016). I checked the GNU libc git[2] to determine when GNU first got the function, but didn't quite get a clear answer, becuase much of the history prior to 1994 was mangled in the conversion from whatever they were using before to RCS (which they used before they converted to CVS, which they used before they converted to git). I didn't bother checking the GNU manual; it doesn't tend to do a good job of saying when things were introduced. Next I checked macOS; the macOS 10.15 (Catalina) getline(3) man page confirmed that it appeared in POSIX.1-2008, and also told me that "These routines first appeared in FreeBSD 8.0" ("these routines" referring to their specific implementation of those routines.). I checked through opensource.apple.com archives[3] (actually, I used an import of those archives in to git[4]) to determine which version of macOS they first appeared in. I checked the FreeBSD man page archive[5] to confirm that they first appeared in 8.0 (a getline(3) man page exists in 8.0 but not 7.4). I checked the OpenBSD man page archive[6] next. In recent versions the "HISTORY" section says "These functions were ported from NetBSD to OpenBSD 5.2.", though that section doesn't exist in the 5.2 version of the man page. And I confirmed that no such man page exists in 5.1. Finally, I checked the NetBSD man page archive[7], and bisected through the versions to determine when it was introduced. I used Wikipedia to determine the release year for each of those versions. [1]: https://www.kernel.org/doc/man-pages/ https://www.kernel.org/doc/man-pages/ [2]: git://sourceware.org/git/glibc.git [3]: https://opensource.apple.com/ https://opensource.apple.com/ [4]: https://github.com/apple-open-source/macos https://github.com/apple-open-source/macos [5]: https://www.freebsd.org/cgi/man.cgi https://www.freebsd.org/cgi/man.cgi [6]: https://man.openbsd.org/ https://man.openbsd.org/ [7]: https://man.netbsd.org/ https://man.netbsd.org/
- nanis 6y ago> char *buffer = malloc(sizeof(char) * bufsize); Please don't do this. sizeof(char) is always and everywhere 1. More importantly, don't do: <type> *buffer = malloc(n * sizeof(<type>)); which frequently leads to bugs of the form <new_type> *buffer = malloc(n * sizeof(<old_type>)); Instead, use <type> *buffer = malloc(n * sizeof(*buffer)); which will always keep both sides consistent. PS: See also http://c-faq.com/charstring/wchar.html http://c-faq.com/charstring/wchar.html
- deleted 6y ago[deleted]
- userbinator 6y agoThat's something I like to use for structured types, especially when the name is long and I can't be bothered writing "struct something" again, but I don't do it for char or other simple types. Of course, it doesn't work when you're doing things like OOP in C, where the type on the left is a "superclass", or using flexible array members and allocating more than just a header's worth.
- tediousdemise 6y agoMoreover, don’t use C at all if you have the choice. A std::string in C++ makes mincemeat of the boilerplate that C requires for the same code. In C++, you can make a very simple REPL that is superior to the one in the OP in almost every way. #include <iostream> #include <string> int main() { std::string input; while (true) { std::cout << "> "; std::getline(std::cin, input); if(input == "exit") { return EXIT_SUCCESS; } else { std::cout << "Unrecognized command: " << input << '\n'; } } } Code quoted from “Setting Up A Better REPL” (https://mikeloomisgg.github.io/2019-04-10-set-up-repl/ https://mikeloomisgg.github.io/2019-04-10-set-up-repl/). It offers good insight into why C is bad in light of the alternatives like Rust and C++.
- david2ndaccount 6y agoNothing stops you from coding a semantically equivalent solution in C, just with structs and function calls that abstract over the details of io and calling realloc correctly.
- mindcrime 6y agoFor anybody who's wondering why I chose to post this: I found this while Googling around trying to answer the question "how hard is it to write a custom shell, and how would you get started?" The reason I wanted to know that is this: I've always been intrigued by the idea of making "things" ( hand-waving around the details of what that means) available via ssh. Imagine something like a MUD, except not a MUD exactly. Just the idea of "You login to a server and you're immediately dropped into some program that provides $EXPERIENCE." For the longest time I thought the "way to do that" probably involved some crazy sshd config or something, but then the penny dropped and I realized "Well shit, you could just make it a shell and set the user's shell entry to that program. Duh." If somebody knows a better approach to that sort of thing, I'd love to hear about it. But for now I'm content that I've found an approach that seems workable. (and if you're wondering more: No, this isn't anything interesting, or a startup project, or anything you'd probably even see on Show HN. It's just an example of the kind of random crap I think about in between doing other things.)
- 1vuio0pswjnm7 6y agoI used to think about a similar ideas. Because it should be possible to ssh to programs other than a "full-featured" shell (despite the "sh" in ssh). One idea was something like OpenBSD's auth-pf. The program might have only one purpose and one capabaility, e.g., to ephemerally open a port in the firewall. Then the user would access whatever remote program she wanted to access through that port, maybe opened briefly just for her.
- shadowofneptune 6y agoI have seen "things" served using telnet. In this example, it's a mod for a game that allows for virtual machines to run inside the game, and then has a telnet server that allows you to access them from outside the game. It has a login utility that runs at the start, listing the available virtual machines. https://github.com/KSP-KOS/KOS/blob/develop/src/kOS/UserIO/TelnetMainServer.cs https://github.com/KSP-KOS/KOS/blob/develop/src/kOS/UserIO/T... Unfortunately, it appears the solution was to write an entirely custom telnet server. Author also says in the included documentation that SSH was the original plan but implementing all of SSH is far too complicated.
- john_alan 6y agoStupid question but, why doesn’t the do while tight loop result in 100% cpu usage? I often wonder how modern run loops work. They don’t burn cycles?
- usrnm 6y agoIt spends most of the time waiting for IO.
- llarsson 6y agoIf you would start it and run for ten minutes and not interact with it, you would have the program just sitting there, very efficiently doing nothing, waiting in getchar().
- deleted 6y ago[deleted]
- st_goliath 6y ago> why doesn’t the do while tight loop result in 100% cpu usage? The main loop tries to read characters using the getchar() function, which in turn pokes them out of a buffer inside the "stdin" FILE struct. If the buffer is empty, somewhere inside the libc, it calls the read() system call on the standard input file descriptor to get more. If the kernel doesn't have any more data to return, the system call blocks. I.e. the kernel marks the thread as waiting for I/O and no longer schedules it. Once the kernel has data, the thread is scheduled again and the read() system call returns. And before the obvious follow up question comes: If all programs are blocking on I/O and the kernel doesn't have any threads to schedule, it schedules an "idle thread" that spends its time slice throttling down the CPU. The CPU usage that process monitor programs are displaying is basically the amount of time a CPU core is running something that isn't the idle thread.
- lloeki 6y ago> it schedules an "idle thread" that spends its time slice throttling down the CPU Specifically (AFAIK, at least back then) on x86 the kernel schedules an interrupt and sends a single HLT[0] instruction, and only wakes up when the interrupt triggers. A smartly set up interrupt would have the CPU wake up only as needed for what is required to unblock the pending blocked operations. Smarter schedulers would go as far as coalescing active events together to rush to idle yet wake up less often, and spend as much time down as possible in increasingly deeper C-states[1]. Back in the day some kernels would loop over NOP[1] instructions, so the CPU would effectively never sleep! [0]: https://en.wikipedia.org/wiki/HLT_(x86_instruction) https://en.wikipedia.org/wiki/HLT_(x86_instruction) [1]: https://gist.github.com/wmealing/2dd2b543c4d3cff6cab7 https://gist.github.com/wmealing/2dd2b543c4d3cff6cab7 [2]: https://en.wikipedia.org/wiki/NOP_(code) https://en.wikipedia.org/wiki/NOP_(code)
- datafatmunger 6y agoFun to see, and awarded full-on nostalgia. This was an actual assignment during my CS curriculum in the late 90s.
- st_goliath 6y agoAnd it still is a fairly standard exercise for first or second semester undergrad students.
- alphakilo 6y agoJust did it last week as the first assignment in my university level OS course (albeit a bit more complexity added)
- account-5 6y agoI know its not C but this guy did it in Go among other small projects: https://carltheperson.com/posts/10-things-linux https://carltheperson.com/posts/10-things-linux
- vmladenov 6y agoWriting a basic shell was one of the MPs in Angrave’s Systems Programming course at U of I https://github.com/angrave/SystemProgramming/wiki#2-processes https://github.com/angrave/SystemProgramming/wiki#2-processe...
- peter_d_sherman 6y agoBeautiful! A nice, simple, well-documented shell in C! Not too little, and not too much. Just the right balance between functionality and complexity for education. Favorited!
- alphakilo 6y agoI just finished building a shell last week for my University operating systems course. It's interesting to see the difference in quality when a person is looking to make a project for themselves that they are writing about vs fulfilling requirements from a document. Although this seems to be missing a few things like & and pipe, I find it very interesting how so many people can Implement a shell differently with the same C syscalls.
- mgemard 6y agoIt's already there: https://github.com/danistefanovic/build-your-own-x#build-your-own-shell https://github.com/danistefanovic/build-your-own-x#build-you...