I want to set and echo a Windows variable in Java:
public static void main(String[] args) throws IOException
{
Runtime rt = Runtime.getRuntime();
String[] cmd = { "cmd.exe", "/c", "set HOSTNAME=%COMPUTERNAME% "
+ "&& echo %HOSTNAME%" };
Process proc = rt.exec(cmd);
BufferedReader stdInput = new BufferedReader(
new InputStreamReader(proc.getInputStream()));
BufferedReader stdError = new BufferedReader(
new InputStreamReader(proc.getErrorStream()));
System.out.println("Output:\n");
String s = null;
while ((s = stdInput.readLine()) != null)
{
System.out.println(s);
}
System.out.println("Error (if any):\n");
while ((s = stdError.readLine()) != null)
{
System.out.println(s);
}
}
I expect the program will print out my computer host name or I will use this value for another purpose. But the output is just like this:
Output:
%HOSTNAME%
Error (if any):
How could I get the value that I have set in the command set HOSTNAME=%COMPUTERNAME%
It's irrelevant to Java because it's how cmd parses the command. The whole command will be parsed at once for variable expansion. At the time the command is parsed the variable is not yet available, so it'll be replaced with nothing in a batch file or leave as-is in command line
You need to use delayed expansion and print the variable with !!
cmd.exe /V:ON /c set HOSTNAME=%COMPUTERNAME% && echo !HOSTNAME!
The /V:ON is for enabling delayed expansion
CMD [/A | /U] [/Q] [/D] [/E:ON | /E:OFF] [/F:ON | /F:OFF] [/V:ON | /V:OFF]
[[/S] [/C | /K] string]
...
/V:ON Enable delayed environment variable expansion using ! as the
delimiter. For example, /V:ON would allow !var! to expand the
variable var at execution time. The var syntax expands variables
at input time, which is quite a different thing when inside of a FOR
loop.
In a batch file it can be enabled by setlocal EnableDelayedExpansion
However for that purpose just cmd.exe /V:ON /c echo %COMPUTERNAME% is enough. Yet it's still not the efficient way. There are better ways to get hostname in Java
Map<String, String> env = System.getenv();
if (env.containsKey("COMPUTERNAME"))
return env.get("COMPUTERNAME");
or
InetAddress.getLocalHost().getHostName()
Your syntax for running two commands at once is wrong. Try using a single & in the command line instead of &&.
The real problem, I think, is that cmd.exe does all variable substitution before executing the command line (including parsing the &&). When it finds the syntax %HOSTNAME% for a variable that doesn't exist (yet), it leaves the text as is: %HOSTNAME%. So try issuing two commands to the same process, followed by an exit command.
Another approach is to change the command to:
set HOSTNAME=%COMPUTERNAME% & SET HOSTNAME
Then you will get back the string "HOSTNAME=my_computer_name", from which you can strip out the leading "HOSTNAME=" prefix.
Related
I have a function to execute a system command:
public String cmd(String s) {
String out = "";
try {
Runtime run = Runtime.getRuntime();
Process pr = run.exec(s.split(" "));
pr.waitFor();
BufferedReader buf = new BufferedReader(new InputStreamReader(pr.getInputStream()));
String line = "";
while ((line=buf.readLine())!=null) {
out+=line+"\n";
}
} catch(Exception e) {
e.printStackTrace();
}
return out;
}
The command passes through:
cmd("nmap -sL -n 192.168.1.0/24 | awk '/Nmap scan report/{print $NF}'");
Expected Output:
192.168.1.0
192.168.1.1
...
Actual Output:
Starting Nmap 7.80 ( https://nmap.org ) at 2021-04-12 20:27 EET
Nmap scan report for 192.168.1.0 ...
Similar questions answers this well:
Using Java ProcessBuilder to Execute a Piped Command
Java program not getting output from terminal
To execute a pipeline, you have to invoke a shell, and then run your commands inside that shell.
Process p = new ProcessBuilder().command("bash", "-c", command).start();
bash invokes a shell to execute your command and -c means commands are read from string. So, you don't have to send the command as an array in ProcessBuilder.
Adapted to you case
String cmd(String command) {
ProcessBuilder builder = new ProcessBuilder();
builder.redirectErrorStream(true); // add stdErr to output
Process process = builder.command("bash", "-c", command).start();
StringBuilder processOutput = new StringBuilder(); // add lines easier
// try-with to auto-close resources
try (BufferedReader processOutputReader = new BufferedReader(new InputStreamReader(process.getInputStream()));) {
String readLine;
while ((readLine = processOutputReader.readLine()) != null) {
processOutput.append(readLine + System.lineSeparator()); // use system's line-break
}
process.waitFor();
}
return processOutput.toString().trim();
}
Then call as expected:
cmd("nmap -sL -n 192.168.1.0/24 | awk '/Nmap scan report/{print $NF}'");
Note: I enhanced it a bit to
use try-with-resources to deal cleanly with resources
add output from StdErr
use a StringBuilder to concatenate output lines
use System.lineSeparator for platform-independency (Win/Mac/Linux/Unix)
Inspired by:
read the output from java exec
The pipe is interpreted by the shell. It executes one command then passes the output of one command into the next one. You could emulate this in Java starting both commands and then pumping the OutputStream of the first program to the InputStream of the second.
Alternatively if you don't want to do this you can still call something like "sh -c 'command1 | command2"
I am getting an exception like java.io.IOException: Cannot run program cat /home/talha/* | grep -c TEXT_TO_SEARCH": error=2, No such file or directory while executing the command below despite that there are no issues when I execute the same command through the terminal. I need to execute and return the output of the command below:
cat /home/talha/* | grep -c TEXT_TO_SEARCH
Here is the method used to execute commands using Runtime class:
public static String executeCommand(String command) {
StringBuffer output = new StringBuffer();
Process p;
try {
p = Runtime.getRuntime().exec(command);
p.waitFor();
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line = "";
while ((line = reader.readLine()) != null) {
output.append(line + "\n");
}
} catch (Exception e) {
e.printStackTrace();
}
return output.toString();
}
Runtime.exec does not use a shell (like, say, /bin/bash); it passes the command directly to the operating system. This means wildcards like * and pipes (|) will not be understood, since cat (like all Unix commands) does not do any parsing of those characters. You need to use something like
p = new ProcessBuilder("bash", "-c", command).start();
or, if for some bizarre reason you need to stick to using the obsolete Runtime.exec methods:
p = Runtime.getRuntime().exec(new String[] { "bash", "-c", command });
If you are only running that cat/grep command, you should consider abandoning the use of an external process, since Java code can easily traverse a directory, read lines from each file, and match them against a regular expression:
Pattern pattern = Pattern.compile("TEXT_TO_SEARCH");
Charset charset = Charset.defaultCharset();
long count = 0;
try (DirectoryStream<Path> dir =
Files.newDirectoryStream(Paths.get("/home/talha"))) {
for (Path file : dir) {
count += Files.lines(file, charset).filter(pattern.asPredicate()).count();
}
}
Update: To recursively read all files in a tree, use Files.walk:
try (Stream<Path> tree =
Files.walk(Paths.get("/home/talha")).filter(Files::isReadable)) {
Iterator<Path> i = tree.iterator();
while (i.hasNext()) {
Path file = i.next();
try (Stream<String> lines = Files.lines(file, charset)) {
count += lines.filter(pattern.asPredicate()).count();
}
};
}
$PATH is an environment variable that tells the system where to search for executable programs (it's a list of directories separated by colons). It is usually set in your .bashrc or .cshrc file but this is only loaded when you log in. When Java runs, $PATH is likely not set because the rc file is not executed automatically, so the system can't find programs without specifying exactly where they are. Try using /bin/cat or /usr/bin/cat instead of just cat and see if it works. If it does, $PATH is your problem. You can add $PATH=/bin:/usr/bin to your script or just leave it with the directory name specified (e.g. /bin/cat).
Just because you can execute it in a login session doesn't mean it will work the same when a daemon like your Java program runs. You have to know what's in your .bashrc or .cshrc file and even sometimes how the system file is written (/etc/bashrc) in order to know how to write a script that runs under a daemon. Another consideration is that daemons often run under the context of a different user, and that throws things off, too.
I tried to piping password to smbpassword via Java application, here how I pipe via terminal:
(echo newPassword; echo confirmPassword) | smbpasswd -a -s client1
and the output show the command is nicely done:
Added user client1
However, I cannot accomplish this via Java application, here the codes that I use:
public void run(String command, String[] prompt) {
try {
String[] args = new String[] {"/bin/bash","-c","echo " + rootPassword + "| sudo -S " + command};
Process proc = new ProcessBuilder(args).start();
if (prompt != null && prompt.length > 0) {
for (int i = 0; i < prompt.length; i++) {
proc.getOutputStream().write((prompt[i] + "\r\n").getBytes());
proc.getOutputStream().flush();
}
}
proc.waitFor();
String output = "";
String line;
BufferedReader input = new BufferedReader(new InputStreamReader(proc.getInputStream()));
while ((line = input.readLine()) != null) {
System.out.println(line);
output += line + "\n";
}
System.out.println(output);
input.close();
}
catch (Exception ex) {
System.out.println(null, ex.getMessage());
}
}
and I tried to use OutputStream it like:
String[] pipe = { password, password };
run("smbpasswd -s -a " + username, pipe);
or pipe it like:
run("(echo " + password + "; echo " + password + ") | smbpasswd -s -a " + username, null);
but both doesn't work and I got no output and the user seems not created after I check via pdbedit -L. However, I able to execute another command with pipe such as echo username:password | chpasswd via that function.
Any idea?
Thanks in advance.
The main conceptual problem is that shell code such as you are trying to run:
(echo newPassword; echo confirmPassword) | smbpasswd -a -s client1
is not a "command" in the sense that sudo requires. In fact, it's not a "command" even in shell terminology; rather, it's a pipeline, consisting of a compound command and a simple command. sudo expects you to provide a simple command.
But that's actually a bit of an aside. Your problem isn't really specific to sudo; rather, it's a shell problem. In the failure case, your run() method invokes bash to execute the following shell code:
echo rootPassword | sudo -S (echo newPassword; echo confirmPassword) | smbpasswd -a -s client1
That's problematic for at least two reasons:
A subshell invocation cannot appear in a simple command's (i.e. sudo's) argument list like that. You could put a command substitution there ($(echo ...)), but that will not serve your purpose because the internal line break will not then be preserved.
Even if you solved (1), your second pipe is at the wrong level: it would pipe the output of sudo into smbpasswd. That's fine data-wise, but it leaves smbpasswd running without privilege -- the sudo does not apply to it.
You could probably solve the problem by instructing sudo to run the code via a shell. You could do that by embedding the ultimate command inside an inner bash -c command, but it would probably be easier to use sudo's -s option (but beware that -s can choose a different shell than bash if you let it do). Something along those lines is probably the most general solution if you want to encode the whole operation in a single String in your Java source:
String[] args = new String[] { "/bin/bash", "-c",
"echo " + rootPassword + " | sudo -S -s '" + command + "'" };
String[] args = new String[] { "/bin/bash", "-c",
"echo " + rootPassword + " | sudo -S bash -c '" + command + "'" };
Much of your problem, however, arises from your reliance on echo to feed data to your commands. You should consider instead using your Process's OutputStream to feed data to it from your Java program. That does not lend itself to quite as simple a Java-side API, but I think you're fooling yourself a bit about how simple your API really is. It has hidden gotchas, as you discovered, and even the revision I offered above still has at least one: it will likely break if the command submitted to it contains a single-quote character (').
Update:
Here's one way to feed the process the needed data via its OutputStream:
public void run2(String command, String commandInput)
throws IOException, InterruptedException {
String[] args = new String[] { "sudo", "-S", "bash", "-c", command };
Process proc = new ProcessBuilder(args).start();
Writer toProc = new OutputStreamWriter(proc.getOutputStream());
toProc.write(rootPassword, 0, rootPassword.length());
toProc.write('\n');
toProc.write(commandInput, 0, commandInput.length());
toProc.close();
proc.waitFor();
BufferedReader input = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuilder output = new StringBuilder();
String line;
while ((line = input.readLine()) != null) {
output.append(line).append('\n');
}
System.out.println("Command output:");
System.out.println(output.toString());
input.close();
}
You might use that from within its class as
run2("smbpasswd -a -s client1", "newPassword\nnewPassword");
Note in particular that the Process's output stream must, in general, be closed after you've written everything you intend to write to it. Some commands you could run will not exit if you do not do so.
Note also
For the general case, you must read every Process's input stream and error stream (or just the former if you combine them), in parallel with the process itself. If you have not combined the streams then you must read them in parallel with each other. If you are writing to the process's output stream then that, too, must be in parallel. If you do not arrange to handle each stream via its own thread then it is possible for the process to get stuck.
On the other hand, you don't need a separate thread to run the Process itself in -- after all, it represents an entire separate process. Just defer waiting for it until you have closed its output stream and read the end of its input and error streams.
I have the following windows batch file (run.bat):
#echo off
echo hello batch file to sysout
And the following java code, which runs the batch files and redirects output to a file:
public static void main(String[] args) throws IOException {
System.out.println("Current java version is: " + System.getProperty("java.version"));
ProcessBuilder pb =
new ProcessBuilder("cmd.exe", "/c",
"run.bat"
,">>", "stdout.txt","2>>", "stderr.txt"
);
System.out.println("Command is: " + pb.command());
Process proc = pb.start();
InputStream in = proc.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String line = null;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
int exitValue = proc.exitValue();
System.out.println("Exit value: " + exitValue);
}
On JDKs up to and including JDK6u43 I get the following output:
Current java version is: 1.6.0_29
Command is: [cmd.exe, /c, run.bat, >>, stdout.txt, 2>>, stderr.txt]
Exit value: 0
and the script output is written to the file.
As of JDK 6u45 and 7, I get the following output:
Current java version is: 1.6.0_45
Command is: [cmd.exe, /c, run.bat, >>, stdout.txt, 2>>, stderr.txt]
hello batch file to sysout
Exit value: 0
And nothing is written to the output file.
This may or may not be related to the changes made in Runtime.exec() , described at: http://www.oracle.com/technetwork/java/javase/6u45-relnotes-1932876.html
What is the correct way of starting a process on Windows with output redirected to files?
Note: In a real world scenario, the command to execute may include parameters with spaces, as in:
ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c",
"run.bat", "Some Input With Spaces",
">>", "stdout.txt","2>>", "stderr.txt");
This is the simplest method i found on http://tamanmohamed.blogspot.in/2012/06/jdk7-processbuilder-and-how-redirecting.html
File output = new File("C:/PBExample/ProcessLog.txt");
ProcessBuilder pb = new ProcessBuilder("cmd");
pb.redirectOutput(output);
Several suggestions here:
Does the input with the spaces need to be treated as single String (with spaces),or id it in actual several inputs? If the first Option is the case I would suggest to quote it for the windows runtime:
ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c",
"run.bat", "\"Some Input With Spaces\"",
">>", "stdout.txt","2>>", "stderr.txt");
Instead of redirecting the input to stdout.txt and stderr.txt using the shell, why not do it using Java using getOutputStream() and getErrorStream()? Here is an example using Guava's IO package. Of course you may want to have those in separate threads, you need proper exception handling, etc.
InputStream stdout = new BufferedInputStream(proc.getInputStream());
FileOutputStream stdoutFile = new FileOutputStream("stdout.txt");
ByteStreams.copy(stdout, stdoutFile);
InputStream stderr = new BufferedInputStream(proc.getErrorStream());
FileOutputStream stderrFile = new FileOutputStream("stderr.txt");
ByteStreams.copy(stderr, stderrFile);
stdout.close();
stderr.close();
stdoutFile.close();
stderrFile.close();
Another option, why not create a run.bat wrapper that will make the redirections?
#echo off
cmd.exe /c run.bat "%1" >> "%2" 2>> "%3"
Use getOutputStream() on the process, instead of using System.out.println(). Sometimes the semantics change between Java implementations.
This seems to be a bugfix actually - the newer implementation makes sense.
I am trying to print my mac's [edit: Apple computer] serial number in a java program. I am familiar with the Unix command
ioreg -l | awk '/IOPlatformSerialNumber/ { print $4;}'
which accomplishes this task in terminal.
When I try
String command = "ioreg -l | awk '/IOPlatformSerialNumber/ { print $4; }'"
Runtime terminal = Runtime.getRuntime();
String input = new BufferedReader(
new InputStreamReader(
terminal.exec(commands).getInputStream())).readLine();
System.out.println(new BufferedReader(
new InputStreamReader(
terminal.exec(command, args).getInputStream())).readLine());
my serial number is not printed. Instead it prints:
<+-o Root class IORegistryEntry, id 0x100000100, retain 10>
I think the problem is that terminal.exec() is not meant to take the whole command string. Is there something in java similar to the argument shell = True in python's Popen(command, stdout=PIPE, shell=True) that will allow me to pass the whole command string?
I see two possibilities:
Parse the output of ioreg -l using, say, Scanner.
Wrap the command in a shell script and exec() it:
#!/bin/sh
ioreg -l | awk '/IOPlatformSerialNumber/ { print $4;}'
Addendum: As an example of using ProcessBuilder, and incorporating a helpful suggestion by Paul Cager, here's a third alternative:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class PBTest {
public static void main(String[] args) {
ProcessBuilder pb = new ProcessBuilder("bash", "-c",
"ioreg -l | awk '/IOPlatformSerialNumber/ { print $4;}'");
pb.redirectErrorStream(true);
try {
Process p = pb.start();
String s;
// read from the process's combined stdout & stderr
BufferedReader stdout = new BufferedReader(
new InputStreamReader(p.getInputStream()));
while ((s = stdout.readLine()) != null) {
System.out.println(s);
}
System.out.println("Exit value: " + p.waitFor());
p.getInputStream().close();
p.getOutputStream().close();
p.getErrorStream().close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
Pipes aren't supported by Runtime.exec(..) since they are a feature of shells. Instead, you'd have to emulate the pipe yourself, e.g.
String ioreg = toString(Runtime.exec("ioreg -l ").getInputStream());
Process awk = Runtime.exec("awk '/IOPlatformSerialNumber/ { print $4;}'");
write(awk.getOutputStream(), ioreg);
String input = new BufferedReader(new InputStreamReader(awk.getInputStream())).readLine();
Alternatively, you could of course run a shell as a process, e.g. Runtime.exec("bash"), and interact with it by reading and writing its IO streams. Interacting with processes is a bit tricky though and has some gotchas and let it execute your command (see comments)
To get the MAC addres via Java you can use java.net.NetworkInterface:
NetworkInterface.getByName("xxx").getHardwareAddress()
If you don't know the name (I assume it to be 'eth0' on linux) of your network interface, you can even iterate throug all of your network interfaces using NetworkInterface.getNetworkInterfaces().