web-dev-qa-db-fra.com

Comment puis-je diriger l'entrée initiale dans un processus qui sera ensuite interactif?

J'aimerais pouvoir injecter une commande initiale dans le lancement d'un processus interactif, pour pouvoir faire quelque chose comme ça:

echo "initial command" | INSERT_MAGIC_HERE some_tool

tool> initial command 

[result of initial command] 

tool> [now I type an interactive command]

Ce qui ne fonctionne pas:

  • Le simple fait de canaliser la commande initiale ne fonctionne pas, car cela ne permet pas à stdin d'être connecté au terminal

  • L'écriture dans/dev/pts/[nombre] envoie la sortie au terminal, et non l'entrée dans le processus comme si elle provenait du terminal

Que serait, mais avec des inconvénients:

  • Faites une commande qui bifurque un enfant, écrit dans son stdin puis transfère tout depuis son propre stdin. Inconvénient - les choses de contrôle du terminal (comme le mode ligne vs caractère) ne fonctionneront pas. Peut-être que je pourrais faire quelque chose avec le proxy de pseudo-terminaux?

  • Créez une version modifiée de xterm (j'en lance quand même une pour cette tâche) avec une option de ligne de commande pour injecter des commandes supplémentaires après avoir rencontré la chaîne d'invite souhaitée. Laid.

  • Créez une version modifiée de l'outil que j'essaie d'exécuter afin qu'il accepte une commande initiale sur la ligne de commande. Interrompt l'installation standard.

(L'outil qui nous intéresse actuellement est le shell adb d'Android - je veux ouvrir un shell interactif sur le téléphone, exécuter une commande automatiquement, puis avoir une session interactive)

42
Chris Stratton

Vous n'avez pas besoin d'écrire un nouvel outil pour transférer stdin - un a déjà été écrit (cat):

(echo "initial command" && cat) | some_tool

Cela présente l'inconvénient de connecter un tuyau à some_tool, pas un terminal.

41
caf

La réponse acceptée est simple et généralement bonne.

Mais il a un inconvénient: les programmes reçoivent un tuyau comme entrée, pas un terminal. Cela signifie que la saisie semi-automatique ne fonctionnera pas. Dans de nombreux cas, cela désactive également les jolies sorties, et j'ai entendu certains programmes refuser de fonctionner si stdin n'est pas un terminal.

Le programme suivant résout le problème. Il crée un pseudoterminal, génère un programme connecté à ce pseudoterminal. Il alimente d'abord les entrées supplémentaires transmises via la ligne de commande, puis alimente les entrées fournies par l'utilisateur via stdin.

Par exemple, ptypipe "import this" python3 fait Python exécutez "importez ceci" d'abord, puis il vous dépose à l'invite de commande interactive, avec l'achèvement du travail et d'autres choses.

Également, ptypipe "date" bash exécute Bash, qui exécute date puis vous donne un Shell. Encore une fois, avec l'achèvement du travail, l'invite colorisée et ainsi de suite.

#!/usr/bin/env python3

import sys
import os
import pty
import tty
import select
import subprocess

STDIN_FILENO = 0
STDOUT_FILENO = 1
STDERR_FILENO = 2

def _writen(fd, data):
    while data:
        n = os.write(fd, data)
        data = data[n:]

def main_loop(master_fd, extra_input):
    fds = [master_fd, STDIN_FILENO]

    _writen(master_fd, extra_input)

    while True:
        rfds, _, _ = select.select(fds, [], [])
        if master_fd in rfds:
            data = os.read(master_fd, 1024)
            if not data:
                fds.remove(master_fd)
            else:
                os.write(STDOUT_FILENO, data)
        if STDIN_FILENO in rfds:
            data = os.read(STDIN_FILENO, 1024)
            if not data:
                fds.remove(STDIN_FILENO)
            else:
                _writen(master_fd, data)

def main():
    extra_input = sys.argv[1]
    interactive_command = sys.argv[2]

    if hasattr(os, "fsencode"):
        # convert them back to bytes
        # http://bugs.python.org/issue8776
        interactive_command = os.fsencode(interactive_command)
        extra_input = os.fsencode(extra_input)

    # add implicit newline
    if extra_input and extra_input[-1] != b'\n':
        extra_input += b'\n'

    # replace LF with CR (shells like CR for some reason)
    extra_input = extra_input.replace(b'\n', b'\r')

    pid, master_fd = pty.fork()

    if pid == 0:
        os.execlp("sh", "/bin/sh", "-c", interactive_command)

    try:
        mode = tty.tcgetattr(STDIN_FILENO)
        tty.setraw(STDIN_FILENO)
        restore = True
    except tty.error:    # This is the same as termios.error
        restore = False

    try:
        main_loop(master_fd, extra_input)
    except OSError:
        if restore:
            tty.tcsetattr(0, tty.TCSAFLUSH, mode)

    os.close(master_fd)
    return os.waitpid(pid, 0)[1]

if __name__ == "__main__":
    main()

(Remarque: je crains que cette solution ne contienne un blocage possible. Vous souhaiterez peut-être alimenter extra_input en petits morceaux pour l'éviter)

5
WGH

C'est facile à faire avec le programme "expect" qui est destiné à vous permettre d'écrire des scripts pour interagir avec les programmes.

J'ai testé cela en écrivant un script attendu bc.exp pour lancer la calculatrice "bc" et lui envoyer la commande "obase = 16" pour le mettre en mode de sortie hexadécimal, puis me retourner le contrôle.

Le script (dans un fichier nommé bc.exp) est

spawn bc
send "obase=16\n"
interact {
 \003 exit
}

On l'exécute avec

expect bc.exp
2
Larry Stewart