Bash? Nah, I Have Bun.

Bun is redefining how we run scripts by merging the power of JavaScript with the simplicity of shell scripting, making cross-platform coding easier than ever.

At this point, y'all are hopefully familiar with Bun. It's a new alternative toolchain for JavaScript developers, encompassing everything from bundling to running to installing your packages. Bun is a really exciting project, and I've already covered it extensively. Make sure you check out my Truth About Bun video if you haven't already, as it provides a lot of important context that I think will help with this video.

Anyways, what are we talking about today? Right now, we're just discussing JavaScript. How is Bash involved? Well, crazy enough, the title isn't clickbait: Bun is competing with Bash. As crazy as that sounds, I do actually think this is a great idea. What the hell am I talking about? Well, let's take a look at the Bun shell.

JavaScript is the world's most popular scripting language, so why is it hard to run shell scripts in JavaScript? A fun fact not a lot of people know is that quick scripting like this was actually one of the original goals of Node. What if we could use the beauty of JavaScript's flexibility as a way to quickly run scripts on the server side? Despite that being a goal, Node became a much bigger thing for running large JavaScript applications on servers, as well as the ecosystem we use for managing all our packages and everything else associated with it.

This is why this is such an interesting development: previously, Deno tried to go back to this concept of putting the script in JavaScript in your terminal and on your servers. I honestly feel like Bun has done a better job at this than Deno. This is how I want to write my JavaScript CLI code going forward. They provide examples of how these things work in Node, such as importing spawnSync and then calling all of these values for a command, collecting the status, standard output, standard error, and using this in your codebase. There are also APIs for doing some of those things, like reading directories, but none of this is as simple as writing a shell script like ls *.js. Wouldn't it be really cool if we could just write this in our server-side JavaScript code? This is the goal of what they've done with the Bun shell.

Existing shells don't work in JavaScript for a bunch of reasons, which they go in-depth on. I love the fake Hacker News comment here: "shells are a solved problem." A Hacker News commenter probably thinks that if we go to Hacker News for this release, someone will say that they don't work well in JavaScript. Why? Because macOS's zsh implementation is different from Linux's bash and Windows command. They are all slightly different, with different syntaxes and commands. The commands available for each platform are different, and even the same command can have different flags and behaviors.

I can't tell you how many times I've run into random scripts that I was promised by the internet would work great, only to find that they did not run in zsh, even though they ran fine in bash. There's one performance flag that I always forget about, which only works in Linux bash and doesn't work in macOS. I remember it was annoying me recently. I'm sure we've all had the experience of finding a bash script from ChatGPT or Stack Overflow, running it on our Mac, and it just doesn't work because it has some weird expectations about arguments that just aren't a thing in macOS.

We've all dealt with that before. The problem here isn't just that you have to modify your script slightly to run it on macOS versus Linux; it's that you don't have one file with code in it that runs properly everywhere. Writing cross-platform scripting is actually really difficult. As a result, we often opt out of doing that in the shell and instead do it using packages that bind to random things per platform. For example, fs in Node has different bindings for all these different OSs, so you don't have to worry about what shell they're using. But what if we could just write shell code, and it worked properly in all these different places?

=> 00:03:53

Cross-platform scripting shouldn't be a headache; it's time for a unified solution that just works everywhere.

Aren't things in Mac something we've all dealt with before? The problem here isn't just that you have to modify your script slightly to run it on Mac versus Linux; it's that you don't have one file with code in it that runs properly everywhere. Writing cross-platform scripting is actually really difficult, and as a result, we often opt out of doing that in the shell. Instead, we use packages that bind to random things per platform. For example, Fs in Node has different bindings for all these different OS's, so you don't have to worry about what shell they're using.

But what if we could just write shell code and it worked properly in all these different places? As mentioned here, npm solutions rely on the community to polyfill missing commands with JavaScript implementations. For instance, things like rm -rf don't work on Windows. There's a package called Rimraf that gets downloaded 60 million times a week just so you can perform rm -rf on Windows. That's insane!

Moreover, environment variables also don't work on Windows. You're going to notice a theme here: Windows is a bit of a lesser child when it comes to these things. This is because Windows is the only non-Unix platform that's relevant at all; Unix, Linux, and Mac are all very similar cores, while Windows is its own world. This is where I often encounter issues on Windows. Another important detail is that shells take too long to start. For example, how long does it take to spawn a shell on a Linux x64 machine? It takes about 7 milliseconds to launch a new shell, which is very annoying if you're intending to run a single command.

Starting the shell can take longer than running the command itself. If you're running many commands in a loop, this gets expensive quickly. Imagine you use ls to get a bunch of directories and then have a for loop that runs through each of those directories to check for a file. Now you have that 7-millisecond block on every single one of those runs unless you parallelize it externally yourself, which is obnoxious.

Now, the core point: Are all these polyfills really necessary? In the world of 2009 to 2016, when JavaScript was still relatively new and experimental, relying on the community to polyfill missing functionality made a lot of sense. But now it's 2024; JavaScript's on the server and it's mature and widely adopted. The JavaScript ecosystem understands the requirements today in a way no one did in 2009. We can do better.

I really like the way this is written; the tone of this article is very good for a release introducing the Bun shell. The Bun shell is a new experimental embedded language and interpreter in Bun that allows you to run cross-platform shell scripts in JavaScript and TypeScript. This is the important thing: when I first looked at this, I assumed it was just spawning out a shell the same way things did all the way up here with a child process, where it was just spawning using whatever your native shell was.

That's not what's happening here. Bun wrote their own shell scripting language, which is very different and very interesting. We see here the standard output used to wait with $ ls. And yes, the dollar sign isn't for jQuery; we'll be sure to try some fun things with it regardless. We await $ ls, or if we want to get the string, we just await do text. Cool! Now we have the text for whatever this command did.

You can await a fetch call, get some response, and then throw that into gzip to get the standard output array buffer. This is such an interesting thing; it's blending JavaScript semantics like awaiting a fetch and shell things like calling gzip. The use of string templating to keep your inputs safe is really cool. The reason they have this syntax is similar to why the Vercel SQL examples use the syntax. This is a template string literal, which means this is effectively calling a function where all of the things you put here are called as arguments, and Bun can then sanitize it so you don't break out of the expected behavior.

=> 00:07:22

Blending JavaScript with shell commands opens up a world of possibilities, making complex tasks simple and secure.

Did you know you can await a fetch call, get some response, and then throw that into Gzip to get the standard output array buffer? That's pretty cool! This is such an interesting concept; it's blending JavaScript semantics like awaiting a fetch with shell commands, such as calling Gzip. The use of string templating to keep your inputs safe is really impressive.

The reason they have this syntax is similar to why the Vercel SQL examples use it. This is a template string literal, which means it effectively calls a function where all of the things you input are treated as arguments. Bun can then sanitize it, preventing you from breaking out of the command. For example, you could just pipe, use a bar, or a semicolon, and then execute some nasty native commands. However, since this is interpolated, that can't happen. There’s no way this response could return something like rm -rf, and this would then run it. That's really nice!

I love these tag template literals; it's cool to see this becoming more and more normal. I just wish developers wouldn't look at this and immediately assume it's some sketchy thing that they shouldn't be touching. This isn't just embedding a string in a string; this is interpolating a string in a function, which is very different and very important to know. They even called this out for security; all template variables are escaped.

For instance, if we have Fuji JS; rm -rf, I love that I didn't pre-read this; it's just the exact thing I was saying. You pass the file name, and now you're going to get an error: cannot access fjs rm -rf because it will wrap this in quotes, treating it as one input instead of calling two different commands. It's really nice that they handle that.

Using Bun feels like regular JavaScript. You can redirect standard output to buffers, which is wild! You can just pipe the output to a JavaScript buffer. What I didn't know was that they were doing such crazy stuff; that's really interesting. You can also redirect standard output to a file, which makes a little more sense. It's interesting that you have to use their file-like function to do that.

You can just pipe with standard bash and zsh pipe syntax for whatever shell you're using. It's pretty standard; you pipe to the escaped file output, output.text, or you can just pass it as a literal. That's good! I was concerned you might need this, but it's cool that you have that as an option. You can pipe standard output to other commands, which I would hope you could do. You can even use the response as standard input, which is really cool.

If you're curious what this does, it allows input from an external source. For example, we're going to grep "Foo" from a set of things, and it’s going to be "bar new line Foo new line bar new line Foo new line." This would grab those two Foo instances; really cool, wild stuff! The ability to send response objects that you've created into your commands is super cool.

Someone mentioned that this makes FFmpeg way easier. Yes, I did a lot of stuff like this with Elixir, where I would take a file, run it through FFmpeg, take the buffer response, and pipe that to another FFmpeg process that would then stream it to Twitch. Those types of tasks are now not just doable in JavaScript but trivial with this. Really cool stuff; I'm hyped!

We also have built-in commands like CD, Echo, and RM. Obviously, it seems like most of the common things you would do in a shell are supported. I'd love to see documentation that lists everything, but most of the core stuff you would run seems to work here. It works on Mac OS, Windows, and Linux. They have implemented many common commands and features like globbing, environment variables, redirection, piping, and more. This is really interesting as a drop-in replacement for simple shell scripts in Bun for Windows; it will power the package.json scripts in Bun run.

=> 00:10:49

Embrace the chaos of blending technologies—JavaScript, jQuery, and shell commands—because innovation thrives in the mix.

Those types of things are now not just doable in JavaScript but trivial with this really cool stuff. I'm hyped! We also have built-in commands like CD, Echo, and RM. Obviously, it seems like most of the common things you would do in a shell are supported. I'd love to see a doc that says everything, but most of the core stuff you would run seems to work here. It works in Mac OS, Windows, and Linux. They have implemented many common commands and features like globbing, environment variables, redirection, piping, and more.

It's really interesting to note that this is a drop-in replacement for simple shell scripts in bun for Windows. It will power the package.json scripts in bun run. This is cool! It seems like a lot of their energy here is because of bun for Windows and how little they could make things universal. Previously, they were probably already building a layer that would let them write one pile of code that would then work properly on Windows, Mac, and Linux. They decided to make that this abstraction where now it's its own shell, and you can use JavaScript code to command these layers. This is really, really interesting. I should chat with Jared about what led to this. We chatted a bunch about it but not why they did it.

I think we have to try it now, right? You all probably looked at this dollar sign and thought, "Wait, isn't that a jQuery thing?" Well, it's being used now. That doesn't mean I don't want to use jQuery here, though. So how would we do that? First and foremost, we have to go with a different name. So, we'll set double dollar sign equals require jQuery. Sadly, jQuery needs a window to work, and since we're running on the server, there's no window object. So, we need to make a DOM and then grab the window from it to bind here. Thankfully, there's already a package for this called JS Dom.

We can use const jss = require('JS Dom') and we can make a new one and get the window from it this way. We just pass that as the argument to the require, and now we have proper classic everyone's favorite jQuery. I screwed up, though; we don't want const because we're blending old and new, so we're going to ve our way through this. Let's use this. Well, we need some data, so let's grab it. Everyone's favorite dollar sign Ajax needs to give it a URL. Oh, that's pretty smart! I'll autocomplete that part. I don't want to spoil things yet, so we figure out what to do here.

Now we have this response. Since we specified the data type here, we already know it's JSON, but that's cheating. We're going to delete that. Let's quickly see what we get back. We can log the result. I also want to know the type, so let's see what we get from this Fun Run. That's a bunch of stuff, and it's an object. Interesting! Despite removing the JSON part here, jQuery is smart enough to recognize it as JSON and parse it for us. So, we're just going to have to turn that back to a string. That's totally fine, but I want to select a key off this. Let's say I want name.

We could do the classic thing of like name = res.name, and sure, that works, but that's so boring. We have a whole separate dollar sign here we could be using, so let's have some fun. Context equals await dollar sign. We need to do something here; we need to parse this JSON somehow. Thankfully, there's something else that will continue our naming confusion here with dollar signs and jQuery. It's a great thing called JQ, which is a shell package for parsing JSON and getting things out of it. We want the name, so we'll do -r.name. This will get us the name, but how do we actually get the JSON here?

We're going to pass it the classic way with an echo. We pass in re | pipe, but wait, isn't re an object? Yes, it is! Good catch! json.stringify(res) and now we can, in the simplest way possible, grab the name key off of this response and then console.log(name). I forgot to do the do text at the end here. Now, if all works properly, we are grabbing the name value from the JSON blob that is passed to the echo. People are pointing out, "Why console log when I can echo?" Very fair point! await echo text—who needs console log anyways?

Oh, I forgot to put name. Isn't this beautiful? Isn't this incredible? Aren't y'all proud that we can blend so many wonderful technologies from jQuery to JS Dom to bun to JavaScript to bash to JQ? There are more technologies than there are lines of code in this file, and I'm very proud. This is wonderful! Am I looking at some kind of war crime here? No, this is just your brain on JavaScript, my guy. This could have prevented so much of the chaos that we've experienced in Node, and we have to make more to make up for it.

Anyways, I am very happy with my work here. I guess this is a better time than ever to wrap up. There are real use cases for this, like build scripts and so many other things you'd run in CI, but I just wanted to have some fun. If you want to see more ways to use bun correctly, let me know in the comments, and I'll be sure to show you that in the future. If you haven't already watched my Truth About Bun video, I'll pin that in the corner. Thank you guys again; I appreciate you all a ton. See you in the next one! Peace, nerds!