Patrice Ferlet
Patrice Ferlet
Créateur de ce blog.
Publié le avr. 11, 2016 Temps de lecture: 4 min

Me suivre sur Mastodon

Go-Pipe, streamez à la unix/like en Go

Vous connaissiez les “pipelines” de Gulp, et bien voici celles de Go grâce à un boulot intéressant (mais datant d’un an déjà) de Gustavo Niemeyer sur sa page Go.pipe - un package permettant de traiter des flux, d’ajouter ses propres traitements, et surtout sans trop de douleurs.

L’idée est simple. En bash, on utilise des pipelines. C’est une notion courante qui utilise une notation spéciale qui permet de rediriger des entrée/sorties de commandes en commandes. L’idée de Gustavo est de permettre une notion de “pipeline” tout comme le fait Gulp-pipe.

Comment ça marche ?

Installation

Pas de grande surprise:

go get gopkg.in/pipe.v2

Exemple simple

Prenons l’exemple en bash:

cat /etc/passwd | sort | cut -f 1 -d ":"

Cette commande lit le fichier “/etc/password”, envoit la sortie dans la commande “sort” qui va classer le contenu. Enfin, on envoit ceci dans la commande “cut” pour récupéérer la première partie (parties séparées par des “:“).

Et bien en Go, grâce à “pipe”, le code va être:

package main

import (
    "fmt"

    "gopkg.in/pipe.v2"
)

func main() {
    p := pipe.Line(
        pipe.ReadFile("/etc/passwd"),
        pipe.Exec("sort"),
        pipe.Exec("cut", "-f", "1", "-d", ":"),
    )

    if output, err := pipe.CombinedOutput(p); err != nil {
        fmt.Printf("%v\n", err)
    } else {
        fmt.Printf("%s", output)
    }
}

Pour faire simple: “pipe.Line” attend autant d’argument que de partie de pipeline à brancher entre eux. À la fin, vous pouvez récupérer la sortie “combinée”. Vous pouvez aussi bien utiliser “pipe.DividedOutput()” qui retournera la sortie standard, la sortie d’errer et une “erreur” (dans cet ordre).

Bref, c’est jolie.

Et quoi d’autre ?

On ne s’arrête pas aux simples “commandes brnachées entre elles”, car Gustavo a pensé à ajouter deux trois fonctions bien sympatiques.

Par exemple, on trouvera:

  • pipe.ReadFile(path) qui lit le contenu d’un fichier
  • pipe.WriteFile(path) qui écrit dans un fichier
  • pipe.Filter() qui prend en paramètre une fonction pour filtrer la sortie
  • pip.Tee() pour permettre la double redirection (comme le fait la commande “tee”)
  • et d’autres…

Vous pouvez voir la liste des fonctions ici: https://godoc.org/gopkg.in/pipe.v2 - inutile de vous préciser qu’il est important de lire la doc !.

Créer sa propre commande

Au lieu d’appeler des commande “sh”, vous pouvez créer votre propre fonction qui peut être injectée dans le pipeline. C’est très simple (ou presque).

L’idée c’est que “pipe.Line()” attend des “pipe.Pipe”. Et justement, une fonction du package permet de créer cette structure en fonction de l’état du pipeline. Si c’est pas génial ça !

Admettons que je veuille créer une fonction “ToUpper” qui puisse chopper le flux et mettre les caractères en lettre capitale.

La fonction sera alors:

func ToUpper() pipe.Pipe {
    // returns the pipe.Pipe created by
    // a new Task
    return pipe.TaskFunc(func(s *pipe.State) error {
        var err error
        b := make([]byte, bytes.MinRead)

        // read stream
        for err == nil {
            if _, err = s.Stdin.Read(b); err != nil {
                break
            }

            // create buffer with uppercase letters
            // and copy it to output
            buff := bytes.NewBuffer(nil)
            buff.Write(bytes.ToUpper(b))
            _, err = io.Copy(s.Stdout, buff)
        }

        if err != io.EOF {
            return err
        }
        return nil
    })
}

Je clarifie un point: une tâche (pipe.Task) utilise un état (pipe.State) dans lequel les entrées/sorties sont définies. Le “pipe.Pipe” aura donc le contexte du pipeline en cours. C’est évident quand on y réfléchis.

J’aurai put utiliser “ioutil” et faire un “ReadAll” sur “s.Stdin”, mais l’idée c’est de “streamer”, donc de récupérer “coup par coup” la sortie de la tâche qui fourni le flux. Donc, je préfère lire par paquet de 512 octets (bytes.MinRead), et envoyer les données dans “s.Stdout” au fur et à mesure.

Il ne reste plus qu’à injecter ma fonction dans le pipeline:

p := pipe.Line(
    pipe.ReadFile("/etc/passwd"),
    pipe.Exec("sort"),
    ToUpper(),
)

Et ça marche super bien !

Vous imaginez maintenant comment faire des choses sympas ? Par exemple, lire le contenu d’un flux HTTP, l’envoyer dans une commande de traitement et l’enregistrer dans un fichier tout en l’envoyant dans un port ouvert.

Ça ressemblerai à ça:

p := pipe.Line(
    ReadHTTP("http://url.to.read"),
    ToUpper(),
    Tee(portWriter),
    WriteFile("save.stream.txt"),
)

En gros, ça va soulager pas mal de projets qui ont des flux à balancer un peu partout.

En bref, c’est du “pipeline” propre et sans bavure.

Bref, bilan ?

Inutile de préciser que certaines choses ne peuvent pas marcher partout. Par exemple la commande “Exec” devra utiliser des commandes qui existent sur l’OS cible.

L’auteur ne précise pas franchement comment créer un “pipe.Pipe” proprement, j’ai du lire le code pour comprendre. C’est un peu dommage mais honnêtement un peu de lecture de la doc plus approfondie m’aurait permi de trouver, de suite, la section qui en parle

Voilà, personnellement je vais garder ça dans un coin parce que je suis persuadé que ça va me servir très bientôt.

comments powered by Disqus