org.jruby.util.cli.ArgumentProcessor Maven / Gradle / Ivy
/***** BEGIN LICENSE BLOCK *****
* Version: EPL 1.0/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Eclipse Public
* License Version 1.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.eclipse.org/legal/epl-v10.html
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* Copyright (C) 2007-2011 Nick Sieger
* Copyright (C) 2009 Joseph LaFata
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the EPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the EPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****/
package org.jruby.util.cli;
import org.jruby.CompatVersion;
import org.jruby.Ruby;
import org.jruby.RubyInstanceConfig;
import org.jruby.exceptions.MainExitException;
import org.jruby.runtime.profile.builtin.ProfileOutput;
import org.jruby.util.JRubyFile;
import org.jruby.util.FileResource;
import org.jruby.util.KCode;
import org.jruby.util.SafePropertyAccessor;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Encapsulated logic for processing JRuby's command-line arguments.
*
* This class holds the processing logic for JRuby's non-JVM command-line arguments.
* All standard Ruby options are processed here, as well as nonstandard JRuby-
* specific options.
*
* Options passed directly to the JVM are processed separately, by either a launch
* script or by a native executable.
*/
public class ArgumentProcessor {
private static final class Argument {
public final String originalValue;
public final String dashedValue;
public Argument(String value, boolean dashed) {
this.originalValue = value;
this.dashedValue = dashed && !value.startsWith("-") ? "-" + value : value;
}
public String toString() {
return dashedValue;
}
}
private List arguments;
private int argumentIndex = 0;
private boolean processArgv;
RubyInstanceConfig config;
private boolean endOfArguments = false;
private int characterIndex = 0;
public ArgumentProcessor(String[] arguments, RubyInstanceConfig config) {
this(arguments, true, false, config);
}
public ArgumentProcessor(String[] arguments, boolean processArgv, boolean dashed, RubyInstanceConfig config) {
this.config = config;
this.arguments = new ArrayList();
if (arguments != null && arguments.length > 0) {
for (String argument : arguments) {
this.arguments.add(new Argument(argument, dashed));
}
}
this.processArgv = processArgv;
}
public void processArguments() {
processArguments(true);
}
public void processArguments(boolean inline) {
while (argumentIndex < arguments.size() && isInterpreterArgument(arguments.get(argumentIndex).originalValue)) {
processArgument();
argumentIndex++;
}
if (inline && !config.isInlineScript() && config.getScriptFileName() == null) {
if (argumentIndex < arguments.size()) {
config.setScriptFileName(arguments.get(argumentIndex).originalValue); //consume the file name
argumentIndex++;
}
}
if (processArgv) {
processArgv();
}
}
private void processArgv() {
ArrayList arglist = new ArrayList();
for (; argumentIndex < arguments.size(); argumentIndex++) {
String arg = arguments.get(argumentIndex).originalValue;
if (config.isArgvGlobalsOn() && arg.startsWith("-")) {
arg = arg.substring(1);
int split = arg.indexOf('=');
if (split > 0) {
final String key = arg.substring(0, split);
final String val = arg.substring(split + 1);
// argv globals getService their dashes replaced with underscores
String globalName = key.replace('-', '_');
config.getOptionGlobals().put(globalName, val);
} else {
config.getOptionGlobals().put(arg, null);
}
} else {
config.setArgvGlobalsOn(false);
arglist.add(arg);
}
}
// Remaining arguments are for the script itself
arglist.addAll(Arrays.asList(config.getArgv()));
config.setArgv(arglist.toArray(new String[arglist.size()]));
}
private boolean isInterpreterArgument(String argument) {
return argument.length() > 0 && (argument.charAt(0) == '-' || argument.charAt(0) == '+') && !endOfArguments;
}
private String getArgumentError(String additionalError) {
return "jruby: invalid argument\n" + additionalError + "\n";
}
private void processArgument() {
String argument = arguments.get(argumentIndex).dashedValue;
FOR:
for (characterIndex = 1; characterIndex < argument.length(); characterIndex++) {
switch (argument.charAt(characterIndex)) {
case '0':
{
String temp = grabOptionalValue();
if (null == temp) {
config.setRecordSeparator("\u0000");
} else if (temp.equals("0")) {
config.setRecordSeparator("\n\n");
} else if (temp.equals("777")) {
config.setRecordSeparator("\uffff"); // Specify something that can't separate
} else {
try {
int val = Integer.parseInt(temp, 8);
config.setRecordSeparator("" + (char) val);
} catch (Exception e) {
MainExitException mee = new MainExitException(1, getArgumentError(" -0 must be followed by either 0, 777, or a valid octal value"));
mee.setUsageError(true);
throw mee;
}
}
break FOR;
}
case 'a':
config.setSplit(true);
break;
case 'c':
config.setShouldCheckSyntax(true);
break;
case 'C':
try {
String saved = grabValue(getArgumentError(" -C must be followed by a directory expression"));
File base = new File(config.getCurrentDirectory());
File newDir = new File(saved);
if (saved.startsWith("uri:classloader:")) {
config.setCurrentDirectory(saved);
} else if (newDir.isAbsolute()) {
config.setCurrentDirectory(newDir.getCanonicalPath());
} else {
config.setCurrentDirectory(new File(base, newDir.getPath()).getCanonicalPath());
}
if (!(new File(config.getCurrentDirectory()).isDirectory()) && !config.getCurrentDirectory().startsWith("uri:classloader:")) {
MainExitException mee = new MainExitException(1, "jruby: Can't chdir to " + saved + " (fatal)");
throw mee;
}
} catch (IOException e) {
MainExitException mee = new MainExitException(1, getArgumentError(" -C must be followed by a valid directory"));
throw mee;
}
break FOR;
case 'd':
config.setDebug(true);
config.setVerbosity(RubyInstanceConfig.Verbosity.TRUE);
break;
case 'e':
config.getInlineScript().append(grabValue(getArgumentError(" -e must be followed by an expression to report")));
config.getInlineScript().append('\n');
config.setHasInlineScript(true);
break FOR;
case 'E':
processEncodingOption(grabValue(getArgumentError("unknown encoding name")));
break FOR;
case 'F':
config.setInputFieldSeparator(grabValue(getArgumentError(" -F must be followed by a pattern for input field separation")));
break FOR;
case 'h':
config.setShouldPrintUsage(true);
config.setShouldRunInterpreter(false);
break;
case 'i':
config.setInPlaceBackupExtension(grabOptionalValue());
if (config.getInPlaceBackupExtension() == null) {
config.setInPlaceBackupExtension("");
}
break FOR;
case 'I':
String s = grabValue(getArgumentError("-I must be followed by a directory name to add to lib path"));
String[] ls = s.split(java.io.File.pathSeparator);
config.getLoadPaths().addAll(Arrays.asList(ls));
break FOR;
case 'J':
String js = grabOptionalValue();
config.getError().println("warning: " + argument + " argument ignored (launched in same VM?)");
if (js.equals("-cp") || js.equals("-classpath")) {
for(;grabOptionalValue() != null;) {}
grabValue(getArgumentError(" -J-cp must be followed by a path expression"));
}
break FOR;
case 'K':
// FIXME: No argument seems to work for -K in MRI plus this should not
// siphon off additional args 'jruby -K ~/scripts/foo'. Also better error
// processing.
String eArg = grabValue(getArgumentError("provide a value for -K"));
config.setKCode(KCode.create(null, eArg));
// source encoding
config.setSourceEncoding(config.getKCode().getEncoding().toString());
// set external encoding if not already specified
if (config.getExternalEncoding() == null) {
config.setExternalEncoding(config.getKCode().getEncoding().toString());
}
break;
case 'l':
config.setProcessLineEnds(true);
break;
case 'n':
config.setAssumeLoop(true);
config.setKernelGsubDefined(true);
break;
case 'p':
config.setAssumePrinting(true);
config.setAssumeLoop(true);
config.setKernelGsubDefined(true);
break;
case 'r':
config.getRequiredLibraries().add(grabValue(getArgumentError("-r must be followed by a package to require")));
break FOR;
case 's':
config.setArgvGlobalsOn(true);
break;
case 'G':
config.setLoadGemfile(true);
break;
case 'S':
runBinScript();
break FOR;
case 'T':
{
grabOptionalValue();
break FOR;
}
case 'U':
config.setInternalEncoding("UTF-8");
break;
case 'v':
config.setVerbosity(RubyInstanceConfig.Verbosity.TRUE);
config.setShowVersion(true);
break;
case 'w':
config.setVerbosity(RubyInstanceConfig.Verbosity.TRUE);
break;
case 'W':
{
String temp = grabOptionalValue();
if (temp == null) {
config.setVerbosity(RubyInstanceConfig.Verbosity.TRUE);
} else {
if (temp.equals("0")) {
config.setVerbosity(RubyInstanceConfig.Verbosity.NIL);
} else if (temp.equals("1")) {
config.setVerbosity(RubyInstanceConfig.Verbosity.FALSE);
} else if (temp.equals("2")) {
config.setVerbosity(RubyInstanceConfig.Verbosity.TRUE);
} else {
MainExitException mee = new MainExitException(1, getArgumentError(" -W must be followed by either 0, 1, 2 or nothing"));
mee.setUsageError(true);
throw mee;
}
}
break FOR;
}
case 'x':
try {
String saved = grabOptionalValue();
if (saved != null) {
File base = new File(config.getCurrentDirectory());
File newDir = new File(saved);
if (saved.startsWith("uri:classloader:")) {
config.setCurrentDirectory(saved);
} else if (newDir.isAbsolute()) {
config.setCurrentDirectory(newDir.getCanonicalPath());
} else {
config.setCurrentDirectory(new File(base, newDir.getPath()).getCanonicalPath());
}
if (!(new File(config.getCurrentDirectory()).isDirectory()) && !config.getCurrentDirectory().startsWith("uri:classloader:")) {
MainExitException mee = new MainExitException(1, "jruby: Can't chdir to " + saved + " (fatal)");
throw mee;
}
}
config.setXFlag(true);
} catch (IOException e) {
MainExitException mee = new MainExitException(1, getArgumentError(" -x must be followed by a valid directory"));
throw mee;
}
break FOR;
case 'X':
String extendedOption = grabOptionalValue();
if (extendedOption == null) {
if (SafePropertyAccessor.getBoolean("jruby.launcher.nopreamble", false)) {
throw new MainExitException(0, OutputStrings.getExtendedHelp());
} else {
throw new MainExitException(0, "jruby: missing argument\n" + OutputStrings.getExtendedHelp());
}
} else if (extendedOption.equals("-O")) {
config.setObjectSpaceEnabled(false);
} else if (extendedOption.equals("+O")) {
config.setObjectSpaceEnabled(true);
} else if (extendedOption.equals("-C")) {
config.setCompileMode(RubyInstanceConfig.CompileMode.OFF);
} else if (extendedOption.equals("-CIR")) {
config.setCompileMode(RubyInstanceConfig.CompileMode.OFFIR);
} else if (extendedOption.equals("+C")) {
config.setCompileMode(RubyInstanceConfig.CompileMode.FORCE);
} else if (extendedOption.equals("+CIR")) {
config.setCompileMode(RubyInstanceConfig.CompileMode.FORCEIR);
} else {
MainExitException mee = new MainExitException(1, "jruby: invalid extended option " + extendedOption + " (-X will list valid options)\n");
mee.setUsageError(true);
throw mee;
}
break FOR;
case 'y':
config.setParserDebug(true);
break FOR;
case '-':
if (argument.equals("--command") || argument.equals("--bin")) {
characterIndex = argument.length();
runBinScript();
break;
} else if (argument.equals("--compat")) {
characterIndex = argument.length();
config.setCompatVersion(CompatVersion.getVersionFromString(grabValue(getArgumentError("--compat must be RUBY1_8 or RUBY1_9"))));
break FOR;
} else if (argument.equals("--copyright")) {
config.setShowCopyright(true);
config.setShouldRunInterpreter(false);
break FOR;
} else if (argument.equals("--debug")) {
RubyInstanceConfig.FULL_TRACE_ENABLED = true;
config.setCompileMode(RubyInstanceConfig.CompileMode.OFF);
break FOR;
} else if (argument.equals("--jdb")) {
config.setDebug(true);
config.setVerbosity(RubyInstanceConfig.Verbosity.TRUE);
break;
} else if (argument.equals("--help")) {
config.setShouldPrintUsage(true);
config.setShouldRunInterpreter(false);
break;
} else if (argument.equals("--properties")) {
config.setShouldPrintProperties(true);
config.setShouldRunInterpreter(false);
break;
} else if (argument.equals("--version")) {
config.setShowVersion(true);
config.setShouldRunInterpreter(false);
break FOR;
} else if (argument.equals("--bytecode")) {
config.setShowBytecode(true);
break FOR;
} else if (argument.equals("--fast")) {
config.setCompileMode(RubyInstanceConfig.CompileMode.FORCE);
break FOR;
} else if (argument.startsWith("--profile")) {
characterIndex = argument.length();
int dotIndex = argument.indexOf('.');
if (dotIndex == -1) {
config.setProfilingMode(RubyInstanceConfig.ProfilingMode.FLAT);
} else {
String profilingMode = argument.substring(dotIndex + 1, argument.length());
if (profilingMode.equals("out")) {
// output file for profiling results
String outputFile = grabValue(getArgumentError("--profile.out requires an output file argument"));
try {
config.setProfileOutput(new ProfileOutput(new File(outputFile)));
} catch (FileNotFoundException e) {
throw new MainExitException(1, String.format("jruby: %s", e.getMessage()));
}
} else if (profilingMode.equals("service")) {
// service class name
String service = grabValue(getArgumentError("--profile.service requires an class name argument"));
config.setProfilingMode( RubyInstanceConfig.ProfilingMode.SERVICE);
config.setProfilingService(service);
} else {
try {
config.setProfilingMode(RubyInstanceConfig.ProfilingMode.valueOf(profilingMode.toUpperCase()));
} catch (IllegalArgumentException e) {
throw new MainExitException(1, String.format("jruby: unknown profiler mode \"%s\"", profilingMode));
}
}
}
break FOR;
} else if (argument.equals("--1.9")) {
config.setCompatVersion(CompatVersion.RUBY1_9);
break FOR;
} else if (argument.equals("--2.0")) {
config.setCompatVersion(CompatVersion.RUBY2_0);
break FOR;
} else if (argument.equals("--1.8")) {
config.setCompatVersion(CompatVersion.RUBY1_8);
break FOR;
} else if (argument.equals("--disable-gems")) {
config.setDisableGems(true);
break FOR;
} else if (argument.equals("--gemfile")) {
config.setLoadGemfile(true);
break FOR;
} else if (argument.equals("--dump")) {
characterIndex = argument.length();
String error = "--dump only supports [version, copyright, usage, yydebug, syntax, insns] on JRuby";
String dumpArg = grabValue(getArgumentError(error));
if (dumpArg.equals("version")) {
config.setShowVersion(true);
config.setShouldRunInterpreter(false);
break FOR;
} else if (dumpArg.equals("copyright")) {
config.setShowCopyright(true);
config.setShouldRunInterpreter(false);
break FOR;
} else if (dumpArg.equals("usage")) {
config.setShouldPrintUsage(true);
config.setShouldRunInterpreter(false);
break FOR;
} else if (dumpArg.equals("yydebug")) {
config.setParserDebug(true);
break FOR;
} else if (dumpArg.equals("syntax")) {
config.setShouldCheckSyntax(true);
} else if (dumpArg.equals("insns")) {
config.setShowBytecode(true);
} else {
MainExitException mee = new MainExitException(1, error);
mee.setUsageError(true);
throw mee;
}
break;
} else if (argument.equals("--dev")) {
Options.COMPILE_INVOKEDYNAMIC.force("false");
config.setCompileMode(RubyInstanceConfig.CompileMode.OFF);
break FOR;
} else {
if (argument.equals("--")) {
// ruby interpreter compatibilty
// Usage: ruby [switches] [--] [programfile] [arguments])
endOfArguments = true;
break;
}
}
default:
throw new MainExitException(1, "jruby: unknown option " + argument);
}
}
}
private void processEncodingOption(String value) {
String[] encodings = value.split(":", 3);
switch (encodings.length) {
case 3:
throw new MainExitException(1, "extra argument for -E: " + encodings[2]);
case 2:
config.setInternalEncoding(encodings[1]);
case 1:
config.setExternalEncoding(encodings[0]);
// Zero is impossible
}
}
private void runBinScript() {
String scriptName = grabValue("jruby: provide a bin script to execute");
if (scriptName.equals("irb")) {
scriptName = "jirb";
}
config.setScriptFileName(resolveScript(scriptName));
// run as a command if we couldn't find a script
if (config.getScriptFileName() == null) {
config.setScriptFileName(scriptName);
config.getRequiredLibraries().add("jruby/commands");
config.getInlineScript().append("JRuby::Commands.").append(scriptName);
config.getInlineScript().append("\n");
config.setHasInlineScript(true);
}
endOfArguments = true;
}
private String resolve(String path, String scriptName) {
if (RubyInstanceConfig.DEBUG_SCRIPT_RESOLUTION) {
config.getError().println("Trying path: " + path);
}
try {
FileResource fullName = JRubyFile.createRestrictedResource(path, scriptName);
if (fullName.exists() && fullName.isFile()) {
if (RubyInstanceConfig.DEBUG_SCRIPT_RESOLUTION) {
config.getError().println("Found: " + fullName.absolutePath());
}
return fullName.absolutePath();
}
} catch (Exception e) {
// keep going
}
return null;
}
private String resolveScript(String scriptName) {
// These try/catches are to allow failing over to the "commands" logic
// when running from within a jruby-complete jar file, which has
// jruby.home = a jar file URL that does not resolve correctly with
// JRubyFile.create.
String result = resolve(config.getCurrentDirectory(), scriptName);
if (result != null) return scriptName;// use relative filename
result = resolve(config.getJRubyHome() + "/bin", scriptName);
if (result != null) return result;
// since the current directory is also on the classpath we
// want to find it on filesystem first
result = resolve(config.getCurrentDirectory() + "/bin", scriptName);
if (result != null) return result;
result = resolve("uri:classloader:/bin", scriptName);
if (result != null) return result;
Object maybePath = config.getEnvironment().get("PATH");
if (maybePath != null) {
String path = maybePath.toString();
String[] paths = path.split(System.getProperty("path.separator"));
for (int i = 0; i < paths.length; i++) {
result = resolve(new File(paths[i]).getAbsolutePath(), scriptName);
if (result != null) return result;
}
}
if (config.isDebug()) {
config.getError().println("warning: could not resolve -S script: " + scriptName);
}
// fall back to JRuby::Commands
return null;
}
private String grabValue(String errorMessage) {
String optValue = grabOptionalValue();
if (optValue != null) {
return optValue;
}
argumentIndex++;
if (argumentIndex < arguments.size()) {
return arguments.get(argumentIndex).originalValue;
}
MainExitException mee = new MainExitException(1, errorMessage);
mee.setUsageError(true);
throw mee;
}
private String grabOptionalValue() {
characterIndex++;
String argValue = arguments.get(argumentIndex).originalValue;
if (characterIndex < argValue.length()) {
return argValue.substring(characterIndex);
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy