在有关Rake的第二篇文章中,我们将更深入地探讨,并介绍一些稍高级的主题,例如文件任务,规则,多任务等等,这些将大大改善Rake的排骨。
主题
- 免责声明
- 默认任务
- 任务对象
- 文件任务
- 目录方法
- 文件实用程序
- 规则
- 跟踪标志
- 并行任务
免责声明
我想从更一般的角度探讨这个主题。 本文不是为您显示带有巧妙解决方案的Rake任务列表,这些解决方案可以在无需过多考虑的情况下进行复制和粘贴。 它更适合在幕后看待,同时对新手友好,对那些除了在Rails中具有明显的Rake任务而尚未与Rake玩过很多游戏的人也很感兴趣。
我认为,对工具及其提供的工具的理解会带来更高的回报。 我希望你不要介意。 对我而言,涵盖这些原则更有价值,并且对于初学者而言更容易接触到,这取决于您在自己的应用程序中如何使用它。
默认任务
我相信您至少在某个地方以前听说过该词。 但是,默认任务到底是什么? 这没什么神奇的,但是让我们快速解决这个问题。 当您运行rake
没有一个耙子任务的任何其他名称,默认任务被执行。
贝壳
rake
一些Rakefile
- desc 'This is the default task. No arguments necessary'
- task :default do
- puts 'Some task that you might wanna run on a regular basis'
- end
在Rails中,默认任务应该运行您的测试。 您的猜测与我的猜测一样好,但是我想这是测试需要比任何其他任务更频繁运行的结果。 当您在Rails中重新定义default
Rake任务时,它只会累加Rails定义的任务-不会重新定义它。 这实际上是Rake的工作方式。 当您重新定义Rake任务时,您将添加之前的定义。
文件任务
顾名思义,它们是您在文件(和目录)上执行的任务。 但是,他们有一些技巧。 当然,Rake将在很多时间处理文件。 有人为您的方便而认识到这种模式并创建了专门的文件任务也就不足为奇了,特别是出于避免重复或浪费处理能力的简单原因。
将文件从一种类型转换为另一种类型是非常常见的任务。 来源是您的依赖关系,任务名称是file
关键字之后的名称。 将Markdown转换为HTML文件,将HTML文件转换为电子书格式,将JPG图像转换为PNG图像,编译源代码,构建静态页面或仅更改文件扩展名以及许多其他选项供您使用。 我们可以手动完成所有这些操作,但这当然是乏味且无效的。 为此编写代码更加优雅和可扩展。
使用文件任务与“常规”任务没有太大区别。 如果您通过rake -T
请求Rake任务列表,它们也会显示出来。 实际上,Rake平等地对待所有任务-除了multitask
任务之外。 添加描述和前提条件对于处理文件任务也没有问题。
实际上,先决条件是在处理源文件之前先提及它们的必要条件。 我们需要存在源才能使它正常工作-当然,作为依赖项是有意义的。 没有它,Rake将不知道如何继续—毕竟它不能凭空创建新文件。
一些Rakefile
- file 'mi6/q/gadgets/secret_list.md' => 'mi6/research/secret_list.md' do
- cp 'mi6/research/secret_list.md', 'mi6/q/gadgets/secret_list.md'
- end
文件任务的名称基本上是目标文件,即您要创建的文件。 前提条件是任务所需的源文件。 在该块内部,您正在告诉Rake如何创建所需的输出-如何使用已经存在的必备文件来构建它。 输入输出。 例如,这可以是使用pandoc
工具的shell命令,该命令可将Markdown文件转换为HTML文件。 文件任务的应用程序不胜枚举。 不过,语法起初可能有些怪异。 我知道了。
Rake首先检查目标文件是否存在,如果存在,则检查时间戳是否早于必备文件(基于时间的依赖关系)。 如果时间戳早于必备条件,或者文件尚不存在,Rake将运行文件任务。 如果您需要处理多个文件,那么这非常方便-这特别酷,因为您不需要仅因为更改了集合中的单个文件就可以重建大量文件。 与此相反,常规的Rake任务总是运行-他们不检查任何时间戳或其他更改,除非您这样做,否则当然。
文件实用程序
一些Rakefile
- desc 'Change some file extension'
- file 'some_file.new_extension' => 'some_file.old_extension' do
- mv 'some_file.old_extension', 'some_file.new_extension'
- end
贝壳
- $rake some_file.new_extension
-
- => mv some_file.old_extension some_file.new_extension
如果您想知道上一个示例或上面的mv
命令中的cp
方法,让我们来谈谈文件实用程序。 我们本可以使用sh mv ...
在Rake任务中执行Shell命令。 对我们来说幸运的是,我们可以使用一个模块,该模块使像这样的Shell命令变得不再那么冗长且独立于平台。 FileUtils
是一个Ruby模块,其中包含许多用于文件操作的unixy命令:
-
rm
-
cp
-
mv
-
mkdir
- 等等…
如果不是发明轮子,FileUtils将是处理文件的有用伴侣。 通常,Rake是您所需要的,但是偶尔,您会很高兴这个方便的模块得到您的支持。 为了您的方便, RakeUtils
对该模块进行了稍微扩展。
让我们看一下您可以使用的清单,然后放大一些您可能感兴趣的特定清单:
- cd(dir, options)
- cd(dir, options) {|dir| .... }
- pwd()
- mkdir(dir, options)
- mkdir(list, options)
- mkdir_p(dir, options)
- mkdir_p(list, options)
- rmdir(dir, options)
- rmdir(list, options)
- ln(old, new, options)
- ln(list, destdir, options)
- ln_s(old, new, options)
- ln_s(list, destdir, options)
- ln_sf(src, dest, options)
- cp(src, dest, options)
- cp(list, dir, options)
- cp_r(src, dest, options)
- cp_r(list, dir, options)
- mv(src, dest, options)
- mv(list, dir, options)
- rm(list, options)
- rm_r(list, options)
- rm_rf(list, options)
- install(src, dest, mode = <src's>, options)
- chmod(mode, list, options)
- chmod_R(mode, list, options)
- chown(user, group, list, options)
- chown_R(user, group, list, options)
- touch(list, options)
尽管我假设您是新手,但我也假设您曾经使用过Rails,并且知道非常基本的Unix实用程序-诸如mv
, cd
, pwd
, mkdir
东西。 如果没有,请做功课再回来。
在Rakefiles中,您可以立即使用这些方法。 为避免误解,这是一个Ruby层,它“模仿”了这些Unix命令,您可以在Rakefiles中使用它们,而无需使用诸如sh
类的任何前缀-执行Shell命令。 顺便说一句,您在上面列表中看到的options
意味着选项的哈希值{}
。 让我们看一下一些有趣的命令,这些命令可以很方便地编写文件任务:
-
sh
这使您可以从Ruby文件中执行Shell命令。
-
cd
这是一个非常基本的命令,但是此命令有些很棒。 如果为cd
提供一个块,它将把当前目录更改为它的目的地,按照块中的定义进行业务,然后返回到上一个工作目录以继续。 整洁,实际上!
-
cp_r
使您可以递归地批量复制文件和目录。
-
mkdir_p
创建目标目录及其所有指定的父目录。 对我们来说幸运的是,我们在Rake中拥有directory
方法,它甚至更加方便,因此我们不需要它。
-
touch
如果文件存在,它将更新文件的时间戳;如果不存在,则创建它。
-
identical?
让您检查两个文件是否相同。
目录方法
在Rake中,您无需使用mkdir
或mkdir_p
即可方便地定义目录。 当您需要建立嵌套目录时,它特别方便。 如果您需要通过多个文件任务来构建目录结构,而这些文件任务具有大量的目录结构先决条件,那么文件夹树可能会很痛苦。 将directory
方法视为文件夹任务。
一些Rakefile
directory 'mi6/q/special_gadgets'
这样就可以毫无问题地创建相关目录。 马上就可能不明显的是,您可以像其他任何rake任务一样依赖它作为先决条件。 只要确保文件任务的名称(其名称)包括您依赖的目录即可。 如果有多个任务依赖于它,它将仍然仅被创建一次。
- directory 'mi6/q/gadgets'
-
- desc 'Transfer secret research gadgets'
- file 'mi6/q/gadgets/gadget_list.md' => 'mi6/q/gadgets' do
- cp 'gadget_list.md', 'mi6/q/special_gadgets/secret_gadget_list.md'
- end
正如您在此处看到的那样,Rake非常一致,并考虑将所有内容作为任务构建。 谢谢,吉姆,这使生活变得轻松!
规则
在处理任务(实际上是文件任务)时,规则可以帮助我们减少重复。 代替指示Rake在特定文件(例如somefile.markdown
上执行任务,我们可以教Rake在某种类型的文件(例如模式或蓝图)上执行这些任务。 转换一组文件而不是单个文件是一种更加通用和干燥的方法。 当我们为共享相似特征的文件定义模式时,此类任务的伸缩性会更好。
一些Rakefile
- file "quartermaster_gadgets.html" => "quartermaster_gadgets.markdown" do
- sh "pandoc -s quartermaster_gadgets.markdown -o quartermaster_gadgets.html"
- end
如您所见,拥有一堆文件将很难保持这种方式。 是的,我们可以编写自己的脚本,在其中将文件列表保存在数组中并对其进行迭代,但是我们可以做得更好—更好。
另一个不希望看到的副作用是,每次我们运行这样的脚本时,所有HTML文件都会被重建-即使它们根本没有变化。 大量文件会使您等待时间更长或占用的资源过多。 我们不需要任何额外的代码即可处理多个文件。 Rake在该部门中做得更好,效率更高,因为它仅在此期间触摸文件时才执行其文件任务或规则。
一些Rakefile
- rule ".html" => ".markdown" do |rule|
- sh "pandoc -s #{rule.source} -o #{rule.name}"
- end
当我们定义上述规则时,我们拥有一种机制,可以将任何扩展名为.markdown
的文件转换为.html
文件。 有了规则,Rake首先会为特定文件(例如quartermaster_gadgets.html
寻找任务。 但是,当找不到它时,它使用简单的.html
规则来寻找可以成功执行的源。 这样,您不必创建一长串文件,而只需使用定义了如何处理某些文件任务的通用“规则”。 太棒了!
任务对象
在上面的规则中,我们使用任务对象(在本例中为规则对象,更确切地说是)。 我们可以将它作为一个块参数传递给闭包,并在其上调用方法。 与文件任务一样,规则都与任务源,其依赖关系(例如降价文件)及其任务名称有关。
从块(和文件任务)的规则正文中,我们可以访问规则的名称和源。 我们可以通过提取从参数传入的名称信息rule.name
及其源(又名文件源)通过rule.source
。 上面,我们可以避免重复文件的名称,而将模式通用化。 同样,我们可以使用rules.prerequisites
获取先决条件或依赖项的列表。 当然,对于文件任务或任何其他任务,同样适用。
谈到依赖关系,它们可以像要迭代的列表一样起作用。 如果您正确玩纸牌,则无需创建单独的each
循环。
- task :html => %W[quartermaster_gadgets.html, research_gadgets.html]
-
- rule ".html" => ".md" do |r|
- sh "pandoc -s #{r.source} -o #{r.name}"
- end
如您所见,我们不需要手动遍历文章列表。 我们只是简单地将Rake投入使用并使用了依赖项-这更加简单和简洁。
对于DRYing而言,更酷的是规则可以将proc对象(匿名函数对象,基本上是lambda)作为先决条件。 这意味着我们不仅可以传递单个模式作为前提,还可以传递更具动态性的内容,使我们能够投射出比单个鱼捕获更多的模式。 例如, .markdown
和.md
文件的规则。
他们将具有相同的规则主体,但前提条件不同。 这就像为proc对象返回的每个对象定义一个新的File任务。 当然,使用规则的另一种方法是正则表达式。 您将模式作为依赖项进行传递,如果匹配,则可以执行文件任务。 甜蜜的选择,不是吗?
- some_markdown_list = [...]
-
- detect_source = proc do |html_file_name|
- some_markdown_list.detect { |markdown_source| markdown_source.ext == html_file_name.ext }
- end
-
- rule '.html' => detect_source do |r|
- sh "pandoc -s #{r.source} -o #{r.name}"
- end
Procs和Lambdas之间的区别
如果您是lambda领域的新手,或者还没有完全了解它,可以尝试一下。 Procs是可以传递并稍后执行的对象,lambda也是。 顺便说一下,它们都是Proc对象。 差异是细微的,可以归结为传递给它们的参数。 Lambda会检查参数的数量,因此可能会因ArgumentError
而崩溃—进程无关紧要。 另一个不同之处在于它们对return语句的处理。 Procs超出了执行proc对象的范围。 Lambda仅退出lambda范围,并继续触发下一个符合要求的代码。 在这里不是很重要,但是我认为对于你们当中的新手来说,也不会感到伤害。
有用的标志
这是可以传递给rake任务的标志的简短列表。
-
--rules
向您展示Rake如何尝试应用规则-规则的跟踪。 如果您处理一些规则并遇到错误,这将是非常宝贵的。
贝壳
- $ rake quartermaster_gadgets.html --rules
- Attempting Rule quartermaster_gadgets.html => quartermaster_gadgets.md
- (quartermaster_gadgets.html => quartermaster_gadgets.md ... EXIST)
- pandoc -s quartermaster_gadgets.md -o quartermaster_gadgets.html
-
-t
还记得第一solve_bonnie_situation
文章中的solve_bonnie_situation
任务吗? 让我们将此标记添加到此Rake任务中并打开跟踪。 如果遇到错误,我们也会得到追溯。 这对于调试当然很方便。
贝壳
- $ rake solve_bonnie_situation -t
- ** Invoke solve_bonnie_situation (first_time)
- ** Invoke get_mr_wolf (first_time)
- ** Execute get_mr_wolf
- You ain’t got no problem Jules, I’m on it! Go in there and chill them out and wait for the wolf who should be coming directly!
- ** Invoke calm_down_jimmy (first_time)
- ** Execute calm_down_jimmy
- Jimmy, do me a favor, will you? I smelled some coffee back there. Would you make me a cup?
- ** Invoke figure_out_bonnie_situation (first_time)
- ** Execute figure_out_bonnie_situation
- If I was informed correctly, the clock is ticking. Is that right Jimmy?
- ** Invoke get_vince_vega_in_line (first_time)
- ** Execute get_vince_vega_in_line
- Come again? Get it straight buster. I’m not here to say please! I’m here to tell you what to do!
- ** Invoke clean_car (first_time)
- ** Execute clean_car
- I need you two fellas to take those cleaning products and clean the inside of the car. I’m talking fast, fast, fast!
- ** Invoke clean_crew (first_time)
- ** Execute clean_crew
- Jim, the soap! O.K. gentlemen, you both been to county before I’m sure. Here it comes!
- ** Invoke get_rid_of_evidence_at_monster_joes (first_time)
- ** Execute get_rid_of_evidence_at_monster_joes
- So what’s with the outfits? You guys are going to a Volleyball game or something?
- ** Invoke drive_into_the_sunrise (first_time)
- ** Execute drive_into_the_sunrise
- Call me Winston!
- ** Execute solve_bonnie_situation
- You know, I’d go for breakfast. Feel like having breakfast with me?
追踪规则
在Rakefile本身中设置Rake.application.options.trace_rules = true
会告诉Rake在运行任务时向我们显示有关规则的跟踪信息。 这很酷,因为当我们通过rake -t
运行跟踪时,只有一个标志,我们会获得所需的所有调试信息。 我们不仅获得任务调用的列表,而且还可以看到应用或尝试了哪些规则。
-
-P
显示所有任务的先决条件列表。 在这里,我们再次使用solve_bonnie_situation
任务。 省略其他任务的输出,这将是其单独输出:
贝壳
- $ rake solve_bonnie_situation -P
- ...
- rake solve_bonnie_situation
- get_mr_wolf
- calm_down_jimmy
- figure_out_bonnie_situation
- get_vince_vega_in_line
- clean_car
- clean_crew
- get_rid_of_evidence_at_monster_joes
- drive_into_the_sunrise
- ...
如果您好奇,请运行rake -P
。 非常有趣的输出。
-
-m
将任务作为多任务运行。
并行任务
让我向您介绍multitask
方法。 这可以帮助您加快处理速度-毕竟,在大多数现代计算机上我们拥有多个内核,因此让我们利用它们。 当然,您总是可以通过编写没有任何内容的可靠代码来提高速度,但是并行运行任务肯定可以在这方面给您带来额外的好处。 但是,这里也有陷阱,我们也将介绍。
到目前为止,我们执行的任务依次运行所有任务。 可以肯定的是,如果您的代码正确无误,但它也较慢。 如果速度对于某些任务很重要,我们可以通过多线程任务提供一些帮助。 但是请记住,在某些情况下,顺序方法是更好的选择。
假设我们有三个Rake任务,它们需要作为先决条件运行才能执行第四个任务。 基本上,这是四个线程。 从更大的角度来看,当您一次运行多个应用程序(或更具体而言,流程)时,相同的想法正在起作用。
- multitask :shoot_bond_movie => [:shoot_car_chase, :shoot_love_scene, :shoot_final_confrontation] do
- puts "Principal photography is done and we can start editing."
- end
使用multitask
,前提条件数组中的依赖项现在不再按此顺序执行。 相反,它们是并行传播和运行的,但是当然要在shoot_bond_movie
任务之前执行。 每个任务的Ruby线程将同时运行。 完成后, shoot_bond_movie
将处理业务。 此处任务的执行方式与随机分配相似,但实际上它们是在同一时间简单执行的。
棘手的部分只是确保按您需要的顺序处理某些依赖项。 因此,我们需要注意比赛条件。 基本上,这意味着某些任务会遇到问题,因为执行顺序会产生意想不到的副作用。 这是一个错误。
如果我们能够避免这种情况,我们就可以实现线程安全。 关于常见的先决条件,有趣的是,这些先决条件将仅运行一次,因为多任务先决条件首先等待其完成。
- task :shoot_love_scene do
- ...
- end
-
- task :prepare_italy_set do
- ...
- end
-
- task :shoot_car_chase => [:prepare_italy_set] do
- ...
- end
-
- task :shoot_final_confrontation => [:prepare_italy_set] do
- ...
- end
-
- multitask :shoot_bond_movie => [:shoot_car_chase, :shoot_love_scene, :shoot_final_confrontation] do
- puts "Principal photography is done and we can start editing."
- end
shoot_car_chase
和shoot_final_confrontation
任务都取决于prepare_italy_set
首先完成-顺便说一句,它只能运行一次。 当并行运行任务时,我们可以使用该机制来预测顺序。 如果它对您的任务很重要,请不要只相信执行顺序。
最后的想法
好吧,我想您现在已经准备好撰写一些严肃的Rake业务。 正确使用该工具有望使您作为Ruby开发人员的生活更加愉快。 在第二篇文章中,我希望我能传达出Rake到底是什么简单但奇妙的工具。 它是由一位真正的Craft.io大师创造的。
我们所有人都对Jim Weirich怀有极大的敬意,他们想出了这个优雅的构建工具。 自从他去世以来,Ruby社区肯定并不完全相同。 不过,吉姆的遗产显然可以保留。 我们很荣幸地建立在另一个巨人之上。