extra comments
[org.ibex.xt-crawshaw.git] / src / java / org / ibex / xt / shell / Command.java
1 package org.ibex.xt.shell;
2
3 import java.io.StringReader;
4 import java.io.Writer;
5 import java.io.IOException;
6
7 import java.util.*;
8 import java.util.regex.*;
9
10 import org.ibex.js.*;
11 import org.ibex.util.*;
12
13 /** Provides implementations for the shell commands. General contract
14  *  is commands named after standard unix tools (eg. rm, mkdir) are
15  *  as safe as possible. Extra functions such as replace have no such
16  *  saftey features.
17  */
18 public abstract class Command {
19     protected Shell shell;
20
21     protected Command(Shell s) { shell = s; }
22
23     /** Returns the command name. */
24     public abstract String name();
25
26     /** Returns a single-line of parameter information, eg. <tt>[pattern]</tt> */
27     public abstract String params();
28
29     /** Returns single-line description of command. */
30     public abstract String summary();
31
32     /** Returns multi-line description. */
33     public abstract String help();
34
35     /** Writes result of execution, even if result is an error. */
36     public abstract void execute(Writer w, String[] args) throws IOException;
37
38
39     /** Returns single-line of usage information, eg. <tt>usage: ls [pattern]</tt> */
40     public void usage(Writer w) throws IOException {
41         w.write("usage: ");
42         w.write(name());
43         w.write(" ");
44         w.write(params());
45         w.write("\n");
46     }
47
48     public List fromShellPath(String s) throws Shell.BadPathException {
49         return fromShellPath(s, null);
50     }
51
52     /** Converts a shell path "/foo/etc/../bar" to its component form,
53      *  { "foo", "bar" } and loads it into the returned list.
54      *  Handles relative positioning to shell.getPath(). */
55     public List fromShellPath(String s, List l) throws Shell.BadPathException {
56         if (s == null) return null;
57         if (l == null) l = new ArrayList();
58
59         s = s.trim().replace("/+", "/");
60         if (s.charAt(0) != '/') l.addAll(Arrays.asList(shell.getPath()));
61
62         StringTokenizer st = new StringTokenizer(s, "/");
63         while (st.hasMoreTokens()) {
64             String part = st.nextToken();
65             if (part == null || part.equals("") || part.equals(".")) continue;
66             else if (part.equals("..")) { if (l.size() > 0) l.remove(l.size() - 1); }
67             else l.add(part);
68         }
69
70         return l;
71     }
72
73     public static class Help extends Command {
74         public Help(Shell s) { super(s); }
75         public String name() { return "help"; }
76         public String params() { return "[command name]"; }
77         public String summary() { return "Lists available commands."; }
78         public String help() { return ""; }
79
80         public void execute(Writer w, String[] c) throws IOException {
81             if (c.length > 1) {
82                 Command cmd = shell.getCommand(c[1]);
83                 if (c == null) {
84                     w.write("help: ");
85                     w.write(c[1]);
86                     w.write(": command not found\n");
87                 } else {
88                     cmd.usage(w);
89                     w.write("\n");
90                     w.write(cmd.help());
91                     w.write("\n");
92                 }
93             } else {
94                 int len = 3;
95                 Command[] cmds = shell.getCommands();
96                 for (int i=0; i < cmds.length; i++)
97                     len = Math.max(cmds[i].name().length(), len);
98
99                 w.write("Available commands:\n");
100                 for (int i=0; i < cmds.length; i++) {
101                     Command cmd = cmds[i];
102                     w.write("  ");
103                     w.write(cmd.name());
104                     for (int j=len - cmd.name().length(); j >= 0; j--) w.write(" ");
105                     w.write(" - ");
106                     w.write(cmd.summary());
107                     w.write("\n");
108                 }
109                 w.write("\nFor usage details, type help [command name].\n");
110             }
111         }
112     }
113
114     public static class Ls extends Command {
115         public Ls(Shell s) { super(s); }
116         public String name() { return "ls"; }
117         public String params() { return "[path]"; }
118         public String summary() { return "List object entries."; }
119         public String help() { return
120             "Lists the keys in an object. Modelled after the UNIX ls command.";
121         }
122
123         public void execute(Writer w, String[] c) throws IOException {
124             if (c.length > 2) { usage(w); return; }
125             String p = c.length == 1 ? "*" : c[1];
126             if (p.endsWith("/")) p += "*";
127
128             try {
129                 List path = fromShellPath(p);
130                 Object key = path.remove(path.size() - 1);
131                 Object po = shell.getFromPath(path.toArray());
132                 if (po == null || !(po instanceof JS))
133                     throw new Shell.BadPathException();
134                 JS cur = (JS)po;
135
136                 if (key instanceof String &&
137                         ((String)key).indexOf('*') >= 0) {
138                     String last = (String)key;
139                     last = last.replaceAll("\\.", "\\.");
140                     last = last.replaceAll("\\*", ".*");
141                     Pattern pat = Pattern.compile(last);
142                     Iterator i = cur.keys().iterator(); while (i.hasNext()) {
143                         Object o = i.next();
144                         if (o == null || !(o instanceof String)) continue;
145                         String s = (String)o;
146                         if (!pat.matcher(s).matches()) continue;
147                         w.write(s);
148                         w.write("\n");
149                     }
150                 } else if (cur.containsKey(key)) {
151                     w.write(key.toString());
152                     w.write("\n");
153                 }
154             } catch (Shell.BadPathException e) {
155                 w.write("error: no such path: ");
156                 w.write(p);
157                 w.write("\n");
158             } catch (JSExn e) {
159                 w.write("error: no such path: ");
160                 w.write(p);
161                 w.write(" (");
162                 w.write(e.getMessage());
163                 w.write(")\n");
164             }
165         }
166     }
167
168     public static class Replace extends Command {
169         public Replace(Shell s) { super(s); }
170         public String name() { return "replace"; }
171         public String params() { return "[key] [value]"; }
172         public String summary() { return "Sets a key to a specific value."; }
173         public String help() { return
174             "Sets a key to a specific value. This function accepts " +
175             "an xt shell path for the key name (eg. /foo/bar), and a " +
176             "script object for the value (eg. {}, [], \"foo\").\n\n" +
177
178             "If the key does not already exist, it is created, but an " +
179             "error is thrown if the parent object of the key does not " +
180             "exist.\n\n" +
181
182             "WARNING: This function is dangerous. It gives you a " +
183             "quick and easy way to replace your entire data set with "+
184             "an empty object.";
185         }
186         public void execute(Writer w, String[] c) throws IOException {
187             if (c.length < 3) { usage(w); return; }
188             try {
189                 List path = fromShellPath(c[1]);
190                 Object key = path.remove(path.size() - 1);
191                 Object po = shell.getFromPath(path.toArray());
192
193                 if (po == null || !(po instanceof JS))
194                     throw new Shell.BadPathException();
195                 JS parent = (JS)po;
196
197                 String func = "prevalent.";
198                 for (int i=0; i < path.size(); i++)
199                     func += path.get(i) + ".";
200                 func += key + " = ";
201                 for (int i=2; i < c.length; i++)
202                     func += c[i];
203                 func += ";\n";
204
205                 shell.transaction(JS.fromReader(
206                     "replace-transaction", 0, new StringReader(func)));
207
208             } catch (JSExn e) {
209                 w.write("error: cannot replace '");
210                 w.write(c[1]);
211                 w.write("': ");
212                 w.write(e.getMessage());
213                 w.write("\n");
214             } catch (Shell.BadPathException e) {
215                 w.write("error: cannot replace '");
216                 w.write(c[1]);
217                 w.write("': ");
218                 w.write(e.getMessage() == null ? "no such path" : e.getMessage());
219                 w.write("\n");
220             }
221         }
222     }
223
224     public static class Mkdir extends Command {
225         public Mkdir(Shell s) { super(s); }
226         public String name() { return "mkdir"; }
227         public String params() { return "[path]"; }
228         public String summary() { return "Creates a new object ready to handle keys."; }
229         public String help() { return
230             "Creates a new object ready to handle keys. This function " +
231             "is similar to calling replace [path] {}, only it will not " +
232             "overwrite an existing key.";
233         }
234         public void execute(Writer w, String[] c) throws IOException {
235             if (c.length != 2) { usage(w); return; }
236             try {
237                 List path = fromShellPath(c[1]);
238                 Object key = path.remove(path.size() - 1);
239                 Object po = shell.getFromPath(path.toArray());
240
241                 if (po == null || !(po instanceof JS))
242                     throw new Shell.BadPathException();
243
244                 JS parent = (JS)po;
245                 if (parent.containsKey(key))
246                     throw new Shell.BadPathException("already exists");
247
248                 String func = "prevalent.";
249                 for (int i=0; i < path.size(); i++)
250                     func += path.get(i) + ".";
251                 func += key + " = {};\n";
252
253                 shell.transaction(JS.fromReader(
254                     "mkdir-transaction", 0, new StringReader(func)));
255
256             } catch (JSExn e) {
257                 w.write("error: cannot create '");
258                 w.write(c[1]);
259                 w.write("': ");
260                 w.write(e.getMessage());
261                 w.write("\n");
262             } catch (Shell.BadPathException e) {
263                 w.write("error: cannot create '");
264                 w.write(c[1]);
265                 w.write("': ");
266                 w.write(e.getMessage() == null ? "no such path" : e.getMessage());
267                 w.write("\n");
268             }
269         }
270     }
271
272     public static class Rm extends Command {
273         public Rm(Shell s) { super(s); }
274         public String name() { return "rm"; }
275         public String params() { return "[options] [path]"; }
276         public String summary() { return "Removes objects."; }
277         public String help() { return
278             "Removes objects. If any one of the specified paths does " +
279             "not exist, the entire remove process is cancelled."; }
280         public void execute(Writer w, String[] c) throws IOException {
281             if (c.length == 1) { usage(w); return; }
282
283             boolean force = false; // FIXME provide ability to set
284
285             StringBuffer func = new StringBuffer();
286
287             for (int ic=1; ic < c.length; ic++) {
288                 String p = c.length == 1 ? "*" : c[ic];
289                 if (p.endsWith("/")) p += "*";
290
291                 try {
292                     // get the base of the path
293                     List path = fromShellPath(p);
294                     Object key = path.remove(path.size() - 1);
295                     Object po = shell.getFromPath(path.toArray());
296                     if (po == null || !(po instanceof JS))
297                         throw new Shell.BadPathException();
298                     JS cur = (JS)po;
299
300                     if (cur.containsKey(key)) {
301                         Object o = cur.get(key);
302                         if (!force && o != null && o instanceof JS && ((JS)o).keys().size() > 0)
303                             throw new Shell.BadPathException("key is not empty");
304                         func.append("prevalent.");
305                         for(int i=0; i < path.size(); i++) {
306                             func.append(path.get(i)); func.append('.'); }
307                         func.append("Delete(\"");
308                         func.append(key.toString());
309                         func.append("\");\n");
310                     } else if (key instanceof String && ((String)key).indexOf('*') >= 0) {
311                         String last = (String)key;
312                         last = last.replaceAll("\\.", "\\.");
313                         last = last.replaceAll("\\*", ".*");
314                         Pattern pat = Pattern.compile(last);
315                         Collection curkeys = cur.keys();
316                         if (curkeys.size() == 0) throw new Shell.BadPathException();
317                         Iterator it = curkeys.iterator(); while (it.hasNext()) {
318                             Object o = it.next();
319                             if (o == null || !(o instanceof String)) continue;
320                             String s = (String)o;
321                             if (!pat.matcher(s).matches()) continue;
322
323                             func.append("prevalent.");
324                             for(int i=0; i < path.size(); i++) {
325                                 func.append(path.get(i)); func.append('.'); }
326                             func.append("Delete(\"");
327                             func.append(s);
328                             func.append("\");\n");
329                         }
330                     } else throw new Shell.BadPathException();
331
332                 } catch (JSExn e) {
333                     w.write("error: cannot remove '");
334                     w.write(p);
335                     w.write("': ");
336                     w.write(e.getMessage());
337                     w.write("\n");
338                     return;
339                 } catch (Shell.BadPathException e) {
340                     w.write("error: cannot remove '");
341                     w.write(p);
342                     w.write("': ");
343                     w.write(e.getMessage() != null ? e.getMessage() : "no such path");
344                     w.write("\n");
345                     return;
346                 }
347             }
348
349             shell.transaction(JS.fromReader(
350                 "rm-transaction", 0, new StringReader(func.toString())));
351         }
352     }
353
354     public static class Pwd extends Command {
355         public Pwd(Shell s) { super(s); }
356         public String name() { return "pwd"; }
357         public String params() { return ""; }
358         public String summary() { return "Path to current object."; }
359         public String help() { return "Print the path to the current object."; }
360         public void execute(Writer w, String[] c) throws IOException {
361             if (c.length != 1) { usage(w); return; }
362             Object[] path = shell.getPath();
363             for (int i=0; i < path.length; i++) {
364                 w.write("/");
365                 w.write(path[i].toString());
366             }
367             if (path.length == 0) w.write("/");
368             w.write("\n");
369         }
370     }
371
372     public static class Cd extends Command {
373         public Cd(Shell s) { super(s); }
374         public String name() { return "cd"; }
375         public String params() { return "[path]"; }
376         public String summary() { return "Change current object."; }
377         public String help() { return
378             "Chnages the current object that all other commands use "+
379             "as the base for running.\n Pass either a relative path "+
380             "(e.g. in .prevalent, type cd myob, now in .prevalent.myob) "+
381             "or an absolute path (e.g. cd .prevalent.myob).\n\n" +
382             "To go up one level, cd .. can be used.";
383         }
384         public void execute(Writer w, String[] c) throws IOException {
385             if (c.length > 2) { usage(w); return; }
386             String path = c.length == 1 ? "/" : c[1];
387
388             try { shell.setPath(fromShellPath(path).toArray()); }
389             catch (Shell.BadPathException e) {
390                 w.write("error: no such path: ");
391                 w.write(path);
392                 w.write("\n");
393             }
394         }
395     }
396
397 }