diff --git a/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java b/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java
index 5ec1918c231b82e8d4867b822a0258ed63b0ee40..afabd40c580fc1d889af6143c318fed7deb23f9d 100755
--- a/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java
+++ b/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java
@@ -53,19 +53,7 @@ public class ExclusiveClient extends InteractiveClient {
 
     @Override
     public CommandResult handleCommand(String command) {
-        CommandResult result = server.doCommand(command, false);
-
-        if (result.failed()) {
-            Exception e = result.getFailure();
-            if (e instanceof ParseCancellationException) {
-                System.out.println("ERROR:  Could not parse command");
-            }
-            else {
-                System.out.println("ERROR:  " + e.getMessage());
-            }
-        }
-
-        return result;
+        return server.doCommand(command, false);
     }
 
 
diff --git a/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java b/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java
index 032820edf0af663814234a3d8d1e393518670490..45c01492d4d1a79b927ab92b7d669d4792f93c64 100644
--- a/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java
+++ b/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java
@@ -2,12 +2,16 @@ package edu.caltech.nanodb.client;
 
 
 import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
 import java.io.InputStreamReader;
 
 import org.apache.logging.log4j.Logger;
 import org.apache.logging.log4j.LogManager;
 
 import edu.caltech.nanodb.server.CommandResult;
+import org.antlr.v4.runtime.misc.ParseCancellationException;
 
 
 /**
@@ -29,11 +33,25 @@ public abstract class InteractiveClient {
     /** The buffer that accumulates each command's text. */
     private StringBuilder enteredText;
 
+    /** A flag that records if we are exiting the interactive client. */
+    private boolean exiting;
 
+
+    /**
+     * Start up the interactive client.  The specific way the client
+     * interacts with the server dictates how this startup mechanism
+     * will work.
+     *
+     * @throws Exception if any error occurs during startup
+     */
     public abstract void startup() throws Exception;
 
 
-    public void mainloop() {
+    /**
+     * This is the interactive mainloop that handles input from the standard
+     * input stream of the program.
+     */
+    protected void mainloop() {
         // We don't use the console directly, since we can't read/write it
         // if someone redirects a file onto the client's input-stream.
         boolean hasConsole = (System.console() != null);
@@ -43,13 +61,13 @@ public abstract class InteractiveClient {
                 "Welcome to NanoDB.  Exit with EXIT or QUIT command.\n");
         }
 
-        boolean exiting = false;
-        BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in));
+        exiting = false;
+        BufferedReader bufReader =
+            new BufferedReader(new InputStreamReader(System.in));
         while (!exiting) {
             enteredText = new StringBuilder();
             boolean firstLine = true;
 
-getcmd:
             while (true) {
                 try {
                     if (hasConsole) {
@@ -73,21 +91,9 @@ getcmd:
 
                     enteredText.append(line).append('\n');
 
-                    // Process any commands in the entered text.
-                    while (true) {
-                        String command = getCommandString();
-                        if (command == null)
-                            break;  // No more complete commands.
-
-                        // if (logger.isDebugEnabled())
-                        //     logger.debug("Command string:\n" + command);
-
-                        CommandResult result = handleCommand(command);
-                        if (result.isExit()) {
-                            exiting = true;
-                            break getcmd;
-                        }
-                    }
+                    processEnteredText();
+                    if (exiting)
+                        break;
 
                     if (enteredText.length() == 0)
                         firstLine = true;
@@ -102,6 +108,111 @@ getcmd:
     }
 
 
+    /**
+     * This helper function processes the contents of the {@link #enteredText}
+     * field, consuming comments, handling client "shell commands" and
+     * regular commands that are handled by the
+     * {@link edu.caltech.nanodb.server.NanoDBServer}.  Whatever command or
+     * comment is processed will also be removed from the {@code enteredText}
+     * buffer by this function.  Note also that multiple commands will be
+     * processed, if present.
+     */
+    private void processEnteredText() {
+        // Process any commands in the entered text.
+        while (true) {
+            // Consume leading whitespace
+            while (enteredText.length() > 0 &&
+                Character.isWhitespace(enteredText.charAt(0))) {
+                enteredText.deleteCharAt(0);
+            }
+
+            // Consume comments
+            if (enteredText.length() >= 2) {
+                if ("--".equals(enteredText.substring(0, 2))) {
+                    // Consume single-line comment
+                    int endIdx = 2;
+
+                    // Look for the end of the line.
+                    while (endIdx < enteredText.length() &&
+                           enteredText.charAt(endIdx) != '\n') {
+                        endIdx++;
+                    }
+
+                    if (endIdx == enteredText.length()) {
+                        // Didn't find newline character.  Can't consume this
+                        // comment yet.
+                        return;
+                    }
+
+                    endIdx++;  // Skip the newline character as well.
+                    enteredText.delete(0, endIdx);
+
+                    // Go back and try to find more commands.
+                    continue;
+                }
+                else if ("/*".equals(enteredText.substring(0, 2))) {
+                    // Consume block comment
+
+                    int endIdx = 2;
+
+                    // Look for the end of the block comment.
+                    while (endIdx + 1 < enteredText.length() &&
+                           (enteredText.charAt(endIdx) != '*' ||
+                            enteredText.charAt(endIdx + 1) != '/')) {
+                        endIdx++;
+                    }
+
+                    if (endIdx + 1 == enteredText.length()) {
+                        // Didn't find end of block comment.  Can't consume
+                        // this comment yet.
+                        return;
+                    }
+
+                    endIdx += 2;  // Skip the end of the block-comment.
+                    enteredText.delete(0, endIdx);
+
+                    // Go back and try to find more commands.
+                    continue;
+                }
+            }
+
+            // Look for shell commands
+            if (enteredText.length() > 0 && enteredText.charAt(0) == '\\') {
+                // This is a shell command, which continues to the
+                // end of the current line.
+                int endIdx = 0;
+                while (endIdx < enteredText.length() &&
+                    enteredText.charAt(endIdx) != '\n') {
+                    endIdx++;
+                }
+                endIdx++;  // Include the newline character too
+
+                String shellCommand = enteredText.substring(0, endIdx);
+                enteredText.delete(0, endIdx);
+
+                handleShellCommand(shellCommand);
+                continue;
+            }
+
+            String command = getCommandString();
+            if (command == null)
+                break;  // Couldn't find a complete command.
+
+            // if (logger.isDebugEnabled())
+            //     logger.debug("Command string:\n" + command);
+
+            CommandResult result = handleCommand(command);
+            if (result.isExit()) {
+                exiting = true;
+                break;
+            }
+            else {
+                outputCommandResult(result);
+            }
+        }
+    }
+
+
     /**
      * This helper method goes through the {@link #enteredText} buffer, trying
      * to identify the extent of the next command string.  This is done using
@@ -165,6 +276,99 @@ getcmd:
     public abstract CommandResult handleCommand(String command);
 
 
+    /**
+     * Handle "shell commands," which are commands that the client itself
+     * handles on behalf of the user.  Shell commands start with a backslash
+     * "\" character.
+     *
+     * @param shellCommand the command to handle.
+     */
+    private void handleShellCommand(String shellCommand) {
+        // Split the shell command into parts
+        String[] parts = shellCommand.split("\\s+", 2);
+        parts[0] = parts[0].toLowerCase();
+
+        if ("\\source".equals(parts[0])) {
+            // Source the requested SQL file.
+
+            StringBuilder oldText = enteredText;
+            enteredText = new StringBuilder();
+            String filename = parts[1].strip();
+
+            // Open the file with a try-with-resources so it will always be
+            // closed when we are done with the file.
+            try (BufferedReader reader =
+                     new BufferedReader(new FileReader(filename))) {
+
+                while (true) {
+                    String line = reader.readLine();
+                    if (line == null)
+                        break;
+
+                    // Inject the line of the file into the buffer.
+                    enteredText.append(line).append('\n');
+
+                    // Attempt to process any operations in the entered text.
+                    // If there are no complete commands, this will be a
+                    // no-op.  If there are multiple complete commands, they
+                    // will all be processed.
+                    processEnteredText();
+                }
+            }
+            catch (FileNotFoundException e) {
+                System.out.println("ERROR:  Could not open file \"" +
+                    filename + "\"");
+            }
+            catch (IOException e) {
+                System.out.println("ERROR:  Could not read file \"" +
+                    filename + "\":  " + e.getMessage());
+            }
+
+            enteredText = oldText;
+        }
+        else if ("\\help".equals(parts[0])) {
+            // Show help information.
+            System.out.println("You can enter any NanoDB SQL command, or " +
+                "the following built-in commands.");
+            System.out.println("EXIT; or QUIT; will exit the NanoDB client.");
+            System.out.println();
+            System.out.println("\\help");
+            System.out.println("\tDisplays this help information.");
+            System.out.println();
+            System.out.println("\\source filename.sql");
+            System.out.println("\tLoads and executes the contents of \"filename.sql\".");
+            System.out.println();
+        }
+        else {
+            System.out.println("ERROR:  Unrecognized shell command \"" +
+                parts[0] + "\"");
+        }
+
+    }
+
+
+    /**
+     * Outputs relevant information from the command-result object.
+     *
+     * @param result the command-result object to output
+     */
+    private void outputCommandResult(CommandResult result) {
+        // TODO:  Right now we only print out error information.  In the
+        //        future we will want to output other details.  This
+        //        functionality also needs to be integrated with the tuple-
+        //        output code from the server.  Need to think about this more.
+        if (result.failed()) {
+            Exception e = result.getFailure();
+            if (e instanceof ParseCancellationException) {
+                System.out.println("ERROR:  Could not parse command");
+            }
+            else {
+                System.out.println("ERROR:  " + e.getMessage());
+            }
+        }
+    }
+
+
     /**
      * Shut down the interactive client.  The specific way the client
      * interacts with the server dictates how this shutdown mechanism