Photon, a commandline shell in less than 300 lines of JavaScript

I have a problem. Sometimes I get something into my head and it sticks there, taunting me, until I do something about it. Much like the stupid song stuck in your brain, you must play the song to be released from it's grasp. So it is with software.

Last week I had to spend a lot of time in Windows working on a port of Electron. This means lots of Node scripts and Git on the command line.

Windows Pains

It may sound like it sometimes, but I really don't hate Windows. It's a fine GUI operating system but the command shell sucks. Really, really bad. Powershell is an improvement but still pretty bad. There has to be something better. I don't want to hate myself and throw my laptop across the room while coding. It dampens productivity. This blog was the result of that rage face. I tiny birdy told me things will get a lot better in Windows 10. I sure hope so.

In the past I would have used Cygwin, which is a port of Bash and a bunch of unix utilities. Sadly it never worked very well (getting POSIX compliant apps to run on Windows is just a big ball of pain) and support has dwindled in recent years.

Then something happened. After pondering for a while I realized I didn't actually care about having standard Unix utilities. Really I just want the Bash interface. I want a command line interpreter that has a proper history, tab completion, and directory navigation. I want ls and more and cd. I don't actually care if they are spec compliant and can be used in Bash shell scripts. I don't really care about shell scripts at all, since I write everything in Node now. I just want the interface.

I could make a new shell, something simple that would get the job done. Node is already ported to Windows, it's built around streams, and NPM gives me access to endless existing modules. That's 90% of the work already done. I just need to stitch it together.

Photon

And so Photon was born.

Photon is about 250 lines of Javascript that give a command line with ls, cp, mv, rm, rmdir, mkdir, more, pwd, and the ability to call other programs like git. It has a very simple form of tab completion (rather buggy), and uses ANSI colors and tables for formatting. (For some reason there are approximately 4.8 billion ANSI color modules for Node).

All you need to do is npm install -g photonsh then photonsh to get this:

Most features were trivial to implement. Here is the function for cp.

    cp: function(a,b) {
        if(!fs.existsSync(a))         return fileError("No such file: ",a);
        if(!fs.statSync(a).isFile())  return fileError("Not a file: ",a);
        var ip = fs.createReadStream(path.join(cwd,a));
        var op = fs.createWriteStream(path.join(cwd,b));
        ip.pipe(op);
    },

Pretty much exactly what you would expect. For the buffered editor with history I used Node's built in readline module which includes callbacks for tab completion.

The hard part

The grand irony here is that I wrote it because of my Windows pain but have yet to actually run it on Windows. I stopped that Windows porting effort for other reasons; so now I just have this program I randomly wrote. Rather than waste the man-months of effort (okay, it was really only about 3 hours), I figured something like this should be shared with the world so that others might learn from my mistakes.

Speaking of mistakes, Photon is horribly buggy and you probably shouldn't run it. No really, it could totally delete your hard drive and stuff. More importantly, Node TTY support is iffy. It turns out Unix shells are very hard to write because of lots of semi-documented assumptions. Go try to write Xterm sometime. There's a reason few people have done it.

In theory a unix shell is simple. You exec a program and pipe it's output to stdout until it's done. The same with input. But what about buffering? But what about ANSI codes? But what about raw keyboard input? Apparently there is a whole world of adhoc specs for how command line apps do 'interactive' things. Running grep from exec is easy. Running vim is not.

In the end I found pausing Node's own REPL interface then execing with the 'inherit' flag worked most of the time. I'm sure there's a better way to do it, but casual Googling with Bing hasn't found it yet.

Onward!

So where does Photon go from here? I have no idea. There's tons of things you could do with it. Node can stream anything, so copying a remote URL to a local file should be trivial. Or you could build a text mode raytracer. Whatever. The choice is yours. Choose wisely. Or don't. The code will still be here (on github).

Enjoy!

Talk to me about it on Twitter

Posted October 15th, 2014

Tagged: nodejs javascript programming