Search code examples
javawindowsshellwindows-subsystem-for-linuxfile-association

How to run .sh and get return value from Java with file association on Windows


Background

I am writing a Java program that runs a .sh file specified in .properties files and get its return value. The path of .sh file is specified from .properties files like:

command=C:/tmp/hoge.sh
args[0]=foo
args[1]=bar
args[2]=baz

And the Java program reads it like:

ClassLoader cl = Thread.currentThread().getContextClassLoader();
InputStream is = cl.getResourceAsStream("resources/command.properties");
Properties prop = new Properties();
prop.load(is);

List<String> list = new ArrayList<String>();
list.add(prop.getProperty("command"));
list.add(prop.getProperty("args[0]"));
list.add(prop.getProperty("args[1]"));
list.add(prop.getProperty("args[2]"));

ProcessBuilder pb = new ProcessBuilder(list);
Process p = pb.start();
int returnValue = p.exitValue();

This program runs on Linux in the production environment. But due to development environment management, we are using Windows as a development environment.

Prerequisites

  • There are so many .sh and .properties files on our system. So I want to use the same .properties files on the development environment and the production environment, or at least I want to use the .properties files with simple string replacement (on the example above, what I have to do is just replace the command=C: to command=). I want to avoid to prepare the dedicated .properties files for the development environment and the production environment separately.
  • We are using the Windows version of Java mainly due to two reasons: 1) The middleware we call from Java program (it only supports Windows and RHEL, but doesn't support WSL environment), and 2) The skillset of project members (there are only a few members who can use both of Windows and Linux fluently).
  • We want to avoid the additional code that is used only in the development environment as much as possible, because of the code coverage measurement and the quality management.

What I have tried and confirmed

To test the Java program and .sh file in the Windows environment, I have decided to use WSL2.

To run .sh on Windows, I have set up WSL2 and modified file association for .sh files. I have added following default value to the key HKEY_LOCAL_MACHINE\SOFTWARE\Classes\sh_auto_file\shell\open\command to run .sh on WSL2:

"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" "$arg=\"%1\";wsl (\"/mnt/\"+[System.Char]::ToLower($arg[0])+$arg.Substring(2,$arg.Length-2) -replace '\\', '/'); exit $LASTEXITCODE"

On the command prompt, I have confirmed that I can run .sh with Bash on WSL2 via the file association and get the return value of .sh properly.

C:\tmp>hoge.sh

C:\tmp>echo %ERRORLEVEL%
5

The content of hoge.sh is:

sleep 1
exit 5

Problem

The Java program runs .sh file properly on Linux. But on Windows, it causes an error like this:

Exception in thread "main" java.io.IOException: Cannot run program "C:/tmp/hoge.sh": CreateProcess error=193, %1 is not a valid Win32 application
    at java.lang.ProcessBuilder.start(ProcessBuilder.java:1048)
    at org.example.Main.main(Main.java:16)
Caused by: java.io.IOException: CreateProcess error=193, %1 is not a valid Win32 application
    at java.lang.ProcessImpl.create(Native Method)
    at java.lang.ProcessImpl.<init>(ProcessImpl.java:386)
    at java.lang.ProcessImpl.start(ProcessImpl.java:137)
    at java.lang.ProcessBuilder.start(ProcessBuilder.java:1029)
    ... 1 more

Question

How can I run .sh from Java with file association on WSL2? Or, is there no way to launch programs according to file association and get the return value in Java?

Note that you can launch programs with the file association via java.awt.Desktop#open(), but it doesn't have a method to get the return value.

Note also that preparing a .properties file for the development environment like below needs very hard work on our prerequisites.

command=wsl
args[0]=C:/tmp/hoge.sh
args[1]=foo
args[2]=bar
args[3]=baz

Solution

  • You could make both Windows and Linux versions run BASH -c "yourcommand" to simplify the differences in OS handling. WSL installs BASH.EXE, confirm it is there with CMD.EXE:

    > where bash
    C:\Windows\System32\bash.exe
    

    If your PATH is set correctly on both OS you can just use "bash" as the command:

    String [] cmd = new String[] { "bash" , "-c", "Insert command 'args[0]' 'args[1]' ... here"};
    

    For the above to work though you'd need to construct cmd[2] as one string containing the args[0..N] values separated by space AND escaped with quotes if they also contained spaces so that the correct parameters are given to your script. Something like:

    cmd[2] = prop.getProperty("command")+" "+ prop.getProperty("arg[0]") ...
    

    If your Path is not setup you can adjust your launch code to pick bash.exe or /bin/bash based on os.name:

    final String OS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
    //final boolean IS_WINDOWS = OS.startsWith("windows");
    final boolean IS_LINUX   = OS.startsWith("linux");
    
    String bash = IS_LINUX ? "/bin/bash" : "bash.exe" ;  // May need full path shown by where bash.EXE
    String [] cmd = new String[] { bash , "-c", "Insert command  args[0] args[1] ... here"};