]> jfr.im git - z_archive/twitter.git/blame - twitter/cmdline.py
Cleanup help text
[z_archive/twitter.git] / twitter / cmdline.py
CommitLineData
3756d82e 1# encoding: utf-8
7364ea65 2"""
5251ea48 3USAGE:
7364ea65 4
5251ea48 5 twitter [action] [options]
6
086fc282 7
5251ea48 8ACTIONS:
086fc282 9 authorize authorize the command-line tool to interact with Twitter
527c550f 10 follow follow a user
5251ea48 11 friends get latest tweets from your friends (default action)
45688301 12 help print this help text that you are currently reading
527c550f
MV
13 leave stop following a user
14 list get list of a user's lists; give a list name to get
15 tweets from that list
16 mylist get list of your lists; give a list name to get tweets
17 from that list
5251ea48 18 public get latest public tweets
527c550f 19 replies get latest replies to you
6af9fa5f 20 search search twitter (Beware: octothorpe, escape it)
5251ea48 21 set set your twitter status
527c550f 22 shell login to the twitter shell
5251ea48 23
086fc282 24
5251ea48 25OPTIONS:
26
0ea01db7 27 -r --refresh run this command forever, polling every once
28 in a while (default: every 5 minutes)
29 -R --refresh-rate <rate> set the refresh rate (in seconds)
30 -f --format <format> specify the output format for status updates
21e3bd23 31 -c --config <filename> read username and password from given config
39a6f562
MV
32 file (default ~/.twitter)
33 -l --length <count> specify number of status updates shown
34 (default: 20, max: 200)
35 -t --timestamp show time before status lines
527c550f
MV
36 -d --datestamp show date before status lines
37 --no-ssl use less-secure HTTP instead of HTTPS
7f1dd286 38 --oauth <filename> filename to read/store oauth credentials to
086fc282 39
0ea01db7 40FORMATS for the --format option
41
42 default one line per status
43 verbose multiple lines per status, more verbose status info
327e556b
MV
44 urls nothing but URLs
45 ansi ansi colour (rainbow mode)
05b85831 46
086fc282 47
21e3bd23 48CONFIG FILES
49
086fc282
MV
50 The config file should be placed in your home directory and be named .twitter.
51 It must contain a [twitter] header, and all the desired options you wish to
52 set, like so:
21e3bd23 53
54[twitter]
327e556b 55format: <desired_default_format_for_output>
05b85831 56prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
086fc282
MV
57
58 OAuth authentication tokens are stored in the file .twitter_oauth in your
59 home directory.
7364ea65 60"""
61
6c527e72
MV
62CONSUMER_KEY='uS6hO2sV6tDKIOeVjhnFnQ'
63CONSUMER_SECRET='MEYTOS97VvlHX7K1rwHPEqVpTSqZ71HtvoK4sVuYk'
64
5251ea48 65import sys
0ea01db7 66import time
f2a7ce46 67from getopt import gnu_getopt as getopt, GetoptError
f068ff42 68from getpass import getpass
0ea01db7 69import re
21e3bd23 70import os.path
71from ConfigParser import SafeConfigParser
a4b5e65b 72import datetime
fd2bc885 73from urllib import quote
6c527e72 74import webbrowser
5251ea48 75
76from api import Twitter, TwitterError
1b31d642
MV
77from oauth import OAuth, write_token_file, read_token_file
78from oauth_dance import oauth_dance
0b9960a3 79import ansi
5251ea48 80
327e556b 81OPTIONS = {
5251ea48 82 'action': 'friends',
0ea01db7 83 'refresh': False,
84 'refresh_rate': 600,
85 'format': 'default',
05b85831 86 'prompt': '[cyan]twitter[R]> ',
21e3bd23 87 'config_filename': os.environ.get('HOME', '') + os.sep + '.twitter',
6c527e72 88 'oauth_filename': os.environ.get('HOME', '') + os.sep + '.twitter_oauth',
39a6f562
MV
89 'length': 20,
90 'timestamp': False,
91 'datestamp': False,
9a148ed1 92 'extra_args': [],
6c527e72 93 'secure': True,
5251ea48 94}
95
96def parse_args(args, options):
7f1dd286 97 long_opts = ['help', 'format=', 'refresh', 'oauth=',
8ddd8500 98 'refresh-rate=', 'config=', 'length=', 'timestamp',
9a148ed1 99 'datestamp', 'no-ssl']
39a6f562 100 short_opts = "e:p:f:h?rR:c:l:td"
8ddd8500 101 opts, extra_args = getopt(args, short_opts, long_opts)
efa0ba89 102
5251ea48 103 for opt, arg in opts:
086fc282 104 if opt in ('-f', '--format'):
0ea01db7 105 options['format'] = arg
106 elif opt in ('-r', '--refresh'):
107 options['refresh'] = True
108 elif opt in ('-R', '--refresh-rate'):
109 options['refresh_rate'] = int(arg)
39a6f562
MV
110 elif opt in ('-l', '--length'):
111 options["length"] = int(arg)
112 elif opt in ('-t', '--timestamp'):
113 options["timestamp"] = True
114 elif opt in ('-d', '--datestamp'):
115 options["datestamp"] = True
5251ea48 116 elif opt in ('-?', '-h', '--help'):
05b85831 117 options['action'] = 'help'
21e3bd23 118 elif opt in ('-c', '--config'):
119 options['config_filename'] = arg
9a148ed1
MV
120 elif opt == '--no-ssl':
121 options['secure'] = False
7f1dd286
MV
122 elif opt == '--oauth':
123 options['oauth_filename'] = arg
efa0ba89 124
05b85831 125 if extra_args and not ('action' in options and options['action'] == 'help'):
ae1d86aa 126 options['action'] = extra_args[0]
127 options['extra_args'] = extra_args[1:]
a8b5ad3e 128
87be041f 129def get_time_string(status, options, format="%a %b %d %H:%M:%S +0000 %Y"):
39a6f562
MV
130 timestamp = options["timestamp"]
131 datestamp = options["datestamp"]
87be041f 132 t = time.strptime(status['created_at'], format)
a4b5e65b
MV
133 i_hate_timezones = time.timezone
134 if (time.daylight):
135 i_hate_timezones = time.altzone
136 dt = datetime.datetime(*t[:-3]) - datetime.timedelta(
137 seconds=i_hate_timezones)
138 t = dt.timetuple()
39a6f562
MV
139 if timestamp and datestamp:
140 return time.strftime("%Y-%m-%d %H:%M:%S ", t)
141 elif timestamp:
142 return time.strftime("%H:%M:%S ", t)
143 elif datestamp:
144 return time.strftime("%Y-%m-%d ", t)
8ddd8500 145 return ""
5251ea48 146
147class StatusFormatter(object):
a55c0ac8
MV
148 def __call__(self, status, options):
149 return (u"%s%s %s" %(
39a6f562 150 get_time_string(status, options),
0ea01db7 151 status['user']['screen_name'], status['text']))
5251ea48 152
0b9960a3
MV
153class AnsiStatusFormatter(object):
154 def __init__(self):
155 self._colourMap = ansi.ColourMap()
a8b5ad3e 156
39a6f562 157 def __call__(self, status, options):
0b9960a3 158 colour = self._colourMap.colourFor(status['user']['screen_name'])
39a6f562
MV
159 return (u"%s%s%s%s %s" %(
160 get_time_string(status, options),
0b9960a3 161 ansi.cmdColour(colour), status['user']['screen_name'],
05b85831
HN
162 ansi.cmdReset(), status['text']))
163
f068ff42 164class VerboseStatusFormatter(object):
39a6f562 165 def __call__(self, status, options):
f068ff42 166 return (u"-- %s (%s) on %s\n%s\n" %(
167 status['user']['screen_name'],
168 status['user']['location'],
169 status['created_at'],
0ea01db7 170 status['text']))
f068ff42 171
0ea01db7 172class URLStatusFormatter(object):
173 urlmatch = re.compile(r'https?://\S+')
39a6f562 174 def __call__(self, status, options):
0ea01db7 175 urls = self.urlmatch.findall(status['text'])
176 return u'\n'.join(urls) if urls else ""
177
3c2b5e3e 178
69851f4c
AL
179class ListsFormatter(object):
180 def __call__(self, list):
181 if list['description']:
182 list_str = u"%-30s (%s)" % (list['name'], list['description'])
183 else:
184 list_str = u"%-30s" % (list['name'])
185 return u"%s\n" % list_str
186
187class ListsVerboseFormatter(object):
188 def __call__(self, list):
189 list_str = u"%-30s\n description: %s\n members: %s\n mode:%s\n" % (list['name'], list['description'], list['member_count'], list['mode'])
190 return list_str
191
192class AnsiListsFormatter(object):
193 def __init__(self):
194 self._colourMap = ansi.ColourMap()
195
196 def __call__(self, list):
197 colour = self._colourMap.colourFor(list['name'])
198 return (u"%s%-15s%s %s" %(
199 ansi.cmdColour(colour), list['name'],
200 ansi.cmdReset(), list['description']))
201
202
1c11e6d7 203class AdminFormatter(object):
efa0ba89 204 def __call__(self, action, user):
da45d039
MV
205 user_str = u"%s (%s)" %(user['screen_name'], user['name'])
206 if action == "follow":
e02facc9 207 return u"You are now following %s.\n" %(user_str)
da45d039 208 else:
e02facc9 209 return u"You are no longer following %s.\n" %(user_str)
efa0ba89 210
1c11e6d7 211class VerboseAdminFormatter(object):
efa0ba89
MV
212 def __call__(self, action, user):
213 return(u"-- %s: %s (%s): %s" % (
05b85831
HN
214 "Following" if action == "follow" else "Leaving",
215 user['screen_name'],
efa0ba89
MV
216 user['name'],
217 user['url']))
218
87be041f
WD
219class SearchFormatter(object):
220 def __call__(self, result, options):
221 return(u"%s%s %s" %(
222 get_time_string(result, options, "%a, %d %b %Y %H:%M:%S +0000"),
223 result['from_user'], result['text']))
224
225class VerboseSearchFormatter(SearchFormatter):
a8b5ad3e
MV
226 pass #Default to the regular one
227
87be041f
WD
228class URLSearchFormatter(object):
229 urlmatch = re.compile(r'https?://\S+')
230 def __call__(self, result, options):
231 urls = self.urlmatch.findall(result['text'])
232 return u'\n'.join(urls) if urls else ""
233
234class AnsiSearchFormatter(object):
235 def __init__(self):
236 self._colourMap = ansi.ColourMap()
a8b5ad3e 237
87be041f
WD
238 def __call__(self, result, options):
239 colour = self._colourMap.colourFor(result['from_user'])
240 return (u"%s%s%s%s %s" %(
241 get_time_string(result, options, "%a, %d %b %Y %H:%M:%S +0000"),
242 ansi.cmdColour(colour), result['from_user'],
243 ansi.cmdReset(), result['text']))
244
5a77e17a
MV
245_term_encoding = None
246def get_term_encoding():
247 global _term_encoding
248 if not _term_encoding:
249 lang = os.getenv('LANG', 'unknown.UTF-8').split('.')
250 if lang[1:]:
251 _term_encoding = lang[1]
252 else:
253 _term_encoding = 'UTF-8'
254 return _term_encoding
255
87be041f 256formatters = {}
1c11e6d7 257status_formatters = {
0ea01db7 258 'default': StatusFormatter,
259 'verbose': VerboseStatusFormatter,
0b9960a3
MV
260 'urls': URLStatusFormatter,
261 'ansi': AnsiStatusFormatter
05b85831 262}
87be041f 263formatters['status'] = status_formatters
1c11e6d7
WD
264
265admin_formatters = {
efa0ba89
MV
266 'default': AdminFormatter,
267 'verbose': VerboseAdminFormatter,
327e556b
MV
268 'urls': AdminFormatter,
269 'ansi': AdminFormatter
1c11e6d7 270}
87be041f 271formatters['admin'] = admin_formatters
efa0ba89 272
87be041f
WD
273search_formatters = {
274 'default': SearchFormatter,
275 'verbose': VerboseSearchFormatter,
276 'urls': URLSearchFormatter,
277 'ansi': AnsiSearchFormatter
278}
279formatters['search'] = search_formatters
280
69851f4c
AL
281lists_formatters = {
282 'default': ListsFormatter,
283 'verbose': ListsVerboseFormatter,
284 'urls': None,
285 'ansi': AnsiListsFormatter
286}
287formatters['lists'] = lists_formatters
288
87be041f
WD
289def get_formatter(action_type, options):
290 formatters_dict = formatters.get(action_type)
291 if (not formatters_dict):
a8b5ad3e 292 raise TwitterError(
87be041f
WD
293 "There was an error finding a class of formatters for your type (%s)"
294 %(action_type))
295 f = formatters_dict.get(options['format'])
296 if (not f):
efa0ba89 297 raise TwitterError(
87be041f
WD
298 "Unknown formatter '%s' for status actions" %(options['format']))
299 return f()
efa0ba89 300
0ea01db7 301class Action(object):
ec894371
MV
302
303 def ask(self, subject='perform this action', careful=False):
05b85831
HN
304 '''
305 Requests fromt he user using `raw_input` if `subject` should be
306 performed. When `careful`, the default answer is NO, otherwise YES.
307 Returns the user answer in the form `True` or `False`.
308 '''
f47ab046
MV
309 sample = '(y/N)'
310 if not careful:
311 sample = '(Y/n)'
a8b5ad3e 312
05b85831
HN
313 prompt = 'You really want to %s %s? ' %(subject, sample)
314 try:
315 answer = raw_input(prompt).lower()
316 if careful:
f47ab046 317 return answer in ('yes', 'y')
05b85831 318 else:
f47ab046 319 return answer not in ('no', 'n')
05b85831
HN
320 except EOFError:
321 print >>sys.stderr # Put Newline since Enter was never pressed
322 # TODO:
323 # Figure out why on OS X the raw_input keeps raising
324 # EOFError and is never able to reset and get more input
325 # Hint: Look at how IPython implements their console
f47ab046
MV
326 default = True
327 if careful:
328 default = False
05b85831 329 return default
a8b5ad3e 330
05b85831
HN
331 def __call__(self, twitter, options):
332 action = actions.get(options['action'], NoSuchAction)()
333 try:
334 doAction = lambda : action(twitter, options)
335 if (options['refresh'] and isinstance(action, StatusAction)):
336 while True:
337 doAction()
338 time.sleep(options['refresh_rate'])
339 else:
340 doAction()
341 except KeyboardInterrupt:
342 print >>sys.stderr, '\n[Keyboard Interrupt]'
343 pass
344
345class NoSuchActionError(Exception):
0ea01db7 346 pass
347
348class NoSuchAction(Action):
349 def __call__(self, twitter, options):
05b85831 350 raise NoSuchActionError("No such action: %s" %(options['action']))
0ea01db7 351
8ddd8500 352def printNicely(string):
862cce81
MV
353 if sys.stdout.encoding:
354 print string.encode(sys.stdout.encoding, 'replace')
355 else:
356 print string.encode('utf-8')
a8b5ad3e 357
0ea01db7 358class StatusAction(Action):
359 def __call__(self, twitter, options):
39a6f562 360 statuses = self.getStatuses(twitter, options)
87be041f 361 sf = get_formatter('status', options)
0ea01db7 362 for status in statuses:
39a6f562 363 statusStr = sf(status, options)
0ea01db7 364 if statusStr.strip():
862cce81 365 printNicely(statusStr)
1c11e6d7 366
87be041f
WD
367class SearchAction(Action):
368 def __call__(self, twitter, options):
369 # We need to be pointing at search.twitter.com to work, and it is less
370 # tangly to do it here than in the main()
371 twitter.domain="search.twitter.com"
e15fbef3 372 twitter.uriparts=()
fd2bc885
WD
373 # We need to bypass the TwitterCall parameter encoding, so we
374 # don't encode the plus sign, so we have to encode it ourselves
5a77e17a
MV
375 query_string = "+".join(
376 [quote(term.decode(get_term_encoding()))
377 for term in options['extra_args']])
fd2bc885 378
e15fbef3 379 results = twitter.search(q=query_string)['results']
87be041f
WD
380 f = get_formatter('search', options)
381 for result in results:
382 resultStr = f(result, options)
383 if resultStr.strip():
384 printNicely(resultStr)
a8b5ad3e 385
1c11e6d7 386class AdminAction(Action):
efa0ba89 387 def __call__(self, twitter, options):
ec894371 388 if not (options['extra_args'] and options['extra_args'][0]):
e02facc9 389 raise TwitterError("You need to specify a user (screen name)")
87be041f 390 af = get_formatter('admin', options)
e02facc9
MV
391 try:
392 user = self.getUser(twitter, options['extra_args'][0])
393 except TwitterError, e:
394 print "There was a problem following or leaving the specified user."
f47ab046
MV
395 print "You may be trying to follow a user you are already following;"
396 print "Leaving a user you are not currently following;"
397 print "Or the user may not exist."
398 print "Sorry."
e02facc9 399 print
45688301 400 print e
e02facc9 401 else:
862cce81 402 printNicely(af(options['action'], user))
efa0ba89 403
69851f4c
AL
404class ListsAction(StatusAction):
405 def getStatuses(self, twitter, options):
3c2b5e3e
MV
406 if not options['extra_args']:
407 raise TwitterError("Please provide a user to query for lists")
408
409 screen_name = options['extra_args'][0]
410
411 if not options['extra_args'][1:]:
412 lists = twitter.user.lists(user=screen_name)['lists']
413 if not lists:
414 printNicely("This user has no lists.")
415 for list in lists:
69851f4c
AL
416 lf = get_formatter('lists', options)
417 printNicely(lf(list))
3c2b5e3e
MV
418 return []
419 else:
420 return reversed(twitter.user.lists.list.statuses(
421 user=screen_name, list=options['extra_args'][1]))
422
423
424class MyListsAction(ListsAction):
425 def getStatuses(self, twitter, options):
426 screen_name = twitter.account.verify_credentials()['screen_name']
427 options['extra_args'].insert(0, screen_name)
428 return ListsAction.getStatuses(self, twitter, options)
429
69851f4c 430
0ea01db7 431class FriendsAction(StatusAction):
39a6f562
MV
432 def getStatuses(self, twitter, options):
433 return reversed(twitter.statuses.friends_timeline(count=options["length"]))
efa0ba89 434
0ea01db7 435class PublicAction(StatusAction):
39a6f562
MV
436 def getStatuses(self, twitter, options):
437 return reversed(twitter.statuses.public_timeline(count=options["length"]))
0ea01db7 438
9a9f7ae7 439class RepliesAction(StatusAction):
39a6f562
MV
440 def getStatuses(self, twitter, options):
441 return reversed(twitter.statuses.replies(count=options["length"]))
9a9f7ae7 442
1c11e6d7 443class FollowAction(AdminAction):
efa0ba89 444 def getUser(self, twitter, user):
70955aae 445 return twitter.friendships.create(id=user)
efa0ba89 446
1c11e6d7 447class LeaveAction(AdminAction):
efa0ba89 448 def getUser(self, twitter, user):
70955aae 449 return twitter.friendships.destroy(id=user)
1c11e6d7 450
0ea01db7 451class SetStatusAction(Action):
452 def __call__(self, twitter, options):
5a77e17a 453 statusTxt = (" ".join(options['extra_args']).decode(get_term_encoding())
05b85831 454 if options['extra_args']
772fbdd1 455 else unicode(raw_input("message: ")))
456 status = (statusTxt.encode('utf8', 'replace'))
0ea01db7 457 twitter.statuses.update(status=status)
5251ea48 458
05b85831 459class TwitterShell(Action):
ec894371
MV
460
461 def render_prompt(self, prompt):
05b85831
HN
462 '''Parses the `prompt` string and returns the rendered version'''
463 prompt = prompt.strip("'").replace("\\'","'")
464 for colour in ansi.COLOURS_NAMED:
465 if '[%s]' %(colour) in prompt:
466 prompt = prompt.replace(
a8b5ad3e 467 '[%s]' %(colour), ansi.cmdColourNamed(colour))
05b85831
HN
468 prompt = prompt.replace('[R]', ansi.cmdReset())
469 return prompt
a8b5ad3e 470
05b85831
HN
471 def __call__(self, twitter, options):
472 prompt = self.render_prompt(options.get('prompt', 'twitter> '))
473 while True:
ec894371 474 options['action'] = ""
05b85831
HN
475 try:
476 args = raw_input(prompt).split()
477 parse_args(args, options)
478 if not options['action']:
479 continue
480 elif options['action'] == 'exit':
481 raise SystemExit(0)
482 elif options['action'] == 'shell':
483 print >>sys.stderr, 'Sorry Xzibit does not work here!'
484 continue
485 elif options['action'] == 'help':
486 print >>sys.stderr, '''\ntwitter> `action`\n
a8b5ad3e 487 The Shell Accepts all the command line actions along with:
05b85831 488
a8b5ad3e 489 exit Leave the twitter shell (^D may also be used)
05b85831 490
a8b5ad3e 491 Full CMD Line help is appended below for your convinience.'''
05b85831
HN
492 Action()(twitter, options)
493 options['action'] = ''
494 except NoSuchActionError, e:
495 print >>sys.stderr, e
496 except KeyboardInterrupt:
497 print >>sys.stderr, '\n[Keyboard Interrupt]'
498 except EOFError:
499 print >>sys.stderr
500 leaving = self.ask(subject='Leave')
501 if not leaving:
502 print >>sys.stderr, 'Excellent!'
503 else:
504 raise SystemExit(0)
505
45688301
MV
506class HelpAction(Action):
507 def __call__(self, twitter, options):
508 print __doc__
509
086fc282
MV
510class DoNothingAction(Action):
511 def __call__(self, twitter, options):
512 pass
513
5251ea48 514actions = {
086fc282 515 'authorize' : DoNothingAction,
05b85831
HN
516 'follow' : FollowAction,
517 'friends' : FriendsAction,
69851f4c 518 'list' : ListsAction,
3c2b5e3e 519 'mylist' : MyListsAction,
05b85831
HN
520 'help' : HelpAction,
521 'leave' : LeaveAction,
522 'public' : PublicAction,
523 'replies' : RepliesAction,
87be041f 524 'search' : SearchAction,
05b85831
HN
525 'set' : SetStatusAction,
526 'shell' : TwitterShell,
5251ea48 527}
528
21e3bd23 529def loadConfig(filename):
327e556b 530 options = dict(OPTIONS)
21e3bd23 531 if os.path.exists(filename):
532 cp = SafeConfigParser()
533 cp.read([filename])
086fc282 534 for option in ('format', 'prompt'):
327e556b
MV
535 if cp.has_option('twitter', option):
536 options[option] = cp.get('twitter', option)
537 return options
ae1d86aa 538
327e556b
MV
539def main(args=sys.argv[1:]):
540 arg_options = {}
44405280 541 try:
327e556b 542 parse_args(args, arg_options)
44405280
MV
543 except GetoptError, e:
544 print >> sys.stderr, "I can't do that, %s." %(e)
545 print >> sys.stderr
05b85831 546 raise SystemExit(1)
21e3bd23 547
7f22c021 548 config_path = os.path.expanduser(
327e556b 549 arg_options.get('config_filename') or OPTIONS.get('config_filename'))
7f22c021 550 config_options = loadConfig(config_path)
efa0ba89 551
327e556b
MV
552 # Apply the various options in order, the most important applied last.
553 # Defaults first, then what's read from config file, then command-line
554 # arguments.
555 options = dict(OPTIONS)
556 for d in config_options, arg_options:
557 for k,v in d.items():
558 if v: options[k] = v
05b85831 559
e02facc9
MV
560 if options['refresh'] and options['action'] not in (
561 'friends', 'public', 'replies'):
562 print >> sys.stderr, "You can only refresh the friends, public, or replies actions."
0ea01db7 563 print >> sys.stderr, "Use 'twitter -h' for help."
086fc282 564 return 1
05b85831 565
7f22c021
MV
566 oauth_filename = os.path.expanduser(options['oauth_filename'])
567
086fc282 568 if (options['action'] == 'authorize'
7f22c021 569 or not os.path.exists(oauth_filename)):
1b31d642
MV
570 oauth_dance(
571 "the Command-Line Tool", CONSUMER_KEY, CONSUMER_SECRET,
572 options['oauth_filename'])
6c527e72 573
7f22c021 574 oauth_token, oauth_token_secret = read_token_file(oauth_filename)
8ddd8500 575
9a148ed1 576 twitter = Twitter(
086fc282
MV
577 auth=OAuth(
578 oauth_token, oauth_token_secret, CONSUMER_KEY, CONSUMER_SECRET),
1cc9ab0b 579 secure=options['secure'],
9c07e547
MV
580 api_version='1',
581 domain='api.twitter.com')
086fc282 582
5251ea48 583 try:
05b85831
HN
584 Action()(twitter, options)
585 except NoSuchActionError, e:
586 print >>sys.stderr, e
587 raise SystemExit(1)
5251ea48 588 except TwitterError, e:
9c07e547 589 print >> sys.stderr, str(e)
5251ea48 590 print >> sys.stderr, "Use 'twitter -h' for help."
05b85831 591 raise SystemExit(1)