web-dev-qa-db-fra.com

Pourquoi Runtime.exec (String) fonctionne-t-il pour certaines commandes mais pas pour toutes?

Lorsque j'essaie d'exécuter Runtime.exec(String), certaines commandes fonctionnent, tandis que d'autres commandes sont exécutées mais échouent ou font des choses différentes de celles de mon terminal. Voici un cas de test autonome qui démontre l'effet:

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'exemple fonctionne très bien si je remplace la commande par echo hello world, mais pour d'autres commandes - en particulier celles impliquant des noms de fichiers avec des espaces comme ici - j'obtiens des erreurs même si la commande est clairement en cours d'exécution:

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

pendant ce temps, copier-coller sur mon Shell:

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

Pourquoi y a-t-il une différence? Quand ça marche et quand ça échoue? Comment puis-je le faire fonctionner pour toutes les commandes?

35
that other guy

Pourquoi certaines commandes échouent?

Cela se produit car la commande passée à Runtime.exec(String) n'est pas exécutée dans un shell. Le shell exécute de nombreux services de support communs pour les programmes, et lorsque le shell n'est pas là pour les faire, la commande échoue.

Quand les commandes échouent-elles?

Une commande échoue chaque fois qu'elle dépend des fonctionnalités d'un shell. Le Shell fait beaucoup de choses courantes et utiles auxquelles nous ne pensons pas normalement:

  1. Le Shell se divise correctement entre guillemets et espaces

    Cela garantit que le nom de fichier dans "My File.txt" Reste un seul argument.

    Runtime.exec(String) se divise naïvement sur les espaces et passerait cela comme deux noms de fichiers séparés. Cela échoue évidemment.

  2. Le Shell élargit les globes/caractères génériques

    Lorsque vous exécutez ls *.doc, Le shell le réécrit dans ls letter.doc notes.doc.

    Runtime.exec(String) ne le fait pas, il les passe juste comme arguments.

    ls n'a aucune idée de ce qu'est *, donc la commande échoue.

  3. Le Shell gère les pipes et les redirections.

    Lorsque vous exécutez ls mydir > output.txt, Le shell ouvre "output.txt" pour la sortie de commande et le supprime de la ligne de commande, donnant ls mydir.

    Runtime.exec(String) non. Il les passe simplement en arguments.

    ls n'a aucune idée de ce que signifie >, donc la commande échoue.

  4. Le Shell étend les variables et les commandes

    Lorsque vous exécutez ls "$HOME" Ou ls "$(pwd)", le shell le réécrit dans ls /home/myuser.

    Runtime.exec(String) ne le fait pas, il les passe juste comme arguments.

    ls n'a aucune idée de ce que signifie $, donc la commande échoue.

Que pouvez-vous faire à la place?

Il existe deux façons d'exécuter des commandes arbitrairement complexes:

Simple et bâclé: déléguer à un Shell.

Vous pouvez simplement utiliser Runtime.exec(String[]) (notez le paramètre du tableau) et passer votre commande directement à un Shell qui peut faire tout le gros du travail:

// 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 });

Sécurisé et robuste: assumer les responsabilités du Shell.

Ce n'est pas un correctif qui peut être appliqué mécaniquement, mais nécessite une compréhension du modèle d'exécution Unix, de ce que font les shells et comment vous pouvez faire de même. Cependant, vous pouvez obtenir une solution solide, sécurisée et robuste en retirant le Shell de l'image. Ceci est facilité par ProcessBuilder.

La commande de l'exemple précédent qui nécessite que quelqu'un gère 1. les guillemets, 2. les variables et 3. les redirections, peut être écrite comme suit:

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();
79
that other guy