homeASCIIcasts

242: Thor 

(view original Railscast)

Other translations: En Ru

Other formats:

Written by Juan Lupión

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.

/lib/tasks/setup.thor

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.

/lib/tasks/setup.thor

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.

/lib/tasks/setup.thor

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.

/lib/tasks/setup.thor

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 config/private.yml 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.

/lib/tasks/setup.thor

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.

/lib/tasks/setup.thor

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.

/lib/tasks/setup.thor

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.