PageRenderTime 139ms CodeModel.GetById 70ms app.highlight 58ms RepoModel.GetById 1ms app.codeStats 0ms

/bundles/plugins-trunk/Console/console/SystemShell.java

#
Java | 1201 lines | 1061 code | 57 blank | 83 comment | 63 complexity | fa44e66869b28d39763d5da59cfd1747 MD5 | raw file
   1package console;
   2
   3// {{{ Imports
   4import java.io.*;
   5import java.util.*;
   6import java.util.regex.*;
   7
   8import org.gjt.sp.jedit.BeanShell;
   9import org.gjt.sp.jedit.Buffer;
  10import org.gjt.sp.jedit.MiscUtilities;
  11import org.gjt.sp.jedit.OperatingSystem;
  12import org.gjt.sp.jedit.View;
  13import org.gjt.sp.jedit.jEdit;
  14import org.gjt.sp.jedit.browser.VFSBrowser;
  15import org.gjt.sp.util.Log;
  16import org.gjt.sp.util.StringList;
  17
  18// }}}
  19
  20/**
  21 * A SystemShell is shared across all instances of Console. 
  22 * It has own environment (variables), and executes system statements in
  23 * a shell which resembles in terms of user interface, something that is
  24 * a cross between the Windows "cmd.exe" and the Linux bash shell, so it
  25 * should be easy to use for both.
  26 * 
  27 * It manages a mapping of Console to ConsoleState objects, where the ConsoleState
  28 * manages the actual ConsoleProcess and the state of that shell.
  29 *  
  30 * When SystemShell executes something, the process itself is started indirectly by
  31 * ProcessRunner.exec().
  32 * @author 1999, 2005 Slava Pestov
  33 * @author 2006, 2009 Alan Ezust
  34 */
  35// {{{ class SystemShell
  36public class SystemShell extends Shell
  37{
  38	// {{{ private members
  39	private String userHome;
  40
  41	/** common shell variables shared across all instances of the System Shell. */
  42	Map<String, String> variables;
  43
  44	/** The state of each console System Shell instance. */
  45	private Hashtable<Console, ConsoleState> consoleStateMap;
  46
  47	static private final char dosSlash = 127;
  48
  49	/** Map of aliases */
  50	private Hashtable<String, String> aliases;
  51
  52	/** Built in commands */
  53	private Hashtable<String, SystemShellBuiltIn> commands;
  54
  55	private boolean initialized;
  56
  57	private byte[] lineSep;
  58
  59	// }}}
  60
  61	// {{{ SystemShell constructor
  62	public SystemShell()
  63	{
  64		super("System");
  65		lineSep = toBytes(System.getProperty("line.separator"));
  66		consoleStateMap = new Hashtable<Console, ConsoleState>();
  67		userHome = System.getProperty("user.home");
  68		if (File.separator.equals("\\"))
  69		{
  70			userHome = userHome.replace("\\", "\\\\");
  71		}
  72
  73	} // }}}
  74
  75	// {{{ public methods
  76	// {{{ openConsole() method
  77	/**
  78	 * Called when a Console dockable first selects this shell.
  79	 *
  80	 * @since Console 4.0.2
  81	 */
  82	public void openConsole(Console console)
  83	{
  84		ConsoleState cs = new ConsoleState();
  85		consoleStateMap.put(console, cs);
  86		if (jEdit.getBooleanProperty("console.rememberCWD" )) {
  87			String propName = "console.cwd." + console.getId();
  88			String val = jEdit.getProperty(propName, "null");
  89			if (!val.equals("null")) cs.currentDirectory = val;
  90		}
  91	} // }}}
  92
  93	// {{{ closeConsole() method
  94	/**
  95	 * Called when a Console dockable is closed.
  96	 *
  97	 * @since Console 4.0.2
  98	 */
  99	public void closeConsole(Console console)
 100	{
 101		ConsoleProcess process = getConsoleState(console).process;
 102		if (process != null)
 103			process.stop();
 104
 105		consoleStateMap.remove(console);
 106	} // }}}
 107
 108	// {{{ printInfoMessage() method
 109	public void printInfoMessage(Output output)
 110	{
 111		if (jEdit.getBooleanProperty("console.shell.info.toggle"))
 112			output.print(null, jEdit.getProperty("console.shell.info"));
 113	} // }}}
 114
 115	// {{{ printPrompt()
 116	/**
 117	 * Prints a prompt to the specified console.
 118	 *
 119	 * @param output
 120	 *                The output
 121	 */
 122	public void printPrompt(Console console, Output output)
 123	{
 124		ConsoleState cstate = getConsoleState(console);
 125		String currentDirectory;
 126		if (cstate == null)
 127			currentDirectory = System.getProperty("user.dir");
 128		else
 129		{
 130			currentDirectory = cstate.currentDirectory;
 131		}
 132		output.writeAttrs(ConsolePane.colorAttributes(console.getPlainColor()), jEdit
 133			.getProperty("console.shell.prompt", 
 134                new String[] { MiscUtilities.abbreviate(currentDirectory)}) + " ");
 135	}
 136
 137	// }}}
 138
 139	// {{{ executeBuiltIn() method
 140	public void executeBuiltIn(Console console, Output output, Output error, String command,
 141		Vector<String> args)
 142	{
 143		SystemShellBuiltIn builtIn = (SystemShellBuiltIn) commands.get(command);
 144		if (builtIn == null)
 145		{
 146			String[] pp = { command };
 147			error.print(console.getErrorColor(), jEdit.getProperty(
 148				"console.shell.unknown-builtin", pp));
 149		}
 150		else
 151		{
 152			builtIn.execute(console, output, error, args);
 153		}
 154	} // }}}
 155
 156	// {{{ executeInDir()
 157	public void executeInDir(final Console console, String input, final Output output, Output error,
 158			String command, String dir)
 159	{
 160		ConsoleState state = getConsoleState(console);
 161		String cwd = state.currentDirectory;
 162
 163		state.setCurrentDirectory(console, dir);
 164		this.execute(console, input, output, error, command);
 165		state.setCurrentDirectory(console, cwd);
 166	} // }}}
 167	
 168	// {{{ execute()
 169	public void execute(final Console console, String input, final Output output, Output error,
 170		String command)
 171	{
 172
 173		if (error == null)
 174			error = output;
 175		ConsoleState state = getConsoleState(console);
 176
 177		// If a process is running under this shell and the pipe to its
 178		// stdin is open, treat this command line as a input line for
 179		// the process to make interactive processes usable as in
 180		// general shells.
 181		if (state.process != null)
 182		{
 183			PipedOutputStream out = state.process.getPipeOutput();
 184			if (out != null)
 185			{
 186				try
 187				{
 188					out.write(toBytes(command));
 189					out.write(lineSep);
 190					out.flush();
 191				}
 192				catch (IOException e)
 193				{
 194					Log.log (Log.ERROR, this, "execute()", e);
 195				}
 196				return;
 197			}
 198		}
 199
 200		// comments, for possible future scripting support
 201		if (command.startsWith("#"))
 202		{
 203			output.commandDone();
 204			return;
 205		}
 206
 207		// lazily initialize aliases and variables
 208		init();
 209
 210		Vector<String> args = parse(command);
 211		// will be null if the command is an empty string
 212		if (args == null)
 213		{
 214			output.commandDone();
 215			return;
 216		}
 217
 218		args = preprocess(console.getView(), console, args);
 219
 220		String commandName = args.elementAt(0);
 221		// check for drive letter changedirs (windows only)
 222		if (OperatingSystem.isWindows()
 223			&& commandName.endsWith(":")) {
 224			char driveLetter = commandName.charAt(0);
 225			args = state.changeDrive(driveLetter);
 226			commandName = args.elementAt(0);
 227		}
 228		if (commandName.charAt(0) == '%')
 229		{
 230			// a console built-in
 231			args.removeElementAt(0);
 232			executeBuiltIn(console, output, error, commandName, args);
 233			output.commandDone();
 234			return;
 235		}
 236
 237		// if current working directory doesn't exist, print an error.
 238		String cwd = state.currentDirectory;
 239		if (!new File(cwd).exists())
 240		{
 241			output.print(console.getErrorColor(), jEdit.getProperty(
 242				"console.shell.error.working-dir", new String[] { cwd }));
 243			output.commandDone();
 244			// error.commandDone();
 245			return;
 246		}
 247
 248		String fullPath = MiscUtilities.constructPath(cwd, commandName);
 249
 250		// Java resolves this relative to user.dir, not
 251		// the directory we pass to exec()...
 252		if (commandName.startsWith("./") || commandName.startsWith("." + File.separator))
 253		{
 254			args.setElementAt(fullPath, 0);
 255		}
 256
 257		if (new File(fullPath).isDirectory() && args.size() == 1)
 258		{
 259			args.setElementAt(fullPath, 0);
 260			executeBuiltIn(console, output, error, "%cd", args);
 261			output.commandDone();
 262			// error.commandDone();
 263		}
 264		else
 265		{
 266			boolean foreground;
 267
 268			if (args.elementAt(args.size() - 1).equals("&"))
 269			{
 270				// run in background
 271				args.removeElementAt(args.size() - 1);
 272				foreground = false;
 273				output.commandDone();
 274				// error.commandDone();
 275			}
 276			else
 277			{
 278				// run in foreground
 279				foreground = true;
 280			}
 281
 282			String[] _args = new String[args.size()];
 283			args.copyInto(_args);
 284			state.currentDirectory = cwd;
 285			final ConsoleProcess proc = new ConsoleProcess(console, output, _args,
 286				variables, state, foreground);
 287
 288			/* If startup failed its no longer running */
 289			if (foreground && proc.isRunning())
 290			{
 291				console.getErrorSource().clear();
 292				state.process = proc;
 293			}
 294
 295			/*
 296			 * Check if we were doing a "run command with selection
 297			 * as input"
 298			 */
 299			if (input != null)
 300			{
 301				Log.log(Log.DEBUG, this, "sending input to system shell: "+input);
 302				OutputStream out = proc.getPipeOutput();
 303				if (out != null)
 304				{
 305					try
 306					{
 307						out.write(toBytes(input));
 308						out.close();
 309					}
 310					catch (IOException e)
 311					{
 312						Log.log (Log.ERROR, this, "execute.pipeout", e);
 313					}
 314				}
 315			}
 316		}
 317	} // }}}
 318
 319	// {{{ stop() method
 320	public void stop(Console console)
 321	{
 322		ConsoleState consoleState = getConsoleState(console);
 323		if (consoleState == null)
 324			return;
 325		ConsoleProcess process = consoleState.process;
 326		if (process != null)
 327			process.stop();
 328		else
 329		{
 330			console.getOutput().print(console.getErrorColor(),
 331				jEdit.getProperty("console.shell.noproc"));
 332		}
 333	} // }}}
 334
 335	// {{{ waitFor() method
 336	/**
 337	 * Waits for currently running Console processes to finish execution.
 338	 *
 339	 * @return true if all was successful (i.e. the error status code was 0)
 340	 */
 341	public boolean waitFor(Console console)
 342	{
 343		ConsoleState consoleState = getConsoleState(console);
 344		if (consoleState == null)
 345			return true;
 346		ConsoleProcess process = consoleState.process;
 347		if (process != null)
 348		{
 349			try
 350			{
 351				return (process.waitFor() == 0);
 352			}
 353			catch (InterruptedException e)
 354			{
 355			}
 356
 357			return process.getExitStatus() == 0;
 358		}
 359		else
 360			return true;
 361	} // }}}
 362
 363	// {{{ endOfFile() method
 364	/**
 365	 * Sends an end of file.
 366	 *
 367	 * @param console
 368	 *                The console
 369	 */
 370	public void endOfFile(Console console)
 371	{
 372		ConsoleState state = getConsoleState(console);
 373
 374		if (state.process != null)
 375		{
 376			console.getOutput().writeAttrs(
 377				ConsolePane.colorAttributes(console.getInfoColor()), "^D\n");
 378			PipedOutputStream out = state.process.getPipeOutput();
 379			if (out != null)
 380			{
 381				try
 382				{
 383					out.close();
 384				}
 385				catch (IOException e)
 386				{
 387				}
 388			}
 389		}
 390	} // }}}
 391
 392	// {{{ detach() method
 393	/**
 394	 * Detaches the currently running process.
 395	 *
 396	 * @param console
 397	 *                The console
 398	 */
 399	public void detach(Console console)
 400	{
 401		SystemShell.ConsoleState state = getConsoleState(console);
 402
 403		ConsoleProcess process = state.process;
 404		if (process == null)
 405		{
 406			console.getOutput().print(console.getErrorColor(),
 407				jEdit.getProperty("console.shell.noproc"));
 408			return;
 409		}
 410
 411		process.detach();
 412	} // }}}
 413
 414	// {{{ getCompletions() method
 415	/**
 416	 * Returns possible completions for the specified command.
 417	 *
 418	 * @param console
 419	 *                The console instance
 420	 * @param command
 421	 *                The command
 422	 * @since Console 3.6
 423	 */
 424	public CompletionInfo getCompletions(Console console, String command)
 425	{
 426		// lazily initialize aliases and variables
 427		init();
 428
 429		View view = console.getView();
 430		String currentDirectory = (console == null ? System.getProperty("user.dir")
 431			: getConsoleState(console).currentDirectory);
 432
 433		final String fileDelimiters = "=\'\" \\" + File.pathSeparator;
 434
 435		String lastArg;
 436		CompletionInfo completionInfo = new CompletionInfo();
 437		completionInfo.offset = 0;
 438		if (File.separatorChar == '\\')
 439		{
 440			// Escaping impossible
 441			String lastArgEscaped = (String) parse(command).lastElement();
 442			// We want to allow completion on the forward slash too
 443			lastArg = lastArgEscaped.replace('/', File.separatorChar);
 444			if (lastArg.startsWith("\\")) {
 445				ConsoleState state = getConsoleState(console);
 446				char drive = state.currentDirectory.charAt(0);
 447				lastArg = drive + ":\\" + lastArg.substring(1);
 448				completionInfo.offset = 2;
 449			}
 450		}
 451		else
 452		{
 453			// Escaping possible
 454			/* I use findLastArgument and then unescape instead of
 455			   (String)parse(command).lastElement() because there's
 456			   no way to get parse(String) to also return the original
 457			   length of the unescaped argument, which we need to calculate
 458			   the completion offset. */
 459			String lastArgEscaped = findLastArgument(command, fileDelimiters);
 460			lastArg = unescape(lastArgEscaped, fileDelimiters);
 461		}
 462
 463		completionInfo.offset += command.length() - lastArg.length();
 464
 465		Matcher m = homeDir.matcher(lastArg);
 466		lastArg = m.replaceFirst(System.getProperty("user.home"));
 467
 468		if (completionInfo.offset == 0)
 469		{
 470			completionInfo.completions = (String[]) getCommandCompletions(view,
 471				currentDirectory, lastArg).toArray(new String[0]);
 472		}
 473		else
 474		{
 475			completionInfo.completions = (String[]) getFileCompletions(view,
 476				currentDirectory, lastArg, false).toArray(new String[0]);
 477		}
 478
 479		// On systems where the file separator is the same as the escape
 480		// character (Windows), it's impossible to do escaping properly,
 481		// so we just assume that escaping is not needed (which is true
 482		// for windows).
 483		if (File.separatorChar != '\\')
 484		{
 485			for (int i = 0; i < completionInfo.completions.length; i++)
 486			{
 487				completionInfo.completions[i] = escape(
 488					completionInfo.completions[i], fileDelimiters);
 489			}
 490		}
 491
 492		// We add a double quote at the beginning of any completion with
 493		// special characters because the current argument parsing
 494		// (done in parse()) uses StreamTokenizer, which only handles
 495		// escaping if it's done within a string. The purpose here is
 496		// dual - to get parse() to unescape the characters and to get
 497		// it to recognize the string as a single argument.
 498		// On systems where we don't support escaping, we do this
 499		// only for the 2nd purpose.
 500		boolean isDoubleQuoted = (completionInfo.offset > 0)
 501			&& (command.charAt(completionInfo.offset - 1) == '\"');
 502		if (!isDoubleQuoted)
 503		{
 504			final String specialCharacters = (File.separatorChar == '\\') ? " "
 505				: fileDelimiters;
 506			for (int i = 0; i < completionInfo.completions.length; i++)
 507			{
 508				String result = completionInfo.completions[i];
 509				if (containsCharacters(result, specialCharacters))
 510					result = "\"" + result;
 511				completionInfo.completions[i] = result;
 512			}
 513		}
 514
 515		return completionInfo;
 516	}
 517
 518	// }}}
 519
 520	// {{{ expandVariables()
 521
 522	static final String varPatternString = "([$%])([a-zA-Z0-9_]+)(\\1?)";
 523
 524	static final String varPatternString2 = "([$%])\\{([^}]+)\\}";
 525
 526	static final Pattern varPattern = Pattern.compile(varPatternString);
 527
 528	static final Pattern varPattern2 = Pattern.compile(varPatternString2);
 529
 530	static final Pattern homeDir = Pattern.compile("^~");
 531
 532	/**
 533	 * returns a string after it's been processed by jedit's internal
 534	 * command processor
 535	 *
 536	 * @param view
 537	 *                A view corresponding to this console's state.
 538	 * @param arg
 539	 *                A single argument on the command line
 540	 * @return A string after it's been processed, with variables replaced
 541	 *         with their values.
 542	 */
 543	String expandVariables(View view, String arg)
 544	{
 545		
 546		// StringBuffer buf = new StringBuffer();
 547		String varName = null;
 548		// Expand homedir
 549		Matcher m = homeDir.matcher(arg);
 550		if (m.find())
 551		{
 552			arg = m.replaceFirst(userHome);
 553		}
 554		
 555		m = varPattern.matcher(arg);
 556		if (!m.find())
 557		{
 558			m = varPattern2.matcher(arg);
 559			if (!m.find())
 560				return arg;
 561		}
 562		varName = m.group(2);
 563		String expansion = getVariableValue(view, varName);
 564		if (expansion == null)
 565		{
 566			varName = varName.toUpperCase();
 567			expansion = getVariableValue(view, varName);
 568		}
 569		if (expansion != null)
 570		{
 571			expansion = expansion.replace("\\", "\\\\");
 572			return m.replaceFirst(expansion);
 573		}
 574		return arg;
 575	}
 576
 577	// }}}
 578
 579	// {{{ getVariableValue() method
 580	public String getVariableValue(View view, String varName)
 581	{
 582		init();
 583
 584		if (view == null)
 585			return variables.get(varName);
 586
 587		// ick, I don't like this at all... multiple consoles/view?
 588		ConsoleState cs = getConsoleState(ConsolePlugin.getConsole(view));
 589		String expansion;
 590
 591		// Expand some special variables
 592		Buffer buffer = view.getBuffer();
 593
 594		// What is this for?
 595		if (varName.equals("$") || varName.equals("%"))
 596			expansion = varName;
 597		else if (varName.equals("d"))
 598		{
 599			expansion = MiscUtilities.getParentOfPath(buffer.getPath());
 600			if (expansion.endsWith("/") || expansion.endsWith(File.separator))
 601			{
 602				expansion = expansion.substring(0, expansion.length() - 1);
 603			}
 604		}
 605		else if (varName.equals("l"))
 606		{
 607			int line = view.getTextArea().getCaretLine()+1;
 608			expansion = new Integer(line).toString();
 609		}
 610		else if (varName.equals("u"))
 611		{
 612			expansion = buffer.getPath();
 613			if (!MiscUtilities.isURL(expansion))
 614			{
 615				
 616				// FIXME: This is totally incorrect! make a proper URL. 
 617				expansion = "file:/" + expansion.replace(File.separatorChar, '/');
 618			}
 619		}
 620		else if (varName.equals("f"))
 621			expansion = buffer.getPath();
 622		else if (varName.equalsIgnoreCase("pwd")) {
 623			expansion = cs.currentDirectory;
 624		}
 625		else if (varName.equals("n"))
 626			expansion = buffer.getName();
 627		else if (varName.equals("c"))
 628			expansion = ConsolePlugin.getClassName(buffer);
 629		else if (varName.equals("PKG"))
 630		{
 631			expansion = ConsolePlugin.getPackageName(buffer);
 632			if (expansion == null)
 633				expansion = "";
 634		}
 635		else if (varName.equals("ROOT"))
 636			expansion = ConsolePlugin.getPackageRoot(buffer);
 637		else if (varName.equals("p"))		
 638			expansion = BeanShell.eval(view, BeanShell.getNameSpace(),
 639				"getProjectRoot(view, buffer)").toString();
 640		else if (varName.equals("BROWSER_DIR"))
 641		{
 642			VFSBrowser browser = (VFSBrowser) view.getDockableWindowManager()
 643				.getDockable("vfs.browser");
 644			if (browser == null)
 645				expansion = null;
 646			else
 647				expansion = browser.getDirectory();
 648		}
 649		else
 650			expansion = (String) variables.get(varName);
 651
 652		return expansion;
 653	} // }}}
 654
 655	// {{{ getAliases() method
 656	public Hashtable<String, String> getAliases()
 657	{
 658		init();
 659		return aliases;
 660	} // }}}
 661	// }}}
 662
 663	// {{{ methods
 664	// {{{ getConsoleState() method
 665
 666	ConsoleState getConsoleState(Console console)
 667	{
 668		ConsoleState retval = (ConsoleState) consoleStateMap.get(console);
 669		if (retval == null)
 670			openConsole(console);
 671		return (ConsoleState) consoleStateMap.get(console);
 672	} // }}}
 673
 674	// {{{ getVariables() method
 675	Map<String, String> getVariables()
 676	{
 677        init();
 678		return variables;
 679	} // }}}
 680
 681	// {{{ propertiesChanged() method
 682	static void propertiesChanged()
 683	{
 684		// aliases = null;
 685		// variables = null;
 686
 687		// next time execute() is called, init() will reload everything
 688	} // }}}
 689
 690	// {{{ toBytes() method
 691	private static byte[] toBytes(String str)
 692	{
 693		try
 694		{
 695			return str.getBytes(jEdit.getProperty("console.encoding"));
 696		}
 697		catch (UnsupportedEncodingException e)
 698		{
 699			Log.log (Log.ERROR, SystemShell.class, "toBytes()", e);
 700			return null;
 701		}
 702	} // }}}
 703
 704	// {{{ init() method
 705	private void init()
 706	{
 707		if (initialized)
 708			return;
 709
 710		initialized = true;
 711
 712		initCommands();
 713		initAliases();
 714		initVariables();
 715	} // }}}
 716
 717	// {{{ initCommands() method
 718	private void initCommands()
 719	{
 720		commands = new Hashtable<String, SystemShellBuiltIn>();
 721		commands.put("%alias", new SystemShellBuiltIn.alias());
 722		commands.put("%aliases", new SystemShellBuiltIn.aliases());
 723		commands.put("%browse", new SystemShellBuiltIn.browse());
 724		commands.put("%cd", new SystemShellBuiltIn.cd());
 725		commands.put("%clear", new SystemShellBuiltIn.clear());
 726		commands.put("%dirstack", new SystemShellBuiltIn.dirstack());
 727		commands.put("%echo", new SystemShellBuiltIn.echo());
 728		commands.put("%edit", new SystemShellBuiltIn.edit());
 729		commands.put("%env", new SystemShellBuiltIn.env());
 730		commands.put("%help", new SystemShellBuiltIn.help());
 731		commands.put("%kill", new SystemShellBuiltIn.kill());
 732		commands.put("%popd", new SystemShellBuiltIn.popd());
 733		commands.put("%pushd", new SystemShellBuiltIn.pushd());
 734		commands.put("%printwd", new SystemShellBuiltIn.pwd());
 735		commands.put("%run", new SystemShellBuiltIn.run());
 736		commands.put("%set", new SystemShellBuiltIn.set());
 737		commands.put("%unalias", new SystemShellBuiltIn.unalias());
 738		commands.put("%unset", new SystemShellBuiltIn.unset());
 739		commands.put("%version", new SystemShellBuiltIn.version());
 740	} // }}}
 741
 742	// {{{ initAliases() method
 743	private void initAliases()
 744	{
 745		aliases = new Hashtable<String,String>();
 746		ProcessRunner pr = ProcessRunner.getProcessRunner();
 747		pr.setUpDefaultAliases(aliases);
 748
 749		// some built-ins can be invoked without the % prefix
 750		
 751		aliases.put("cd", "%cd");
 752		aliases.put("pushd", "%pushd");
 753		aliases.put("popd", "%popd");
 754		aliases.put("pwd", "%printwd");
 755		aliases.put("aliases", "%aliases");
 756		aliases.put("alias", "%alias");
 757		aliases.put("ver", "%version");
 758		
 759		aliases.put("-", "%cd -");
 760
 761		/* run ant without adornments to make error parsing easier */
 762		aliases.put("ant", "ant -emacs");
 763		// load aliases from properties
 764		String alias;
 765		int i = 0;
 766		while ((alias = jEdit.getProperty("console.shell.alias." + i)) != null)
 767		{
 768			aliases.put(alias, jEdit.getProperty("console.shell.alias." + i
 769				+ ".expansion"));
 770			i++;
 771		}
 772	} // }}}
 773
 774	// {{{ initVariables() method
 775	private void initVariables()
 776	{
 777		variables = new HashMap<String, String>();
 778		variables.putAll(System.getenv());
 779
 780		if (File.separator.equals("\\"))
 781		{
 782			Map<String, String> upcased = new HashMap<String, String>();
 783			for (String key : variables.keySet())
 784			{
 785				upcased.put(key.toUpperCase(), variables.get(key));
 786			}
 787			variables = upcased;
 788		}
 789
 790		if (jEdit.getJEditHome() != null)
 791			variables.put("JEDIT_HOME", jEdit.getJEditHome());
 792
 793		if (jEdit.getSettingsDirectory() != null)
 794			variables.put("JEDIT_SETTINGS", jEdit.getSettingsDirectory());
 795
 796		// for the sake of Unix programs that try to be smart
 797		variables.put("TERM", "dumb");
 798
 799		// load variables from properties
 800		
 801		 String varname; 
 802		 int i = 0; 
 803		 while((varname = jEdit.getProperty("console.shell.variable." + i)) != null) {
 804		     variables.put(varname, jEdit.getProperty("console.shell.variable." + i + ".value")); i++; 
 805		 }
 806		 
 807	} // }}}
 808
 809	// {{{ parse() method
 810	/**
 811	 * Convert a command into a vector of arguments.
 812	 */
 813	private Vector<String> parse(String command)
 814	{
 815		Vector<String> args = new Vector<String>();
 816
 817		// We replace \ with a non-printable char because
 818		// StreamTokenizer handles \ specially, which causes
 819		// problems on Windows as \ is the file separator
 820		// there.
 821
 822		// After parsing is done, the non printable char is
 823		// changed to \ once again.
 824
 825		// StreamTokenizer needs a way to disable backslash
 826		// handling...
 827		if (File.separatorChar == '\\')
 828			command = command.replace('\\', dosSlash);
 829
 830		StreamTokenizer st = new StreamTokenizer(new StringReader(command));
 831		st.resetSyntax();
 832		st.wordChars('!', 255);
 833		st.whitespaceChars(0, ' ');
 834		st.quoteChar('"');
 835		st.quoteChar('\'');
 836
 837		try
 838		{
 839			loop: for (;;)
 840			{
 841				switch (st.nextToken())
 842				{
 843				case StreamTokenizer.TT_EOF:
 844					break loop;
 845				case StreamTokenizer.TT_WORD:
 846				case '"':
 847				case '\'':
 848					if (File.separatorChar == '\\')
 849						args.addElement(st.sval.replace(dosSlash, '\\'));
 850					else
 851						args.addElement(st.sval);
 852					break;
 853				}
 854			}
 855		}
 856		catch (IOException io)
 857		{
 858			// won't happen
 859		}
 860
 861		if (args.size() == 0)
 862			return null;
 863		else
 864			return args;
 865	} // }}}
 866
 867	// {{{ preprocess() method
 868	/**
 869	 * Expand aliases, variables and globs.
 870	 *
 871	 * @return a new vector of arguments after vars have been substituted.
 872	 */
 873	private Vector<String> preprocess(View view, Console console, Vector<String> args)
 874	{
 875		Vector<String> newArgs = new Vector<String>();
 876
 877		// expand aliases
 878		String commandName = args.elementAt(0);
 879
 880		String expansion = aliases.get(commandName);
 881		if (expansion != null)
 882		{
 883			Vector<String> expansionArgs = parse(expansion);
 884			for (int i = 0; i < expansionArgs.size(); i++)
 885			{
 886				expandGlobs(view, newArgs, (String) expansionArgs.elementAt(i));
 887			}
 888		}
 889		else
 890			expandGlobs(view, newArgs, commandName);
 891
 892		// add remaining arguments
 893		for (int i = 1; i < args.size(); i++)
 894			expandGlobs(view, newArgs, (String) args.elementAt(i));
 895
 896		return newArgs;
 897	} // }}}
 898
 899	// {{{ expandGlobs() method
 900	/**
 901	 * @param arg -
 902	 *                a single input argument
 903	 * @param args -
 904	 *                an output variable where we place resulting expanded
 905	 *                args
 906	 *
 907	 */
 908	private void expandGlobs(View view, Vector<String> args, String arg)
 909	{
 910		// XXX: to do
 911		args.addElement(expandVariables(view, arg));
 912	} // }}}
 913
 914
 915	// {{{ getFileCompletions() method
 916	private List<String> getFileCompletions(View view, String currentDirName, String typedFilename,
 917		boolean directoriesOnly)
 918	{
 919		String expandedTypedFilename = expandVariables(view, typedFilename);
 920
 921		int lastSeparatorIndex = expandedTypedFilename.lastIndexOf(File.separator);
 922
 923		// The directory part of what the user typed, including the file
 924		// separator.
 925		String typedDirName = lastSeparatorIndex == -1 ? "" : expandedTypedFilename
 926			.substring(0, lastSeparatorIndex + 1);
 927
 928		// The file typed by the user.
 929		File typedFile = new File(expandedTypedFilename).isAbsolute() ? new File(
 930			expandedTypedFilename) : new File(currentDirName, expandedTypedFilename);
 931
 932		boolean directory = expandedTypedFilename.endsWith(File.separator)
 933			|| expandedTypedFilename.length() == 0;
 934
 935		// The parent directory of the file typed by the user (or itself
 936		// if it's already a directory).
 937		File dir = directory ? typedFile : typedFile.getParentFile();
 938
 939		// The filename part of the file typed by the user, or "" if
 940		// it's a directory.
 941		String fileName = directory ? "" : typedFile.getName();
 942
 943		// The list of files we're going to try to match
 944		String[] filenames = dir.list();
 945
 946		if ((filenames == null) || (filenames.length == 0))
 947			return null;
 948
 949		boolean isOSCaseSensitive = ProcessRunner.getProcessRunner().isCaseSensitive();
 950		StringList matchingFilenames = new StringList();
 951//		int matchingFilenamesCount = 0;
 952		String matchedString = isOSCaseSensitive ? fileName : fileName.toLowerCase();
 953		for (int i = 0; i < filenames.length; i++)
 954		{
 955			String matchedAgainst = isOSCaseSensitive ? filenames[i] : filenames[i]
 956				.toLowerCase();
 957
 958			if (matchedAgainst.startsWith(matchedString))
 959			{
 960				StringBuffer match = new StringBuffer();
 961
 962				File matchFile = new File(dir, filenames[i]);
 963				if (directoriesOnly && !matchFile.isDirectory())
 964					continue;
 965
 966				match.append(typedDirName + filenames[i]);
 967				
 968				// Add a separator at the end if it's a
 969				// directory
 970				if (matchFile.isDirectory() && match.charAt(match.length()-1) != File.separatorChar)
 971					match.append(File.separator);
 972
 973				matchingFilenames.add(match.toString());
 974			}
 975		}
 976		return matchingFilenames;
 977	} // }}}
 978
 979	// {{{ getCommandCompletions() method
 980	private List<String> getCommandCompletions(View view, String currentDirName, String command)
 981	{
 982		StringList list = new StringList();
 983
 984		Iterator<String> iter = commands.keySet().iterator();
 985		while (iter.hasNext())
 986		{
 987			String cmd = (String) iter.next();
 988			if (cmd.startsWith(command))
 989				list.add(cmd);
 990		}
 991
 992		iter = aliases.keySet().iterator();
 993		while (iter.hasNext())
 994		{
 995			String cmd = (String) iter.next();
 996			if (cmd.startsWith(command))
 997				list.add(cmd);
 998		}
 999
1000		list.addAll(getFileCompletions(view, currentDirName, command, false));
1001
1002		return list;
1003	} // }}}
1004
1005	// {{{ findLastArgument() method
1006	/**
1007	 * Returns the last argument in the given command by using the given
1008	 * delimiters. The delimiters can be escaped.
1009	 */
1010	private static String findLastArgument(String command, String delimiters)
1011	{
1012		int i = command.length() - 1;
1013		while (i >= 0)
1014		{
1015			char c = command.charAt(i);
1016			if (delimiters.indexOf(c) != -1)
1017			{
1018				if ((i == 0) || (command.charAt(i - 1) != '\\'))
1019					break;
1020				else
1021					i--;
1022			}
1023			i--;
1024		}
1025
1026		return command.substring(i + 1);
1027	} // }}}
1028
1029	// {{{ unescape() method
1030	/**
1031	 * Unescapes the given delimiters in the given string.
1032	 */
1033	private static String unescape(String s, String delimiters)
1034	{
1035		StringBuffer buf = new StringBuffer(s.length());
1036		int i = s.length() - 1;
1037		while (i >= 0)
1038		{
1039			char c = s.charAt(i);
1040			buf.append(c);
1041			if (delimiters.indexOf(c) != -1)
1042			{
1043				if (s.charAt(i - 1) == '\\')
1044					i--;
1045			}
1046			i--;
1047		}
1048
1049		return buf.reverse().toString();
1050	} // }}}
1051
1052	// {{{ escape() method
1053	/**
1054	 * Escapes the given delimiters in the given string.
1055	 */
1056	private static String escape(String s, String delimiters)
1057	{
1058		StringBuffer buf = new StringBuffer();
1059		int length = s.length();
1060		for (int i = 0; i < length; i++)
1061		{
1062			char c = s.charAt(i);
1063			if (delimiters.indexOf(c) != -1)
1064				buf.append('\\');
1065			buf.append(c);
1066		}
1067
1068		return buf.toString();
1069	} // }}}
1070
1071	// {{{ containsWhitespace() method
1072	/**
1073	 * Returns <code>true</code> if the first string contains any of the
1074	 * characters in the second string. Returns <code>false</code>
1075	 * otherwise.
1076	 */
1077	private static boolean containsCharacters(String s, String characters)
1078	{
1079		int stringLength = s.length();
1080		for (int i = 0; i < stringLength; i++)
1081			if (characters.indexOf(s.charAt(i)) != -1)
1082				return true;
1083
1084		return false;
1085	}
1086
1087	// }}}
1088
1089	// }}}
1090
1091	// {{{ ConsoleState inner class
1092	/**
1093	 * A SystemShell is a singleton instance - one per plugin. There are a
1094	 * number of ConsoleStates, one for each Console instance.
1095	 *
1096	 * The ConsoleState contains information that the SystemShell needs to
1097	 * know "where it is". This includes: ConsoleProcess process, String
1098	 * currentDirectory, String lastDirectory, Stack<String>
1099	 * directoryStack.
1100	 */
1101	static class ConsoleState
1102	{
1103		// {{{ ConsoleState members
1104		private ConsoleProcess process;
1105
1106		private ConsoleProcess lastProcess;
1107
1108		/* used only for windows, to keep track of current directories
1109		 * for each drive letter
1110		 */
1111		private HashMap<Character, String> driveDirectories = null;
1112		
1113		String currentDirectory = System.getProperty("user.dir");
1114
1115		String lastDirectory = System.getProperty("user.dir");
1116
1117		Stack<String> directoryStack = new Stack<String>();
1118		// }}}
1119				
1120		// {{{ setProcess method
1121		void setProcess(ConsoleProcess cp)
1122		{
1123			if (process != null)
1124				lastProcess = process;
1125			process = cp;
1126		} // }}}
1127
1128		// {{{ getProcess
1129		ConsoleProcess getProcess()
1130		{
1131			return process;
1132		} // }}}
1133
1134		// {{{ getLastProcess method
1135		ConsoleProcess getLastProcess()
1136		{
1137			if (process != null)
1138				return process;
1139			return lastProcess;
1140		} // }}}
1141
1142		// {{{ gotoLastDirectory method
1143		void gotoLastDirectory(Console console)
1144		{
1145			setCurrentDirectory(console, lastDirectory);
1146		} // }}}
1147
1148		// {{{ changeDrive()
1149		Vector<String> changeDrive(char driveLetter) {
1150			driveLetter = Character.toUpperCase(driveLetter);
1151			Vector<String> retval = new Vector<String>();
1152			retval.add("%cd");
1153			char curDrive = Character.toUpperCase(currentDirectory.charAt(0));
1154			if (driveDirectories == null) driveDirectories = new HashMap<Character, String>();
1155			driveDirectories.put(Character.valueOf(curDrive), currentDirectory);
1156			String path = driveLetter + ":" + File.separator;
1157			if (driveDirectories.containsKey(Character.valueOf(driveLetter))) {
1158				path = driveDirectories.get(Character.valueOf(driveLetter));
1159			}
1160			retval.add(path);
1161			return retval;
1162			
1163		} // }}}
1164		
1165		// {{{ setCurrentDirectory()
1166		void setCurrentDirectory(Console console, String newDir)
1167		{
1168			String[] pp = { newDir };
1169			File file = new File(newDir);
1170			if (!file.isAbsolute())
1171				file = new File(currentDirectory, newDir);
1172			if (!file.exists())
1173			{
1174				console.getOutput().print(console.getErrorColor(),
1175					jEdit.getProperty("console.shell.cd.error", pp));
1176			}
1177			else if (!file.isDirectory())
1178			{
1179				console.getOutput().print(console.getErrorColor(),
1180					jEdit.getProperty("console.shell.cd.file", pp));
1181			}
1182			else
1183			{
1184				lastDirectory = currentDirectory;
1185				try
1186				{
1187					currentDirectory = file.getCanonicalPath();
1188					if (jEdit.getBooleanProperty("console.rememberCWD")) 
1189					{
1190						String propName = "console.cwd." + console.getId();
1191						jEdit.setProperty(propName, currentDirectory);
1192					}
1193				}
1194				catch (IOException ioe)
1195				{
1196					Log.log (Log.ERROR, this, "setCurrentDirectory()", ioe);					
1197				}
1198			}
1199		} // }}}
1200	} // }}}
1201} // }}}