10

I have a Gradle project composed of several sub projects. I just created a new one to add support for an interactive Groovy shell that I would like to run with:

gradle console

or

gradle console:run

So my new console module's build.gradle file is the following:

apply plugin: 'groovy'
apply plugin:'application'

mainClassName = 'org.codehaus.groovy.tools.shell.Main'

dependencies {
  compile 'org.codehaus.groovy:groovy-all:2.2.2'
  compile 'org.fusesource.jansi:jansi:1.11'
  compile 'commons-cli:commons-cli:1.2'
  compile 'jline:jline:2.11'
  compile project(':my-module')
}

task(console, dependsOn: 'classes', type: JavaExec) {
  main = 'org.codehaus.groovy.tools.shell.Main'
  classpath = sourceSets.main.runtimeClasspath
}

However, when I run gradle :console:run or gradle console I get something like:

:console:run
Groovy Shell (2.2.2, JVM: 1.6.0_45)
Type 'help' or '\h' for help.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
groovy:000> 
BUILD SUCCESSFUL

Total time: 4.529 secs
giovanni@mylaptop:~/Projects/my-project$

So the interactive shell seems to start but it exits right away.

Am I doing something wrong?

EDIT: Added the following to the build.gradle file:

run.standardInput = System.in

Now the standard input gets read from the input stream (thanks to the comments).

However, Gradle seems to get stuck on this:

Groovy Shell (2.2.2, JVM: 1.6.0_45)
Type 'help' or '\h' for help.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
groovy:000> 
> Building 88% > :console:run

And no input gets accepted. Even this leads to the same thing:

gradle --no-daemon console:run

UPDATE 2018:

Dylons accepted answer don't seem to work anymore, ./gradlew console exits immediately:

$ ./gradlew console

Configure project : The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead. at build_8qb2gvs00xed46ejq1p63fo92.run(/home/jhe052/eclipse-workspace/QuinCe/build.gradle:118) (Run with --stacktrace to get the full stack trace of this deprecation warning.)

BUILD SUCCESSFUL in 3s 3 actionable tasks: 1 executed, 2 up-to-date

Replacing the leftShift (<<) with doLast gets rid of the deprecated message but with the same result. Version info:

$ ./gradlew  --version

Gradle 4.4.1

Build time: 2017-12-20 15:45:23 UTC Revision: 10ed9dc355dc39f6307cc98fbd8cea314bdd381c

Groovy: 2.4.12 Ant: Apache Ant(TM) version 1.9.9 compiled on February 2 2017 JVM: 1.8.0_151 (Oracle Corporation 25.151-b12) OS: Linux 4.13.0-32-generic amd64

jonasfh
  • 4,151
  • 2
  • 21
  • 37
Giovanni Botta
  • 9,626
  • 5
  • 51
  • 94

3 Answers3

7

This works for JDK 7+ (for JDK 6, look at the next figure):

configurations {
    console
}

dependencies {
    // ... compile dependencies, runtime dependencies, etc.
    console 'commons-cli:commons-cli:1.2'
    console('jline:jline:2.11') {
        exclude(group: 'junit', module: 'junit')
    }
    console 'org.codehaus.groovy:groovy-groovysh:2.2.+'
}

task(console, dependsOn: 'classes') << {
    def classpath = sourceSets.main.runtimeClasspath + configurations.console

    def command = [
        'java',
        '-cp', classpath.collect().join(System.getProperty('path.separator')),
        'org.codehaus.groovy.tools.shell.Main',
        '--color'
    ]

    def proc = new ProcessBuilder(command)
        .redirectOutput(ProcessBuilder.Redirect.INHERIT)
        .redirectInput(ProcessBuilder.Redirect.INHERIT)
        .redirectError(ProcessBuilder.Redirect.INHERIT)
        .start()

    proc.waitFor()

    if (0 != proc.exitValue()) {
        throw new RuntimeException("console exited with status: ${proc.exitValue()}")
    }
}

To make this work for JDK 6, I modified the solution from https://stackoverflow.com/a/4274535/206543. My solution is tailored to a standard Linux terminal, so if you are running a shell that uses a char sequence other than '\n' for newlines or that encodes backspaces as a value other other 127, you may need to modify it some. I didn't determine how to make colors print correctly, so its output is rather monotone, but it will get the job done:

configurations {
    console
}

dependencies {
    // ... compile dependencies, runtime dependencies, etc.
    console 'commons-cli:commons-cli:1.2'
    console('jline:jline:2.11') {
        exclude(group: 'junit', module: 'junit')
    }
    console 'org.codehaus.groovy:groovy-groovysh:2.2.+'
}

class StreamCopier implements Runnable {
    def istream
    def ostream
    StreamCopier(istream, ostream) {
        this.istream = istream
        this.ostream = ostream
    }
    void run() {
        int n
        def buffer = new byte[4096]
        while ((n = istream.read(buffer)) != -1) {
            ostream.write(buffer, 0, n)
            ostream.flush()
        }
    }
}

class InputCopier implements Runnable {
    def istream
    def ostream
    def stdout
    InputCopier(istream, ostream, stdout) {
        this.istream = istream
        this.ostream = ostream
        this.stdout = stdout
    }
    void run() {
        try {
            int n
            def buffer = java.nio.ByteBuffer.allocate(4096)
            while ((n = istream.read(buffer)) != -1) {
                ostream.write(buffer.array(), 0, n)
                ostream.flush()
                buffer.clear()
                if (127 == buffer.get(0)) {
                    stdout.print("\b \b")
                    stdout.flush()
                }
            }
        }
        catch (final java.nio.channels.AsynchronousCloseException exception) {
            // Ctrl+D pressed
        }
        finally {
            ostream.close()
        }
    }
}

def getChannel(istream) {
    def f = java.io.FilterInputStream.class.getDeclaredField("in")
    f.setAccessible(true)
    while (istream instanceof java.io.FilterInputStream) {
        istream = f.get(istream)
    }
    istream.getChannel()
}

task(console, dependsOn: 'classes') << {
    def classpath = sourceSets.main.runtimeClasspath + configurations.console

    def command = [
        'java',
        '-cp', classpath.collect().join(System.getProperty('path.separator')),
        'org.codehaus.groovy.tools.shell.Main'
    ]

    def proc = new ProcessBuilder(command).start()

    def stdout = new Thread(new StreamCopier(proc.getInputStream(), System.out))
    stdout.start()

    def stderr = new Thread(new StreamCopier(proc.getErrorStream(), System.err))
    stderr.start()

    def stdin  = new Thread(new InputCopier(
        getChannel(System.in),
        proc.getOutputStream(),
        System.out))
    stdin.start()

    proc.waitFor()
    System.in.close()
    stdout.join()
    stderr.join()
    stdin.join()

    if (0 != proc.exitValue()) {
        throw new RuntimeException("console exited with status: ${proc.exitValue()}")
    }
}

Then, execute it via:

gradle console

or, if you get a lot of noise from gradle:

gradle console -q
Community
  • 1
  • 1
Dylon
  • 1,730
  • 15
  • 14
  • Unfortunately I am using JDK 6 so the `ProcessBuilder` class is much different and the above does not work. – Giovanni Botta Apr 21 '14 at 17:51
  • Optimally, you should upgrade to JDK 7 or 8 since JDK 6 is no longer supported by Oracle: http://www.oracle.com/technetwork/java/eol-135779.html In the meantime, you could try playing around with the ProcessBuilder API to get it to work, such as setting redirectErrorStream(true). I'll play around with it in a little while and try to come up with a solution for JDK 6. – Dylon Apr 21 '14 at 18:20
  • Preaching to the choir. I'm working on a legacy app. – Giovanni Botta Apr 21 '14 at 18:26
  • I added a solution for JDK 6 to my original reply. – Dylon Apr 22 '14 at 12:44
  • ...Some years later: For me, this approach don't work (using java 1.8). Running `./gradlew console` exits immediately after building successfully. Anyone knows why? Gradle 4.4.1, Groovy 2.4.12, Java 1.8.0_151 on Ubuntu 16.04 – jonasfh Feb 14 '18 at 09:51
  • Minor correction: regarding backspace and 127. As per the standard ASCII chart: Backspace is ASCII 8. Del is ASCII 127. – user3624334 Nov 08 '19 at 22:40
2

I created a gradle plugin that allowed this (https://github.com/tkruse/gradle-groovysh-plugin). Sadly gradle does not provide a useful API / contract for IO that supports a REPL, so the plugin does not work with newer versions of Gradle.

However, in the docs on github you can find a manual solution without a plugin.

The outline of that solution is:

package shell;

import org.codehaus.groovy.tools.shell.Main;
import org.fusesource.jansi.AnsiConsole;

class ShellMain {
  public static void main(String[] args) {
    // workaround for jAnsi problems, (backspace and arrow keys not working)
    AnsiConsole.systemUninstall();
    Main.main(args);
  }
}

Then Use a JavaExec task to run this

apply plugin: 'java'

dependencies {
  compile('commons-cli:commons-cli:1.2')
  compile('org.codehaus.groovy:groovy-all:2.4.4') { force = true }
  // when using groovy < 2.2 above: "jline:jline:1.0"
  compile("jline:jline:2.11") {
    exclude(group: 'junit', module: 'junit')
  }
}

task shell(dependsOn: 'testClasses', type: JavaExec) {
  doFirst {
    if (getProperty('org.gradle.daemon') == 'true') {
        throw new IllegalStateException('Do not run shell with gradle daemon, it will eat your arrow keys.')
    }
  }
  standardInput = System.in
  main = 'shell.ShellMain'
  classpath = sourceSets.main.runtimeClasspath
  jvmArgs = []
}
tkruse
  • 10,222
  • 7
  • 53
  • 80
  • Thanks! The `getProperty('org.gradle.daemon')` failed, but this sort of worked when I removed the doFirst block. However, there is som Gradle output interfering with the shell input, so it is not super useful. I'll try to fiddle with it some more... – jonasfh Mar 01 '18 at 08:35
  • It never worked perfectly, gradle does not have an IO API, and they change IO behavior between versions. Gradle output can look pretty cool this way, but it kills the REPL usecase. Possibly you can turn the custom "ShellMain" class into an actual application started without gradle (also startable from IDE) or build by gradle. – tkruse Mar 01 '18 at 08:47
  • Starting gradle with `./gradlew --console plain -q --no-daemon shell` works better, good enough for me now. Thanks for your help! – jonasfh Mar 01 '18 at 08:49
0

Gradle doesn't currently handle ncurses style console applications well. Either run groovysh in a separate console, or run groovyConsole instead.

Peter Niederwieser
  • 121,412
  • 21
  • 324
  • 259