/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.command; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.util.FileSet; import java.io.File; import java.util.List; import java.util.Vector; /** * Compiled shell commands. * <p> * A command is composed of three elements: * <ul> * <li>An {@link #getAlias() alias}, used to identify the command through the application.</li> * <li>A {@link #getCommand() command}, which is what will be executed by the instance of <code>Command</code>.</li> * <li> * A {@link #getType() type}, which can be any of {@link #SYSTEM_COMMAND system} (invisible and inmutable), * {@link #INVISIBLE_COMMAND invisible} (invisible and mutable) or {@link #NORMAL_COMMAND} (visible and mutable). * </li> * </ul> * </p> * <p> * The basic command syntax is fairly simple: * <ul> * <li>Any non-escaped <code>\</code> character will escape the following character and be removed from the tokens.</li> * <li>Any non-escaped <code>"</code> character will escape all characters until the next occurence of <code>"</code>, except for <code>\</code>.</li> * <li>Non-escaped space characters are used as token separators.</li> * </ul> * It is important to remember that <code>"</code> characters are <b>not</b> removed from the resulting tokens. * </p> * <p> * It's also possible to include keywords in a command: * <ul> * <li><code>$f</code> is replaced by a file's full path.</li> * <li><code>$n</code> is replaced by a file's name.</li> * <li><code>$e</code> is replaced by a file's extension.</li> * <li><code>$N</code> is replaced by a file's name without its extension.</li> * <li><code>$p</code> is replaced by a file's parent's path.</li> * <li><code>$j</code> is replaced by the path of the folder in which the JVM was started.</li> * </ul> * </p> * <p> * Once a <code>Command</code> instance has been retrieved, execution tokens can be retrieved through the * {@link #getTokens(AbstractFile)} method. This will return a tokenized version of the command and replace any * keyword by the corresponding file value . It's also possible to skip keyword replacement through the {@link #getTokens()} method. * </p> * <p> * A command's executable tokens are typically meant to be used with {@link com.mucommander.process.ProcessRunner#execute(String[],AbstractFile)} * in order to generate instances of {@link com.mucommander.process.AbstractProcess}. * </p> * @author Nicolas Rinaudo * @see CommandManager * @see com.mucommander.process.ProcessRunner * @see com.mucommander.process.AbstractProcess */ public class Command implements Comparable<Command> { // - Keywords ------------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** Header of replacement keywords. */ private static final char KEYWORD_HEADER = '$'; /** Instances of this keyword will be replaced by the file's full path. */ private static final char KEYWORD_PATH = 'f'; /** Instances of this keyword will be replaced by the file's name. */ private static final char KEYWORD_NAME = 'n'; /** Instances of this keyword will be replaced by the file's parent directory. */ private static final char KEYWORD_PARENT = 'p'; /** Instances of this keyword will be replaced by the JVM's current directory. */ private static final char KEYWORD_VM_PATH = 'j'; /** Instances of this keyword will be replaced by the file's extension. */ private static final char KEYWORD_EXTENSION = 'e'; /** Instances of this keyword will be replaced by the file's name without its extension. */ private static final char KEYWORD_NAME_WITHOUT_EXTENSION = 'b'; // - Instance variables -------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** Command's alias. */ private final String alias; /** Original command. */ private final String command; /** Name used to display the command to users. */ private final String displayName; /** Command type. */ private final CommandType type; // - Initialisation ------------------------------------------------------------------------------------------------ // ----------------------------------------------------------------------------------------------------------------- /** * Creates a new command. * @param alias alias of the command. * @param command command that will be executed. * @param type type of the command. * @param displayName name of the command as seen by users (if <code>null</code>, defaults to <code>alias</code>). */ public Command(String alias, String command, CommandType type, String displayName) { this.alias = alias; this.type = type; this.displayName = displayName; this.command = command; } /** * Creates a new command. * <p> * This is a convenience constructor and is strictly equivalent to calling * <code>{@link #Command(String,String,int,String) Command(}alias, command, {@link #NORMAL_COMMAND}, null)</code>. * </p> * @param alias alias of the command. * @param command command that will be executed. */ public Command(String alias, String command) { this(alias, command, CommandType.NORMAL_COMMAND, null); } /** * Creates a new command. * <p> * This is a convenience constructor and is strictly equivalent to calling * <code>{@link #Command(String,String,int,String) Command(}alias, command, type, null)</code>. * </p> * @param alias alias of the command. * @param command command that will be executed. * @param type type of the command. */ public Command(String alias, String command, CommandType type) { this(alias, command, type, null); } // - Token retrieval ----------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- /** * Returns this command's tokens without performing keyword substitution. * @return this command's tokens without performing keyword substitution. */ public synchronized String[] getTokens() { return getTokens(command, (AbstractFile[])null); } /** * Returns this command's tokens, replacing keywords by the corresponding values from the specified file. * @param file file from which to retrieve keyword substitution values. * @return this command's tokens, replacing keywords by the corresponding values from the specified file. */ public synchronized String[] getTokens(AbstractFile file) { return getTokens(command, file); } /** * Returns this command's tokens, replacing keywords by the corresponding values from the specified fileset. * @param files files from which to retrieve keyword substitution values. * @return this command's tokens, replacing keywords by the corresponding values from the specified fileset. */ public synchronized String[] getTokens(FileSet files) { return getTokens(command, files); } /** * Returns this command's tokens, replacing keywords by the corresponding values from the specified files. * @param files files from which to retrieve keyword substitution values. * @return this command's tokens, replacing keywords by the corresponding values from the specified files. */ public synchronized String[] getTokens(AbstractFile[] files) { return getTokens(command, files); } /** * Returns the specified command's tokens without performing keyword substitution. * @param command command to tokenize. * @return the specified command's tokens without performing keyword substitution. */ public static String[] getTokens(String command) { return getTokens(command, (AbstractFile[])null); } /** * Returns the specified command's tokens after replacing keywords by the corresponding values from the specified file. * @param command command to tokenize. * @param file file from which to retrieve keyword substitution values. * @return the specified command's tokens after replacing keywords by the corresponding values from the specified file. */ public static String[] getTokens(String command, AbstractFile file) { return getTokens(command, new AbstractFile[] {file}); } /** * Returns the specified command's tokens after replacing keywords by the corresponding values from the specified fileset. * @param command command to tokenize. * @param files file from which to retrieve keyword substitution values. * @return the specified command's tokens after replacing keywords by the corresponding values from the specified fileset. */ public static String[] getTokens(String command, FileSet files) { return getTokens(command, files.toArray(new AbstractFile[files.size()])); } /** * Returns the specified command's tokens after replacing keywords by the corresponding values from the specified files. * @param command command to tokenize. * @param files file from which to retrieve keyword substitution values. * @return the specified command's tokens after replacing keywords by the corresponding values from the specified files. */ public static String[] getTokens(String command, AbstractFile[] files) { List<String> tokens; // All tokens. char[] buffer; // All the characters that compose command. StringBuilder currentToken; // Buffer for the current token. boolean isInQuotes; // Whether we're currently within quotes or not. // Initialises parsing. tokens = new Vector<String>(); command = command.trim(); currentToken = new StringBuilder(command.length()); buffer = command.toCharArray(); isInQuotes = false; // Parses the command. for(int i = 0; i < command.length(); i++) { // Quote escaping: toggle isInQuotes. if(buffer[i] == '\"') { currentToken.append(buffer[i]); isInQuotes = !isInQuotes; } // Backslash escaping: the next character is not analyzed. else if(buffer[i] == '\\') { if(i + 1 != command.length()) currentToken.append(buffer[++i]); } // Whitespace: end of token if we're not between quotes. else if(buffer[i] == ' ' && !isInQuotes) { // Skips un-escaped blocks of spaces. while(i + 1 < command.length() && buffer[i + 1] == ' ') i++; // Stores the current token. tokens.add(currentToken.toString()); currentToken.setLength(0); } // Keyword: perform keyword substitution. else if(buffer[i] == KEYWORD_HEADER) { // Skips keyword replacement if we're not interested // in it. if(files == null) currentToken.append(KEYWORD_HEADER); // If this is the last character, append it. else if(++i == buffer.length) currentToken.append(KEYWORD_HEADER); // If we've found a legal keyword, perform keyword replacement else if(isLegalKeyword(buffer[i])) { // Deals with the first file. currentToken.append(getKeywordReplacement(buffer[i], files[0])); // $j is a special case, we only ever insert it once. if(buffer[i] != KEYWORD_VM_PATH) { // If we're not between quotes and there's more than one file, // each file will be in its own token. if(!isInQuotes && files.length != 1) { tokens.add(currentToken.toString()); currentToken.setLength(0); } // Deals with all subsequent files: // - if we're in quotes, separates each files by a space. // - if we're not in quotes, each new file is its own token. for(int j = 1; j < files.length; j++) { if(isInQuotes) { currentToken.append(' '); currentToken.append(getKeywordReplacement(buffer[i], files[j])); } // When not in quotes, the last file is the beginning of a new token // rather than a single one. else if(j != files.length - 1) tokens.add(getKeywordReplacement(buffer[i], files[j])); else currentToken.append(getKeywordReplacement(buffer[i], files[j])); } } } // If we've found an illegal keyword, ignore it. else { currentToken.append(KEYWORD_HEADER); currentToken.append(buffer[i]); } } // Nothing special about this character. else currentToken.append(buffer[i]); } // Adds a possible last token. if(currentToken.length() != 0) tokens.add(currentToken.toString()); // Empty commands are returned as an empty token rather than an empty array. if(tokens.size() == 0) return new String[] {""}; return tokens.toArray(new String[tokens.size()]); } /** * Returns whether this command contains keywords referencing selected file. * <p> * Returns true if command contains keywords referencing selected file, e.g. $f,$n,$p,$e,$b. * Returns false otherwise, e.g. $j, $xyz, etc. * </p> * @return whether this command contains keywords referencing selected file. */ public synchronized boolean hasSelectedFileKeyword() { String[] tokens = getTokens(); for (String token : tokens) { // Not using regexp because it depends on the definition of KEYWORD_* if (token.startsWith("" + KEYWORD_HEADER + KEYWORD_PATH) || token.startsWith("" + KEYWORD_HEADER + KEYWORD_NAME) || token.startsWith("" + KEYWORD_HEADER + KEYWORD_EXTENSION) || token.startsWith("" + KEYWORD_HEADER + KEYWORD_NAME_WITHOUT_EXTENSION) || token.startsWith("" + KEYWORD_HEADER + KEYWORD_PARENT)) { return true; } } // No token with file referencing keyword found return false; } /** * Returns <code>true</code> if the specified character is a legal keyword. * @param keyword character to check. * @return <code>true</code> if the specified character is a legal keyword, <code>false</code> otherwise. */ private static boolean isLegalKeyword(char keyword) { return keyword == KEYWORD_PATH || keyword == KEYWORD_NAME || keyword == KEYWORD_PARENT || keyword == KEYWORD_VM_PATH || keyword == KEYWORD_EXTENSION || keyword == KEYWORD_NAME_WITHOUT_EXTENSION; } /** * Gets the value from <code>file</code> that should be used to replace <code>keyword</code>. * @param keyword character to replace. * @param file file from which to retrieve the replacement value. * @return the requested replacement value. */ private static String getKeywordReplacement(char keyword, AbstractFile file) { switch(keyword) { case KEYWORD_PATH: return file.getAbsolutePath(); case KEYWORD_NAME: return file.getName(); case KEYWORD_PARENT: AbstractFile parentFile = file.getParent(); return parentFile==null?"":parentFile.getAbsolutePath(); case KEYWORD_VM_PATH: return new File(System.getProperty("user.dir")).getAbsolutePath(); case KEYWORD_EXTENSION: String extension; if((extension = file.getExtension()) == null) return ""; return extension; case KEYWORD_NAME_WITHOUT_EXTENSION: return file.getNameWithoutExtension(); } throw new IllegalArgumentException(); } // - Misc. --------------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------------------- public int hashCode() { int hashCode; hashCode = alias.hashCode(); hashCode = hashCode * 31 + command.hashCode(); hashCode = hashCode * 31 + getDisplayName().hashCode(); hashCode = hashCode * 31 + type.hashCode(); return hashCode; } public boolean equals(Object object) { if(object == null || !(object instanceof Command)) return false; Command cmd; cmd = (Command)object; return command.equals(cmd.command) && alias.equals(cmd.alias) && type == cmd.type && getDisplayName().equals(cmd.getDisplayName()); } public int compareTo(Command command) { int buffer; if((buffer = getDisplayName().compareTo(command.getDisplayName())) != 0) return buffer; if((buffer = getAlias().compareTo(command.getAlias())) != 0) return buffer; return this.command.compareTo(command.command); } /** * Returns the original, un-tokenised command. * @return the original, un-tokenised command. */ public synchronized String getCommand() { return command; } /** * Returns this command's alias. * @return this command's alias. */ public synchronized String getAlias() { return alias; } /** * Returns the command's type. * @return the command's type. */ public synchronized CommandType getType() { return type; } /** * Returns the command's display name. * <p> * If it hasn't been set, returns this command's alias. * </p> * @return the command's display name. */ public synchronized String getDisplayName() { return displayName != null ? displayName : alias; } /** * Returns <code>true</code> if the command's display name has been set. * @return <code>true</code> if the command's display name has been set, <code>false</code> otherwise. */ synchronized boolean isDisplayNameSet() { return displayName != null; } @Override public String toString() { return alias + (displayName == null ? "" : ":" + displayName) + ":" + command; } }