Perché Runtime.exec (String) funziona con alcuni ma non tutti i comandi?

Quando provo ad eseguire Runtime.exec(String) , alcuni comandi funzionano, mentre altri comandi sono eseguiti ma falliscono o fanno cose diverse rispetto al mio terminale. Ecco un caso di test autonomo che dimostra l’effetto:

 public class ExecTest { static void exec(String cmd) throws Exception { Process p = Runtime.getRuntime().exec(cmd); int i; while( (i=p.getInputStream().read()) != -1) { System.out.write(i); } while( (i=p.getErrorStream().read()) != -1) { System.err.write(i); } } public static void main(String[] args) throws Exception { System.out.print("Runtime.exec: "); String cmd = new java.util.Scanner(System.in).nextLine(); exec(cmd); } } 

L’esempio funziona alla grande se sostituisco il comando con echo hello world , ma per altri comandi, in particolare quelli che riguardano nomi di file con spazi come qui, ottengo errori anche se il comando è chiaramente in esecuzione:

 myshell$ javac ExecTest.java && java ExecTest Runtime.exec: ls -l 'My File.txt' ls: cannot access 'My: No such file or directory ls: cannot access File.txt': No such file or directory 

nel frattempo, copia-incolla nella mia shell:

 myshell$ ls -l 'My File.txt' -rw-r--r-- 1 me me 4 Aug 2 11:44 My File.txt 

Perché c’è una differenza? Quando funziona e quando fallisce? Come faccio a farlo funzionare per tutti i comandi?

Perché alcuni comandi falliscono?

Ciò accade perché il comando passato a Runtime.exec(String) non viene eseguito in una shell. La shell esegue molti servizi di supporto comuni per i programmi e quando la shell non è in giro per eseguirli, il comando fallirà.

Quando i comandi falliscono?

Un comando fallirà ogni volta che dipenderà dalle caratteristiche della shell. La shell fa un sacco di cose utili e comuni a cui normalmente non pensiamo:

  1. La shell si divide correttamente tra virgolette e spazi

    Questo assicura che il nome del file in "My File.txt" rimanga un singolo argomento.

    Runtime.exec(String) divide in modo originale sugli spazi e lo passerebbe come due nomi di file separati. Questo ovviamente fallisce.

  2. La shell espande globs / caratteri jolly

    Quando si esegue ls *.doc , la shell lo riscrive in ls letter.doc notes.doc .

    Runtime.exec(String) no, li passa semplicemente come argomenti.

    Non ho idea di cosa sia * , quindi il comando fallisce.

  3. La shell gestisce pipe e reindirizzamenti.

    Quando esegui ls mydir > output.txt , la shell apre “output.txt” per l’output del comando e lo rimuove dalla riga di comando, dando ls mydir .

    Runtime.exec(String) no. Li passa semplicemente come argomenti.

    Non ho idea di cosa significhi > , quindi il comando fallisce.

  4. La shell espande variabili e comandi

    Quando si esegue ls "$HOME" o ls "$(pwd)" , la shell lo riscrive in ls /home/myuser .

    Runtime.exec(String) no, li passa semplicemente come argomenti.

    Non ho idea di cosa significhi $ , quindi il comando fallisce.

Cosa puoi fare invece?

Esistono due modi per eseguire comandi arbitrariamente complessi:

Semplice e sciatto: delegato a una shell.

Puoi semplicemente usare Runtime.exec(String[]) (nota il parametro dell’array) e passare il tuo comando direttamente ad una shell che può fare tutto il lavoro pesante:

 // Simple, sloppy fix. May have security and robustness implications String myFile = "some filename.txt"; String myCommand = "cp -R '" + myFile + "' $HOME 2> errorlog"; Runtime.getRuntime().exec(new String[] { "bash", "-c", myCommand }); 

Sicuro e robusto: assumere le responsabilità della shell.

Questa non è una correzione che può essere applicata meccanicamente, ma richiede una comprensione del modello di esecuzione Unix, di cosa fanno le shell e di come si può fare lo stesso. Tuttavia, puoi ottenere una soluzione solida, sicura e robusta togliendo la shell dall’immagine. Questo è facilitato da ProcessBuilder .

Il comando dell’esempio precedente che richiede a qualcuno di gestire 1. virgolette, 2. variabili e 3. reindirizzamenti, può essere scritto come:

 String myFile = "some filename.txt"; ProcessBuilder builder = new ProcessBuilder( "cp", "-R", myFile, // We handle word splitting System.getenv("HOME")); // We handle variables builder.redirectError( // We set up redirections ProcessBuilder.Redirect.to(new File("errorlog"))); builder.start();