introduce cd function into shell
[org.ibex.xt-crawshaw.git] / src / java / org / ibex / xt / Shell.java
1 package org.ibex.xt;
2
3 import java.io.*;
4 import java.net.*;
5 import java.util.*;
6 import java.util.regex.*;
7
8 import org.ibex.util.*;
9 import org.ibex.util.Collections;
10 import org.ibex.js.*;
11
12 public class Shell {
13
14     public static void main(String[] args) throws Exception {
15         if (args.length == 0 || args.length > 2||  !args[0].startsWith("http://")) {
16             printUsage(); return;
17         }
18         Shell shell = new Shell(new URL(args[0]));
19
20         if (args.length == 2) {
21             // FIXME
22         } else {
23             shell.listen(new InputStreamReader(System.in), new OutputStreamWriter(System.out));
24         }
25     }
26
27     private static void printUsage() {
28         System.out.println("Usage: xish url [command]");
29     }
30
31     protected Command[] commands = new Command[] {
32         new LsCommand(),
33         new PwdCommand(),
34         new CdCommand(),
35         new HelpCommand()
36     };
37
38     /** URL of server. */
39     protected URL server;
40
41     /** Current JS path using '.' as a seperator. */
42     protected String pwd = ".";
43
44     /** Create a new Shell using the given url for the server. */
45     public Shell(URL url) { server = url; }
46
47     public void listen(Reader r, Writer w) throws IOException {
48         LineNumberReader in = new LineNumberReader(r);
49         PrintWriter out = new PrintWriter(w);
50
51         out.println("ibex xt shell: type help or exit");
52         out.print("xt: ");
53         out.flush();
54
55         String line;
56         String buffer = "";
57         while ((line = in.readLine()) != null) {
58             if (line.length() > 0) {
59                 if (line.startsWith("exit")) return;
60                 if (line.charAt(line.length() - 1) == '\\') {
61                     buffer += line.substring(0, line.length() - 1);
62                     out.print('>');
63                     out.flush(); continue;
64                 }
65
66                 buffer += line;
67             }
68
69             if (buffer.length() > 0) {
70                 String[] c = buffer.split(" ");
71                 int i=0; while (i < commands.length) {
72                     if (commands[i].name().equals(c[0])) {
73                         commands[i].execute(out, c); break;
74                     }
75                     i++;
76                 }
77                 if (i == commands.length) {
78                     out.write(c[0]);
79                     w.write(": command not found\n");
80                 }
81                 buffer = "";
82             }
83             out.print("xt: ");
84             out.flush();
85         }
86     }
87
88     private String cookie = null;
89     public Object send(Request request) throws IOException {
90         URLConnection c = server.openConnection();
91         ((HttpURLConnection)c).setRequestMethod("POST");
92         c.setDoOutput(true);
93         if (cookie != null) c.setRequestProperty("Cookie", cookie);
94
95         c.connect();
96
97         ObjectOutputStream out = new ObjectOutputStream(c.getOutputStream());
98         out.writeObject(request);
99         out.close();
100
101         String cook = c.getHeaderField("Set-Cookie");
102         if (cook != null && !cook.equals("")) cookie = cook.substring(0, cook.indexOf(';'));
103
104         try {
105             return new ObjectInputStream(c.getInputStream()).readObject();
106         } catch (ClassNotFoundException e) {
107             e.printStackTrace();
108             throw new IOException("unexpected ClassNotFoundException");
109         }
110     }
111
112     public abstract class Command {
113         /** Returns the command name. */
114         public abstract String name();
115
116         /** Returns single-line of usage information, eg. <tt>pattern]</tt> */
117         public abstract String usage();
118
119         /** Returns single-line description of command. */
120         public abstract String summary();
121
122         /** Returns multi-line description. */
123         public abstract String help();
124
125         /** Writes result of execution, even if result is an error. */
126         public abstract void execute(Writer w, String[] args) throws IOException;
127     }
128
129     /** Returns the command matching the given name. */
130     protected Command command(String name) {
131         for (int i=0; i < commands.length; i++)
132             if (commands[i].name().equals(name)) return commands[i];
133         return null;
134     }
135
136     public class HelpCommand extends Command {
137         public String name() { return "help"; }
138         public String usage() { return "[command name]"; }
139         public String summary() { return "Lists available commands."; }
140         public String help() { return ""; }
141
142         public void execute(Writer w, String[] c) throws IOException {
143             if (c.length > 1) {
144                 Command cmd = command(c[1]);
145                 if (c == null) {
146                     w.write("help: ");
147                     w.write(c[1]);
148                     w.write(": command not found\n");
149                 } else {
150                     w.write("usage: ");
151                     w.write(cmd.name());
152                     w.write(" ");
153                     w.write(cmd.usage());
154                     w.write("\n\n");
155                     w.write(cmd.help());
156                     w.write("\n");
157                 }
158             } else {
159                 int len = 3;
160                 for (int i=0; i < commands.length; i++)
161                     len = Math.max(commands[i].name().length(), len);
162
163                 w.write("Available commands:\n");
164                 for (int i=0; i < commands.length; i++) {
165                     Command cmd = commands[i];
166                     w.write("  ");
167                     w.write(cmd.name());
168                     for (int j=len - cmd.name().length(); j >= 0; j--) w.write(" ");
169                     w.write(" - ");
170                     w.write(cmd.summary());
171                     w.write("\n");
172                 }
173                 w.write("\nFor usage details, type help [command name].\n");
174             }
175         }
176     }
177
178     public class LsCommand extends Command {
179         public String name() { return "ls"; }
180         public String usage() { return "[path]"; }
181         public String summary() { return "List object entries."; }
182         public String help() { return
183             "Lists the keys in an object. Modelled after the UNIX ls command.";
184         }
185
186         public void execute(Writer w, String[] c) throws IOException {
187             if (c.length > 2) { w.write(usage()); return; }
188
189             String path = pwd;
190             String matcher = ".*";
191             if (c.length == 2) {
192                 int pos = c[1].lastIndexOf('/') + 1;
193                 path += "." + c[1].substring(0, pos);
194                 path = path.replaceAll("/+", ".");
195                 if (pos < c[1].length()) {
196                     matcher = c[1].substring(pos);
197                     matcher = matcher.replaceAll("\\.", "\\.");
198                     matcher = matcher.replaceAll("\\*", ".*");
199                 }
200             }
201
202             Object ret = send(new KeyRequest(path, matcher));
203             if (ret == null) {
204                 w.write("error: (unexpected) returned object is null\n");
205             } else if (ret instanceof JSExn) {
206                 String e = ((JSExn)ret).getMessage();
207                 // FIXME: messy way to get info from server
208                 if (e.endsWith("does not exist")) {
209                     w.write("ls ");
210                     w.write(c[1]);
211                     w.write(": no such path\n");
212                 } else {
213                     w.write("error: ");
214                     w.write(e);
215                     w.write("\n");
216                 }
217             } else if (ret instanceof List) {
218                 List l = (List)ret; Collections.sort(l);
219                 Iterator i = l.iterator(); while (i.hasNext()) {
220                     w.write(i.next().toString());
221                     w.write("\n");
222                 }
223             } else {
224                 w.write("error: (unexpected) returned object is of unknown type: ");
225                 w.write(ret.getClass().getName());
226                 w.write("\n");
227             }
228         }
229     }
230
231     public class PwdCommand extends Command {
232         public String name() { return "pwd"; }
233         public String usage() { return ""; }
234         public String summary() { return "Path to current object."; }
235         public String help() { return "Print the path to the current object."; }
236         public void execute(Writer w, String[] c) throws IOException {
237             w.write(c.length == 1 ? pwd.replace('.', '/') : usage());
238             w.write("\n");
239         }
240     }
241
242     public class CdCommand extends Command {
243         public String name() { return "cd"; }
244         public String usage() { return "[path]"; }
245         public String summary() { return "Change current object."; }
246         public String help() { return
247             "Chnages the current object that all other commands use "+
248             "as the base for running. Pass either a relative path "+
249             "(e.g. in /prevalent, type cd myob, now in /prevalent/myob) "+
250             "or an absolute path (e.g. cd /prevalent/myob).\n\n" +
251             "To go up one level, cd .. can be used.";
252         }
253         public void execute(Writer w, String[] c) throws IOException {
254             if (c.length > 2) w.write(usage());
255             else if (c.length == 1 || c[1].equals("") || c[1].equals("/")) pwd = ".";
256             else if (c[1].equals("..")) {
257                 String n = pwd.substring(0, pwd.lastIndexOf('.'));
258                 pwd = n.equals("") ? "." : n;
259             } else {
260                 String n = c[1];
261                 if (n.charAt(0) != '/') n = pwd + n;
262                 n = n.replaceAll("/+", ".");
263                 if (n.length() > 1 && n.charAt(n.length() - 1) == '.')
264                     n = n.substring(0, n.length() - 1);
265
266                 int pos = n.lastIndexOf('.');
267                 String path = n.substring(0, pos);
268                 String matcher = n.substring(pos + 1);
269                 Object ret = send(new KeyRequest(path, matcher));
270
271                 if (ret == null) {
272                     w.write("error: (unexpected) server returned null\n");
273                 } else if (ret instanceof List && ((List)ret).size() == 1) {
274                     pwd = n;
275                 } else if (ret instanceof JSExn ||
276                            (ret instanceof List && ((List)ret).size() == 0)) {
277                     w.write("cd ");
278                     w.write(c[1]);
279                     w.write(": no such path\n");
280                 } else {
281                     w.write("error: (unexpected) server returned ");
282                     w.write(ret.toString());
283                     w.write("\n");
284                 }
285             }
286         }
287     }
288
289     public static abstract class Request implements Serializable {
290         public abstract Object process(JSScope root) throws JSExn;
291     }
292
293     public static class KeyRequest extends Request {
294         private String path, matcher;
295         public KeyRequest() {}
296         public KeyRequest(String path, String matcher) {
297             this.path = path; this.matcher = matcher;
298         }
299
300         /** Returns a List. */
301         public Object process(JSScope root) throws JSExn {
302             String p = path == null ? "" : path.replaceAll("\\.+", "\\.");
303             if (p.length() > 0 && p.charAt(0) == '.') p = p.substring(1);
304             if (p.length() > 0 && p.charAt(p.length() - 1) == '.') p = p.substring(0, p.length() - 1);
305             System.out.println("searching path '"+p+"' for pattern '"+matcher+"'");
306
307             Object o = p.equals("") ? root : root.get(p);
308             if (o == null || o instanceof JSDate ||
309                              o instanceof JSArray ||
310                              !(o instanceof JS)) {
311                 System.out.println("hit bad object: "+o+", class="+
312                     (o == null ? null : o.getClass().getName()));
313                 throw new JSExn("path /" + p + " does not exist");
314             } else {
315                 Pattern pat = Pattern.compile(matcher);
316                 List keys = new ArrayList();
317
318                 Iterator i = ((JS)o).keys().iterator(); while(i.hasNext()) {
319                     String k = i.next().toString();
320                     if (pat.matcher(k).matches()) keys.add(k);
321                 }
322
323                 return keys;
324             }
325         }
326     }
327
328     public static class ExecRequest extends Request {
329         private JS exec;
330         public ExecRequest() {}
331         public ExecRequest(JS exec) { this.exec = exec; }
332         public ExecRequest(String source) throws IOException, JSExn {
333             this(new StringReader(source));
334         }
335         public ExecRequest(Reader source) throws IOException, JSExn {
336             exec = JS.fromReader("xsh", 0, source);
337         }
338
339         /** Returns the result of <tt>JS.eval()</tt>. */
340         public Object process(JSScope root) throws JSExn {
341             return JS.eval(JS.cloneWithNewParentScope(exec, root));
342         }
343     }
344 }