droxi 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)