Construyendo un comando CLI con Go: cowsay

¿Te gustan las aplicaciones CLI?No te pierdas elgato graciosotutorial también!

Cowsayes una de esas aplicaciones sin las que no puedes vivir.

Básicamente genera imágenes ASCII de una vaca con cualquier mensaje al que se la pase, en la captura de pantalla anterior usandofortunepara generarlo. Pero no se limita al dominio de las vacas, puede imprimir pingüinos, alces y muchos otros animales.

¡Suena como una aplicación útil para portar a Go!

Además, me gusta la licencia en inglés simple adjunta:

==============
cowsay License
==============

cowsay is distributed under the same licensing terms as Perl: the Artistic License or the GNU General Public License. If you don’t want to track down these licenses and read them for yourself, use the parts that I’d prefer:

(0) I wrote it and you didn’t.

(1) Give credit where credit is due if you borrow the code for some other purpose.

(2) If you have any bugfixes or suggestions, please notify me so that I may incorporate them.

(3) If you try to make money off of cowsay, you suck.

Comencemos por definir el problema. Queremos aceptar información a través de una tubería y que nuestra vaca lo diga.

La primera iteración lee la entrada del usuario de la tubería y la imprime. No es demasiado complicado.

package main

import ( “bufio” “fmt” “io” “os” )

func main() { info, _ := os.Stdin.Stat()

<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">info</span>.<span style="color:#a6e22e">Mode</span>()<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ModeCharDevice</span> <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"The command is intended to work with pipes."</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"Usage: fortune | gocowsay"</span>)
	<span style="color:#66d9ef">return</span>
}

<span style="color:#a6e22e">reader</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufio</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>)
<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">output</span> []<span style="color:#66d9ef">rune</span>

<span style="color:#66d9ef">for</span> {
	<span style="color:#a6e22e">input</span>, <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">ReadRune</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">EOF</span> {
		<span style="color:#66d9ef">break</span>
	}
	<span style="color:#a6e22e">output</span> = append(<span style="color:#a6e22e">output</span>, <span style="color:#a6e22e">input</span>)
}

<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">j</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">j</span> &lt; len(<span style="color:#a6e22e">output</span>); <span style="color:#a6e22e">j</span><span style="color:#f92672">++</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">"%c"</span>, <span style="color:#a6e22e">output</span>[<span style="color:#a6e22e">j</span>])
}

}

Nos falta la vaca, y también tenemos que envolver el mensaje en un globo, bien formateado.

Aquí está la primera iteración de nuestro programa:

package main

import ( “bufio” “fmt” “io” “os” “strings” “unicode/utf8” )

// buildBalloon takes a slice of strings of max width maxwidth // prepends/appends margins on first and last line, and at start/end of each line // and returns a string with the contents of the balloon func buildBalloon(lines []string, maxwidth int) string { var borders []string count := len(lines) var ret []string

<span style="color:#a6e22e">borders</span> = []<span style="color:#66d9ef">string</span>{<span style="color:#e6db74">"/"</span>, <span style="color:#e6db74">"\\"</span>, <span style="color:#e6db74">"\\"</span>, <span style="color:#e6db74">"/"</span>, <span style="color:#e6db74">"|"</span>, <span style="color:#e6db74">"&lt;"</span>, <span style="color:#e6db74">"&gt;"</span>}

<span style="color:#a6e22e">top</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">" "</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Repeat</span>(<span style="color:#e6db74">"_"</span>, <span style="color:#a6e22e">maxwidth</span><span style="color:#f92672">+</span><span style="color:#ae81ff">2</span>)
<span style="color:#a6e22e">bottom</span> <span style="color:#f92672">:=</span> <span style="color:#e6db74">" "</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Repeat</span>(<span style="color:#e6db74">"-"</span>, <span style="color:#a6e22e">maxwidth</span><span style="color:#f92672">+</span><span style="color:#ae81ff">2</span>)

<span style="color:#a6e22e">ret</span> = append(<span style="color:#a6e22e">ret</span>, <span style="color:#a6e22e">top</span>)
<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">count</span> <span style="color:#f92672">==</span> <span style="color:#ae81ff">1</span> {
	<span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">"%s %s %s"</span>, <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">5</span>], <span style="color:#a6e22e">lines</span>[<span style="color:#ae81ff">0</span>], <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">6</span>])
	<span style="color:#a6e22e">ret</span> = append(<span style="color:#a6e22e">ret</span>, <span style="color:#a6e22e">s</span>)
} <span style="color:#66d9ef">else</span> {
	<span style="color:#a6e22e">s</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">`%s %s %s`</span>, <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">0</span>], <span style="color:#a6e22e">lines</span>[<span style="color:#ae81ff">0</span>], <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">1</span>])
	<span style="color:#a6e22e">ret</span> = append(<span style="color:#a6e22e">ret</span>, <span style="color:#a6e22e">s</span>)
	<span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">1</span>
	<span style="color:#66d9ef">for</span> ; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">count</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
		<span style="color:#a6e22e">s</span> = <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">`%s %s %s`</span>, <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">4</span>], <span style="color:#a6e22e">lines</span>[<span style="color:#a6e22e">i</span>], <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">4</span>])
		<span style="color:#a6e22e">ret</span> = append(<span style="color:#a6e22e">ret</span>, <span style="color:#a6e22e">s</span>)
	}
	<span style="color:#a6e22e">s</span> = <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">`%s %s %s`</span>, <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">2</span>], <span style="color:#a6e22e">lines</span>[<span style="color:#a6e22e">i</span>], <span style="color:#a6e22e">borders</span>[<span style="color:#ae81ff">3</span>])
	<span style="color:#a6e22e">ret</span> = append(<span style="color:#a6e22e">ret</span>, <span style="color:#a6e22e">s</span>)
}

<span style="color:#a6e22e">ret</span> = append(<span style="color:#a6e22e">ret</span>, <span style="color:#a6e22e">bottom</span>)
<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Join</span>(<span style="color:#a6e22e">ret</span>, <span style="color:#e6db74">"\n"</span>)

}

// tabsToSpaces converts all tabs found in the strings // found in the lines slice to 4 spaces, to prevent misalignments in // counting the runes func tabsToSpaces(lines []string) []string { var ret []string for _, l := range lines { l = strings.Replace(l, “\t”, " ", -1) ret = append(ret, l) } return ret }

// calculatemaxwidth given a slice of strings returns the length of the // string with max length func calculateMaxWidth(lines []string) int { w := 0 for _, l := range lines { len := utf8.RuneCountInString(l) if len > w { w = len } }

<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">w</span>

}

// normalizeStringsLength takes a slice of strings and appends // to each one a number of spaces needed to have them all the same number // of runes func normalizeStringsLength(lines []string, maxwidth int) []string { var ret []string for _, l := range lines { s := l + strings.Repeat(" ", maxwidth-utf8.RuneCountInString(l)) ret = append(ret, s) } return ret }

func main() { info, _ := os.Stdin.Stat()

<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">info</span>.<span style="color:#a6e22e">Mode</span>()<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">ModeCharDevice</span> <span style="color:#f92672">!=</span> <span style="color:#ae81ff">0</span> {
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"The command is intended to work with pipes."</span>)
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"Usage: fortune | gocowsay"</span>)
	<span style="color:#66d9ef">return</span>
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">lines</span> []<span style="color:#66d9ef">string</span>

<span style="color:#a6e22e">reader</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufio</span>.<span style="color:#a6e22e">NewReader</span>(<span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Stdin</span>)

<span style="color:#66d9ef">for</span> {
	<span style="color:#a6e22e">line</span>, <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">reader</span>.<span style="color:#a6e22e">ReadLine</span>()
	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">io</span>.<span style="color:#a6e22e">EOF</span> {
		<span style="color:#66d9ef">break</span>
	}
	<span style="color:#a6e22e">lines</span> = append(<span style="color:#a6e22e">lines</span>, string(<span style="color:#a6e22e">line</span>))
}

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">cow</span> = <span style="color:#e6db74">`         \  ^__^

\ (oo)_______ (__)\ )/
||----w | || || `

<span style="color:#a6e22e">lines</span> = <span style="color:#a6e22e">tabsToSpaces</span>(<span style="color:#a6e22e">lines</span>)
<span style="color:#a6e22e">maxwidth</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">calculateMaxWidth</span>(<span style="color:#a6e22e">lines</span>)
<span style="color:#a6e22e">messages</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">normalizeStringsLength</span>(<span style="color:#a6e22e">lines</span>, <span style="color:#a6e22e">maxwidth</span>)
<span style="color:#a6e22e">balloon</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">buildBalloon</span>(<span style="color:#a6e22e">messages</span>, <span style="color:#a6e22e">maxwidth</span>)
<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">balloon</span>)
<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">cow</span>)
<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>()

}

Hagamos ahora configurable la figura, agregando unestegosaurio

La aplicación original utiliza el-fbandera para aceptar una figura personalizada. Así que hagamos lo mismo porprocesando una bandera de línea de comando.

Cambio brevemente el programa anterior para introducirprintFigure()

// printFigure given a figure name prints it.
// Currently accepts `cow` and `stegosaurus`.
func printFigure(name string) {
<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">cow</span> = <span style="color:#e6db74">`         \  ^__^

\ (oo)_______ (__)\ )/
||----w | || || `

<span style="color:#66d9ef">var</span> <span style="color:#a6e22e">stegosaurus</span> = <span style="color:#e6db74">`         \                      .       .

\ / </span> <span style="color:#f92672">+</span> <span style="color:#e6db74">"" + . .' " </span><span style="color:#e6db74"> \ .---. &lt; &gt; &lt; &gt; .---. </span><span style="color:#e6db74"> \ | \ \ - ~ ~ - / / | </span><span style="color:#e6db74"> _____ ..-~ ~-..-~ </span><span style="color:#e6db74"> | | \~~~\\.' + ""</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">./~/ --------- _/ _/ .’ O \ / / \ " (_____, </span> <span style="color:#f92672">+</span> <span style="color:#e6db74">"" + ._.' | } \/~~~/ </span><span style="color:#e6db74"> + ""</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">----. / } | / __/ </span> <span style="color:#f92672">+</span> <span style="color:#e6db74">"" + -. | / | / + ""</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">. ,| ~-.| /_ - ~ ^| /- _ </span> <span style="color:#f92672">+</span> <span style="color:#e6db74">"" + ..-' </span><span style="color:#e6db74"> | / | / ~-. + ""</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">-. _ _ _ || |__| ~ - . _ _ _ _ _> `

<span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">name</span> {
<span style="color:#66d9ef">case</span> <span style="color:#e6db74">"cow"</span>:
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">cow</span>)
<span style="color:#66d9ef">case</span> <span style="color:#e6db74">"stegosaurus"</span>:
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#a6e22e">stegosaurus</span>)
<span style="color:#66d9ef">default</span>:
	<span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Println</span>(<span style="color:#e6db74">"Unknown figure"</span>)
}

}

y cambiandomain()aceptar una bandera y pasarla aprintFigure():

func main() {
	//...

	var figure string
	flag.StringVar(&figure, "f", "cow", "the figure name. Valid values are `cow` and `stegosaurus`")
	flag.Parse()
<span style="color:#75715e">//...

printFigure(figure) fmt.Println() }

tocar

Creo que estamos en un buen momento. Solo quiero que este sistema sea utilizable, sin ejecutargo run main.go, así que solo escribirégo buildygo install.

Ahora puedo pasar el día congololcaty gocowsay


Más tutoriales de go: