droxi 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aa77e9e1c5c65c6d9552b77bee94b0c4395b4a74
4
- data.tar.gz: 404cfd0e77d97c811daa867f544b6a2faf440a4a
3
+ metadata.gz: db93fae1cb4f2b8945d7bac19ad38940817f2f6e
4
+ data.tar.gz: 8a8d36e14900fefebba6a090dae94df16cbe7c64
5
5
  SHA512:
6
- metadata.gz: 273cb971fa6a4412c44aafe050e33767063cb4d0c38203858e8b8e6988c19b1884c4c99b688dffdc7cbb661a8e3851a9322ee6fcd2ca3b3f487964eae0a869ac
7
- data.tar.gz: 7c18c86aef2cdfb0cb46c571bf01db930ab87b396ac6ee1f6b5e26de758151b4ce0eb16ce3775245b173cb6eb50579c8e3df1f71a14c0985a3978bde474f7123
6
+ metadata.gz: 8212f5b0d45da0538697258becc8d3f7e6971c6e9b72e66b2fee4aa001c78a4831fc3fe1358384e1a0b574c423a46bc5ebf4264ef647dc359150035a28aaeddb
7
+ data.tar.gz: 99355ccb042b1f05b784d19e4c8ba723c248ce51c292514be89575d1d6ba2a34a3ec468e05c3eafcc77b5ff8af79b3986ed52931b9333b9beea14a51cf031833
data/bin/droxi CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
2
  require_relative '../lib/droxi'
3
- Droxi.run(*(ARGV.reject { |arg| arg == '--debug' }))
3
+ Droxi.run(ARGV.dup)
data/droxi.1.template CHANGED
@@ -2,7 +2,7 @@
2
2
  .SH NAME
3
3
  droxi \- ftp-like command-line interface to Dropbox
4
4
  .SH SYNOPSIS
5
- droxi [COMMAND] [ARGUMENT]...
5
+ droxi [OPTION]... [COMMAND] [ARGUMENT]...
6
6
  .SH DESCRIPTION
7
7
  A command-line Dropbox interface inspired by GNU coreutils, GNU ftp, and lftp.
8
8
  Features include smart tab completion, globbing, and interactive help. If
@@ -10,5 +10,12 @@ invoked without arguments, runs in interactive mode. If invoked with arguments,
10
10
  parses the arguments as a command invocation, executes the command, and exits.
11
11
  .SH COMMANDS
12
12
  %{commands}
13
+ .SH OPTIONS
14
+ .TP
15
+ --debug
16
+ Enable the 'debug' command for the session.
17
+ .TP
18
+ --version
19
+ Print version information and exit.
13
20
  .SH AUTHOR
14
21
  Written by Brandon Mulcahy (brandon@jangler.info).
data/droxi.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'droxi'
3
- s.version = '0.1.1'
4
- s.date = '2014-06-06'
3
+ s.version = IO.read('lib/droxi.rb')[/VERSION = '(.+)'/, 1]
4
+ s.date = '2014-06-07'
5
5
  s.summary = 'ftp-like command-line interface to Dropbox'
6
6
  s.description = "A command-line Dropbox interface inspired by GNU \
7
7
  coreutils, GNU ftp, and lftp. Features include smart tab \
@@ -20,22 +20,19 @@ module Commands
20
20
  attr_reader :description
21
21
 
22
22
  # Create a new +Command+ with the given metadata and a +Proc+ specifying
23
- # its behavior. The +Proc+ will receive four arguments: the
24
- # +DropboxClient+, the +State+, an +Array+ of command-line arguments, and
25
- # a +Proc+ to be called for output.
23
+ # its behavior. The +Proc+ will receive three arguments: the
24
+ # +DropboxClient+, the +State+, and an +Array+ of command-line arguments.
26
25
  def initialize(usage, description, procedure)
27
26
  @usage = usage
28
27
  @description = description.squeeze(' ')
29
28
  @procedure = procedure
30
29
  end
31
30
 
32
- # Attempt to execute the +Command+, yielding lines of output if a block is
33
- # given. Raises a +UsageError+ if an invalid number of command-line
34
- # arguments is given.
31
+ # Attempt to execute the +Command+. Raises a +UsageError+ if an invalid
32
+ # number of command-line arguments is given.
35
33
  def exec(client, state, *args)
36
34
  fail UsageError, @usage unless num_args_ok?(args.size)
37
- block = proc { |line| yield line if block_given? }
38
- @procedure.yield(client, state, args, block)
35
+ @procedure.yield(client, state, args)
39
36
  end
40
37
 
41
38
  # Return a +String+ describing the type of argument at the given index.
@@ -71,7 +68,8 @@ module Commands
71
68
  Dropbox root. With a remote directory name as the argument, changes to \
72
69
  that directory. With - as the argument, changes to the previous working \
73
70
  directory.",
74
- lambda do |_client, state, args, output|
71
+ lambda do |_client, state, args|
72
+ extract_flags('cd', args, '')
75
73
  case
76
74
  when args.empty? then state.pwd = '/'
77
75
  when args.first == '-' then state.pwd = state.oldpwd
@@ -80,7 +78,7 @@ module Commands
80
78
  if state.directory?(path)
81
79
  state.pwd = path
82
80
  else
83
- output.call("cd: #{args.first}: no such directory")
81
+ warn "cd: #{args.first}: no such directory"
84
82
  end
85
83
  end
86
84
  end
@@ -88,13 +86,14 @@ module Commands
88
86
 
89
87
  # Copy remote files.
90
88
  CP = Command.new(
91
- 'cp REMOTE_FILE... REMOTE_FILE',
89
+ 'cp [-f] REMOTE_FILE... REMOTE_FILE',
92
90
  "When given two arguments, copies the remote file or folder at the first \
93
91
  path to the second path. When given more than two arguments or when the \
94
92
  final argument is a directory, copies each remote file or folder into \
95
- that directory.",
96
- lambda do |client, state, args, output|
97
- cp_mv(client, state, args, output, 'cp')
93
+ that directory. Will refuse to overwrite existing files unless invoked \
94
+ with the -f option.",
95
+ lambda do |client, state, args|
96
+ cp_mv(client, state, args, 'cp')
98
97
  end
99
98
  )
100
99
 
@@ -104,16 +103,18 @@ module Commands
104
103
  "Evaluates the given string as Ruby code and prints the result. Won't \
105
104
  work unless the program was invoked with the --debug flag.",
106
105
  # rubocop:disable Lint/UnusedBlockArgument, Lint/Eval
107
- lambda do |client, state, args, output|
106
+ lambda do |client, state, args|
108
107
  if ARGV.include?('--debug')
109
108
  begin
110
- output.call(eval(args.join(' ')).inspect)
109
+ p eval(args.join(' '))
111
110
  # rubocop:enable Lint/UnusedBlockArgument, Lint/Eval
111
+ rescue SyntaxError => error
112
+ warn error
112
113
  rescue => error
113
- output.call(error.inspect)
114
+ warn error.inspect
114
115
  end
115
116
  else
116
- output.call('debug: not enabled.')
117
+ warn 'debug: not enabled.'
117
118
  end
118
119
  end
119
120
  )
@@ -122,7 +123,8 @@ module Commands
122
123
  EXIT = Command.new(
123
124
  'exit',
124
125
  'Exit the program.',
125
- lambda do |_client, state, _args, _output|
126
+ lambda do |_client, state, args|
127
+ extract_flags('exit', args, '')
126
128
  state.exit_requested = true
127
129
  end
128
130
  )
@@ -133,12 +135,13 @@ module Commands
133
135
  "Clear the client-side cache of remote filesystem metadata. With no \
134
136
  arguments, clear the entire cache. If given directories as arguments, \
135
137
  (recursively) clear the cache of those directories only.",
136
- lambda do |_client, state, args, output|
138
+ lambda do |_client, state, args|
139
+ extract_flags('forget', args, '')
137
140
  if args.empty?
138
141
  state.cache.clear
139
142
  else
140
143
  args.each do |arg|
141
- state.forget_contents(arg) { |line| output.call(line) }
144
+ state.forget_contents(arg) { |line| warn line }
142
145
  end
143
146
  end
144
147
  end
@@ -146,19 +149,25 @@ module Commands
146
149
 
147
150
  # Download remote files.
148
151
  GET = Command.new(
149
- 'get REMOTE_FILE...',
152
+ 'get [-f] REMOTE_FILE...',
150
153
  "Download each specified remote file to a file of the same name in the \
151
- local working directory.",
152
- lambda do |client, state, args, output|
154
+ local working directory. Will refuse to overwrite existing files unless \
155
+ invoked with the -f option.",
156
+ lambda do |client, state, args|
157
+ flags = extract_flags('get', args, '-f')
153
158
  state.expand_patterns(args).each do |path|
154
159
  if path.is_a?(GlobError)
155
- output.call("get: #{path}: no such file or directory")
160
+ warn "get: #{path}: no such file or directory"
156
161
  else
157
- try_and_handle(DropboxError, output) do
158
- contents = client.get_file(path)
159
- basename = File.basename(path)
160
- File.open(basename, 'wb') { |file| file.write(contents) }
161
- output.call("#{basename} <- #{path}")
162
+ basename = File.basename(path)
163
+ try_and_handle(DropboxError) do
164
+ if flags.include?('-f') || !File.exist?(basename)
165
+ contents = client.get_file(path)
166
+ IO.write(basename, contents, mode: 'wb')
167
+ puts "#{basename} <- #{path}"
168
+ else
169
+ warn "get: #{basename}: local file already exists"
170
+ end
162
171
  end
163
172
  end
164
173
  end
@@ -170,17 +179,18 @@ module Commands
170
179
  'help [COMMAND]',
171
180
  "Print usage and help information about a command. If no command is \
172
181
  given, print a list of commands instead.",
173
- lambda do |_client, _state, args, output|
182
+ lambda do |_client, _state, args|
183
+ extract_flags('help', args, '')
174
184
  if args.empty?
175
- Text.table(NAMES).each { |line| output.call(line) }
185
+ Text.table(NAMES).each { |line| puts line }
176
186
  else
177
187
  cmd_name = args.first
178
188
  if NAMES.include?(cmd_name)
179
189
  cmd = const_get(cmd_name.upcase.to_s)
180
- output.call(cmd.usage)
181
- Text.wrap(cmd.description).each { |line| output.call(line) }
190
+ puts cmd.usage
191
+ Text.wrap(cmd.description).each { |line| puts line }
182
192
  else
183
- output.call("Unrecognized command: #{cmd_name}")
193
+ warn "help: #{cmd_name}: no such command"
184
194
  end
185
195
  end
186
196
  end
@@ -193,7 +203,8 @@ module Commands
193
203
  home directory. With a local directory name as the argument, changes to \
194
204
  that directory. With - as the argument, changes to the previous working \
195
205
  directory.",
196
- lambda do |_client, state, args, output|
206
+ lambda do |_client, state, args|
207
+ extract_flags('lcd', args, '')
197
208
  path = case
198
209
  when args.empty? then File.expand_path('~')
199
210
  when args.first == '-' then state.local_oldpwd
@@ -209,7 +220,7 @@ module Commands
209
220
  state.local_oldpwd = Dir.pwd
210
221
  Dir.chdir(path)
211
222
  else
212
- output.call("lcd: #{args.first}: no such file or directory")
223
+ warn "lcd: #{args.first}: no such file or directory"
213
224
  end
214
225
  end
215
226
  )
@@ -222,13 +233,13 @@ module Commands
222
233
  arguments, list the contents of the directories. When given remote files \
223
234
  as arguments, list the files. If the -l option is given, display \
224
235
  information about the files.",
225
- lambda do |_client, state, args, output|
226
- long = args.delete('-l')
236
+ lambda do |_client, state, args|
237
+ long = extract_flags('ls', args, '-l').include?('-l')
227
238
 
228
239
  files, dirs = [], []
229
240
  state.expand_patterns(args, true).each do |path|
230
241
  if path.is_a?(GlobError)
231
- output.call("ls: #{path}: no such file or directory")
242
+ warn "ls: #{path}: no such file or directory"
232
243
  else
233
244
  type = state.directory?(path) ? dirs : files
234
245
  type << path
@@ -238,16 +249,16 @@ module Commands
238
249
  dirs << state.pwd if args.empty?
239
250
 
240
251
  # First list files.
241
- list(state, files, files, long) { |line| output.call(line) }
242
- output.call('') unless dirs.empty? || files.empty?
252
+ list(state, files, files, long) { |line| puts line }
253
+ puts unless dirs.empty? || files.empty?
243
254
 
244
255
  # Then list directory contents.
245
256
  dirs.each_with_index do |dir, i|
246
- output.call(dir + ':') if dirs.size + files.size > 1
257
+ puts "#{dir}:" if dirs.size + files.size > 1
247
258
  contents = state.contents(dir)
248
259
  names = contents.map { |path| File.basename(path) }
249
- list(state, contents, names, long) { |line| output.call(line) }
250
- output.call('') if i < dirs.size - 1
260
+ list(state, contents, names, long) { |line| puts line }
261
+ puts if i < dirs.size - 1
251
262
  end
252
263
  end
253
264
  )
@@ -257,14 +268,15 @@ module Commands
257
268
  'media REMOTE_FILE...',
258
269
  "Create Dropbox links to publicly share remote files. The links are \
259
270
  time-limited and link directly to the files themselves.",
260
- lambda do |client, state, args, output|
271
+ lambda do |client, state, args|
272
+ extract_flags('media', args, '')
261
273
  state.expand_patterns(args).each do |path|
262
274
  if path.is_a?(GlobError)
263
- output.call("media: #{path}: no such file or directory")
275
+ warn "media: #{path}: no such file or directory"
264
276
  else
265
- try_and_handle(DropboxError, output) do
277
+ try_and_handle(DropboxError) do
266
278
  url = client.media(path)['url']
267
- output.call("#{File.basename(path)} -> #{url}")
279
+ puts "#{File.basename(path)} -> #{url}"
268
280
  end
269
281
  end
270
282
  end
@@ -275,9 +287,10 @@ module Commands
275
287
  MKDIR = Command.new(
276
288
  'mkdir REMOTE_DIR...',
277
289
  'Create remote directories.',
278
- lambda do |client, state, args, output|
290
+ lambda do |client, state, args|
291
+ extract_flags('mkdir', args, '')
279
292
  args.each do |arg|
280
- try_and_handle(DropboxError, output) do
293
+ try_and_handle(DropboxError) do
281
294
  path = state.resolve_path(arg)
282
295
  metadata = client.file_create_folder(path)
283
296
  state.cache.add(metadata)
@@ -288,35 +301,42 @@ module Commands
288
301
 
289
302
  # Move/rename remote files.
290
303
  MV = Command.new(
291
- 'mv REMOTE_FILE... REMOTE_FILE',
304
+ 'mv [-f] REMOTE_FILE... REMOTE_FILE',
292
305
  "When given two arguments, moves the remote file or folder at the first \
293
306
  path to the second path. When given more than two arguments or when the \
294
307
  final argument is a directory, moves each remote file or folder into \
295
- that directory.",
296
- lambda do |client, state, args, output|
297
- cp_mv(client, state, args, output, 'mv')
308
+ that directory. Will refuse to overwrite existing files unless invoked \
309
+ with the -f option.",
310
+ lambda do |client, state, args|
311
+ cp_mv(client, state, args, 'mv')
298
312
  end
299
313
  )
300
314
 
301
315
  # Upload a local file.
302
316
  PUT = Command.new(
303
- 'put LOCAL_FILE [REMOTE_FILE]',
317
+ 'put [-f] LOCAL_FILE [REMOTE_FILE]',
304
318
  "Upload a local file to a remote path. If the remote path names a \
305
319
  directory, the file will be placed in that directory. If a remote file \
306
- of the same name already exists, Dropbox will rename the upload. When \
307
- given only a local file path, the remote path defaults to a file of the \
308
- same name in the remote working directory.",
309
- lambda do |client, state, args, output|
320
+ of the same name already exists, Dropbox will rename the upload unless \
321
+ the -f option is given, in which case the remote file will be \
322
+ overwritten. When given only a local file path, the remote path defaults \
323
+ to a file of the same name in the remote working directory.",
324
+ lambda do |client, state, args|
325
+ flags = extract_flags('put', args, '-f')
310
326
  from_path = args.first
311
327
  to_path = (args.size == 2) ? args[1] : File.basename(from_path)
312
328
  to_path = state.resolve_path(to_path)
313
329
  to_path << "/#{from_path}" if state.directory?(to_path)
314
330
 
315
- try_and_handle(Exception, output) do
331
+ try_and_handle(StandardError) do
316
332
  File.open(File.expand_path(from_path), 'rb') do |file|
333
+ if flags.include?('-f') && state.metadata(to_path)
334
+ client.file_delete(to_path)
335
+ state.cache.remove(to_path)
336
+ end
317
337
  data = client.put_file(to_path, file)
318
338
  state.cache.add(data)
319
- output.call("#{from_path} -> #{data['path']}")
339
+ puts "#{from_path} -> #{data['path']}"
320
340
  end
321
341
  end
322
342
  end
@@ -324,14 +344,49 @@ module Commands
324
344
 
325
345
  # Remove remote files.
326
346
  RM = Command.new(
327
- 'rm REMOTE_FILE...',
328
- 'Remove each specified remote file or directory.',
329
- lambda do |client, state, args, output|
347
+ 'rm [-r] REMOTE_FILE...',
348
+ "Remove each specified remote file. If the -r option is given, will \
349
+ also remove directories recursively.",
350
+ lambda do |client, state, args|
351
+ flags = extract_flags('rm', args, '-r')
352
+ state.expand_patterns(args).each do |path|
353
+ if path.is_a?(GlobError)
354
+ warn "rm: #{path}: no such file or directory"
355
+ else
356
+ if state.directory?(path) && !flags.include?('-r')
357
+ warn "rm: #{path}: is a directory"
358
+ next
359
+ end
360
+ try_and_handle(DropboxError) do
361
+ client.file_delete(path)
362
+ state.cache.remove(path)
363
+ end
364
+ end
365
+ end
366
+ check_pwd(state)
367
+ end
368
+ )
369
+
370
+ # Remove remote directories.
371
+ RMDIR = Command.new(
372
+ 'rmdir REMOTE_DIR...',
373
+ 'Remove each specified empty remote directory.',
374
+ lambda do |client, state, args|
375
+ extract_flags('rmdir', args, '')
330
376
  state.expand_patterns(args).each do |path|
331
377
  if path.is_a?(GlobError)
332
- output.call("rm: #{path}: no such file or directory")
378
+ warn "rmdir: #{path}: no such file or directory"
333
379
  else
334
- try_and_handle(DropboxError, output) do
380
+ unless state.directory?(path)
381
+ warn "rmdir: #{path}: not a directory"
382
+ next
383
+ end
384
+ contents = state.metadata(path)['contents']
385
+ if contents && !contents.empty?
386
+ warn "rmdir: #{path}: directory not empty"
387
+ next
388
+ end
389
+ try_and_handle(DropboxError) do
335
390
  client.file_delete(path)
336
391
  state.cache.remove(path)
337
392
  end
@@ -348,14 +403,15 @@ module Commands
348
403
  shortened and direct to 'preview' pages of the files. Links created by \
349
404
  this method are set to expire far enough in the future so that \
350
405
  expiration is effectively not an issue.",
351
- lambda do |client, state, args, output|
406
+ lambda do |client, state, args|
407
+ extract_flags('share', args, '')
352
408
  state.expand_patterns(args).each do |path|
353
409
  if path.is_a?(GlobError)
354
- output.call("share: #{path}: no such file or directory")
410
+ warn "share: #{path}: no such file or directory"
355
411
  else
356
- try_and_handle(DropboxError, output) do
412
+ try_and_handle(DropboxError) do
357
413
  url = client.shares(path)['url']
358
- output.call("#{File.basename(path)} -> #{url}")
414
+ puts "#{File.basename(path)} -> #{url}"
359
415
  end
360
416
  end
361
417
  end
@@ -385,11 +441,11 @@ module Commands
385
441
  private
386
442
 
387
443
  # Attempt to run the associated block, handling the given type of +Exception+
388
- # by passing its +String+ representation to an output +Proc+.
389
- def self.try_and_handle(exception_class, output)
444
+ # by issuing a warning using its +String+ representation.
445
+ def self.try_and_handle(exception_class)
390
446
  yield
391
447
  rescue exception_class => error
392
- output.call(error.to_s)
448
+ warn error
393
449
  end
394
450
 
395
451
  # Run a command with the given name, or print an error message if usage is
@@ -400,10 +456,10 @@ module Commands
400
456
  command = const_get(command_name.upcase.to_sym)
401
457
  command.exec(client, state, *args) { |line| puts line }
402
458
  rescue UsageError => error
403
- puts "Usage: #{error}"
459
+ warn "Usage: #{error}"
404
460
  end
405
461
  else
406
- puts "droxi: #{command_name}: command not found"
462
+ warn "droxi: #{command_name}: command not found"
407
463
  end
408
464
  end
409
465
 
@@ -438,12 +494,12 @@ module Commands
438
494
  yield error.to_s if block_given?
439
495
  end
440
496
 
441
- # Return an +Array+ of paths from an +Array+ of globs, passing error messages
442
- # to the output +Proc+ for non-matches.
497
+ # Return an +Array+ of paths from an +Array+ of globs, printing error
498
+ # messages if +output+ is true.
443
499
  def self.expand(state, paths, preserve_root, output, cmd)
444
500
  state.expand_patterns(paths, preserve_root).map do |item|
445
501
  if item.is_a?(GlobError)
446
- output.call("#{cmd}: #{item}: no such file or directory") if output
502
+ warn "#{cmd}: #{item}: no such file or directory" if output
447
503
  nil
448
504
  else
449
505
  item
@@ -451,42 +507,49 @@ module Commands
451
507
  end.compact
452
508
  end
453
509
 
454
- # Copies or moves a file and passes a description of the operation to the
455
- # output +Proc+.
456
- def self.copy_move(method, args, client, state, output)
510
+ def self.overwrite(path, client, state)
511
+ return unless state.metadata(path)
512
+ client.file_delete(path)
513
+ state.cache.remove(path)
514
+ end
515
+
516
+ # Copies or moves a file.
517
+ def self.copy_move(method, args, flags, client, state)
457
518
  from_path, to_path = args.map { |p| state.resolve_path(p) }
458
- try_and_handle(DropboxError, output) do
519
+ try_and_handle(DropboxError) do
520
+ overwrite(to_path, client, state) if flags.include?('-f')
459
521
  metadata = client.send(method, from_path, to_path)
460
522
  state.cache.remove(from_path) if method == :file_move
461
523
  state.cache.add(metadata)
462
- output.call("#{args.first} -> #{args[1]}")
524
+ puts "#{args.first} -> #{args[1]}"
463
525
  end
464
526
  end
465
527
 
466
528
  # Execute a 'mv' or 'cp' operation depending on arguments given.
467
- def self.cp_mv(client, state, args, output, cmd)
468
- sources = expand(state, args.take(args.size - 1), true, output, cmd)
529
+ def self.cp_mv(client, state, args, cmd)
530
+ flags = extract_flags(cmd, args, '-f')
531
+ sources = expand(state, args.take(args.size - 1), true, true, cmd)
469
532
  method = (cmd == 'cp') ? :file_copy : :file_move
470
533
  dest = state.resolve_path(args.last)
471
534
 
472
535
  if sources.size == 1 && !state.directory?(dest)
473
- copy_move(method, [sources.first, args.last], client, state, output)
536
+ copy_move(method, [sources.first, args.last], flags, client, state)
474
537
  else
475
- cp_mv_to_dir(args, client, state, cmd, output)
538
+ cp_mv_to_dir(args, flags, client, state, cmd)
476
539
  end
477
540
  end
478
541
 
479
542
  # Copies or moves files into a directory.
480
- def self.cp_mv_to_dir(args, client, state, cmd, output)
481
- sources = expand(state, args.take(args.size - 1), true, nil, cmd)
543
+ def self.cp_mv_to_dir(args, flags, client, state, cmd)
544
+ sources = expand(state, args.take(args.size - 1), true, false, cmd)
482
545
  method = (cmd == 'cp') ? :file_copy : :file_move
483
546
  if state.metadata(state.resolve_path(args.last))
484
547
  sources.each do |source|
485
548
  to_path = args.last.chomp('/') + '/' + File.basename(source)
486
- copy_move(method, [source, to_path], client, state, output)
549
+ copy_move(method, [source, to_path], flags, client, state)
487
550
  end
488
551
  else
489
- output.call("#{cmd}: #{args.last}: no such directory")
552
+ warn "#{cmd}: #{args.last}: no such directory"
490
553
  end
491
554
  end
492
555
 
@@ -495,4 +558,18 @@ module Commands
495
558
  def self.check_pwd(state)
496
559
  (state.pwd = File.dirname(state.pwd)) until state.metadata(state.pwd)
497
560
  end
561
+
562
+ # Removes flags (e.g. -f) from the +Array+ and returns an +Array+ of the
563
+ # removed flags. Prints warnings if the flags are not in the given +String+
564
+ # of valid flags (e.g. '-rf').
565
+ def self.extract_flags(cmd, args, valid_flags)
566
+ tokens = args.take_while { |s| s[/^-\w+$/] }
567
+ args.shift(tokens.size)
568
+
569
+ # Process compound flags like -rf into -r, -f.
570
+ flags = tokens.join.chars.uniq.drop(1).map { |c| "-#{c}" }
571
+ invalid_flags = flags.reject { |f| valid_flags[f[1]] }
572
+ invalid_flags.each { |f| warn "#{cmd}: #{f}: invalid option" }
573
+ flags
574
+ end
498
575
  end
@@ -33,7 +33,7 @@ module Complete
33
33
  # Return a +String+ representing the type of tab-completion that should be
34
34
  # performed, given the current line buffer state.
35
35
  def self.completion_type(tokens)
36
- index = tokens.size
36
+ index = tokens.drop_while { |token| token[/^-\w+$/] }.size
37
37
  if index <= 1
38
38
  'COMMAND'
39
39
  elsif Commands::NAMES.include?(tokens.first)
data/lib/droxi.rb CHANGED
@@ -8,24 +8,40 @@ require_relative 'droxi/state'
8
8
 
9
9
  # Command-line Dropbox client module.
10
10
  module Droxi
11
+ # Version number of the program.
12
+ VERSION = '0.1.2'
13
+
11
14
  # Run the client.
12
- def self.run(*args)
15
+ def self.run(args)
13
16
  client = DropboxClient.new(access_token)
14
17
  state = State.new(client)
15
18
 
16
- if args.empty?
17
- run_interactive(client, state)
18
- else
19
- with_interrupt_handling do
20
- Commands.exec(join_cmd(args), client, state)
21
- end
22
- end
19
+ options = handle_options(args)
20
+ args.shift(options.size)
21
+
22
+ args.empty? ? run_interactive(client, state) : invoke(args, client, state)
23
23
 
24
24
  Settings.save
25
25
  end
26
26
 
27
27
  private
28
28
 
29
+ # Handles command-line options extracted from an +Array+ and returns an
30
+ # +Array+ of the extracted options.
31
+ def self.handle_options(args)
32
+ options = args.take_while { |s| s.start_with?('--') }
33
+ if options.include?('--version')
34
+ puts "droxi v#{VERSION}"
35
+ exit
36
+ end
37
+ options
38
+ end
39
+
40
+ # Invokes a single command formed by joining an +Array+ of +String+ args.
41
+ def self.invoke(args, client, state)
42
+ with_interrupt_handling { Commands.exec(join_cmd(args), client, state) }
43
+ end
44
+
29
45
  # Return a +String+ of joined command-line args, adding backslash escapes for
30
46
  # spaces.
31
47
  def self.join_cmd(args)