MENU

Ruby 命令行实战(一):设定目标

Cover

现实开发中我们总是会碰到各种各样的问题。它可能是为两个不同的系统构建一套通信服务,亦或者是定期运行将一些复杂的自动化任务,再或者是你想实现的一些效率工具。

正如 Unix 哲学中描述的那样,多个功能单一的程序远比一个大而全的复杂的程序更符合工程实践,前者无论是维护性还是组合灵活性都远胜后者。而这一点在命令行程序中体现的尤为明显,几乎所有的类 Unix 的命令行工具都非常简单,但这些命令工具组合后的功能却灵活多变。

本文将会是整个系列文章的开端,我们将会着眼于两个问题:数据备份和任务管理。并且通过两个理想化的命令行工具,我们来一窥整个编码过程。当然本文的代码示例并不是一个完美的方案,我们会在后续文章中逐步进行迭代完善。

问题 1 :数据备份

创业公司或者小团队工作时,我们总是会习惯性的引入敏捷开发流程。大部分采用该方式的项目都会强调测试覆盖率,用于检测各种边缘和复杂情形。为此我们有时可能会预先为数据库准备一组测试数据。虽然测试数据是模拟出来的,但是依旧需要进行备份,因为敏捷开发过程中功能会频繁进行修改甚至是回滚也不一定(多么痛的领悟 😂 )。

在明确问题后,接下来就是将数据库备份工作通过程序自动化了。该程序需要具备以下几个特性:

  • 完整备份任意 MySQL 数据库

  • 备份文件使用日期或者用户输入进行区分

  • 自动压缩数据库备份

下面是该功能的一个简易版本实现,其中数据库信息存储在字典中,另外它还调用了 mysqldumpgzip 命令来实现相关功能:

#!/usr/bin/env ruby

databases = {
  big_client: { 
    database: 'big_client', 
    username: 'big', 
    password: 'big', 
  }, 
  small_client: { 
    database: 'small_client', 
    username: 'small', 
    password: 'p@ssWord!', 
   }
}

end_of_iter = ARGV.shift

databases.each do |name,config|
  if end_of_iter.nil?
    backup_file = config[:database] + '_' + Time.now.strftime('%Y%m%d') 
  else
    backup_file = config[:database] + '_' + end_of_iter 
  end 
  mysqldump = "mysqldump -u#{config[:username]} -p#{config[:password]} " + "#{config[:database]}"

  `#{mysqldump} > #{backup_file}.sql` 
  `gzip #{backup_file}.sql` 
end

请注意:上诉代码中我们使用 ARGV.shift 来接收用户输入并依此作为备份文件名处理依据

下面我们通过该命令处理简单的备份任务:

$ db_backup_initial.rb 
# => creates big_client_20110103.sql.gz 
# => creates small_client_20110103.sql.gz 
$ db_backup_initial.rb iteration_3 
# => creates big_client_iteration_3.sql.gz 
# => creates small_client_iteration_3.sql.gz

当然,上面的代码太过简单而且存在一些问题和可提升空间。例如,我们可以将硬编码的数据库信息也改为从输入中读取,那么该命令行工具的易用性将会得到提升:

#!/usr/bin/env ruby 

database = ARGV.shift 
username = ARGV.shift 
password = ARGV.shift 
end_of_iter = ARGV.shift 

if end_of_iter.nil?
  backup_file = database + Time.now.strftime("%Y%m%d") 
else
  backup_file = database + end_of_iter 
end 
`mysqldump -u#{username} -p#{password} #{database} > #{backup_file}.sql`
`gzip #{backup_file}.sql`

相应的调用方式就变成了:

$ db_backup.rb big_client big big 
# => creates big_client_20110103.sql.gz 
$ db_backup.rb small_client small "p@ssWord!"
# => creates small_client_20110103.sql.gz 
$ db_backup.rb big_client big big iteration_3 
# => creates big_client_iteration_3.sql.gz 
$ db_backup.rb medium_client medium "med_pass" iteration_4 
# => creates medium_client_iteration_4.sql.gz

看起来用户输入变得复杂了,但实际上程序反而变得更加简单易维护了。我们可以启动定时任务进一步简化备份过程中的人为操作。

我会在后面对 db_backup.rb 代码进行迭代,直到它成为一个完整的命令行工具为止。当然自动执行特定任务只是命令行的一种用法。 无论是编辑代码,运行构建还是测试新工具, 命令行工具都能找到一席之地。

问题 2:待办事项

大多数软件项目在开发过程中都会使用到 JIRA、Bugzilla 这类任务管理系统或者 Bug 跟踪系统。这些系统功能非常完善,但是对于一些短期任务来说这些系统又显得过于复杂。因此很多人都会在电脑或者手机上安装 To-Do 类型的 APP 来管理那些相对来说具体且简单的任务。

下面我们就自己动手写一个简单的命令行 To-Do 工具。该工具主要包含三个部分:创建任务、打印任务、查看完成情况。

$ todo new "Add new field to database for 'accepted terms on date'" 
Task added 
$ todo new "Get DBA approval for new field."
Task added 
$ todo list 
1 - Add new field to database for 'accepted terms on date'
   Created: 2011-06-03 13:45 
2 - Get DBA approval for new field.
   Created: 2011-06-03 13:46 
$ todo done 1 
Task 1 completed 
$ todo list 
1 - Add new field to database for 'accepted terms on date'
    Created: 2011-06-03 13:45
    Completed: 2011-06-03 14:00 
2 - Get DBA approval for new field.
    Created: 2011-06-03 13:46

为了实现上诉功能,我们需要一个文件 todo.txt 进行任务记录另外新建脚本程序文件 todo.rb

#!/usr/bin/env ruby 
TODO_FILE = 'todo.txt'
''
def read_todo(line) _
  line.chomp.split(/,/) 
end 

def write_todo(file,name,created=Time.now,completed='')
  file.puts("#{name},#{created},#{completed}") 
end 

command = ARGV.shift 

case command 
when 'new' 
  new_task = ARGV.shift 
  File.open(TODO_FILE,'a') do |file| 
    write_todo(file,new_task) 
    puts "Task added."
  end 
when 'list' 
  File.open(TODO_FILE,'r') do |file| 
    counter = 1 
    file.readlines.each do |line| 
      name,created,completed = read_todo(line) 
      printf("%3d - %s\n",counter,name) 
      printf(" Created : %s\n",created) 
      unless completed.nil?
        printf(" Completed : %s\n",completed) 
      end 
        counter += 1 
    end 
  end 
when 'done' 
  task_number = ARGV.shift.to_i 
  File.open(TODO_FILE,'r') do |file| 
    File.open("#{TODO_FILE}.new",'w') do |new_file| 
      counter = 1 
      file.readlines.each do |line| 
        name,created,completed = read_todo(line) 
        if task_number == counter
          write_todo(new_file,name,created,Time.now) 
          puts "Task #{counter} completed" 
        else 
          write_todo(new_file,name,created,completed) 
        end 
        counter += 1 
      end 
    end 
  end 
  mv #{TODO_FILE}.new #{TODO_FILE}` 
end

总结

本文只是系列文章的第一步,主要内容就是明确问题、设立目标并给出一个简单可行的代码实现。当然这样的代码还不足与被称为一个优秀的命令行工具,还要非常多的工作需要完成。例如:提升命令行工具的易用性,增加文档说明和使用示例,代码的可维护性。针对这些内容,我会后续文章中持续对上述两个代码进行迭代,一步步演示整个工具的进化流程。

标签: 无