Package org.jnode.shell.bjorne

Source Code of org.jnode.shell.bjorne.BjorneContext

/*
* $Id$
*
* Copyright (C) 2003-2014 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);
    }

}
TOP

Related Classes of org.jnode.shell.bjorne.BjorneContext

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.