diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/Shell.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/Shell.java index 0b5d4e5d9fd..6c74601117a 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/Shell.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/Shell.java @@ -42,7 +42,7 @@ * A base class for running a Shell command. * * Shell can be used to run shell commands like du or - * df. It also offers facilities to gate commands by + * df. It also offers facilities to gate commands by * time-intervals. */ @InterfaceAudience.Public @@ -118,6 +118,21 @@ public static void checkWindowsCommandLineLength(String...commands) } } + /** + * Quote the given arg so that bash will interpret it as a single value. + * Note that this quotes it for one level of bash, if you are passing it + * into a badly written shell script, you need to fix your shell script. + * @param arg the argument to quote + * @return the quoted string + */ + static String bashQuote(String arg) { + StringBuilder buffer = new StringBuilder(arg.length() + 2); + buffer.append('\''); + buffer.append(arg.replace("'", "'\\''")); + buffer.append('\''); + return buffer.toString(); + } + /** a Unix command to get the current user's name: {@value}. */ public static final String USER_NAME_COMMAND = "whoami"; @@ -173,7 +188,7 @@ private static OSType getOSType() { /** a Unix command to get the current user's groups list. */ public static String[] getGroupsCommand() { return (WINDOWS)? new String[]{"cmd", "/c", "groups"} - : new String[]{"bash", "-c", "groups"}; + : new String[]{"groups"}; } /** @@ -184,10 +199,14 @@ public static String[] getGroupsCommand() { */ public static String[] getGroupsForUserCommand(final String user) { //'groups username' command return is inconsistent across different unixes - return WINDOWS ? - new String[] - {getWinUtilsPath(), "groups", "-F", "\"" + user + "\""} - : new String[] {"bash", "-c", "id -gn " + user + "; id -Gn " + user}; + if (WINDOWS) { + return new String[] + {getWinUtilsPath(), "groups", "-F", "\"" + user + "\""}; + } else { + String quotedUser = bashQuote(user); + return new String[] {"bash", "-c", "id -gn " + quotedUser + + "; id -Gn " + quotedUser}; + } } /** @@ -199,17 +218,20 @@ public static String[] getGroupsForUserCommand(final String user) { */ public static String[] getGroupsIDForUserCommand(final String user) { //'groups username' command return is inconsistent across different unixes - return WINDOWS ? - new String[] - {getWinUtilsPath(), "groups", "-F", "\"" + user + "\""} - : new String[] {"bash", "-c", "id -g " + user + "; id -G " + user}; + if (WINDOWS) { + return new String[]{getWinUtilsPath(), "groups", "-F", "\"" + user + + "\""}; + } else { + String quotedUser = bashQuote(user); + return new String[] {"bash", "-c", "id -g " + quotedUser + "; id -G " + + quotedUser}; + } } /** A command to get a given netgroup's user list. */ public static String[] getUsersForNetgroupCommand(final String netgroup) { //'groups username' command return is non-consistent across different unixes - return WINDOWS ? new String [] {"cmd", "/c", "getent netgroup " + netgroup} - : new String [] {"bash", "-c", "getent netgroup " + netgroup}; + return new String[] {"getent", "netgroup", netgroup}; } /** Return a command to get permission information. */ @@ -233,14 +255,15 @@ public static String[] getSetPermissionCommand(String perm, boolean recursive) { /** * Return a command to set permission for specific file. - * + * * @param perm String permission to set * @param recursive boolean true to apply to all sub-directories recursively * @param file String file to set * @return String[] containing command and arguments */ public static String[] getSetPermissionCommand(String perm, - boolean recursive, String file) { + boolean recursive, + String file) { String[] baseCmd = getSetPermissionCommand(perm, recursive); String[] cmdWithFile = Arrays.copyOf(baseCmd, baseCmd.length + 1); cmdWithFile[cmdWithFile.length - 1] = file; @@ -290,9 +313,9 @@ public static String[] getSignalKillCommand(int code, String pid) { if (isSetsidAvailable) { // Use the shell-builtin as it support "--" in all Hadoop supported OSes - return new String[] { "bash", "-c", "kill -" + code + " -- -" + pid }; + return new String[] {"kill", "-" + code, "--", "-" + pid}; } else { - return new String[] { "bash", "-c", "kill -" + code + " " + pid }; + return new String[] {"kill", "-" + code, pid }; } } @@ -310,7 +333,7 @@ public static String getEnvironmentVariableRegex() { * Returns a File referencing a script with the given basename, inside the * given parent directory. The file extension is inferred by platform: * ".cmd" on Windows, or ".sh" otherwise. - * + * * @param parent File parent directory * @param basename String script file basename * @return File referencing the script in the directory @@ -342,8 +365,8 @@ public static String appendScriptExtension(String basename) { public static String[] getRunScriptCommand(File script) { String absolutePath = script.getAbsolutePath(); return WINDOWS ? - new String[] { "cmd", "/c", absolutePath } - : new String[] { "/bin/bash", absolutePath }; + new String[] {"cmd", "/c", absolutePath } + : new String[] {"/bin/bash", bashQuote(absolutePath) }; } /** a Unix command to set permission: {@value}. */ @@ -527,11 +550,11 @@ private static File getHadoopHomeDir() throws FileNotFoundException { /** * Fully qualify the path to a binary that should be in a known hadoop - * bin location. This is primarily useful for disambiguating call-outs - * to executable sub-components of Hadoop to avoid clashes with other - * executables that may be in the path. Caveat: this call doesn't - * just format the path to the bin directory. It also checks for file - * existence of the composed path. The output of this call should be + * bin location. This is primarily useful for disambiguating call-outs + * to executable sub-components of Hadoop to avoid clashes with other + * executables that may be in the path. Caveat: this call doesn't + * just format the path to the bin directory. It also checks for file + * existence of the composed path. The output of this call should be * cached by callers. * * @param executable executable @@ -840,7 +863,7 @@ protected void run() throws IOException { } /** Run the command. */ - private void runCommand() throws IOException { + private void runCommand() throws IOException { ProcessBuilder builder = new ProcessBuilder(getExecString()); Timer timeOutTimer = null; ShellTimeoutTimerTask timeoutTimerTask = null; @@ -869,7 +892,7 @@ private void runCommand() throws IOException { } builder.redirectErrorStream(redirectErrorStream); - + if (Shell.WINDOWS) { synchronized (WindowsProcessLaunchLock) { // To workaround the race condition issue with child processes @@ -890,7 +913,7 @@ private void runCommand() throws IOException { //One time scheduling. timeOutTimer.schedule(timeoutTimerTask, timeOutInterval); } - final BufferedReader errReader = + final BufferedReader errReader = new BufferedReader(new InputStreamReader( process.getErrorStream(), Charset.defaultCharset())); BufferedReader inReader = @@ -928,7 +951,7 @@ public void run() { parseExecResult(inReader); // parse the output // clear the input stream buffer String line = inReader.readLine(); - while(line != null) { + while(line != null) { line = inReader.readLine(); } // wait for the process to finish and check the exit code @@ -1065,13 +1088,13 @@ public interface CommandExecutor { /** * A simple shell command executor. - * + * * ShellCommandExecutorshould be used in cases where the output * of the command needs no explicit parsing and where the command, working * directory and the environment remains unchanged. The output of the command * is stored as-is and is expected to be small. */ - public static class ShellCommandExecutor extends Shell + public static class ShellCommandExecutor extends Shell implements CommandExecutor { private String[] command; @@ -1098,7 +1121,7 @@ public ShellCommandExecutor(String[] execString, File dir, /** * Create a new instance of the ShellCommandExecutor to execute a command. - * + * * @param execString The command to execute with arguments * @param dir If not-null, specifies the directory which should be set * as the current working directory for the command. @@ -1112,7 +1135,7 @@ public ShellCommandExecutor(String[] execString, File dir, * @param inheritParentEnv Indicates if the process should inherit the env * vars from the parent process or not. */ - public ShellCommandExecutor(String[] execString, File dir, + public ShellCommandExecutor(String[] execString, File dir, Map env, long timeout, boolean inheritParentEnv) { command = execString.clone(); if (dir != null) { @@ -1137,7 +1160,7 @@ public void execute() throws IOException { + StringUtils.join(" ", command)); } } - this.run(); + this.run(); } @Override @@ -1190,7 +1213,7 @@ public void close() { /** * To check if the passed script to shell command executor timed out or * not. - * + * * @return if the script timed out. */ public boolean isTimedOut() { @@ -1199,15 +1222,15 @@ public boolean isTimedOut() { /** * Declare that the command has timed out. - * + * */ private void setTimedOut() { this.timedOut.set(true); } - /** - * Static method to execute a shell command. - * Covers most of the simple cases without requiring the user to implement + /** + * Static method to execute a shell command. + * Covers most of the simple cases without requiring the user to implement * the Shell interface. * @param cmd shell command to execute. * @return the output of the executed command. @@ -1229,7 +1252,7 @@ public static String execCommand(String ... cmd) throws IOException { public static String execCommand(Map env, String[] cmd, long timeout) throws IOException { - ShellCommandExecutor exec = new ShellCommandExecutor(cmd, null, env, + ShellCommandExecutor exec = new ShellCommandExecutor(cmd, null, env, timeout); exec.execute(); return exec.getOutput(); @@ -1267,7 +1290,7 @@ public void run() { p.exitValue(); } catch (Exception e) { //Process has not terminated. - //So check if it has completed + //So check if it has completed //if not just destroy it. if (p != null && !shell.completed.get()) { shell.setTimedOut(); diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java index d47b9b4193b..be7b3328eb4 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestShell.java @@ -238,9 +238,9 @@ public void testGetCheckProcessIsAliveCommand() throws Exception { expectedCommand = new String[]{getWinUtilsPath(), "task", "isAlive", anyPid }; } else if (Shell.isSetsidAvailable) { - expectedCommand = new String[] { "bash", "-c", "kill -0 -- -" + anyPid }; + expectedCommand = new String[] {"kill", "-0", "--", "-" + anyPid }; } else { - expectedCommand = new String[]{ "bash", "-c", "kill -0 " + anyPid }; + expectedCommand = new String[] {"kill", "-0", anyPid }; } Assert.assertArrayEquals(expectedCommand, checkProcessAliveCommand); } @@ -258,9 +258,9 @@ public void testGetSignalKillCommand() throws Exception { expectedCommand = new String[]{getWinUtilsPath(), "task", "kill", anyPid }; } else if (Shell.isSetsidAvailable) { - expectedCommand = new String[] { "bash", "-c", "kill -9 -- -" + anyPid }; + expectedCommand = new String[] {"kill", "-9", "--", "-" + anyPid }; } else { - expectedCommand = new String[]{ "bash", "-c", "kill -9 " + anyPid }; + expectedCommand = new String[] {"kill", "-9", anyPid }; } Assert.assertArrayEquals(expectedCommand, checkProcessAliveCommand); } @@ -464,4 +464,10 @@ private void assertExContains(Exception ex, String expectedText) } } + @Test + public void testBashQuote() { + assertEquals("'foobar'", Shell.bashQuote("foobar")); + assertEquals("'foo'\\''bar'", Shell.bashQuote("foo'bar")); + assertEquals("''\\''foo'\\''bar'\\'''", Shell.bashQuote("'foo'bar'")); + } }