Path: blob/master/debugtools/DDR_VM/src/com/ibm/j9ddr/command/CommandParser.java
6005 views
/*******************************************************************************1* Copyright (c) 2011, 2019 IBM Corp. and others2*3* This program and the accompanying materials are made available under4* the terms of the Eclipse Public License 2.0 which accompanies this5* distribution and is available at https://www.eclipse.org/legal/epl-2.0/6* or the Apache License, Version 2.0 which accompanies this distribution and7* is available at https://www.apache.org/licenses/LICENSE-2.0.8*9* This Source Code may also be made available under the following10* Secondary Licenses when the conditions for such availability set11* forth in the Eclipse Public License, v. 2.0 are satisfied: GNU12* General Public License, version 2 with the GNU Classpath13* Exception [1] and GNU General Public License, version 2 with the14* OpenJDK Assembly Exception [2].15*16* [1] https://www.gnu.org/software/classpath/license.html17* [2] http://openjdk.java.net/legal/assembly-exception.html18*19* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 OR LicenseRef-GPL-2.0 WITH Assembly-exception20*******************************************************************************/2122package com.ibm.j9ddr.command;2324import java.io.File;25import java.io.FileOutputStream;26import java.io.IOException;27import java.io.PrintStream;28import java.text.ParseException;29import java.util.ArrayList;30import java.util.Arrays;31import java.util.List;323334/**35* Handles command line entries and does extra interpretation, such as handling quoted strings (whilst discarding36* quotes themselves and handling escaped characters), recognising redirection to file etc.37*38* NOTE: As of Nov 2011, another copy of this class lives in com.ibm.java.diagnostics. Any changes to this file must be copied across.39* @author blazejc40*41*/42public class CommandParser {4344private String originalLine; // used in toString(), primarily for debugging4546private List<String> allTokens = new ArrayList<String>();47private String command;48private List<String> arguments = new ArrayList<String>(); // just the command arguments, ignoring file redirection49private String redirectionFilename;5051private boolean isAppendToFile;52private boolean isRedirectedToFile;5354public CommandParser(String commandLine) throws ParseException {55originalLine = commandLine;56tokenise(commandLine);57parse(allTokens);58}5960public CommandParser(String command, String[] arguments) throws ParseException {61List<String> allTokens = new ArrayList<String>();62allTokens.add(command);63allTokens.addAll(Arrays.asList(arguments));6465originalLine = command;6667for (String arg : arguments) {68originalLine += " " + arg;69}7071parse(allTokens);72}7374@Override75public String toString() {76return originalLine;77}7879public String getOriginalLine() {80return originalLine;81}8283public String getCommand() {84return command;85}8687public boolean isRedirectedToFile() {88return isRedirectedToFile || isAppendToFile;89}9091/**92* Just the command arguments, strips the ">" or ">>" and all strings that follow (i.e. file redirection).93* @return94*/95public String[] getArguments() {96return arguments.toArray(new String[0]);97}9899/**100* Returns a PrintStream, which either writes to a new file or overwrites or appends to an existing one,101* depending on the command.102* @return103* @throws IOException104* @throws IllegalStateException - thrown if the command is not redirected to file. Use isRedirectedToFile() to check.105*/106public PrintStream getOutputFile() throws IOException {107if (!isRedirectedToFile && !isAppendToFile) {108throw new IllegalStateException("Attempt to create output file for a command which does not contain file redirection.");109}110111PrintStream outStream = null;112File outFile = new File(redirectionFilename);113114if (redirectionFilename.indexOf(File.separator) > -1) {115// The last entry is the filename itself (we've checked that during parsing stage)116// so everything before it is the directory, some of which may not exist, so need to create it117String parentDirectories = redirectionFilename.substring(0, redirectionFilename.lastIndexOf(File.separator));118File dir = new File(parentDirectories);119if (!dir.exists() && !dir.mkdirs()) {120throw new IOException("Could not create some of the requested directories: " + parentDirectories);121}122}123124if (isAppendToFile && outFile.exists()) {125// in this case do not create a new file, but use the existing one126outStream = new PrintStream(new FileOutputStream(outFile, true));127} else {128outStream = new PrintStream(outFile); // this will create a new file129}130131return outStream;132}133134/*135* The tokenise() and parse() work together to interpret the command line.136* Initially, tokenise() reads the raw text and applies syntax rules to break it up into tokens137* (e.g. by handling quoted strings). Then, parse() reads the tokens and extracts the command138* itself, its arguments and possible file redirection options.139*/140141private void tokenise(String commandLine) throws ParseException {142char[] characters = commandLine.toCharArray();143144int i = 0;145146TokenisingState currentState = new GenericTokenState(this);147do {148currentState = currentState.process(characters[i++]);149150if (i == characters.length) {151currentState.endOfInput();152}153} while (i < characters.length);154}155156private void parse(List<String> allTokens) throws ParseException {157if (allTokens.size() == 0) {158return;159}160161command = allTokens.get(0);162int i;163for (i = 1; i < allTokens.size(); i++) {164if (!allTokens.get(i).equals(">") && !allTokens.get(i).equals(">>")) {165arguments.add(allTokens.get(i));166} else {167break;168}169}170171// all that follows after and including '>' or '>>' is file redirection172if (i < allTokens.size()) {173if (allTokens.get(i).equals(">")) {174isAppendToFile = false;175isRedirectedToFile = true;176} else { // isn't '>' so must be '>>', otherwise it would be another argument captured in the previous loop177isAppendToFile = true;178isRedirectedToFile = false;179}180181if (i + 1 < allTokens.size()) {182String filename = allTokens.get(i + 1).trim();183if (filename.charAt(filename.length() - 1) == File.separatorChar) {184throw new ParseException("Invalid redirection path - missing filename", i);185}186187redirectionFilename = allTokens.get(i + 1);188} else {189throw new ParseException("Missing file name for redirection", i);190}191}192}193194// TokenisingState classes195196/**197* A mini state machine. Each state receives a character and returns the next state.198* So, for example, if a GenericTokenState encounters a quotation mark (either single or double),199* it returns a QuotedStringState, which has different rules. If a QuotedStringState encounters200* an (unescaped) quotation mark (i.e. end of quotation) it exits and returns a GenericTokenState.201*/202abstract class TokenisingState {203204protected CommandParser context;205protected StringBuilder currentToken = new StringBuilder();206abstract protected TokenisingState process(Character chr) throws ParseException;207208protected TokenisingState(CommandParser context) {209this.context = context;210}211212protected void storeToken() {213if (currentToken.length() > 0) {214context.allTokens.add(currentToken.toString());215currentToken = new StringBuilder();216}217}218219abstract protected void endOfInput() throws ParseException;220}221222/**223* Any token written to the command line as-is, without any quotation or escaping224* @author blazejc225*226*/227class GenericTokenState extends TokenisingState {228229protected GenericTokenState(CommandParser context) {230super(context);231}232233@Override234protected TokenisingState process(Character chr) throws ParseException {235if (Character.isWhitespace(chr)) {236if (currentToken.toString().equals(">") || currentToken.toString().equals(">>")) {237storeToken();238return new FilenameState(context);239} else {240storeToken();241return this;242}243}244245switch (chr) {246case '"':247case '\'':248if (currentToken.length() > 0) {249throw new ParseException("'" + chr + "'" + " invalid inside a string", 0);250} else {251return new QuotedStringState(context, chr);252}253default:254currentToken.append(chr);255return this;256}257}258259@Override260protected void endOfInput() {261storeToken();262}263}264265class QuotedStringState extends TokenisingState {266267private Character quoteType;268private boolean escape = false;269270protected QuotedStringState(CommandParser context, Character quoteType) {271super(context);272this.quoteType = quoteType;273}274275@Override276protected TokenisingState process(Character chr) throws ParseException {277switch (chr) {278case '\\':279if (escape) { // are we're escaping a '\'?280currentToken.append(chr);281escape = false;282} else {283// don't store the escape character itself284escape = true;285}286return this;287case '"':288case '\'': {289if (chr != quoteType) { // a quote different than the surrounding one doesn't need to be escaped, just treat it as any other character290currentToken.append(chr);291return this;292} else { // same quote as the surrounding one293if (escape) {294currentToken.append(chr);295escape = false;296return this;297} else { // same quote as the surrounding one, so we're closing the quote here298storeToken();299return new GenericTokenState(context);300}301}302}303default:304if (escape) {305// the previous character was a backslash (hence escaping flag is set), but what follows is just a normal character,306// so put back the backslash, we're not escaping anything307currentToken.append("\\");308escape = false;309}310currentToken.append(chr); // append any other character, leave QuotedStringState as current state311return this;312}313}314315@Override316protected void endOfInput() throws ParseException {317if (currentToken.length() > 0) { // we haven't closed the quote, but the input has ended - that's an error318throw new ParseException("Unmatched " + quoteType, 0);319}320}321}322323class FilenameState extends TokenisingState {324325protected FilenameState(CommandParser context) {326super(context);327}328329private Character quoteType = null; // the quote the whole filename is surrounded with, null if it isn't330private boolean alreadyStarted = false;331private boolean alreadyFinished = false;332333@Override334protected TokenisingState process(Character chr) throws ParseException {335if (alreadyFinished) {336throw new ParseException("Only one file name is permitted", 0);337}338339if (!alreadyStarted) {340if (chr.equals('"') || chr.equals('\'')) {341quoteType = chr;342} else {343currentToken.append(chr);344}345alreadyStarted = true;346} else {347if (chr.equals(quoteType)) {348storeToken();349alreadyFinished = true;350} else if (Character.isWhitespace(chr)) {351if (quoteType != null) {352currentToken.append(chr); // whitespace within a quoted string is just another character, so store it353} else {354storeToken(); // whitespace in an unquoted string is the end of it355alreadyFinished = true;356}357} else {358currentToken.append(chr);359}360}361362return this;363}364365@Override366protected void endOfInput() throws ParseException {367if (!alreadyStarted) {368throw new ParseException("Missing file name", 0);369}370if (alreadyStarted && !alreadyFinished && quoteType != null) {371throw new ParseException("Unmatched " + quoteType + " in file name", 0);372}373//started; not finished but no quotes, or finished with quotes374storeToken();375}376}377}378379380