シェルライクなTwitterクライアントをざっくり作ってみた
昨日Twitterでシェルっぽい感じのTwitterクライアントあったらおもしろくない? とか言っていて, なんか楽しそうだったのでざっくり作ってみました.
使い方はこんな感じ.
$ ruby twsh.rb -u your_twitter_id -p password twsh% echo "はろー twsh.rb" twsh% ls screen_name1: tweet1 screen_name2: tweet2 screen_name3: tweet3 screen_name4: tweet4 screen_name5: tweet5 twsh% exit $
下記コードをtwsh.rbとかで保存して, Rubyとして実行してあげれば起動します. -pオプションはパスワードで必須ですが, -uによるTwitter IDは省略すると環境変数USERが使われます.
後はechoで発言, lsでTL取得です. exit(quit, logout)でシェルを抜けます*1.
ちなみにTwsh.rbとして実装されていないコマンドを用いると, 普段使っているシェルに実行を委ねます. そしてバッククォートによるコマンドの実行を備えているので:
twsh% echo "ボクの$ unameは" `uname` です
とかやると, "ボクの$ unameは Darwin です"って出せたり.
適当で抜けとかありますが, 興味がある方は適当にいじっちゃってくだしあ.
あ, あとボクがTwitterでぶつくさ言っていたら@omasanoriさんが興味を持ったようでして, PythonでTwsh.pyを作っているみたいです! たぶんTwsh.rbよりクールなやつができあがると思いますよ!
require 'optparse' require 'stringio' require 'readline' require 'rubygems' require 'twitter' module Twsh class Logout < StandardError; end class Shell class << self def login(argv, env) new argv, env end end def initialize(argv, env) username = env['USER'] password = nil OptionParser.new do |opt| opt.on('-u VAL') {|v| username = v} opt.on('-p VAL') {|v| password = v} opt.parse! argv end fail if username.nil? || password.nil? @client = Twitter::Base.new Twitter::HTTPAuth.new username, password input = Input.new self logged_in = true while logged_in begin output = execute input.read puts output if output rescue Logout logged_in = false rescue Interrupt puts logged_in = false end end end attr_reader :client def execute(argv) return if argv.empty? command = argv.shift Command.new(self).execute command, argv end end class Command @@commands = {} class << self def setup fail unless block_given? yield new end end def initialize(twsh = nil) @twsh = twsh end def on(*commands, &process) fail unless block_given? commands.each {|command| @@commands[command] = process} end def execute(command, argv) process = @@commands[command] if process args = [] args << @twsh if process.arity >= 1 args << argv if process.arity >= 2 output = nil StringIO.open '', 'w' do |io| orig = $stdout $stdout = io process.call *args $stdout = orig output = io.string unless io.string.empty? end output else `#{command} #{argv.join ' '}`.chomp end end end class Input def initialize(twsh) @twsh = twsh end def read read_with_context end private def read_with_context(context = nil, argv = []) prompt = context ? "#{context}> " : argv.empty? ? "twsh% " : '> ' input = Readline.readline prompt, true context, argv, continue = parse "#{input}\n", context, argv continue ? read_with_context(context, argv) : argv end def parse(input, context = nil, argv = []) escape = false continue = false v = !argv.empty? ? argv.pop : '' new = lambda do argv << v unless v.empty? v = '' end input.each_byte do |c| if escape escape = false case c when LINE_FEED continue = true v << c if context when ?n v << "\n" if quoted? context when ?t v << "\t" if quoted? context when ?f v << "\f" if quoted? context when ?v v << "\v" if quoted? context else v << c end else case c when ?\n v << c if context when ?\s if context v << c else new.call end when ?' if context == :quote new.call context = nil elsif context v << c else context = :quote end when ?" if context == :dquote new.call context = nil elsif context v << c else context = :dquote end when ?` if context == :bquote v = @twsh.execute(parse(v)[1]) context = nil elsif context v << c else context = :bquote end when ?\\ escape = true else v << c end end end new.call [context, argv, continue || !context.nil?] end def quoted?(context) context == :quote || context == :dquote end end end Twsh::Command.setup do |c| c.on('exit', 'quit', 'logout') { raise Twsh::Logout } c.on 'echo' do |twsh, argv| twsh.client.update argv.join ' ' end c.on 'ls' do |twsh, argv| n = 5 OptionParser.new do |opt| opt.on('-n VAL', Integer) {|v| n = v} opt.parse! argv end twsh.client.home_timeline(:count => n).each do |status| puts "#{status.user.screen_name}: #{status.text}" end end c.on 'cd' do |twsh, argv| if argv.empty? Dir.chdir else Dir.chdir File.expand_path argv.first end end end Twsh::Shell.login ARGV, ENV
*1:Ctrl + cでもいいんですが, Readlineが捉えちゃっているのかInterruptがうまく投げられない……