/* * $Id$ * * Copyright (C) 2003-2015 JNode.org * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; If not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.jnode.shell.bjorne; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_CLOBBER; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_DGREAT; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_DLESS; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_DLESSDASH; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_GREAT; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_GREATAND; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_LESS; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_LESSAND; import static org.jnode.shell.bjorne.BjorneInterpreter.REDIR_LESSGREAT; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jnode.shell.Command; import org.jnode.shell.CommandLine; import org.jnode.shell.CommandShell; import org.jnode.shell.PathnamePattern; import org.jnode.shell.ShellException; import org.jnode.shell.ShellFailureException; import org.jnode.shell.ShellSyntaxException; import org.jnode.shell.io.CommandIO; import org.jnode.shell.io.CommandIOHolder; import org.jnode.shell.io.CommandInput; import org.jnode.shell.io.CommandOutput; /** * This class holds the shell variable and stream state for a bjorne shell * context. A parent context persists between calls to the shell's * <code>interpret</code> method to hold the global shell variables. Others * are created as required to hold the (umm) lexically scoped state for * individual commands, pipelines, subshells and function calls. * * @author crawley@jnode.org */ public class BjorneContext { private static final int NONE = 1; private static final int HASH = 2; private static final int DHASH = 3; private static final int PERCENT = 4; private static final int DPERCENT = 5; private static final int MINUS = 6; private static final int COLONMINUS = 7; private static final int EQUALS = 8; private static final int COLONEQUALS = 9; private static final int PLUS = 10; private static final int COLONPLUS = 11; private static final int QUERY = 12; private static final int COLONQUERY = 13; private final BjorneInterpreter interpreter; private Map<String, VariableSlot> variables; private TreeMap<String, String> aliases; private TreeMap<String, CommandNode> functions; private String command = ""; private List<String> args = new ArrayList<String>(); private int lastReturnCode; private int shellPid; private int lastAsyncPid; private boolean tildeExpansion = true; private boolean globbing = true; private String options = ""; private CommandIOHolder[] holders; private List<CommandIOHolder[]> savedHolders; private boolean echoExpansions; private BjorneContext parent; public BjorneContext(BjorneInterpreter interpreter, CommandIOHolder[] holders) { this.interpreter = interpreter; this.holders = holders; this.variables = new HashMap<String, VariableSlot>(); this.aliases = new TreeMap<String, String>(); this.functions = new TreeMap<String, CommandNode>(); initVariables(); } public BjorneContext(BjorneInterpreter interpreter) { this(interpreter, defaultStreamHolders()); } private static CommandIOHolder[] defaultStreamHolders() { CommandIOHolder[] res = new CommandIOHolder[4]; res[Command.STD_IN] = new CommandIOHolder(CommandLine.DEFAULT_STDIN, false); res[Command.STD_OUT] = new CommandIOHolder(CommandLine.DEFAULT_STDOUT, false); res[Command.STD_ERR] = new CommandIOHolder(CommandLine.DEFAULT_STDERR, false); res[Command.SHELL_ERR] = new CommandIOHolder(CommandLine.DEFAULT_STDERR, false); return res; } private void initVariables() { setVariable("PS1", "$ "); setVariable("PS2", "> "); setVariable("PS4", "+ "); setVariable("IFS", " \t\n"); } /** * Create a copy of a context with the same initial variable bindings and * streams. Stream ownership is not transferred. * * @param parent the context that gives us our initial state. */ public BjorneContext(BjorneContext parent) { this.parent = parent; this.interpreter = parent.interpreter; this.holders = copyStreamHolders(parent.holders); this.variables = copyVariables(parent.variables); this.aliases = new TreeMap<String, String>(parent.aliases); this.functions = new TreeMap<String, CommandNode>(parent.functions); this.globbing = parent.globbing; this.tildeExpansion = parent.tildeExpansion; this.echoExpansions = parent.echoExpansions; } public boolean isTildeExpansion() { return tildeExpansion; } public void setTildeExpansion(boolean tildeExpansion) { this.tildeExpansion = tildeExpansion; } public boolean isGlobbing() { return globbing; } public void setGlobbing(boolean globbing) { this.globbing = globbing; } public boolean isNoClobber() { return isVariableSet("NOCLOBBER"); } void setEchoExpansions(boolean echoExpansions) { this.echoExpansions = echoExpansions; } boolean isEchoExpansions() { return this.echoExpansions; } final int getLastAsyncPid() { return this.lastAsyncPid; } final int getLastReturnCode() { return this.lastReturnCode; } final void setLastReturnCode(int rc) { this.lastReturnCode = rc; } final int getShellPid() { return this.shellPid; } final BjorneContext getParent() { return this.parent; } /** * Create a deep copy of some variable bindings */ private Map<String, VariableSlot> copyVariables( Map<String, VariableSlot> variables) { Map<String, VariableSlot> res = new HashMap<String, VariableSlot>( variables.size()); for (Map.Entry<String, VariableSlot> entry : variables.entrySet()) { res.put(entry.getKey(), new VariableSlot(entry.getValue())); } return res; } /** * Create a copy of some stream holders without passing ownership. */ public static CommandIOHolder[] copyStreamHolders(CommandIOHolder[] holders) { CommandIOHolder[] res = new CommandIOHolder[holders.length]; for (int i = 0; i < res.length; i++) { res[i] = new CommandIOHolder(holders[i]); } return res; } CommandIOHolder[] getCopyOfHolders() { return copyStreamHolders(holders); } CommandIOHolder[] getHolders() { return holders; } void setArgs(String[] args) { this.args = Arrays.asList(args.clone()); } void setCommand(String command) { this.command = command; } /** * This method implements 'NAME=VALUE'. If variable NAME does not exist, it * is created as an unexported shell variable. * * @param name the name of the variable to be set * @param value a non-null value for the variable */ protected void setVariable(String name, String value) { value.length(); // Check that the value is non-null. VariableSlot var = variables.get(name); if (var == null) { variables.put(name, new VariableSlot(name, value, false)); } else if (!var.isReadOnly()) { var.setValue(value); } } /** * Test if the variable is set in this context. * * @param name the name of the variable to be tested * @return <code>true</code> if the variable is set. */ boolean isVariableSet(String name) { return variables.get(name) != null; } /** * Test if the variable is readonly in this context. * * @param name the name of the variable to be tested * @return <code>true</code> if the variable is set. */ boolean isVariableReadonly(String name) { VariableSlot var = variables.get(name); return var != null && var.isReadOnly(); } /** * This method implements 'unset NAME' * * @param name the name of the variable to be unset */ void unsetVariable(String name) { if (!variables.get(name).isReadOnly()) { variables.remove(name); } } /** * This method implements 'readonly NAME' * * @param name the name of the variable to be marked as readonly */ void setVariableReadonly(String name, boolean readonly) { VariableSlot var = variables.get(name); if (var == null) { var = new VariableSlot(name, "", false); variables.put(name, var); } var.setReadOnly(readonly); } /** * This method implements 'export NAME' or 'unexport NAME'. * * @param name the name of the variable to be exported / unexported */ void setVariableExported(String name, boolean exported) { VariableSlot var = variables.get(name); if (var == null) { if (exported) { variables.put(name, new VariableSlot(name, "", exported)); } } else { var.setExported(exported); } } /** * Get the complete alias map. * @return the alias map */ TreeMap<String, String> getAliases() { return aliases; } /** * Lookup an alias * @param aliasName the (possible) alias name * @return the alias string or {@code null} */ String getAlias(String aliasName) { return aliases.get(aliasName); } /** * Define an alias * @param aliasName the alias name * @param alias the alias. */ void defineAlias(String aliasName, String alias) { aliases.put(aliasName, alias); } /** * Undefine an alias * @param aliasName the alias name */ void undefineAlias(String aliasName) { aliases.remove(aliasName); } /** * Perform expand-and-split processing on an array of word tokens. The resulting * wordTokens are assembled into a CommandLine. * * @param tokens the tokens to be expanded and split into words * @return the command line * @throws ShellException */ public CommandLine buildCommandLine(BjorneToken ... tokens) throws ShellException { List<BjorneToken> wordTokens = expandAndSplit(tokens); int nosWords = wordTokens.size(); if (nosWords == 0) { return new CommandLine(null, null); } else { BjorneToken alias = wordTokens.remove(0); BjorneToken[] args = wordTokens.toArray(new BjorneToken[nosWords - 1]); return new CommandLine(alias, args, null); } } /** * Perform expand-and-split processing on a list of word tokens. * * @param tokens the tokens to be expanded and split into words * @throws ShellException */ public List<BjorneToken> expandAndSplit(Iterable<BjorneToken> tokens) throws ShellException { List<BjorneToken> wordTokens = new LinkedList<BjorneToken>(); for (BjorneToken token : tokens) { dollarBacktickSplit(token, wordTokens); } wordTokens = fileExpand(wordTokens); wordTokens = dequote(wordTokens); return wordTokens; } /** * Perform full expand-and-split processing on an array of word tokens. * * @param tokens the tokens to be expanded and split into words * @throws ShellException */ public List<BjorneToken> expandAndSplit(BjorneToken ... tokens) throws ShellException { return expandAndSplit(Arrays.asList(tokens)); } /** * Do quote removal on a list of tokens * * @param wordTokens the tokens to be processed. * @return the de-quoted tokens */ private List<BjorneToken> dequote(List<BjorneToken> wordTokens) { List<BjorneToken> resTokens = new LinkedList<BjorneToken>(); for (BjorneToken token : wordTokens) { resTokens.add(token.remake(dequote(token.getText()))); } return resTokens; } /** * Do quote removal on a String * * @param text the text to be processed. * @return the de-quoted text */ static StringBuilder dequote(String text) { int len = text.length(); StringBuilder sb = new StringBuilder(len); int quote = 0; for (int i = 0; i < len; i++) { char ch = text.charAt(i); switch (ch) { case '"': case '\'': if (quote == 0) { quote = ch; } else if (quote == ch) { quote = 0; } else { sb.append(ch); } break; case '\\': if (i + 1 < len) { ch = text.charAt(++i); } sb.append(ch); break; default: sb.append(ch); break; } } return sb; } /** * Do dollar and backtick expansion on a token, split into words, retokenize and * append the resulting tokens to 'wordTokens. * * @param token * @param wordTokens * @throws ShellException */ private void dollarBacktickSplit(BjorneToken token, List<BjorneToken> wordTokens) throws ShellException { String word = token.getText(); CharSequence expanded = dollarBacktickExpand(word); if (expanded == word) { splitAndAppend(token, wordTokens); } else { BjorneToken newToken = token.remake(expanded); if (newToken != null) { splitAndAppend(newToken, wordTokens); } } } private List<BjorneToken> fileExpand(List<BjorneToken> wordTokens) { if (globbing || tildeExpansion) { List<BjorneToken> globbedWordTokens = new LinkedList<BjorneToken>(); for (BjorneToken wordToken : wordTokens) { if (tildeExpansion) { wordToken = tildeExpand(wordToken); } if (globbing) { globAppend(wordToken, globbedWordTokens); } else { globbedWordTokens.add(wordToken); } } return globbedWordTokens; } else { return wordTokens; } } private BjorneToken tildeExpand(BjorneToken wordToken) { String word = wordToken.getText(); if (word.startsWith("~")) { int slashPos = word.indexOf(File.separatorChar); String name = (slashPos >= 0) ? word.substring(1, slashPos) : ""; // FIXME ... support "~username" when we have some kind of user info / management. String home = (name.length() == 0) ? System.getProperty("user.home", "") : ""; if (home.length() == 0) { return wordToken; } String expansion = (slashPos == -1) ? home : (home + word.substring(slashPos)); return wordToken.remake(expansion); } else { return wordToken; } } private void globAppend(BjorneToken wordToken, List<BjorneToken> globbedWordTokens) { // Try to deal with the 'not-a-pattern' case quickly and cheaply. String word = wordToken.getText(); if (!PathnamePattern.isPattern(word)) { globbedWordTokens.add(wordToken); return; } PathnamePattern pattern = PathnamePattern.compilePathPattern(word); // Expand using the current directory as the base for relative path patterns. LinkedList<String> paths = pattern.expand(new File(".")); // If it doesn't match anything, a pattern 'expands' to itself. if (paths.isEmpty()) { globbedWordTokens.add(wordToken); } else { for (String path : paths) { globbedWordTokens.add(wordToken.remake(path)); } } } /** * Split a token into a series of word tokens, leaving quoting intact. * The resulting tokens are appended to a supplied list. * * @param token the token to be split * @param wordTokens the destination for the tokens. * @throws ShellException */ void splitAndAppend(BjorneToken token, List<BjorneToken> wordTokens) throws ShellException { String text = token.getText(); StringBuilder sb = null; int len = text.length(); int quote = 0; for (int i = 0; i < len; i++) { char ch = text.charAt(i); switch (ch) { case '"': case '\'': if (quote == 0) { quote = ch; } else if (quote == ch) { quote = 0; } sb = accumulate(sb, ch); break; case ' ': case '\t': if (quote == 0) { if (sb != null) { wordTokens.add(token.remake(sb)); sb = null; } } else { sb = accumulate(sb, ch); } break; case '\\': if (i + 1 < len) { sb = accumulate(sb, ch); ch = text.charAt(++i); } sb = accumulate(sb, ch); break; default: sb = accumulate(sb, ch); break; } } if (sb != null) { wordTokens.add(token.remake(sb)); } } protected StringBuffer runBacktickCommand(String commandLine) throws ShellException { StringWriter capture = new StringWriter(); interpreter.interpret(interpreter.getShell(), new StringReader(commandLine), false, capture, false); StringBuffer output = capture.getBuffer(); while (output.length() > 0 && output.charAt(output.length() - 1) == '\n') { output.setLength(output.length() - 1); } return output; } private StringBuilder accumulate(StringBuilder sb, char ch) { if (sb == null) { sb = new StringBuilder(); } sb.append(ch); return sb; } /** * Perform '$' expansion and backtick substitution. Any quotes and escapes must * be preserved so that they escape globbing and tilde expansion. * * @param text the characters to be expanded * @return the result of the expansion. * @throws ShellException */ public CharSequence dollarBacktickExpand(CharSequence text) throws ShellException { return dollarBacktickExpand(new CharIterator(text), -1); } private CharSequence dollarBacktickExpand(CharIterator ci, int terminator) throws ShellException { StringBuilder sb = new StringBuilder(ci.nosRemaining()); int ch = ci.peekCh(); while (ch != -1 && ch != terminator) { ci.nextCh(); switch (ch) { case '"': sb.append(doubleQuoteExpand(ci)); break; case '\'': sb.append(singleQuoteExpand(ci)); break; case '`': sb.append(backQuoteExpand(ci)); break; case '$': sb.append(dollarExpand(ci, (char) -1)); break; case '\\': sb.append((char) ch); if ((ch = ci.nextCh()) != -1) { sb.append((char) ch); } break; default: sb.append((char) ch); break; } ch = ci.peekCh(); } return sb; } private StringBuilder doubleQuoteExpand(CharIterator ci) throws ShellException { StringBuilder sb = new StringBuilder(ci.nosRemaining()); sb.append('"'); int ch = ci.nextCh(); while (ch != -1) { switch (ch) { case '\'': sb.append(singleQuoteExpand(ci)); break; case '"': sb.append('"'); return sb; case '$': sb.append(dollarExpand(ci, '"')); break; case '\\': sb.append((char) ch); if ((ch = ci.nextCh()) != -1) { sb.append((char) ch); } break; default: sb.append((char) ch); break; } ch = ci.nextCh(); } throw new ShellSyntaxException("Unmatched \"'\" (double quote)"); } private Object singleQuoteExpand(CharIterator ci) throws ShellSyntaxException { StringBuilder sb = new StringBuilder(ci.nosRemaining()); sb.append('\''); int ch = ci.nextCh(); while (ch != -1) { switch (ch) { case '\'': sb.append('\''); return sb; case '\\': sb.append((char) ch); if ((ch = ci.nextCh()) != -1) { sb.append((char) ch); } break; default: sb.append((char) ch); break; } ch = ci.nextCh(); } throw new ShellSyntaxException("Unmatched '\"' (single quote)"); } private CharSequence backQuoteExpand(CharIterator ci) throws ShellException { StringBuilder sb = new StringBuilder(ci.nosRemaining()); int ch = ci.nextCh(); while (ch != -1) { switch (ch) { case '"': sb.append(doubleQuoteExpand(ci)); break; case '\'': sb.append(singleQuoteExpand(ci)); break; case '`': return runBacktickCommand(sb.toString()); case '$': sb.append(dollarExpand(ci, '`')); break; case '\\': sb.append((char) ch); if ((ch = ci.nextCh()) != -1) { sb.append((char) ch); } break; default: sb.append((char) ch); break; } ch = ci.nextCh(); } throw new ShellSyntaxException("Unmatched \"`\" (back quote)"); } private CharSequence dollarExpand(CharIterator ci, char quote) throws ShellException { int ch = ci.peekCh(); switch (ch) { case -1: return "$"; case '{': ci.nextCh(); return dollarBraceExpand(ci); case '(': ci.nextCh(); return dollarParenExpand(ci); case '$': case '#': case '@': case '*': case '?': case '!': case '-': ci.nextCh(); return specialVariable(ch, quote == '"'); default: String parameter = parseParameter(ci); String value = (parameter.length() == 0) ? "$" : variable(parameter); return value == null ? "" : value; } } private CharSequence dollarBraceExpand(CharIterator ci) throws ShellException { int ch = ci.peekCh(); if (ch == '#') { ci.nextCh(); String parameter = parseParameter(ci); if (ci.nextCh() != '}') { throw new ShellSyntaxException("Unmatched \"{\""); } String value = variable(parameter); return (value != null) ? Integer.toString(value.length()) : "0"; } String parameter = parseParameter(ci); ch = ci.nextCh(); int operator = NONE; switch (ch) { case -1: throw new ShellSyntaxException("Unmatched \"{\""); case '}': break; case '#': if (ci.peekCh() == '#') { ci.nextCh(); operator = DHASH; } else { operator = HASH; } break; case '%': if (ci.peekCh() == '%') { ci.nextCh(); operator = DPERCENT; } else { operator = PERCENT; } break; case ':': switch (ci.peekCh()) { case '=': operator = COLONEQUALS; break; case '+': operator = COLONPLUS; break; case '?': operator = COLONQUERY; break; case '-': operator = COLONMINUS; break; default: throw new ShellSyntaxException("bad substitution operator"); } ci.nextCh(); break; case '=': operator = EQUALS; break; case '?': operator = QUERY; break; case '+': operator = PLUS; break; case '-': operator = MINUS; break; default: throw new ShellSyntaxException("unrecognized substitution operator (\"" + (char) ch + "\")"); } String value = variable(parameter); if (operator == NONE) { return (value != null) ? value : ""; } String word = dollarBacktickExpand(ci, '}').toString(); if (ci.nextCh() != '}') { throw new ShellSyntaxException("Unmatched \"{\""); } switch (operator) { case MINUS: return (value == null) ? word : value; case COLONMINUS: return (value == null || value.length() == 0) ? word : value; case PLUS: return (value == null) ? "" : word; case COLONPLUS: return (value == null || value.length() == 0) ? "" : word; case QUERY: if (value == null) { String msg = word.length() > 0 ? word : (parameter + " is unset"); resolvePrintStream(getIO(Command.STD_ERR)).println(msg); throw new BjorneControlException(BjorneInterpreter.BRANCH_EXIT, 1); } else { return value; } case COLONQUERY: if (value == null || value.length() == 0) { String msg = word.length() > 0 ? word : (parameter + " is unset or null"); resolvePrintStream(getIO(Command.STD_ERR)).println(msg); throw new BjorneControlException(BjorneInterpreter.BRANCH_EXIT, 1); } else { return value; } case EQUALS: if (value == null) { setVariable(parameter, word); return word; } else { return value; } case COLONEQUALS: if (value == null || value.length() == 0) { setVariable(parameter, word); return word; } else { return value; } case HASH: return patternEdit(value.toString(), word, false, false); case DHASH: return patternEdit(value.toString(), word, false, true); case PERCENT: return patternEdit(value.toString(), word, true, false); case DPERCENT: return patternEdit(value.toString(), word, true, true); default: throw new ShellFailureException("unimplemented substitution operator (" + operator + ")"); } } String parseParameter(CharIterator ci) throws ShellSyntaxException { StringBuilder sb = new StringBuilder(); int ch = ci.peekCh(); while (Character.isLetterOrDigit((char) ch) || ch == '_') { sb.append((char) ch); ci.nextCh(); ch = ci.peekCh(); } return sb.toString(); } private String patternEdit(String value, String pattern, boolean suffix, boolean eager) { if (value == null || value.length() == 0) { return ""; } if (pattern == null || pattern.length() == 0) { return value; } // FIXME ... this does not work for a suffix == true, eager == false. We // translate '*' to '.*?', but that won't give us the shortest suffix because // Patterns inherently match from left to right. int flags = (suffix ? PathnamePattern.ANCHOR_RIGHT : PathnamePattern.ANCHOR_LEFT) | (eager ? PathnamePattern.EAGER : 0); Pattern p = PathnamePattern.compilePosixShellPattern(pattern, PathnamePattern.DEFAULT_FLAGS | flags); Matcher m = p.matcher(value); if (m.find()) { if (suffix) { return value.substring(0, m.start()); } else { return value.substring(m.end()); } } else { return value; } } @SuppressWarnings("unused") private String reverse(String str) { StringBuilder sb = new StringBuilder(str.length()); for (int i = str.length() - 1; i >= 0; i--) { sb.append(str.charAt(i)); } return sb.toString(); } protected String variable(String parameter) throws ShellSyntaxException { if (BjorneToken.isName(parameter)) { VariableSlot var = variables.get(parameter); return (var != null) ? var.getValue() : null; } else { try { int argNo = Integer.parseInt(parameter); return argVariable(argNo); } catch (NumberFormatException ex) { throw new ShellSyntaxException("bad substitution"); } } } private String specialVariable(int ch, boolean inDoubleQuotes) { switch (ch) { case '$': return Integer.toString(shellPid); case '#': return Integer.toString(args.size()); case '@': return concatenateArgs(false, inDoubleQuotes); case '*': return concatenateArgs(true, inDoubleQuotes); case '?': return Integer.toString(lastReturnCode); case '!': return Integer.toString(lastAsyncPid); case '-': return options; default: return null; } } private String concatenateArgs(boolean isStar, boolean inDoubleQuotes) { StringBuilder sb = new StringBuilder(); for (String arg : args) { if (sb.length() > 0) { if (isStar || !inDoubleQuotes) { sb.append(' '); } else { sb.append("\" \""); } } sb.append(arg); } return sb.toString(); } private String argVariable(int argNo) { if (argNo == 0) { return command; } else if (argNo <= args.size()) { return args.get(argNo - 1); } else { return null; } } public boolean isSet(String name) { return variables.get(name) != null; } private CharSequence dollarParenExpand(CharIterator ci) throws ShellException { if (ci.peekCh() == '(') { ci.nextCh(); return dollarParenParenExpand(ci); } else { String commandLine = dollarBacktickExpand(ci, ')').toString(); if (ci.nextCh() != ')') { throw new ShellSyntaxException("Unmatched \"(\" (left parenthesis)"); } return runBacktickCommand(commandLine); } } private CharSequence dollarParenParenExpand(CharIterator ci) throws ShellException { StringBuilder sb = new StringBuilder(); int nesting = 0; boolean done = false; do { int ch = ci.peekCh(); switch (ch) { case '(': nesting++; sb.append('('); break; case ')': if (nesting > 0) { nesting--; sb.append(')'); } else if (ci.peekCh() == ')') { ci.nextCh(); done = true; } else { sb.append(')'); } break; case -1: throw new ShellSyntaxException("Unmatched \"((\" (double left parenthesis)"); default: sb.append((char) ch); } ci.nextCh(); } while (!done); CharSequence tmp = dollarBacktickExpand(new CharIterator(sb), '\000'); return new BjorneArithmeticEvaluator(this).evaluateExpression(tmp); } int execute(CommandLine command, CommandIO[] streams) throws ShellException { if (isEchoExpansions()) { StringBuilder sb = new StringBuilder(); sb.append(" + ").append(command.getCommandName()); for (String arg : command.getArguments()) { sb.append(" ").append(interpreter.escapeWord(arg)); } resolvePrintStream(streams[Command.STD_ERR]).println(sb); } Map<String, String> env = buildEnvFromExports(); lastReturnCode = interpreter.executeCommand(command, this, streams, null, env); return lastReturnCode; } private Map<String, String> buildEnvFromExports() { HashMap<String, String> map = new HashMap<String, String>(variables.size()); for (VariableSlot var : variables.values()) { if (var.isExported()) { map.put(var.getName(), var.getValue()); } } return Collections.unmodifiableMap(map); } PrintStream resolvePrintStream(CommandIO commandIOIF) { return interpreter.resolvePrintStream(commandIOIF); } InputStream resolveInputStream(CommandIO stream) { return interpreter.resolveInputStream(stream); } CommandIO getIO(int index) { if (index < 0) { throw new ShellFailureException("negative stream index"); } else if (index < holders.length) { return holders[index].getIO(); } else { return null; } } void setIO(int index, CommandIO io, boolean mine) { if (index < 0 || index >= holders.length) { throw new ShellFailureException("bad stream index"); } else { holders[index].setIO(io, mine); } } void setIO(int index, CommandIOHolder holder) { if (index < 0 || index >= holders.length) { throw new ShellFailureException("bad stream index"); } else { holders[index].setIO(holder); } } void closeIOs() { for (CommandIOHolder holder : holders) { holder.close(); } } void flushIOs() { for (CommandIOHolder holder : holders) { holder.flush(); } } CommandIO[] getIOs() { CommandIO[] io = new CommandIO[holders.length]; int i = 0; for (CommandIOHolder holder : holders) { io[i++] = (holder == null) ? null : holder.getIO(); } return io; } void performAssignments(BjorneToken[] assignments) throws ShellException { if (assignments != null) { for (int i = 0; i < assignments.length; i++) { String assignment = assignments[i].getText(); int pos = assignment.indexOf('='); if (pos <= 0) { throw new ShellFailureException("misplaced '=' in assignment"); } String name = assignment.substring(0, pos); String value = dollarBacktickExpand(assignment.substring(pos + 1)).toString(); this.setVariable(name, dequote(value).toString()); } } } /** * Evaluate the redirections for this command. * * @param redirects the redirection nodes to be evaluated * @return an array representing the mapping of logical fds to * input/outputStreamTuple streams for this command. * @throws ShellException */ CommandIOHolder[] evaluateRedirections(RedirectionNode[] redirects) throws ShellException { CommandIOHolder[] res = copyStreamHolders(holders); evaluateRedirections(redirects, res); return res; } /** * Evaluate the redirections for this command, saving the context's existing IOs * * @param redirects the redirection nodes to be evaluated * @throws ShellException */ void evaluateRedirectionsAndPushHolders(RedirectionNode[] redirects) throws ShellException { if (savedHolders == null) { savedHolders = new ArrayList<CommandIOHolder[]>(1); } savedHolders.add(holders); holders = copyStreamHolders(holders); evaluateRedirections(redirects, holders); } /** * Evaluate the redirections for this command against a set of streams, saving the context's existing IOs. * * @param redirects the redirection nodes to be evaluated * @param streams the base CommandIOs for the redirection calculation. * @throws ShellException */ void evaluateRedirectionsAndPushHolders(RedirectionNode[] redirects, CommandIO[] streams) throws ShellException { if (savedHolders == null) { savedHolders = new ArrayList<CommandIOHolder[]>(1); } savedHolders.add(holders); holders = new CommandIOHolder[streams.length]; for (int i = 0; i < holders.length; i++) { // Don't take ownership of the streams. holders[i] = new CommandIOHolder(streams[i], false); } evaluateRedirections(redirects, holders); } /** * Close the context's current IO, restoring the previous ones. * @throws ShellException */ void popHolders() { closeIOs(); holders = savedHolders.remove(savedHolders.size() - 1); } /** * Evaluate the redirections for this command. * * @param redirects the redirection nodes to be evaluated * @param holders the initial stream state which we will mutate * @return the stream state after redirections * @throws ShellException */ void evaluateRedirections(RedirectionNode[] redirects, CommandIOHolder[] holders) throws ShellException { if (redirects == null) { return; } boolean ok = false; try { for (int i = 0; i < redirects.length; i++) { RedirectionNode redir = redirects[i]; // Work out which fd to redirect ... int fd; BjorneToken io = redir.getIo(); if (io == null) { switch (redir.getRedirectionType()) { case REDIR_DLESS: case REDIR_DLESSDASH: case REDIR_LESS: case REDIR_LESSAND: case REDIR_LESSGREAT: fd = 0; break; default: fd = 1; break; } } else { try { fd = Integer.parseInt(io.getText()); } catch (NumberFormatException ex) { throw new ShellFailureException("Invalid &fd number"); } } // If necessary, grow the fd table. if (fd >= holders.length) { CommandIOHolder[] tmp = new CommandIOHolder[fd + 1]; System.arraycopy(holders, 0, tmp, 0, fd + 1); holders = tmp; } CommandIOHolder stream; CommandInput in; CommandOutput out; switch (redir.getRedirectionType()) { case REDIR_DLESS: case REDIR_DLESSDASH: String here = redir.getHereDocument(); if (redir.isHereDocumentExpandable()) { here = dollarBacktickExpand(here).toString(); } in = new CommandInput(new StringReader(here)); stream = new CommandIOHolder(in, true); break; case REDIR_GREAT: try { File file = new File(redir.getArg().getText()); if (isNoClobber() && file.exists()) { throw new ShellException("File already exists"); } out = new CommandOutput(new FileOutputStream(file)); stream = new CommandIOHolder(out, true); } catch (IOException ex) { throw new ShellException("Cannot open output file", ex); } break; case REDIR_CLOBBER: case REDIR_DGREAT: try { FileOutputStream tmp = new FileOutputStream(redir.getArg().getText(), redir.getRedirectionType() == REDIR_DGREAT); stream = new CommandIOHolder(new CommandOutput(tmp), true); } catch (IOException ex) { throw new ShellException("Cannot open output file", ex); } break; case REDIR_LESS: try { File file = new File(redir.getArg().getText()); in = new CommandInput(new FileInputStream(file)); stream = new CommandIOHolder(in, true); } catch (IOException ex) { throw new ShellException("Cannot open input file", ex); } break; case REDIR_LESSAND: try { int fromFd = Integer.parseInt(redir.getArg().getText()); stream = (fromFd >= holders.length) ? null : new CommandIOHolder(holders[fromFd]); } catch (NumberFormatException ex) { throw new ShellException("Invalid fd after <&"); } break; case REDIR_GREATAND: try { int fromFd = Integer.parseInt(redir.getArg().getText()); stream = (fromFd >= holders.length) ? null : new CommandIOHolder(holders[fromFd]); } catch (NumberFormatException ex) { throw new ShellException("Invalid fd after >&"); } break; case REDIR_LESSGREAT: throw new UnsupportedOperationException("<>"); default: throw new ShellFailureException("unknown redirection type"); } holders[fd] = stream; } ok = true; } finally { if (!ok) { for (CommandIOHolder holder : holders) { holder.close(); } } } } public boolean patternMatch(CharSequence text, CharSequence pat) { int flags = PathnamePattern.EAGER | PathnamePattern.DEFAULT_FLAGS; Pattern regex = PathnamePattern.compilePosixShellPattern(pat, flags); return regex.matcher(text).matches(); } public String[] getArgs() { return args.toArray(new String[args.size()]); } public int nosArgs() { return args.size(); } public CommandShell getShell() { return interpreter.getShell(); } public String getName() { return interpreter.getUniqueName(); } public BjorneToken[] substituteAliases(BjorneToken[] words) throws ShellSyntaxException { String alias = aliases.get(words[0].getText()); if (alias == null) { return words; } List<BjorneToken> list = new LinkedList<BjorneToken>(Arrays.asList(words)); substituteAliases(list, 0, 0); return list.toArray(new BjorneToken[list.size()]); } private void substituteAliases(List<BjorneToken> list, int pos, int depth) throws ShellSyntaxException { if (depth > 10) { throw new ShellFailureException("probable cycle detected in alias expansion"); } String aliasName = list.get(pos).getText(); String alias = aliases.get(aliasName); if (alias == null) { return; } BjorneTokenizer tokens = new BjorneTokenizer(alias); list.remove(pos); int i = 0; while (tokens.hasNext()) { list.add(pos + i, tokens.next()); if (i == 0 && !aliasName.equals(list.get(pos + i).getText())) { substituteAliases(list, pos + i, depth + 1); } i++; } if (alias.endsWith(" ") && pos + i < list.size()) { substituteAliases(list, pos + i, depth + 1); } } BjorneInterpreter getInterpreter() { return interpreter; } public Collection<String> getVariableNames() { return variables.keySet(); } void defineFunction(BjorneToken name, CommandNode body) { functions.put(name.getText(), body); } CommandNode getFunction(String name) { return functions.get(name); } }