Skip to content

REPL Bash

Namek edited this page May 29, 2016 · 1 revision

REPL: Cygwin Bash

To learn more about snippets below:

> Read a blogpost: Cygwin Bash as a REPL inside The Console

1. Asynchronous input and output

Input and output are not synchronized in same thread. That causes two things:

  1. it's a little easier
  2. auto-completion doesn't simply auto-complete command line but instead it only prints possibilites. To see this fixed, look at Synchronous version
var InputStreamReader = Java.type("java.io.InputStreamReader")
var BufferedReader = Java.type("java.io.BufferedReader")
var ProcessBuilder = Java.type("java.lang.ProcessBuilder")
var Thread = Java.type("java.lang.Thread")

var CYGWIN_DIR = "N:/.babun/cygwin/"
var ENTER = 10

var pb, process, inputThread, keepAlive


function getBashCommand() {
	return CYGWIN_DIR + "bin/bash.exe"
}

exports.commandLineHandler = {
	init: function() {
		pb = new ProcessBuilder(getBashCommand())
		pb.redirectErrorStream(true)
		
		var env = pb.environment()
		env.put('Path', env.get('Path') + ";" + CYGWIN_DIR + "bin")
		
		process = pb.start()
		
		keepAlive = true
		inputThread = new Thread(listenToInput.bind(null, this.context))
		inputThread.start()
	},
	
	handleCompletion: function(input) {
		var os = process.getOutputStream()
		var toComplete = 'compgen -c ' + input
		os.write(toComplete.getBytes())
		os.write(ENTER)
		os.flush()
	},
	
	handleExecution: function(input, utils, context) {
		var os = process.getOutputStream()

		// display in console what a user typed in
		context.output.addInputEntry(input)

		// send input text to bash
		os.write(input.getBytes())
		os.write(ENTER)
		os.flush()
	},
	
	dispose: function() {
		console.log('exiting repl')
		keepAlive = false
		inputThread = null
	}
}

function listenToInput(consoleContext) {
	var input = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"))
	var output = consoleContext.getOutput()
	
	while (keepAlive) {
		if (!process.isAlive()) {
			// this can be achieved by invoking `exit` command
			output.addErrorEntry("bash process is dead.")
			
			// bring back standard handler
			consoleContext.commandLineService.resetHandler()
			return
		}
		
		while (input.ready()) {
			output.addTextEntry(input.readLine())
		}
		
		// don't consume too much CPU
		Thread.sleep(50)
	}
}

2. Synchronous input and output

This version synchronizes input with output by introducing input queue. This fixes a problem of how to connect given amount of output with command completion.

var InputStreamReader = Java.type("java.io.InputStreamReader")
var BufferedReader = Java.type("java.io.BufferedReader")
var ProcessBuilder = Java.type("java.lang.ProcessBuilder")
var Thread = Java.type("java.lang.Thread")

var CYGWIN_DIR = "N:/.babun/cygwin/"
var ENTER = 10

var INPUT_TYPE_CMD = 'cmd'
var INPUT_TYPE_COMPLETION = 'completion'

var pb, process, thread, keepAlive
var inputQueue = []


function getBashCommand() {
	return CYGWIN_DIR + "bin/bash.exe"
}

exports.commandLineHandler = {
	init: function() {
		pb = new ProcessBuilder(getBashCommand())
		pb.redirectErrorStream(true)
		
		var env = pb.environment()
		env.put('Path', env.get('Path') + ";" + CYGWIN_DIR + "bin")
		
		process = pb.start()
		
		keepAlive = true
		thread = new Thread(workWithBash.bind(null, this.context, this.utils))
		thread.start()
	},
	
	handleCompletion: function(input) {
		inputQueue.push({
			type: INPUT_TYPE_COMPLETION,
			text: input
		})
	},
	
	handleExecution: function(input, utils, context) {
		// display in console what a user typed in
		context.output.addInputEntry(input)

		// queue request for execution
		inputQueue.push({
			type: INPUT_TYPE_CMD,
			text: input
		})
	},
	
	dispose: function() {
		keepAlive = false
		thread = null
	}
}

function workWithBash(consoleContext, consoleInputUtils) {
	var input = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"))
	var output = process.getOutputStream()
	var consoleOutput = consoleContext.getOutput()
	
	while (keepAlive) {
		if (!process.isAlive()) {
			// this can be achieved by invoking `exit` command
			consoleOutput.addErrorEntry("bash process is dead.")
			
			// bring back standard handler
			consoleContext.commandLineService.resetHandler()
			return
		}
		
		while (inputQueue.length > 0) {
			var inputCommand = inputQueue.shift()
			var cmd = inputCommand.text
			
			if (inputCommand.type == INPUT_TYPE_COMPLETION) {
				cmd = 'compgen -c ' + cmd
			}

			// send input text to bash
			output.write(cmd.getBytes())
			output.write(ENTER)
			output.flush()
			
			if (inputCommand.type == INPUT_TYPE_COMPLETION) {
				var lines = []
				while (input.ready()) {
					lines.push(input.readLine())
				}
				
				// auto-complete command!
				if (lines.length > 0) {
					consoleInputUtils.setInput(lines[0])
				}
			}
			else if (inputCommand.type == INPUT_TYPE_CMD) {
				while (input.ready()) {
					consoleOutput.addTextEntry(input.readLine())
				}
			}
		}
		
		// don't consume too much CPU waiting for another bunch of input
		Thread.sleep(50)
	}
}