Java Language Piège: Runtime.exec, Process et ProcessBuilder ne comprennent pas la syntaxe du shell


Exemple

Les Runtime.exec(String ...) et Runtime.exec(String) vous permettent d'exécuter une commande en tant que processus externe 1 . Dans la première version, vous indiquez le nom de la commande et les arguments de la commande en tant qu'éléments distincts du tableau de chaînes et le runtime Java demande au système d'exécution du système d'exploitation de démarrer la commande externe. La deuxième version est facile à utiliser, mais elle comporte des pièges.

Tout d'abord, voici un exemple d'utilisation de exec(String) utilisé en toute sécurité:

Process p = Runtime.exec("mkdir /tmp/testDir");
p.waitFor();
if (p.exitValue() == 0) {
    System.out.println("created the directory");
}

Espaces dans les chemins

Supposons que nous généralisions l'exemple ci-dessus pour pouvoir créer un répertoire arbitraire:

Process p = Runtime.exec("mkdir " + dirPath);
// ...

Cela fonctionnera généralement, mais cela échouera si dirPath est (par exemple) "/ home / user / My Documents". Le problème est que exec(String) divise la chaîne en une commande et les arguments en recherchant simplement des espaces. La chaîne de commande:

"mkdir /home/user/My Documents"

sera divisé en:

"mkdir", "/home/user/My", "Documents"

et cela provoquera l'échec de la commande "mkdir" car elle attend un argument, pas deux.

Face à cela, certains programmeurs essaient d’ajouter des guillemets autour du chemin. Cela ne fonctionne pas non plus:

"mkdir \"/home/user/My Documents\""

sera divisé en:

"mkdir", "\"/home/user/My", "Documents\""

Les caractères supplémentaires entre guillemets qui ont été ajoutés pour tenter de "citer" les espaces sont traités comme tous les autres caractères non blancs. En effet, tout ce que nous citons ou échappons dans les espaces va échouer.

La manière de gérer ces problèmes particuliers consiste à utiliser la surcharge exec(String ...) .

Process p = Runtime.exec("mkdir", dirPath);
// ...

Cela fonctionnera si dirpath inclut des caractères d' dirpath car cette surcharge de exec ne tente pas de diviser les arguments. Les chaînes sont transmises à l'appel du système d' exec système d'exploitation tel exec .

Redirection, pipelines et autres syntaxes de shell

Supposons que nous voulions rediriger l'entrée ou la sortie d'une commande externe ou exécuter un pipeline. Par exemple:

Process p = Runtime.exec("find / -name *.java -print 2>/dev/null");

ou

Process p = Runtime.exec("find source -name *.java | xargs grep package");

(Le premier exemple répertorie les noms de tous les fichiers Java du système de fichiers et le second affiche les instructions du package 2 dans les fichiers Java de l'arborescence "source".)

Ceux-ci ne vont pas fonctionner comme prévu. Dans le premier cas, la commande "find" sera exécutée avec "2> / dev / null" comme argument de commande. Il ne sera pas interprété comme une redirection. Dans le deuxième exemple, le caractère de tuyau ("|") et les travaux suivants seront donnés à la commande "find".

Le problème est que les méthodes exec et ProcessBuilder ne comprennent aucune syntaxe de shell. Cela inclut les redirections, les pipelines, l'expansion des variables, la mise en mémoire, etc.

Dans quelques cas (par exemple, une simple redirection), vous pouvez facilement obtenir l'effet souhaité en utilisant ProcessBuilder . Cependant, ce n'est pas vrai en général. Une autre approche consiste à exécuter la ligne de commande dans un shell; par exemple:

Process p = Runtime.exec("bash", "-c", 
                         "find / -name *.java -print 2>/dev/null");

ou

Process p = Runtime.exec("bash", "-c", 
                         "find source -name \\*.java | xargs grep package");

Mais notez que dans le deuxième exemple, nous avons dû échapper au caractère générique ("*") car nous voulons que le caractère générique soit interprété par "trouver" plutôt que par le shell.

Les commandes intégrées du shell ne fonctionnent pas

Supposons que les exemples suivants ne fonctionnent pas sur un système avec un shell de type UNIX:

Process p = Runtime.exec("cd", "/tmp");     // Change java app's home directory

ou

Process p = Runtime.exec("export", "NAME=value");  // Export NAME to the java app's environment

Il y a quelques raisons pour lesquelles cela ne fonctionnera pas:

  1. Sur "cd" et "export", les commandes sont des commandes intégrées au shell. Ils n'existent pas en tant qu'exécutables distincts.

  2. Pour que les commandes intégrées à la coquille fassent ce qu’elles sont censées faire (par exemple, modifier le répertoire de travail, mettre à jour l’environnement), elles doivent changer l’emplacement de cet état. Pour une application normale (y compris une application Java), l'état est associé au processus d'application. Ainsi, par exemple, le processus fils qui exécuterait la commande "cd" ne pourrait pas modifier le répertoire de travail de son processus parent "java". De même, un exec processus « d ne peut pas changer le répertoire de travail pour un processus qui suit.

Ce raisonnement s'applique à toutes les commandes intégrées du shell.


1 - Vous pouvez également utiliser ProcessBuilder , mais cela n’est pas pertinent au sens de cet exemple.

2 - C'est un peu dur et prêt ... mais encore une fois, les défauts de cette approche ne sont pas pertinents pour l'exemple.