#242 Thor
- Download:
- source codeProject Files in Zip (96.5 KB)
- mp4Full Size H.264 Video (13.2 MB)
- m4vSmaller H.264 Video (9.11 MB)
- webmFull Size VP8 Video (25 MB)
- ogvFull Size Theora Video (18.1 MB)
La mayoría de desarrolladores estarán familiarizados hasta cierto punto con Rake. Rake fué escrito originalmente como alternativa al comando make
pero en Rails se usa por lo general para ejecutar scripts de tareas administrativas, cosa para la que Rake puede resultar un poco limitado. Por ejemplo pasar argumentos a una tarea Rake no es elegante y por lo general hay que recurrir a pasarlas como variables de entorno. Otro problema con Rake es que no es fácil que las tareas sean globales, siempre son locales al proyecto en el que estemos. Herramientas como Sake vienen a solucionar este problema pero ya nos obligan a utilizar una herramienta adicional.
En este episodio le daremos un repaso a Thor. Se trata de una alternativa a Rake que no tiene las limitaciones anteriores. Viene incluido como dependencia de Rails por lo que si tenemos Rails instalado entonces Thor ya se encuentra en nuestro sistema porque los generadores de Rails hacen uso de esta herramienta. Nos será de gran ayuda, por tanto, dominar Thor si queremos aprender a crear nuestros propios generadores.
Se trabaja con Thor desde la línea de órdenes: si ejecutamos thor help
veremos una lista de las opciones que soporta.
$ thor help Tasks: thor help [TASK] # Describe available tasks or one specific task thor install NAME # Install an optionally named Thor file into your system tasks thor installed # List the installed Thor modules and tasks thor list [SEARCH] # List the available thor tasks (--substring means .*SEARCH) thor uninstall NAME # Uninstall a named Thor module thor update NAME # Update a Thor file from its original location thor version # Show Thor version
Aún no tenemos ningún script propio. Pasemos a escribir uno a continuación.
Una tarea para copiar ficheros de configuración
Puestos a crear nuestro primer script de Thor, mejor escribir uno que haga algo útil. Es costumbre en las aplicaciones Rails no poner bajo control de versiones ciertos ficheros de configuración porque contienen contraseñas y otros datos sensibles. Vamos a poner ciertos archivos de configuración de ejemplo en un subdirectorio examples
y luego crearemos un script de Thor que copiará estos ficheros al directorio config
.
Los scripts de Thor se pueden poner en el directorio lib/tasks
de una aplicación (igual que las tareas de Rake), por lo que crearemos en este directorio un nuevo fichero llamado setup.thor
. Un script de Thor se define como una clase que hereda de thor
, y el nombre de la clase determinará el espacio de nombres para las órdenes, mientras que cada método en la clase que tenga una descripción se convertirá en una orden. Estas descripciones se definen con el método desc
, que recibe dos argumentos: el nombre de la orden y su descripción. Empezaremos creando una orden sencilla llamada config
que mostrará una línea de texto.
class Setup < Thor desc "config", "copy configuration files" def config puts "running config" end end
Podemos lanzar esta orden ejecutando thor setup:config
, que llamará al método config
y veremos la salida en la ventana del terminal.
$ thor setup:config running config
Podemos escribir thor list
para ver el listado de órdenes disponibles.
$ thor list setup ----- thor setup:config # copy configuration files
Vemos que en la salida aparece el comando recién creado junto con su descripción.
Copiado de archivos
Hagamos que la orden config
haga algo realmente útil. El código de debajo se recorre todos los archivos del directorio config/examples
y los va copiando a /config
, saltándose los archivos que ya existan en este último.
class Setup < Thor desc "config", "copy configuration files" def config Dir["config/examples/*"].each do |source| destination = "config/#{File.basename(source)}" if File.exist?(destination) puts "Skipping #{destination} because it already exists" else puts "Generating #{destination}" FileUtils.cp(source, destination) end end end end
Si ahora ejecutamos la orden config
una vez más, debería copiar todos los archivos.
$ thor setup:config Generating config/database.yml Generating config/private.yml
Si volvemos a ejecutar la orden se saltará estos archivos porque ya existen en el directorio config
.
$ thor setup:config Skipping config/database.yml because it already exists Skipping config/private.yml because it already exists
Resultaría útil poder pasar la opción --force
de forma que se sobreescriban los archivos aunque éstos ya existan en el fichero de destino. Esto puede hacerse ejecutando method_options
para declarar las opciones antes de la definición del método.
class Setup < Thor desc "config", "copy configuration files" method_options :force => :boolean def config Dir["config/examples/*"].each do |source| destination = "config/#{File.basename(source)}" FileUtils.rm(destination) if options[:force] if File.exist?(destination) puts "Skipping #{destination} because it already exists" else puts "Generating #{destination}" FileUtils.cp(source, destination) end end end end
Podemos añadir todas las opciones que queramos y se soportan varios tipos diferentes: cadenas, números, etc. Para recuperar el valor asignado a una opción dada se invoca a options
: en el código anterior hemos utilizado options[:force]
para leer la opción :force
y borrar el archivo si dicha opción está armada.
Si ahora ejecutamos la orden con la opción --force
se sobreescribirán los archivos ya existentes.
$ thor setup:config --force Generating config/database.yml Generating config/private.yml
Más opciones
Todos los argumentos adicionales que se le pasen a thor
serán también pasados al método. Supongamos que queremos una forma de especificar qué archivos son los que se copian de forma que si sólo queremos copiar el archivo private.yml
tendríamos que ejecutar
$ thor setup:config private.yml
Este argumento será pasado al método config
. No queremos forzar que se tenga que especificar un nombre de archivo por lo que haremos que el argumento tenga por defecto el valor "*"
para que se copien todos los archivos. Es buena idea tratar de mantener la documentación actualizada por lo que también actualizaremos la descripción para que mencione el argumento NAME
.
class Setup < Thor desc "config [NAME]", "copy configuration files" method_options :force => :boolean def config(name = "*") Dir["config/examples/#{name}"].each do |source| destination = "config/#{File.basename(source)}" FileUtils.rm(destination) if options[:force] if File.exist?(destination) puts "Skipping #{destination} because it already exists" else puts "Generating #{destination}" FileUtils.cp(source, destination) end end end end
Invoquemos la orden con este nuevo argumento para ver si funciona.
$ thor setup:config private.yml Skipping <code>config/private.yml</code> because it already exists
Funciona, y también lo hará si le pasamos un nombre junto con la opción --force
.
$ thor setup:config private.yml --force Generating config/private.yml
Instalación global de órdenes
Este script es tan útil que nos gustaría poder usarlo desde otras aplicaciones Rails. Thor hace que esto sea sencillo, lo único que hay que hacer es ejecutar thor install <ruta_del_archivo>
y esto instalará dicha orden en la biblioteca de órdenes de Thor del sistema.
$ thor install lib/tasks/setup.thor Your Thorfile contains: class Setup < Thor desc "config [NAME]", "copy configuration files" method_options :force => :boolean def config(name = "*") Dir["config/examples/#{name}"].each do |source| destination = "config/#{File.basename(source)}" FileUtils.rm(destination) if options[:force] if File.exist?(destination) puts "Skipping #{destination} because it already exists" else puts "Generating #{destination}" FileUtils.cp(source, destination) end end end end Do you wish to continue [y/N]? y Please specify a name for lib/tasks/setup.thor in the system repository [setup.thor]: Storing thor file in your system repository
Una vez instalado podemos ejecutar thor list
desde cualquier directorio y veremos que nuestra orden ya aparecerá.
$ cd ~ $ thor list setup ----- thor setup:config [NAME] # copy configuration files
Como se ve, ya podemos ejecutar esta nueva orden desde cualquiera de nuestras aplicaciones Rails.
Acceso a una aplicación Rails desde Thor
Hay más cosas que queremos demostrar de Thor. Para esto, vamos a crear una nueva orden en nuestra clase <ocde>Setup</ocde>. Esta nueva orden generará algunos registros en la base de datos, por lo que la bautizaremos como populate
. Se trata de nuestra aplicación de bitácora que tiene un modelo Article
, y haremos que cree diez artículos automáticamente.
class Setup < Thor desc "config [NAME]", "copy configuration files" method_options :force => :boolean def config(name = "*") # Se ha omitido la definicion del metodo config end desc "populate", "generate records" def populate 10.times do |num| puts "Generating article #{num}" Article.create!(:name => "Article #{num}") end end end
Al ejecutar esta orden recibiremos un mensaje de error diciendo que la clase Article
no está definida. Esto es así porque el script no ha cargado los modelos de la aplicación, y Thor no carga por defecto el entorno de Rails así que tenemos que cargar la aplicación antes de que la orden intente crear una instancia del modelo. Por suerte esto es muy fácil, tan sólo tenemos que requerir el fichero config/environment
.
class Setup < Thor desc "config [NAME]", "copy configuration files" method_options :force => :boolean def config(name = "*") # Se ha omitido la definicion del metodo config end desc "populate", "generate records" def populate require ‘./config/environment’ 10.times do |num| puts "Generating article #{num}" Article.create!(:name => "Article #{num}") end end end
Si ahora ejecutamos la orden otra vez debería crear diez artículos tras un pequeño lapso de tiempo durante el que se produce la carga del entorno.
$ thor setup:populate Generating article 0 Generating article 1 Generating article 2 Generating article 3 Generating article 4 Generating article 5 Generating article 6 Generating article 7 Generating article 8 Generating article 9
Podría ser muy útil que fuese configurable el número de articulos a crear, para poder crear cuantos artículos queramos. Podemos hacerlo con method_options
igual que hicimos con la orden config
.
class Setup < Thor desc "config [NAME]", "copy configuration files" method_options :force => :boolean def config(name = "*") # Se ha omitido la definicion del metodo config end desc "populate", "generate records" method_options :count => 10 def populate require './config/environment' options[:count].times do |num| puts "Generating article #{num}" Article.create!(:name => "Article #{num}") end end end
Esta vez, en lugar de especificar un tipo para las opciones hemos pasado un valor por defecto a partir del cual Thor deducirá el tipo. Si ahora pasamos un count
de 5
se crearán cinco artículos.
$ thor setup:populate --count 5 Generating article 0 Generating article 1 Generating article 2 Generating article 3 Generating article 4
Con esto cerramos este episodio dedicado a Thor. Para más información al respecto, la documentación es un buen sitio por donde empezar especialmente para aprender más acerca del paso de opciones.
La gran pregunta es: ¿cuándo deberíamos usar Rake y cuándo usar Thor? Si estamos creando una aplicación Rails sencilla es mejor seguir usando Rake porque se trata de la herramienta más popular y conocida, mientras que si tenemos que crear muchas tareas administrativas en nuestras aplicaciones Rails entonces merecerá másSe ha omitido la definicion del método config la pena considerar el uso de Thor.