Cuando se trata de desarrollo de software, la mayoría de las herramientas disponibles para nosotros son aplicaciones de línea de comandos. También vale la pena señalar que muchas de las herramientas utilizadas en la línea de comandos son bastante poderosas en lo que pueden lograr, desde lo trivial hasta lo tedioso. Además, se pueden combinar las aplicaciones de la línea de comandos para encadenar una secuencia de trabajo y obtener el resultado deseado. El aprendizaje de los comandos de línea de comandos existentes le permite aumentar su productividad y comprender mejor qué capacidades tiene a mano y qué tareas puede necesitar implementar por su cuenta.
Cuando diseñe una herramienta de línea de comandos, realmente debe prestar atención a quién, o qué, es su público objetivo. Por ejemplo, si todas las personas que van a usar esta herramienta utilizan el mismo sistema operativo y el mismo entorno, entonces usted tiene la mayor flexibilidad para aprovechar todo el ecosistema. Sin embargo, si necesitas implementar tu aplicación de línea de comandos para que funcione en varios sistemas operativos, habrás restringido las herramientas que puedes utilizar a las que están disponibles en todos esos sistemas, o tendrás que implementar excepciones para cada sistema operativo en tu aplicación. (Eso puede ser muy tedioso de investigar y probar.)
La solución más utilizada para manejar diversos entornos de sistemas operativos es evitar cualquier herramienta externa específica del sistema operativo cuando sea posible o delegar la responsabilidad a las bibliotecas que ya han hecho el trabajo duro de implementar características para trabajar para múltiples arquitecturas. Cuanto más se pueda mantener la implementación en un lenguaje básico sin depender de dependencias externas, más fácil será mantener el proyecto.
Los componentes de una aplicación de línea de comandos en Ruby
Hay tres áreas de interés que se deben abordar cuando se construye una aplicación de línea de comandos: la entrada, la salida y todo lo que hay en medio. Si estás desarrollando una herramienta de línea de comandos única que simplemente toma una entrada o fuente y la procesa/formatea y escupe la información, entonces tu trabajo es bastante simple. Sin embargo, si usted está desarrollando una interfaz de usuario con menús para navegar, las cosas comienzan a complicarse.
Procesamiento de la entrada
Para comenzar a procesar la entrada, tenemos parámetros que se pueden dar a su aplicación. Estos argumentos son la forma más típica de iniciar un comando, con detalles de cómo quieres que se ejecute y evitar la necesidad de sistemas de menús. En este ejemplo:
ruby -e "puts RUBY_VERSION"
Ruby es el programa de línea de comandos que se está ejecutando, -e
se llama bandera, y "puts RUBY_VERSION"
es el valor dado para la bandera. En este caso, la bandera de Ruby -e
significa ejecutar lo siguiente como código Ruby. Cuando ejecuto lo anterior en la línea de comandos, obtengo de vuelta 2.4.0
impreso en la salida estándar (que simplemente se muestra en la siguiente línea).
Entrada de argumentos
La entrada de argumentos, o parámetros, son todos los bits extra de texto introducidos después de un comando y separados por un espacio. Casi todos los comandos permiten un argumento de ayuda. Un argumento de bandera tiene un guión o dos delante de él.
Las banderas de ayuda estándar son -h
, y --help
. En los días de MSDOS, era (y puede que siga siendo) /?
. No sé si la tendencia de las banderas en Windows ha continuado usando la barra inclinada como marcador de bandera, pero los scripts de línea de comandos multiplataforma estos días usan guiones.
En Ruby, la entrada de una línea de comandos se coloca en dos variables diferentes: ARGV y ARGF. ARGV maneja los parámetros de entrada como una matriz de cadenas; ARGF es para manejar flujos de datos. Puedes utilizar ARGV para los parámetros directamente, pero esto puede suponer más trabajo del que necesitas. Hay un par de bibliotecas de Ruby construidas para trabajar con argumentos de línea de comandos.
OptionParser
OptionParser tiene la ventaja de estar incluido con Ruby, por lo que no es una dependencia externa. OptionParser te da una manera fácil de mostrar las opciones de la línea de comandos disponibles y procesar la entrada en cualquier objeto Ruby que desees. Aquí hay un extracto de una de mis herramientas de línea de comandos dfm:
require 'optionparser'options = {}printers = Array.newOptionParser.new do |opts| opts.banner = "Usage: dfm \nDefaults: dfm -xd ." + File::SEPARATOR opts.on("-f", "--filters FILTERS", Array, "File extension filters") do |filters| options = filters end opts.on("-x", "--duplicates-hex", "Prints duplicate files by MD5 hexdigest") do |dh| printers << "dh" end opts.on("-d", "--duplicates-name", "Prints duplicate files by file name") do |dh| printers << "dn" end opts.on("-s", "--singles-hex", "Prints non-duplicate files by MD5 hexdigest") do |dh| printers << "sh" end opts.on("-n", "--singles-name", "Prints non-duplicate files by file name") do |dh| printers << "sn" endend.parse!
En este ejemplo, cada bloque opts.on
tiene código para ejecutar si la bandera se pasó en la línea de comandos. Las cuatro opciones inferiores son (dentro de su bloque) simplemente añadir la información de la bandera en una matriz para mí para usar más tarde.
El primero tiene Array
dado como uno de los parámetros a on
, por lo que la entrada de esta bandera se convertirá en una matriz de Ruby y se almacena en mi hash llamado options
bajo la clave filters
.
El resto de los parámetros dados al método on
son los detalles de la bandera y la descripción. Tanto las banderas cortas como las largas funcionarán para ejecutar el código dado en el siguiente bloque.
OptionParser también tiene las banderas -h
y --help
incorporadas por defecto, por lo que no es necesario reinventar la rueda. Simplemente escriba dfm -h
para la herramienta de línea de comandos de arriba, y que muy bien las salidas de una descripción útil:
Usage: dfm Defaults: dfm -xd ./ -f, --filters FILTERS File extension filters -x, --duplicates-hex Prints duplicate files by MD5 hexdigest -d, --duplicates-name Prints duplicate files by file name -s, --singles-hex Prints non-duplicate files by MD5 hexdigest -n, --singles-name Prints non-duplicate files by file name
Slop
OptionParser es un poco verboso cuando se trata de escribir las definiciones de línea de comandos. La gema Slop está diseñada para permitirle escribir su propio análisis de la línea de comandos con mucho menos esfuerzo. En lugar de tener que proporcionar los bloques de código en la definición de la bandera, Slop simplemente crea un objeto que puede consultar en su aplicación para ver si se dio una bandera y qué valor(es) puede haber sido proporcionado para ello.
# Excerpt from https://github.com/leejarvis/slopopts = Slop.parse do |o| o.string '-h', '--host', 'a hostname' o.integer '--port', 'custom port', default: 80 o.bool '-v', '--verbose', 'enable verbose mode' o.bool '-q', '--quiet', 'suppress output (quiet mode)' o.bool '-c', '--check-ssl-certificate', 'check SSL certificate for host' o.on '--version', 'print the version' do puts Slop::VERSION exit endendARGV #=> -v --host 192.168.0.1 --check-ssl-certificateopts #=> 192.168.0.1opts.verbose? #=> trueopts.quiet? #=> falseopts.check_ssl_certificate? #=> trueopts.to_hash #=> { host: "192.168.0.1", port: 80, verbose: true, quiet: false, check_ssl_certificate: true }
Esta simplicidad puede ayudar a simplificar su base de código y su conjunto de pruebas, así como acelerar su tiempo de desarrollo.
Salida (STDOUT)
Cuando se escribe una simple herramienta de línea de comandos, a menudo se quiere que la salida de los resultados del trabajo realizado. Teniendo en cuenta cuál es el objetivo de esta herramienta determinará en gran medida cómo quiere que sea la salida.
Puede formatear los datos de salida como una simple cadena, lista, hash, matriz anidada, JSON, XML, u otra forma de datos a consumir. Si tus datos van a ser transmitidos a través de una conexión de red, entonces querrás empaquetar los datos en una cadena de datos apretada. Si está destinado a los ojos de un usuario para ver, entonces usted querrá ampliar de una manera presentable.
Muchas herramientas de línea de comandos de Linux/Mac existentes pueden imprimir detalles en pares o conjuntos de valores. La información puede estar separada por dos puntos, espacios, tabulaciones y bloques de sangría. Cuando no esté seguro de cómo se puede utilizar exactamente, opte por la forma más sencilla y presentable de presentar los datos.
Un ejemplo de un objetivo que puede necesitar considerar es una herramienta de prueba de la API. Muchas APIs proporcionan una respuesta JSON y se puede acceder a ellas con una herramienta de línea de comandos como curl
. Si el ancho de banda es una preocupación, a continuación, utilizar el método de JSON to_json
, pero si está destinado para el trabajo de la máquina local, utilice pretty_generate
.
x = {"hello" => "world", this: {"apple" => 4, tastes: "delicious"}}require 'json'puts x.to_json# {"hello":"world","this":{"apple":4,"tastes":"delicious"}}puts JSON.pretty_generate( x )# {# "hello": "world",# "this": {# "apple": 4,# "tastes": "delicious"# }# }
También puede utilizar YAML para los datos.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Si usted quiere tener una salida más compleja bien formateada, entonces recomiendo encarecidamente el uso de la gema awesome_print. Esto le dará un control específico de su presentación en la salida.
Error estándar (STDERR)
Este es otro tipo de salida que puede ocurrir desde las aplicaciones de línea de comandos. Cuando algo está mal o ha salido mal, es habitual que la herramienta de línea de comandos escriba en la salida conocida como STDERR. Aparecerá como una salida regular, pero otras herramientas pueden verificar que el comando no tuvo éxito.
STDERR.puts "Oops! You broke it!"
La práctica más común es usar un Logger para escribir detalles específicos sobre los errores. Usted puede dirigir eso a la salida STDERR en la línea de comandos o posiblemente a un archivo de registro.
Interfaz de usuario (STDIN y STDOUT)
Escribir una interfaz de usuario puede ser una de las cosas más gratificantes de lograr. Le permite aplicar un poco de diseño artístico y proporcionar diferentes maneras para que un usuario interactúe.
La interfaz de usuario mínima es una pantalla de un poco de texto con una solicitud de entrada. Puede tener un aspecto tan simple como:
Who is your favorite super hero /Batman/Wonder-Woman ?
Lo anterior indica la pregunta, las opciones y una opción por defecto en caso de que el usuario elija pulsar la tecla enter sin introducir nada. En Ruby plano, esto se vería como:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Este es un código muy crudo para estar utilizando para la entrada y salida, y si vas a estar escribiendo una interfaz de usuario, recomiendo encarecidamente probar una gema como highline o tty.
Con la gema highline, podrías escribir lo anterior como:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Aquí, highline mostrará la pregunta, mostrará el valor por defecto, y cogerá cualquier opción incorrecta introducida, notificando al usuario que no ha seleccionado una de las opciones proporcionadas.
Tanto highline como tty tienen una gran cantidad de cosas adicionales que puedes hacer para un menú y la experiencia del usuario, con sutilezas como añadir colores a su pantalla. Pero de nuevo hay que tener en cuenta quién es su público objetivo.
Cuanto más experiencia visual proporcione, más tendrá que prestar atención a las capacidades multiplataforma de estas herramientas. No todas las líneas de comando manejan los mismos datos de presentación de la misma manera, lo que crea una mala experiencia para el usuario.
Compatibilidad entre plataformas
La gran noticia es que Ruby tiene muchas de las soluciones necesarias para las herramientas a través de diferentes sistemas operativos. Cuando Ruby se compila para cualquier sistema operativo específico, el archivo fuente de RbConfig se genera con valores absolutos nativos del sistema en el que se construyó, guardados en formato hash. Por lo tanto, es la clave para detectar y utilizar las características del sistema operativo.
Para ver este archivo con tu editor de texto favorito, puedes ejecutar el siguiente código de Ruby:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Esto te mostrará todo de una manera presentable, que creo que es mejor que ver el hash a través de RbConfig::CONFIG
. Este hash incluye cosas útiles como los comandos usados para manejar el sistema de archivos, la versión de Ruby, la arquitectura del sistema, dónde reside todo lo importante para Ruby, y otras cosas por el estilo.
Para comprobar el sistema operativo, puedes hacer:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
Para otros valores específicos del sistema operativo, puedes mirar el código fuente de Gem::Platform.
Ahora bien, los comandos específicos del sistema de ficheros almacenados aquí no están pensados para ser utilizados desde este recurso. Ruby tiene las clases Dir, File y Pathname escritas para eso.
Cuando necesites saber si un ejecutable existe en el PATH del sistema, entonces querrás usar MakeMakefile.find_executable. Ruby soporta la construcción de extensiones C y una de las buenas características que añadieron para ello es esta capacidad de averiguar si el ejecutable existe para llamar.
Pero esto generará un archivo de registro en el directorio actual de su sistema cada vez que lo ejecute. Así que para evitar escribir este archivo de registro, tendrá que hacer lo siguiente:
require 'mkmf'MakeMakefile::Logging.instance_variable_set(:@log, File.open(File::NULL, 'w'))executable = MakeMakefile.find_executable('clear') # or whatever executable you're looking for
Cuando se dibuja un menú visual de ventana en una línea de comandos, es preferible que el menú permanezca en una posición fija cuando la pantalla se actualiza. La forma más sencilla de hacerlo es borrar la línea de comandos entre cada escritura de la pantalla completa.
En el ejemplo anterior, busqué el comando clear
. En Windows, el comando del shell para limpiar la línea de comandos es cls
, pero no podrá encontrar un ejecutable para él porque es parte del código interno de command.com
.
La buena noticia es que al tomar la cadena de salida del comando clear
de Linux se produce esta secuencia de código de escape: \e[3J\e[H\e[2J
. Yo y otros hemos probado esto en Windows, Mac y Linux, y esto hace exactamente lo que queremos: limpiar la pantalla para redibujarla.
Esta cadena de escape tiene tres acciones diferentes que está realizando:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
Sin embargo, es preferible utilizar una biblioteca de línea de comandos en lugar de escribir los códigos de escape usted mismo. Pero este bien vale la pena mencionarlo.
Testing STDIN/STDOUT/STDERR
Con cualquier cosa que escriba en algo como STDOUT o STDERR, sería más sabio crear una variable interna en su objeto Ruby de la interfaz de usuario que puede ser cambiada con un parámetro opcional a new
. Así que cuando tu programa se ejecuta normalmente, el valor será STDOUT
. Pero cuando escribas las pruebas, pasarás StringIO.new
, con el que podrás probar fácilmente.
Cuando se trata de leer IO desde un comando ejecutado fuera de Ruby, las pruebas son un poco más complicadas. Es probable que tengas que buscar en Open3.popen3 para manejar cada flujo de IO STDIN, STDOUT y STDERR.
Usando comandos Bash
¡Bash ha llegado a Windows (WSL)! Esto significa que usted tiene acceso a los comandos de bash a través de la gran mayoría de los sistemas operativos utilizados en todo el mundo. Esto abre más posibilidades para su propia herramienta de línea de comandos.
Es típico «canalizar» comandos entre sí, enviando la salida de un comando como un flujo al siguiente. Sabiendo esto, es posible que desee considerar la adición de soporte de entrada de flujo, o pensar en cómo formatear mejor su salida para el consumo de otras herramientas de línea de comandos.
Aquí está un ejemplo de una sustitución regex con el comando sed
de Bash recibiendo su entrada canalizada desde echo:
echo hello | sed "s/ll/ck n/g"
Esto produce heck no
. Cuanto más aprendas sobre Bash, mejor capacitado estarás para conseguir cosas en una línea de comandos y mejor equipado estarás para escribir mejores herramientas de línea de comandos propias.
Resumen
Hay mucho que aprender cuando se trata de escribir tus propias herramientas de línea de comandos. Cuantas más situaciones tienes que tener en cuenta, más complejas se vuelven las cosas.
Tómate el tiempo para aprender las herramientas que tienes a mano, y cosecharás muchas recompensas que no sabías que te estabas perdiendo. Te deseo lo mejor.