extra comments
[org.ibex.xt-crawshaw.git] / src / java / org / ibex / xt / shell / Command.java
index 0ff2952..a55ded4 100644 (file)
@@ -1,16 +1,30 @@
 package org.ibex.xt.shell;
 
-import java.util.*;
-
+import java.io.StringReader;
 import java.io.Writer;
 import java.io.IOException;
 
+import java.util.*;
+import java.util.regex.*;
+
+import org.ibex.js.*;
+import org.ibex.util.*;
+
+/** Provides implementations for the shell commands. General contract
+ *  is commands named after standard unix tools (eg. rm, mkdir) are
+ *  as safe as possible. Extra functions such as replace have no such
+ *  saftey features.
+ */
 public abstract class Command {
+    protected Shell shell;
+
+    protected Command(Shell s) { shell = s; }
+
     /** Returns the command name. */
     public abstract String name();
 
-    /** Returns single-line of usage information, eg. <tt>pattern]</tt> */
-    public abstract String usage();
+    /** Returns a single-line of parameter information, eg. <tt>[pattern]</tt> */
+    public abstract String params();
 
     /** Returns single-line description of command. */
     public abstract String summary();
@@ -19,38 +33,72 @@ public abstract class Command {
     public abstract String help();
 
     /** Writes result of execution, even if result is an error. */
-    public abstract void execute(Writer w, String[] args, Env env) throws IOException;
+    public abstract void execute(Writer w, String[] args) throws IOException;
+
+
+    /** Returns single-line of usage information, eg. <tt>usage: ls [pattern]</tt> */
+    public void usage(Writer w) throws IOException {
+        w.write("usage: ");
+        w.write(name());
+        w.write(" ");
+        w.write(params());
+        w.write("\n");
+    }
+
+    public List fromShellPath(String s) throws Shell.BadPathException {
+        return fromShellPath(s, null);
+    }
+
+    /** Converts a shell path "/foo/etc/../bar" to its component form,
+     *  { "foo", "bar" } and loads it into the returned list.
+     *  Handles relative positioning to shell.getPath(). */
+    public List fromShellPath(String s, List l) throws Shell.BadPathException {
+        if (s == null) return null;
+        if (l == null) l = new ArrayList();
+
+        s = s.trim().replace("/+", "/");
+        if (s.charAt(0) != '/') l.addAll(Arrays.asList(shell.getPath()));
+
+        StringTokenizer st = new StringTokenizer(s, "/");
+        while (st.hasMoreTokens()) {
+            String part = st.nextToken();
+            if (part == null || part.equals("") || part.equals(".")) continue;
+            else if (part.equals("..")) { if (l.size() > 0) l.remove(l.size() - 1); }
+            else l.add(part);
+        }
+
+        return l;
+    }
 
     public static class Help extends Command {
+        public Help(Shell s) { super(s); }
         public String name() { return "help"; }
-        public String usage() { return "[command name]"; }
+        public String params() { return "[command name]"; }
         public String summary() { return "Lists available commands."; }
         public String help() { return ""; }
 
-        public void execute(Writer w, String[] c, Env env) throws IOException {
+        public void execute(Writer w, String[] c) throws IOException {
             if (c.length > 1) {
-                Command cmd = env.command(c[1]);
+                Command cmd = shell.getCommand(c[1]);
                 if (c == null) {
                     w.write("help: ");
                     w.write(c[1]);
                     w.write(": command not found\n");
                 } else {
-                    w.write("usage: ");
-                    w.write(cmd.name());
-                    w.write(" ");
-                    w.write(cmd.usage());
-                    w.write("\n\n");
+                    cmd.usage(w);
+                    w.write("\n");
                     w.write(cmd.help());
                     w.write("\n");
                 }
             } else {
                 int len = 3;
-                for (int i=0; i < env.commands.length; i++)
-                    len = Math.max(env.commands[i].name().length(), len);
+                Command[] cmds = shell.getCommands();
+                for (int i=0; i < cmds.length; i++)
+                    len = Math.max(cmds[i].name().length(), len);
 
                 w.write("Available commands:\n");
-                for (int i=0; i < env.commands.length; i++) {
-                    Command cmd = env.commands[i];
+                for (int i=0; i < cmds.length; i++) {
+                    Command cmd = cmds[i];
                     w.write("  ");
                     w.write(cmd.name());
                     for (int j=len - cmd.name().length(); j >= 0; j--) w.write(" ");
@@ -64,97 +112,286 @@ public abstract class Command {
     }
 
     public static class Ls extends Command {
+        public Ls(Shell s) { super(s); }
         public String name() { return "ls"; }
-        public String usage() { return "[path]"; }
+        public String params() { return "[path]"; }
         public String summary() { return "List object entries."; }
         public String help() { return
             "Lists the keys in an object. Modelled after the UNIX ls command.";
         }
 
-        public void execute(Writer w, String[] c, Env env) throws IOException {
-            if (c.length > 2) { w.write(usage()); return; }
-            String p = env.path(c.length == 1 ? "*" : c[1]);
+        public void execute(Writer w, String[] c) throws IOException {
+            if (c.length > 2) { usage(w); return; }
+            String p = c.length == 1 ? "*" : c[1];
+            if (p.endsWith("/")) p += "*";
+
+            try {
+                List path = fromShellPath(p);
+                Object key = path.remove(path.size() - 1);
+                Object po = shell.getFromPath(path.toArray());
+                if (po == null || !(po instanceof JS))
+                    throw new Shell.BadPathException();
+                JS cur = (JS)po;
+
+                if (key instanceof String &&
+                        ((String)key).indexOf('*') >= 0) {
+                    String last = (String)key;
+                    last = last.replaceAll("\\.", "\\.");
+                    last = last.replaceAll("\\*", ".*");
+                    Pattern pat = Pattern.compile(last);
+                    Iterator i = cur.keys().iterator(); while (i.hasNext()) {
+                        Object o = i.next();
+                        if (o == null || !(o instanceof String)) continue;
+                        String s = (String)o;
+                        if (!pat.matcher(s).matches()) continue;
+                        w.write(s);
+                        w.write("\n");
+                    }
+                } else if (cur.containsKey(key)) {
+                    w.write(key.toString());
+                    w.write("\n");
+                }
+            } catch (Shell.BadPathException e) {
+                w.write("error: no such path: ");
+                w.write(p);
+                w.write("\n");
+            } catch (JSExn e) {
+                w.write("error: no such path: ");
+                w.write(p);
+                w.write(" (");
+                w.write(e.getMessage());
+                w.write(")\n");
+            }
+        }
+    }
+
+    public static class Replace extends Command {
+        public Replace(Shell s) { super(s); }
+        public String name() { return "replace"; }
+        public String params() { return "[key] [value]"; }
+        public String summary() { return "Sets a key to a specific value."; }
+        public String help() { return
+            "Sets a key to a specific value. This function accepts " +
+            "an xt shell path for the key name (eg. /foo/bar), and a " +
+            "script object for the value (eg. {}, [], \"foo\").\n\n" +
+
+            "If the key does not already exist, it is created, but an " +
+            "error is thrown if the parent object of the key does not " +
+            "exist.\n\n" +
 
-            Request.Response ret = env.send(new Request.Key(p));
-            if (!(ret instanceof Request.Key.Res)) {
-                w.write("error: ");
-                w.write(ret.error().getMessage());
+            "WARNING: This function is dangerous. It gives you a " +
+            "quick and easy way to replace your entire data set with "+
+            "an empty object.";
+        }
+        public void execute(Writer w, String[] c) throws IOException {
+            if (c.length < 3) { usage(w); return; }
+            try {
+                List path = fromShellPath(c[1]);
+                Object key = path.remove(path.size() - 1);
+                Object po = shell.getFromPath(path.toArray());
+
+                if (po == null || !(po instanceof JS))
+                    throw new Shell.BadPathException();
+                JS parent = (JS)po;
+
+                String func = "prevalent.";
+                for (int i=0; i < path.size(); i++)
+                    func += path.get(i) + ".";
+                func += key + " = ";
+                for (int i=2; i < c.length; i++)
+                    func += c[i];
+                func += ";\n";
+
+                shell.transaction(JS.fromReader(
+                    "replace-transaction", 0, new StringReader(func)));
+
+            } catch (JSExn e) {
+                w.write("error: cannot replace '");
+                w.write(c[1]);
+                w.write("': ");
+                w.write(e.getMessage());
                 w.write("\n");
-            } else {
-                List l = ((Request.Key.Res)ret).keys();
-                Iterator i = l.iterator(); while (i.hasNext()) {
-                    w.write(i.next().toString());
+            } catch (Shell.BadPathException e) {
+                w.write("error: cannot replace '");
+                w.write(c[1]);
+                w.write("': ");
+                w.write(e.getMessage() == null ? "no such path" : e.getMessage());
+                w.write("\n");
+            }
+        }
+    }
+
+    public static class Mkdir extends Command {
+        public Mkdir(Shell s) { super(s); }
+        public String name() { return "mkdir"; }
+        public String params() { return "[path]"; }
+        public String summary() { return "Creates a new object ready to handle keys."; }
+        public String help() { return
+            "Creates a new object ready to handle keys. This function " +
+            "is similar to calling replace [path] {}, only it will not " +
+            "overwrite an existing key.";
+        }
+        public void execute(Writer w, String[] c) throws IOException {
+            if (c.length != 2) { usage(w); return; }
+            try {
+                List path = fromShellPath(c[1]);
+                Object key = path.remove(path.size() - 1);
+                Object po = shell.getFromPath(path.toArray());
+
+                if (po == null || !(po instanceof JS))
+                    throw new Shell.BadPathException();
+
+                JS parent = (JS)po;
+                if (parent.containsKey(key))
+                    throw new Shell.BadPathException("already exists");
+
+                String func = "prevalent.";
+                for (int i=0; i < path.size(); i++)
+                    func += path.get(i) + ".";
+                func += key + " = {};\n";
+
+                shell.transaction(JS.fromReader(
+                    "mkdir-transaction", 0, new StringReader(func)));
+
+            } catch (JSExn e) {
+                w.write("error: cannot create '");
+                w.write(c[1]);
+                w.write("': ");
+                w.write(e.getMessage());
+                w.write("\n");
+            } catch (Shell.BadPathException e) {
+                w.write("error: cannot create '");
+                w.write(c[1]);
+                w.write("': ");
+                w.write(e.getMessage() == null ? "no such path" : e.getMessage());
+                w.write("\n");
+            }
+        }
+    }
+
+    public static class Rm extends Command {
+        public Rm(Shell s) { super(s); }
+        public String name() { return "rm"; }
+        public String params() { return "[options] [path]"; }
+        public String summary() { return "Removes objects."; }
+        public String help() { return
+            "Removes objects. If any one of the specified paths does " +
+            "not exist, the entire remove process is cancelled."; }
+        public void execute(Writer w, String[] c) throws IOException {
+            if (c.length == 1) { usage(w); return; }
+
+            boolean force = false; // FIXME provide ability to set
+
+            StringBuffer func = new StringBuffer();
+
+            for (int ic=1; ic < c.length; ic++) {
+                String p = c.length == 1 ? "*" : c[ic];
+                if (p.endsWith("/")) p += "*";
+
+                try {
+                    // get the base of the path
+                    List path = fromShellPath(p);
+                    Object key = path.remove(path.size() - 1);
+                    Object po = shell.getFromPath(path.toArray());
+                    if (po == null || !(po instanceof JS))
+                        throw new Shell.BadPathException();
+                    JS cur = (JS)po;
+
+                    if (cur.containsKey(key)) {
+                        Object o = cur.get(key);
+                        if (!force && o != null && o instanceof JS && ((JS)o).keys().size() > 0)
+                            throw new Shell.BadPathException("key is not empty");
+                        func.append("prevalent.");
+                        for(int i=0; i < path.size(); i++) {
+                            func.append(path.get(i)); func.append('.'); }
+                        func.append("Delete(\"");
+                        func.append(key.toString());
+                        func.append("\");\n");
+                    } else if (key instanceof String && ((String)key).indexOf('*') >= 0) {
+                        String last = (String)key;
+                        last = last.replaceAll("\\.", "\\.");
+                        last = last.replaceAll("\\*", ".*");
+                        Pattern pat = Pattern.compile(last);
+                        Collection curkeys = cur.keys();
+                        if (curkeys.size() == 0) throw new Shell.BadPathException();
+                        Iterator it = curkeys.iterator(); while (it.hasNext()) {
+                            Object o = it.next();
+                            if (o == null || !(o instanceof String)) continue;
+                            String s = (String)o;
+                            if (!pat.matcher(s).matches()) continue;
+
+                            func.append("prevalent.");
+                            for(int i=0; i < path.size(); i++) {
+                                func.append(path.get(i)); func.append('.'); }
+                            func.append("Delete(\"");
+                            func.append(s);
+                            func.append("\");\n");
+                        }
+                    } else throw new Shell.BadPathException();
+
+                } catch (JSExn e) {
+                    w.write("error: cannot remove '");
+                    w.write(p);
+                    w.write("': ");
+                    w.write(e.getMessage());
+                    w.write("\n");
+                    return;
+                } catch (Shell.BadPathException e) {
+                    w.write("error: cannot remove '");
+                    w.write(p);
+                    w.write("': ");
+                    w.write(e.getMessage() != null ? e.getMessage() : "no such path");
                     w.write("\n");
+                    return;
                 }
             }
+
+            shell.transaction(JS.fromReader(
+                "rm-transaction", 0, new StringReader(func.toString())));
         }
     }
 
     public static class Pwd extends Command {
+        public Pwd(Shell s) { super(s); }
         public String name() { return "pwd"; }
-        public String usage() { return ""; }
+        public String params() { return ""; }
         public String summary() { return "Path to current object."; }
         public String help() { return "Print the path to the current object."; }
-        public void execute(Writer w, String[] c, Env env) throws IOException {
-            if (c.length != 1) { w.write(usage()); return; }
-            w.write(env.path.equals("") ? "/" : env.path.replace('.', '/'));
+        public void execute(Writer w, String[] c) throws IOException {
+            if (c.length != 1) { usage(w); return; }
+            Object[] path = shell.getPath();
+            for (int i=0; i < path.length; i++) {
+                w.write("/");
+                w.write(path[i].toString());
+            }
+            if (path.length == 0) w.write("/");
             w.write("\n");
         }
     }
 
     public static class Cd extends Command {
+        public Cd(Shell s) { super(s); }
         public String name() { return "cd"; }
-        public String usage() { return "[path]"; }
+        public String params() { return "[path]"; }
         public String summary() { return "Change current object."; }
         public String help() { return
             "Chnages the current object that all other commands use "+
             "as the base for running.\n Pass either a relative path "+
-            "(e.g. in /prevalent, type cd myob, now in /prevalent/myob) "+
-            "or an absolute path (e.g. cd /prevalent/myob).\n\n" +
+            "(e.g. in .prevalent, type cd myob, now in .prevalent.myob) "+
+            "or an absolute path (e.g. cd .prevalent.myob).\n\n" +
             "To go up one level, cd .. can be used.";
         }
-        public void execute(Writer w, String[] c, Env env) throws IOException {
-            if (c.length > 2) w.write(usage());
-            else if (c.length == 1 || c[1].equals("") || c[1].equals("/")) env.path = "";
-            else if (c[1].equals("..")) {
-                String n = env.path;
-                n = n.substring(0, n.lastIndexOf('.'));
-                env.path = n;
-            } else {
-                String n = env.path(c[1]);
+        public void execute(Writer w, String[] c) throws IOException {
+            if (c.length > 2) { usage(w); return; }
+            String path = c.length == 1 ? "/" : c[1];
 
-                Request.Response ret = env.send(new Request.Key(n));
-                if (!(ret instanceof Request.Key.Res)) {
-                    w.write("error: ");
-                    w.write(ret.error().getMessage());
-                    w.write("\n");
-                } else {
-                    List l = ((Request.Key.Res)ret).keys();
-                    if (l.size() == 0) {
-                        w.write("cd ");
-                        w.write(c[1]);
-                        w.write(": no such path\n");
-                    } else {
-                        env.path = n;
-                    }
-                }
+            try { shell.setPath(fromShellPath(path).toArray()); }
+            catch (Shell.BadPathException e) {
+                w.write("error: no such path: ");
+                w.write(path);
+                w.write("\n");
             }
         }
     }
 
-    public static class Rm extends Command {
-        public String name() { return "rm"; }
-        public String usage() { return "[options] [path]"; }
-        public String summary() { return "Removes objects."; }
-        public String help() { return "Removes objects."; } // FIXME
-        public void execute(Writer w, String[] c, Env env) throws IOException {
-            if (c.length == 1) { w.write(usage()); }
-
-            String[] r = new String[c.length - 1];
-            for (int i=0; i < r.length; i++) r[i] = env.path(c[i + 1]);
-            // Object ret = send(new KeyRequest( FIXME: CompositeRequest
-        }
-    }
-
-
 }