diff --git a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleCompleter.java b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleCompleter.java
new file mode 100644
index 0000000000000000000000000000000000000000..15cbe86905a861c1fbc6a106cbcd6defac2f9e17
--- /dev/null
+++ b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleCompleter.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.r.engine.shell;
+
+import com.oracle.truffle.api.frame.MaterializedFrame;
+import com.oracle.truffle.r.nodes.function.PromiseHelperNode;
+import com.oracle.truffle.r.runtime.RCaller;
+import com.oracle.truffle.r.runtime.RInternalError;
+import com.oracle.truffle.r.runtime.context.ConsoleHandler;
+import com.oracle.truffle.r.runtime.context.RContext;
+import com.oracle.truffle.r.runtime.data.RFunction;
+import com.oracle.truffle.r.runtime.data.RList;
+import com.oracle.truffle.r.runtime.data.RPromise;
+import com.oracle.truffle.r.runtime.data.model.RAbstractStringVector;
+import com.oracle.truffle.r.runtime.env.REnvironment;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import jline.console.completer.Completer;
+
+public class JLineConsoleCompleter implements Completer {
+    private final ConsoleHandler console;
+
+    private static boolean isTesting = false;
+
+    public static void testingMode() {
+        isTesting = true;
+    }
+
+    public JLineConsoleCompleter(ConsoleHandler console) {
+        this.console = console;
+    }
+
+    @Override
+    public int complete(String buffer, int cursor, List<CharSequence> candidates) {
+        try {
+            return completeImpl(buffer, cursor, candidates);
+        } catch (Throwable e) {
+            if (isTesting) {
+                throw e;
+            }
+            RInternalError.reportErrorAndConsoleLog(e, console, 0);
+        }
+        return cursor;
+    }
+
+    private int completeImpl(String buffer, int cursor, List<CharSequence> candidates) {
+        if (buffer.isEmpty()) {
+            return cursor;
+        }
+
+        REnvironment utils = REnvironment.getRegisteredNamespace("utils");
+        Object o = utils.get(".completeToken");
+        if (o instanceof RPromise) {
+            o = PromiseHelperNode.evaluateSlowPath(null, (RPromise) o);
+        }
+        RFunction completeToken;
+        if (o instanceof RFunction) {
+            completeToken = (RFunction) o;
+        } else {
+            return cursor;
+        }
+
+        o = utils.get(".CompletionEnv");
+        if (!(o instanceof RPromise)) {
+            return cursor;
+        }
+        REnvironment env = (REnvironment) PromiseHelperNode.evaluateSlowPath(null, (RPromise) o);
+        int start = getStart(buffer, env, cursor);
+        env.safePut("start", start);
+        env.safePut("end", cursor);
+        env.safePut("linebuffer", buffer);
+        env.safePut("token", buffer.substring(start, cursor));
+
+        MaterializedFrame callingFrame = REnvironment.globalEnv().getFrame();
+        RContext.getEngine().evalFunction(completeToken, callingFrame, RCaller.createInvalid(callingFrame), null, new Object[]{});
+
+        o = env.get("comps");
+        if (!(o instanceof RAbstractStringVector)) {
+            return cursor;
+        }
+
+        RAbstractStringVector comps = (RAbstractStringVector) o;
+        List<String> ret = new ArrayList<>(comps.getLength());
+        for (int i = 0; i < comps.getLength(); i++) {
+            ret.add(comps.getDataAt(i));
+        }
+        Collections.sort(ret, String.CASE_INSENSITIVE_ORDER);
+        candidates.addAll(ret);
+        return start;
+    }
+
+    private int getStart(String buffer, REnvironment env, int cursor) {
+        int start = 0;
+        Object o = env.get("options");
+        if (o instanceof RList) {
+            RList opt = (RList) o;
+            start = lastIdxOf(buffer, opt, "funarg.suffix", start, cursor);
+            start = lastIdxOf(buffer, opt, "function.suffix", start, cursor);
+        }
+        start = lastIdxOf(buffer, "\"", start, cursor);
+        start = lastIdxOf(buffer, "'", start, cursor);
+        return start;
+    }
+
+    private int lastIdxOf(String buffer, RList opt, String key, int start, int cursor) {
+        int optIdx = opt.getElementIndexByName(key);
+        if (optIdx > -1) {
+            Object o = opt.getDataAt(optIdx);
+            if (o instanceof RAbstractStringVector) {
+                RAbstractStringVector v = (RAbstractStringVector) o;
+                return lastIdxOf(buffer, v.getLength() > 0 ? v.getDataAt(0) : null, start, cursor);
+            }
+        }
+        return start;
+    }
+
+    private int lastIdxOf(String buffer, String subs, int start, int cursor) {
+        if (null != subs && !subs.isEmpty()) {
+            int idx = buffer.lastIndexOf(subs, cursor);
+            if (idx == cursor) {
+                idx = buffer.lastIndexOf(subs, cursor - 1);
+            }
+            if (idx > -1) {
+                idx += subs.length();
+                return idx > start ? idx : start;
+            }
+        }
+        return start;
+    }
+}
diff --git a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java
index 2c6f86297d727f7862e273cf4c5faa1bdeb9ef88..e8f01bbdb7885fff8077d0eb76a71cff257ab672 100644
--- a/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java
+++ b/com.oracle.truffle.r.engine/src/com/oracle/truffle/r/engine/shell/JLineConsoleHandler.java
@@ -47,6 +47,7 @@ class JLineConsoleHandler implements ConsoleHandler {
     JLineConsoleHandler(RStartParams startParams, InputStream inStream, OutputStream outStream) {
         try {
             console = new ConsoleReader(inStream, outStream);
+            console.addCompleter(new JLineConsoleCompleter(this));
             console.setHandleUserInterrupt(true);
             console.setExpandEvents(false);
         } catch (IOException ex) {
diff --git a/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/engine/shell/TestJLineConsoleCompleter.java b/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/engine/shell/TestJLineConsoleCompleter.java
new file mode 100644
index 0000000000000000000000000000000000000000..e7d55912a9dfb820431d590ee1ffa684acb7c531
--- /dev/null
+++ b/com.oracle.truffle.r.test/src/com/oracle/truffle/r/test/engine/shell/TestJLineConsoleCompleter.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.r.test.engine.shell;
+
+import com.oracle.truffle.api.vm.PolyglotEngine;
+import com.oracle.truffle.r.engine.shell.JLineConsoleCompleter;
+import com.oracle.truffle.r.runtime.RCmdOptions;
+import com.oracle.truffle.r.runtime.context.ConsoleHandler;
+import com.oracle.truffle.r.runtime.context.ContextInfo;
+import com.oracle.truffle.r.runtime.context.RContext;
+import java.io.File;
+import org.junit.Test;
+
+import java.util.LinkedList;
+import org.junit.After;
+import org.junit.Assert;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+
+public class TestJLineConsoleCompleter {
+
+    private PolyglotEngine engine;
+    private ConsoleHandler consoleHandler;
+    private JLineConsoleCompleter consoleCompleter;
+
+    @Before
+    public void before() {
+        consoleHandler = new DummyConsoleHandler();
+        consoleCompleter = new JLineConsoleCompleter(consoleHandler);
+        JLineConsoleCompleter.testingMode();
+        ContextInfo info = ContextInfo.createNoRestore(RCmdOptions.Client.R, null, RContext.ContextKind.SHARE_NOTHING, null, consoleHandler);
+        engine = info.createVM(PolyglotEngine.newBuilder());
+    }
+
+    @After
+    public void dispose() {
+        if (engine != null) {
+            engine.dispose();
+        }
+    }
+
+    @Test
+    public void testCompl() {
+        assertCompl("", 0);
+        assertCompl("", 1);
+        assertCompl(" ", 1);
+
+        assertCompl("(", 0);
+        assertCompl("(", 1);
+        assertCompl("=", 1);
+        assertCompl("$", 1);
+
+        assertCompl("strt", 4, "strtoi", "strtrim");
+        assertCompl("strto", 5, "strtoi");
+        assertCompl("strtoi", 5, "strtoi");
+        assertCompl("strtoi", 4, "strtoi", "strtrim");
+        assertCompl("strto ", 6);
+
+        assertCompl("base::strt", 10, "base::strtoi", "base::strtrim");
+        assertCompl("base:::strt", 11, "base:::strtoi", "base:::strtrim");
+        assertCompl("base:::strttrt", 14, "base:::");
+
+        assertCompl("strt(", 4, "strtoi", "strtrim");
+        assertCompl("strt(", 5);
+        assertCompl("f(strt", 6, "strtoi", "strtrim");
+        assertCompl("f(base::strt", 12, "base::strtoi", "base::strtrim");
+        assertCompl("f(strt(trt", 6, "strtoi", "strtrim");
+        assertCompl("f(strt(trt", 10);
+        assertCompl("f(strt(strto", 11, "strtoi", "strtrim");
+        assertCompl("f(strt(strto", 12, "strtoi");
+
+        String noName = "_f_f_f_";
+        assertCompl(noName + ".", 7);
+        assertCompl(noName + ".", 8);
+        assertCompl(noName + "." + File.separator, 9);
+        assertCompl(noName + "'", 7);
+        assertCompl(noName + "'", 8, NOT_EMPTY);
+        assertCompl(noName + "'." + File.separator, 8, NOT_EMPTY);
+        assertCompl(noName + "'." + File.separator, 9, NOT_EMPTY);
+        assertCompl(noName + "'." + File.separator, 10, NOT_EMPTY);
+        assertCompl(noName + "\"." + File.separator, 8, NOT_EMPTY);
+    }
+
+    // e.g. check if the file path completion returned at least something
+    private static final String NOT_EMPTY = "_@_Only.Check.If.Result.Not.Empty_@_";
+
+    private void assertCompl(String buffer, int cursor, String... expected) {
+        LinkedList<CharSequence> l = new LinkedList<>();
+        consoleCompleter.complete(buffer, cursor, l);
+
+        if (expected == null || expected.length == 0) {
+            assertTrue(l.isEmpty());
+        } else if (expected.length == 1 && NOT_EMPTY.equals(expected[0])) {
+            assertFalse(l.isEmpty());
+        } else {
+            Assert.assertArrayEquals(expected, l.toArray(new CharSequence[l.size()]));
+        }
+    }
+
+    private class DummyConsoleHandler implements ConsoleHandler {
+        @Override
+        public void println(String s) {
+        }
+
+        @Override
+        public void print(String s) {
+        }
+
+        @Override
+        public void printErrorln(String s) {
+        }
+
+        @Override
+        public void printError(String s) {
+        }
+
+        @Override
+        public String readLine() {
+            return "";
+        }
+
+        @Override
+        public boolean isInteractive() {
+            return false;
+        }
+
+        @Override
+        public String getPrompt() {
+            return "";
+        }
+
+        @Override
+        public void setPrompt(String prompt) {
+        }
+
+        @Override
+        public String getInputDescription() {
+            return "";
+        }
+    }
+}
diff --git a/mx.fastr/mx_fastr.py b/mx.fastr/mx_fastr.py
index 5e9c6ebfd3344ee1405840869061861a8af54bc6..e19d9ffd436dc1bf13bc64fb0c82faa9eb1738e1 100644
--- a/mx.fastr/mx_fastr.py
+++ b/mx.fastr/mx_fastr.py
@@ -407,7 +407,7 @@ def _test_subpackage(name):
     return '.'.join((_test_package(), name))
 
 def _simple_generated_unit_tests():
-    return ','.join(map(_test_subpackage, ['library.base', 'library.grid', 'library.methods', 'library.stats', 'library.utils', 'library.fastr', 'builtins', 'functions', 'parser', 'S4', 'rng', 'runtime.data']))
+    return ','.join(map(_test_subpackage, ['engine.shell', 'library.base', 'library.grid', 'library.methods', 'library.stats', 'library.utils', 'library.fastr', 'builtins', 'functions', 'parser', 'S4', 'rng', 'runtime.data']))
 
 def _simple_unit_tests():
     return ','.join([_simple_generated_unit_tests(), _test_subpackage('tck')])