问题
最近有个问题本地执行
ssh -p 8084 root@10.224.122.51 \"ssh -p 22 root@192.168.5.157 'mkdir -p /opt/dw-release/pdld-admin'\"
程序执行总是报错: No such file or directory
但是直接在终端执行正常,这就很奇怪。肯定能推出是程序执行做了什么导致执行失败。
用的自带Runtime.exec(String)方法执行,后面网上搜到另外参数String[] 当时感觉是这个问题。
比较
参数是字符串
Runtime.java
public Process exec(String command) throws IOException {
return exec(command, null, null);
}
public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.isEmpty())
throw new IllegalArgumentException("Empty command");
StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}
参数是字符串数组
public Process exec(String cmdarray[]) throws IOException {
return exec(cmdarray, null, null);
}
public Process exec(String[] cmdarray, String[] envp, File dir)
throws IOException {
return new ProcessBuilder(cmdarray)
.environment(envp)
.directory(dir)
.start();
}
通过观察可以看到参数是字符串时候,做个new StringTokenizer(command) 处理,转成字符串数组,在调用字符串数组方式执行。
出现问题的地方就是在这。
继续看StringTokenizer类处理(构造方法)
/**
* Constructs a string tokenizer for the specified string. The
* tokenizer uses the default delimiter set, which is
* <code>" \t\n\r\f"</code>: the space character,
* the tab character, the newline character, the carriage-return character,
* and the form-feed character. Delimiter characters themselves will
* not be treated as tokens.
*
* @param str a string to be parsed.
* @exception NullPointerException if str is <CODE>null</CODE>
*/
public StringTokenizer(String str) {
this(str, " \t\n\r\f", false);
}
从注释中可以看" \t\n\r\f" 有这参数,感觉到是对这个进行作为分隔符
回头看看Runtime.java
StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
调用netxToken方法获取字符串数组
public String nextToken() {
/*
* If next position already computed in hasMoreElements() and
* delimiters have changed between the computation and this invocation,
* then use the computed value.
*/
currentPosition = (newPosition >= 0 && !delimsChanged) ?
newPosition : skipDelimiters(currentPosition);
/* Reset these anyway */
delimsChanged = false;
newPosition = -1;
if (currentPosition >= maxPosition)
throw new NoSuchElementException();
int start = currentPosition;
currentPosition = scanToken(currentPosition);
return str.substring(start, currentPosition);
}
看最后三行,就是截取子串方式。看起始和结束位置
结束位置
/**
* Skips ahead from startPos and returns the index of the next delimiter
* character encountered, or maxPosition if no such delimiter is found.
*/
private int scanToken(int startPos) {
int position = startPos;
while (position < maxPosition) {
if (!hasSurrogates) {
char c = str.charAt(position);
if ((c <= maxDelimCodePoint) && (delimiters.indexOf(c) >= 0))
break;
position++;
} else {
int c = str.codePointAt(position);
if ((c <= maxDelimCodePoint) && isDelimiter(c))
break;
position += Character.charCount(c);
}
}
if (retDelims && (startPos == position)) {
if (!hasSurrogates) {
char c = str.charAt(position);
if ((c <= maxDelimCodePoint) && (delimiters.indexOf(c) >= 0))
position++;
} else {
int c = str.codePointAt(position);
if ((c <= maxDelimCodePoint) && isDelimiter(c))
position += Character.charCount(c);
}
}
return position;
}
注释简单翻译就是起始位置到下个分隔符位置
(这里只是简单了解功能,没详细看实现过程)
验证
public static void main(String[] args) {
String command = "ssh -p 8084 root@10.224.122.51 \"ssh -p 22 root@192.168.5.157 'mkdir -p /opt/dw-release/pdld-admin'\"";
StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++) {
cmdarray[i] = st.nextToken();
}
Arrays.stream(cmdarray).forEach(System.out::println);
}
结果:
看出解析出来后字符数组是存在问题的
总结
使用Runtime执行命令,尽可能使用命令字符串数组方式作为参数,而不是使用字符串
或者可以自己重写Runtime的exec方法中分隔字符串实现