droxi 0.1.0 → 0.1.1

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: 244170f34242a9088ac104e483683219845ac012
4
- data.tar.gz: eb1dab3e763e6749171ae587ef288dd466d74700
3
+ metadata.gz: aa77e9e1c5c65c6d9552b77bee94b0c4395b4a74
4
+ data.tar.gz: 404cfd0e77d97c811daa867f544b6a2faf440a4a
5
5
  SHA512:
6
- metadata.gz: 92865a868baf0e3efbbf0bf57cb844c12f89c58f500aa2151eea276d3cf3cd899aade5c80e4c10468e6e6c8bc542141bba88bea7786efcf2e924643ac944f426
7
- data.tar.gz: eeda04ec25143e42c8e185aa2e88b71aee7d7be4facbe49e5bd3b2f2c80256a5e44e82e01d1963e05566498fe73368ea9ed8f09f3b71cb737c722a569cdbc1e2
6
+ metadata.gz: 273cb971fa6a4412c44aafe050e33767063cb4d0c38203858e8b8e6988c19b1884c4c99b688dffdc7cbb661a8e3851a9322ee6fcd2ca3b3f487964eae0a869ac
7
+ data.tar.gz: 7c18c86aef2cdfb0cb46c571bf01db930ab87b396ac6ee1f6b5e26de758151b4ce0eb16ce3775245b173cb6eb50579c8e3df1f71a14c0985a3978bde474f7123
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- task :default => :build
1
+ task default: :build
2
2
 
3
3
  desc 'run unit tests'
4
4
  task :test do
@@ -52,7 +52,7 @@ task :build do
52
52
 
53
53
  def date(gemspec)
54
54
  require 'time'
55
- Time.parse(/\d{4}-\d{2}-\d{2}/.match(gemspec)[0]).strftime('%B %Y')
55
+ Time.parse(gemspec[/\d{4}-\d{2}-\d{2}/]).strftime('%B %Y')
56
56
  end
57
57
 
58
58
  def commands
@@ -66,15 +66,15 @@ task :build do
66
66
  def build_page
67
67
  gemspec = IO.read('droxi.gemspec')
68
68
 
69
- contents = IO.read('droxi.1.template').
70
- sub('{date}', date(gemspec)).
71
- sub('{version}', /\d+\.\d+\.\d+/.match(gemspec)[0]).
72
- sub('{commands}', commands)
69
+ contents = format(IO.read('droxi.1.template'),
70
+ date: date(gemspec),
71
+ version: gemspec[/\d+\.\d+\.\d+/],
72
+ commands: commands)
73
73
 
74
74
  IO.write('build/droxi.1', contents)
75
75
  end
76
76
 
77
- Dir.mkdir('build') unless Dir.exists?('build')
77
+ Dir.mkdir('build') unless Dir.exist?('build')
78
78
  build_exe
79
79
  build_page
80
80
  end
@@ -91,7 +91,7 @@ task :install do
91
91
  FileUtils.cp('build/droxi', BIN_PATH)
92
92
  FileUtils.mkdir_p(MAN_PATH)
93
93
  FileUtils.cp('build/droxi.1', MAN_PATH)
94
- rescue Exception => error
94
+ rescue => error
95
95
  puts error
96
96
  end
97
97
  end
@@ -102,7 +102,7 @@ task :uninstall do
102
102
  begin
103
103
  FileUtils.rm("#{BIN_PATH}/droxi")
104
104
  FileUtils.rm("#{MAN_PATH}/droxi.1")
105
- rescue Exception => error
105
+ rescue => error
106
106
  puts error
107
107
  end
108
108
  end
data/droxi.1.template CHANGED
@@ -1,4 +1,4 @@
1
- .TH DROXI 1 "{date}" "droxi {version}"
1
+ .TH DROXI 1 "%{date}" "droxi %{version}"
2
2
  .SH NAME
3
3
  droxi \- ftp-like command-line interface to Dropbox
4
4
  .SH SYNOPSIS
@@ -9,6 +9,6 @@ Features include smart tab completion, globbing, and interactive help. If
9
9
  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
- {commands}
12
+ %{commands}
13
13
  .SH AUTHOR
14
14
  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.0'
4
- s.date = '2014-06-05'
3
+ s.version = '0.1.1'
4
+ s.date = '2014-06-06'
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 \
data/lib/droxi.rb CHANGED
@@ -35,7 +35,7 @@ module Droxi
35
35
  # Attempt to authorize the user for app usage.
36
36
  def self.authorize
37
37
  app_key = '5sufyfrvtro9zp7'
38
- app_secret = 'h99ihzv86jyypho'
38
+ app_secret = 'h99ihzv86jyypho' # Not so secret, is it?
39
39
 
40
40
  flow = DropboxOAuth2FlowNoRedirect.new(app_key, app_secret)
41
41
 
@@ -43,7 +43,7 @@ module Droxi
43
43
  code = get_auth_code(authorize_url)
44
44
 
45
45
  begin
46
- Settings[:access_token] = flow.finish(code)[0]
46
+ Settings[:access_token] = flow.finish(code).first
47
47
  rescue DropboxError
48
48
  puts 'Invalid authorization code.'
49
49
  end
@@ -69,54 +69,24 @@ module Droxi
69
69
  init_readline(state)
70
70
  with_interrupt_handling { do_interaction_loop(client, state, info) }
71
71
 
72
- # Set pwd before exiting so that the oldpwd setting is saved to pwd
72
+ # Set pwd before exiting so that the oldpwd setting is saved to the pwd.
73
73
  state.pwd = '/'
74
74
  end
75
75
 
76
- # Return an +Array+ of potential tab-completion options for a given
77
- # completion type, word, and client state.
78
- def self.completion_options(type, word, state)
79
- case type
80
- when 'COMMAND' then Complete.command(word, Commands::NAMES)
81
- when 'LOCAL_FILE' then Complete.local(word)
82
- when 'LOCAL_DIR' then Complete.local_dir(word)
83
- when 'REMOTE_FILE' then Complete.remote(word, state)
84
- when 'REMOTE_DIR' then Complete.remote_dir(word, state)
85
- else []
76
+ def self.init_readline(state)
77
+ Readline.completion_proc = proc do
78
+ Complete.complete(Readline.line_buffer, state)
86
79
  end
87
- end
88
80
 
89
- # Return a +String+ representing the type of tab-completion that should be
90
- # performed, given the current line buffer state.
91
- def self.completion_type
92
- words = Readline.line_buffer.split
93
- index = words.length
94
- index += 1 if Readline.line_buffer.end_with?(' ')
95
- if index <= 1
96
- 'COMMAND'
97
- elsif Commands::NAMES.include?(words[0])
98
- cmd = Commands.const_get(words[0].upcase.to_sym)
99
- cmd.type_of_arg(index - 2)
100
- end
81
+ ignore_not_yet_implemented { Readline.completion_append_character = nil }
101
82
  end
102
83
 
103
- # Set up the Readline library's completion capabilities.
104
- def self.init_readline(state)
105
- Readline.completion_proc = proc do |word|
106
- completion_options(completion_type, word, state).map do |option|
107
- option.gsub(' ', '\ ').sub(/\\ $/, ' ')
108
- end
109
- end
110
-
111
- begin
112
- Readline.completion_append_character = nil
113
- rescue NotImplementedError
114
- nil
115
- end
84
+ def self.ignore_not_yet_implemented
85
+ yield
86
+ rescue NotImplementedError
87
+ nil
116
88
  end
117
89
 
118
- # Run the associated block, handling Interrupt errors by printing a blank
119
- # line.
120
90
  def self.with_interrupt_handling
121
91
  yield
122
92
  rescue Interrupt
@@ -134,13 +104,9 @@ module Droxi
134
104
  puts unless line
135
105
  end
136
106
 
137
- # Instruct the user to enter an authorization code and return the code. If
138
- # the user gives EOF, exit the program.
139
107
  def self.get_auth_code(url)
140
- puts '1. Go to: ' + url
141
- puts '2. Click "Allow" (you might have to log in first)'
142
- puts '3. Copy the authorization code'
143
- print '4. Enter the authorization code here: '
108
+ puts 'Authorize this app to access your Dropbox at: ' + url
109
+ print 'Enter authorization code: '
144
110
  code = $stdin.gets
145
111
  code ? code.strip! : exit
146
112
  end
@@ -0,0 +1,40 @@
1
+ # Special +Hash+ of remote file paths to cached file metadata.
2
+ class Cache < Hash
3
+ # Add a metadata +Hash+ and its contents to the +Cache+ and return the
4
+ # +Cache+.
5
+ def add(metadata)
6
+ path = metadata['path']
7
+ store(path, metadata)
8
+ dirname = File.dirname(path)
9
+ if dirname != path
10
+ contents = fetch(dirname, {}).fetch('contents', nil)
11
+ contents << metadata if contents && !contents.include?(metadata)
12
+ end
13
+ return self unless metadata.include?('contents')
14
+ metadata['contents'].each { |content| add(content) }
15
+ self
16
+ end
17
+
18
+ # Remove a path's metadata from the +Cache+ and return the +Cache+.
19
+ def remove(path)
20
+ recursive_remove(path)
21
+ contents = fetch(File.dirname(path), {}).fetch('contents', nil)
22
+ contents.delete_if { |item| item['path'] == path } if contents
23
+ self
24
+ end
25
+
26
+ # Return +true+ if the path's information is cached, +false+ otherwise.
27
+ def full_info?(path, require_contents = true)
28
+ info = fetch(path, nil)
29
+ info && (!require_contents || !info['is_dir'] || info.include?('contents'))
30
+ end
31
+
32
+ private
33
+
34
+ # Recursively remove a path and its sub-files and directories.
35
+ def recursive_remove(path)
36
+ contents = fetch(path, {}).fetch('contents', nil)
37
+ contents.each { |item| recursive_remove(item['path']) } if contents
38
+ delete(path)
39
+ end
40
+ end
@@ -6,8 +6,7 @@ require_relative 'text'
6
6
  module Commands
7
7
  # Exception indicating that a client command was given the wrong number of
8
8
  # arguments.
9
- class UsageError < ArgumentError
10
- end
9
+ UsageError = Class.new(ArgumentError)
11
10
 
12
11
  # A client command. Contains metadata as well as execution procedure.
13
12
  class Command
@@ -34,7 +33,7 @@ module Commands
34
33
  # given. Raises a +UsageError+ if an invalid number of command-line
35
34
  # arguments is given.
36
35
  def exec(client, state, *args)
37
- fail UsageError, @usage unless num_args_ok?(args.length)
36
+ fail UsageError, @usage unless num_args_ok?(args.size)
38
37
  block = proc { |line| yield line if block_given? }
39
38
  @procedure.yield(client, state, args, block)
40
39
  end
@@ -45,7 +44,7 @@ module Commands
45
44
  def type_of_arg(index)
46
45
  args = @usage.split.drop(1).reject { |arg| arg.include?('-') }
47
46
  return nil if args.empty?
48
- index = [index, args.length - 1].min
47
+ index = [index, args.size - 1].min
49
48
  args[index].tr('[].', '')
50
49
  end
51
50
 
@@ -55,11 +54,11 @@ module Commands
55
54
  # command, +false+ otherwise.
56
55
  def num_args_ok?(num_args)
57
56
  args = @usage.split.drop(1)
58
- min_args = args.reject { |arg| arg.start_with?('[') }.length
57
+ min_args = args.reject { |arg| arg.start_with?('[') }.size
59
58
  max_args = if args.any? { |arg| arg.end_with?('...') }
60
59
  num_args
61
60
  else
62
- args.length
61
+ args.size
63
62
  end
64
63
  (min_args..max_args).include?(num_args)
65
64
  end
@@ -75,13 +74,13 @@ module Commands
75
74
  lambda do |_client, state, args, output|
76
75
  case
77
76
  when args.empty? then state.pwd = '/'
78
- when args[0] == '-' then state.pwd = state.oldpwd
77
+ when args.first == '-' then state.pwd = state.oldpwd
79
78
  else
80
- path = state.resolve_path(args[0])
79
+ path = state.resolve_path(args.first)
81
80
  if state.directory?(path)
82
81
  state.pwd = path
83
82
  else
84
- output.call("cd: #{args[0]}: no such directory")
83
+ output.call("cd: #{args.first}: no such directory")
85
84
  end
86
85
  end
87
86
  end
@@ -114,7 +113,7 @@ module Commands
114
113
  output.call(error.inspect)
115
114
  end
116
115
  else
117
- output.call('Debug not enabled.')
116
+ output.call('debug: not enabled.')
118
117
  end
119
118
  end
120
119
  )
@@ -175,7 +174,7 @@ module Commands
175
174
  if args.empty?
176
175
  Text.table(NAMES).each { |line| output.call(line) }
177
176
  else
178
- cmd_name = args[0]
177
+ cmd_name = args.first
179
178
  if NAMES.include?(cmd_name)
180
179
  cmd = const_get(cmd_name.upcase.to_s)
181
180
  output.call(cmd.usage)
@@ -197,12 +196,12 @@ module Commands
197
196
  lambda do |_client, state, args, output|
198
197
  path = case
199
198
  when args.empty? then File.expand_path('~')
200
- when args[0] == '-' then state.local_oldpwd
199
+ when args.first == '-' then state.local_oldpwd
201
200
  else
202
201
  begin
203
- File.expand_path(args[0])
202
+ File.expand_path(args.first)
204
203
  rescue ArgumentError
205
- args[0]
204
+ args.first
206
205
  end
207
206
  end
208
207
 
@@ -210,7 +209,7 @@ module Commands
210
209
  state.local_oldpwd = Dir.pwd
211
210
  Dir.chdir(path)
212
211
  else
213
- output.call("lcd: #{args[0]}: no such file or directory")
212
+ output.call("lcd: #{args.first}: no such file or directory")
214
213
  end
215
214
  end
216
215
  )
@@ -238,17 +237,17 @@ module Commands
238
237
 
239
238
  dirs << state.pwd if args.empty?
240
239
 
241
- # First list files
240
+ # First list files.
242
241
  list(state, files, files, long) { |line| output.call(line) }
243
242
  output.call('') unless dirs.empty? || files.empty?
244
243
 
245
- # Then list directory contents
244
+ # Then list directory contents.
246
245
  dirs.each_with_index do |dir, i|
247
- output.call(dir + ':') if dirs.length + files.length > 1
246
+ output.call(dir + ':') if dirs.size + files.size > 1
248
247
  contents = state.contents(dir)
249
248
  names = contents.map { |path| File.basename(path) }
250
249
  list(state, contents, names, long) { |line| output.call(line) }
251
- output.call('') if i < dirs.length - 1
250
+ output.call('') if i < dirs.size - 1
252
251
  end
253
252
  end
254
253
  )
@@ -302,14 +301,16 @@ module Commands
302
301
  # Upload a local file.
303
302
  PUT = Command.new(
304
303
  'put LOCAL_FILE [REMOTE_FILE]',
305
- "Upload a local file to a remote path. If a remote file of the same name \
306
- already exists, Dropbox will rename the upload. When given only a local \
307
- file path, the remote path defaults to a file of the same name in the \
308
- remote working directory.",
304
+ "Upload a local file to a remote path. If the remote path names a \
305
+ 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
309
  lambda do |client, state, args, output|
310
- from_path = args[0]
311
- to_path = (args.length == 2) ? args[1] : File.basename(from_path)
310
+ from_path = args.first
311
+ to_path = (args.size == 2) ? args[1] : File.basename(from_path)
312
312
  to_path = state.resolve_path(to_path)
313
+ to_path << "/#{from_path}" if state.directory?(to_path)
313
314
 
314
315
  try_and_handle(Exception, output) do
315
316
  File.open(File.expand_path(from_path), 'rb') do |file|
@@ -373,10 +374,10 @@ module Commands
373
374
  # Parse and execute a line of user input in the given context.
374
375
  def self.exec(input, client, state)
375
376
  if input.start_with?('!')
376
- shell(input[1, input.length - 1]) { |line| puts line }
377
+ shell(input[1, input.size - 1]) { |line| puts line }
377
378
  elsif !input.empty?
378
- tokens = tokenize(input)
379
- cmd, args = tokens[0], tokens.drop(1)
379
+ tokens = Text.tokenize(input)
380
+ cmd, args = tokens.first, tokens.drop(1)
380
381
  try_command(cmd, args, client, state)
381
382
  end
382
383
  end
@@ -406,18 +407,6 @@ module Commands
406
407
  end
407
408
  end
408
409
 
409
- # Split a +String+ into tokens, allowing for backslash-escaped spaces, and
410
- # return the resulting +Array+.
411
- def self.tokenize(string)
412
- string.split.reduce([]) do |list, token|
413
- list << if !list.empty? && list.last.end_with?('\\')
414
- "#{list.pop.chop} #{token}"
415
- else
416
- token
417
- end
418
- end
419
- end
420
-
421
410
  # Return a +String+ of information about a remote file for ls -l.
422
411
  def self.long_info(state, path, name)
423
412
  meta = state.metadata(state.resolve_path(path), false)
@@ -470,18 +459,18 @@ module Commands
470
459
  metadata = client.send(method, from_path, to_path)
471
460
  state.cache.remove(from_path) if method == :file_move
472
461
  state.cache.add(metadata)
473
- output.call("#{args[0]} -> #{args[1]}")
462
+ output.call("#{args.first} -> #{args[1]}")
474
463
  end
475
464
  end
476
465
 
477
466
  # Execute a 'mv' or 'cp' operation depending on arguments given.
478
467
  def self.cp_mv(client, state, args, output, cmd)
479
- sources = expand(state, args.take(args.length - 1), true, output, cmd)
468
+ sources = expand(state, args.take(args.size - 1), true, output, cmd)
480
469
  method = (cmd == 'cp') ? :file_copy : :file_move
481
470
  dest = state.resolve_path(args.last)
482
471
 
483
- if sources.length == 1 && !state.directory?(dest)
484
- copy_move(method, [sources[0], args.last], client, state, output)
472
+ if sources.size == 1 && !state.directory?(dest)
473
+ copy_move(method, [sources.first, args.last], client, state, output)
485
474
  else
486
475
  cp_mv_to_dir(args, client, state, cmd, output)
487
476
  end
@@ -489,7 +478,7 @@ module Commands
489
478
 
490
479
  # Copies or moves files into a directory.
491
480
  def self.cp_mv_to_dir(args, client, state, cmd, output)
492
- sources = expand(state, args.take(args.length - 1), true, nil, cmd)
481
+ sources = expand(state, args.take(args.size - 1), true, nil, cmd)
493
482
  method = (cmd == 'cp') ? :file_copy : :file_move
494
483
  if state.metadata(state.resolve_path(args.last))
495
484
  sources.each do |source|
@@ -1,16 +1,58 @@
1
+ require_relative 'commands'
2
+ require_relative 'text'
3
+
1
4
  # Module containing tab-completion logic and methods.
2
5
  module Complete
6
+ # Return an +Array+ of completion options for the given input line and
7
+ # client state.
8
+ def self.complete(line, state)
9
+ tokens = Text.tokenize(line, include_empty: true)
10
+ type = completion_type(tokens)
11
+ completion_options(type, tokens.last, state).map do |option|
12
+ option.gsub(' ', '\ ').sub(/\\ $/, ' ')
13
+ .split.drop(tokens.last.count(' ')).join(' ')
14
+ .sub(/[^\\\/]$/, '\0 ')
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Return an +Array+ of potential tab-completion options for a given
21
+ # completion type, word, and client state.
22
+ def self.completion_options(type, word, state)
23
+ case type
24
+ when 'COMMAND' then command(word, Commands::NAMES)
25
+ when 'LOCAL_FILE' then local(word)
26
+ when 'LOCAL_DIR' then local_dir(word)
27
+ when 'REMOTE_FILE' then remote(word, state)
28
+ when 'REMOTE_DIR' then remote_dir(word, state)
29
+ else []
30
+ end
31
+ end
32
+
33
+ # Return a +String+ representing the type of tab-completion that should be
34
+ # performed, given the current line buffer state.
35
+ def self.completion_type(tokens)
36
+ index = tokens.size
37
+ if index <= 1
38
+ 'COMMAND'
39
+ elsif Commands::NAMES.include?(tokens.first)
40
+ cmd = Commands.const_get(tokens.first.upcase.to_sym)
41
+ cmd.type_of_arg(index - 2)
42
+ end
43
+ end
44
+
3
45
  # Return an +Array+ of potential command name tab-completions for a +String+.
4
46
  def self.command(string, names)
5
47
  names.select { |n| n.start_with?(string) }.map { |n| n + ' ' }
6
48
  end
7
49
 
8
50
  # Return the directory in which to search for potential local tab-completions
9
- # for a +String+. Defaults to working directory in case of bogus input.
51
+ # for a +String+.
10
52
  def self.local_search_path(string)
11
53
  File.expand_path(strip_filename(string))
12
54
  rescue ArgumentError
13
- Dir.pwd
55
+ string
14
56
  end
15
57
 
16
58
  # Return an +Array+ of potential local tab-completions for a +String+.
@@ -18,9 +60,13 @@ module Complete
18
60
  dir = local_search_path(string)
19
61
  basename = basename(string)
20
62
 
21
- matches = Dir.entries(dir).select { |entry| match?(basename, entry) }
22
- matches.map do |entry|
23
- final_match(string, entry, File.directory?(dir + '/' + entry))
63
+ begin
64
+ matches = Dir.entries(dir).select { |entry| match?(basename, entry) }
65
+ matches.map do |entry|
66
+ final_match(string, entry, File.directory?(dir + '/' + entry))
67
+ end
68
+ rescue Errno::ENOENT
69
+ []
24
70
  end
25
71
  end
26
72
 
@@ -60,14 +106,12 @@ module Complete
60
106
  remote(string, state).select { |result| result.end_with?('/') }
61
107
  end
62
108
 
63
- private
64
-
65
109
  def self.basename(string)
66
110
  string.end_with?('/') ? '' : File.basename(string)
67
111
  end
68
112
 
69
113
  def self.match?(prefix, candidate)
70
- candidate.start_with?(prefix) && !/^\.\.?$/.match(candidate)
114
+ candidate.start_with?(prefix) && !candidate[/^\.\.?$/]
71
115
  end
72
116
 
73
117
  def self.final_match(string, candidate, is_dir)
data/lib/droxi/state.rb CHANGED
@@ -1,53 +1,8 @@
1
+ require_relative 'cache'
1
2
  require_relative 'settings'
2
3
 
3
4
  # Represents a failure of a glob expression to match files.
4
- class GlobError < ArgumentError
5
- end
6
-
7
- # Special +Hash+ of remote file paths to cached file metadata.
8
- class Cache < Hash
9
- # Add a metadata +Hash+ and its contents to the +Cache+ and return the
10
- # +Cache+.
11
- def add(metadata)
12
- store(metadata['path'], metadata)
13
- dirname = File.dirname(metadata['path'])
14
- if dirname != metadata['path']
15
- contents = fetch(dirname, {}).fetch('contents', nil)
16
- contents << metadata if contents && !contents.include?(metadata)
17
- end
18
- return self unless metadata.include?('contents')
19
- metadata['contents'].each { |content| add(content) }
20
- self
21
- end
22
-
23
- # Remove a path from the +Cache+ and return the +Cache+.
24
- def remove(path)
25
- recursive_remove(path)
26
-
27
- dir = File.dirname(path)
28
- return self unless fetch(dir, {}).include?('contents')
29
- fetch(dir)['contents'].delete_if { |item| item['path'] == path }
30
-
31
- self
32
- end
33
-
34
- # Return +true+ if the path's information is cached, +false+ otherwise.
35
- def full_info?(path, require_contents = true)
36
- info = fetch(path, nil)
37
- info && (!require_contents || !info['is_dir'] || info.include?('contents'))
38
- end
39
-
40
- private
41
-
42
- # Recursively remove a path and its sub-files and directories.
43
- def recursive_remove(path)
44
- if fetch(path, {}).include?('contents')
45
- fetch(path)['contents'].each { |item| recursive_remove(item['path']) }
46
- end
47
-
48
- delete(path)
49
- end
50
- end
5
+ GlobError = Class.new(ArgumentError)
51
6
 
52
7
  # Encapsulates the session state of the client.
53
8
  class State
@@ -82,7 +37,7 @@ class State
82
37
  def metadata(path, require_contents = true)
83
38
  tokens = path.split('/').drop(1)
84
39
 
85
- (0..tokens.length).each do |i|
40
+ (0..tokens.size).each do |i|
86
41
  partial_path = '/' + tokens.take(i).join('/')
87
42
  next if @cache.full_info?(partial_path, require_contents)
88
43
  return nil unless fetch_metadata(partial_path)
@@ -117,26 +72,28 @@ class State
117
72
 
118
73
  # Expand a Dropbox file path and return the result.
119
74
  def resolve_path(arg)
75
+ # REVIEW: See if we can do this in fewer lines (e.g. without two gsub!s).
120
76
  path = arg.start_with?('/') ? arg.dup : "#{@pwd}/#{arg}"
121
77
  path.gsub!('//', '/')
122
78
  nil while path.sub!(%r{/([^/]+?)/\.\.}, '')
123
79
  nil while path.sub!('./', '')
124
80
  path.sub!(/\/\.$/, '')
125
81
  path.chomp!('/')
82
+ path.gsub!('//', '/')
126
83
  path.empty? ? '/' : path
127
84
  end
128
85
 
129
86
  # Expand an +Array+ of file globs into an an +Array+ of Dropbox file paths
130
87
  # and return the result.
131
88
  def expand_patterns(patterns, preserve_root = false)
132
- patterns.map do |pattern|
89
+ patterns.flat_map do |pattern|
133
90
  path = resolve_path(pattern)
134
91
  if directory?(path)
135
92
  preserve_root ? pattern : path
136
93
  else
137
94
  get_matches(pattern, path, preserve_root)
138
95
  end
139
- end.flatten
96
+ end
140
97
  end
141
98
 
142
99
  # Recursively remove directory contents from metadata cache. Yield lines of
data/lib/droxi/text.rb CHANGED
@@ -8,7 +8,7 @@ module Text
8
8
  def self.table(items)
9
9
  return [] if items.empty?
10
10
  width = terminal_width
11
- item_width = items.map { |item| item.length }.max + 2
11
+ item_width = items.map { |item| item.size }.max + 2
12
12
  items_per_line = [1, width / item_width].max
13
13
  format_table(items, item_width, items_per_line)
14
14
  end
@@ -18,13 +18,27 @@ module Text
18
18
  def self.wrap(text)
19
19
  width, position = terminal_width, 0
20
20
  lines = []
21
- while position < text.length
22
- lines << get_wrap_segment(text[position, text.length], width)
23
- position += lines.last.length + 1
21
+ while position < text.size
22
+ lines << get_wrap_segment(text[position, text.size], width)
23
+ position += lines.last.size + 1
24
24
  end
25
25
  lines
26
26
  end
27
27
 
28
+ # Split a +String+ into tokens, allowing for backslash-escaped spaces, and
29
+ # return the resulting +Array+.
30
+ def self.tokenize(string, include_empty: false)
31
+ tokens = string.split
32
+ tokens << '' if include_empty && (string.empty? || string.end_with?(' '))
33
+ tokens.reduce([]) do |list, token|
34
+ list << if !list.empty? && list.last.end_with?('\\')
35
+ "#{list.pop.chop} #{token}"
36
+ else
37
+ token
38
+ end
39
+ end
40
+ end
41
+
28
42
  private
29
43
 
30
44
  # Return the width of the terminal in columns.
@@ -51,10 +65,10 @@ module Text
51
65
  loop do
52
66
  head, _, text = text.partition(' ')
53
67
  line << "#{head} "
54
- break if text.empty? || line.length >= width
68
+ break if text.empty? || line.size >= width
55
69
  end
56
70
  line.strip!
57
- trim_last_word = line.length > width && line.include?(' ')
58
- trim_last_word ? line.rpartition(' ')[0] : line
71
+ trim_last_word = line.size > width && line.include?(' ')
72
+ trim_last_word ? line.rpartition(' ').first : line
59
73
  end
60
74
  end
data/spec/all.rb CHANGED
@@ -1,4 +1,4 @@
1
- # Use SimpleCov coverage-tracking library if available
1
+ # Use SimpleCov coverage-tracking library if available.
2
2
  begin
3
3
  require 'simplecov'
4
4
  SimpleCov.start do
@@ -8,7 +8,7 @@ rescue LoadError
8
8
  nil
9
9
  end
10
10
 
11
- # Run all spec tests
11
+ # Run all spec tests.
12
12
  Dir.glob('spec/*_spec.rb').each do |spec|
13
13
  require_relative File.basename(spec, '.rb')
14
14
  end
@@ -28,7 +28,7 @@ describe Commands do
28
28
 
29
29
  it 'must give an error message for an invalid command' do
30
30
  lines = TestUtils.output_of(Commands, :shell, 'bogus')
31
- lines.length.must_equal 1
31
+ lines.size.must_equal 1
32
32
  end
33
33
  end
34
34
 
@@ -104,16 +104,17 @@ describe Commands do
104
104
  it 'must give an error message if trying to copy a bogus file' do
105
105
  lines = TestUtils.output_of(Commands::CP, :exec, client, state,
106
106
  'bogus', '/testing')
107
- lines.length.must_equal 1
108
- lines[0].start_with?('cp: ').must_equal true
107
+ lines.size.must_equal 1
108
+ lines.first.start_with?('cp: ').must_equal true
109
109
  end
110
110
  end
111
111
 
112
112
  describe 'when executing the debug command' do
113
113
  it 'must fail with an error message if debug mode is not enabled' do
114
114
  ARGV.clear
115
- TestUtils.output_of(Commands::DEBUG, :exec, client, state, '1')
116
- .must_equal(['Debug not enabled.'])
115
+ lines = TestUtils.output_of(Commands::DEBUG, :exec, client, state, '1')
116
+ lines.size.must_equal 1
117
+ lines.first.start_with?('debug: ').must_equal true
117
118
  end
118
119
 
119
120
  it 'must evaluate the string if debug mode is enabled' do
@@ -125,8 +126,8 @@ describe Commands do
125
126
  it 'must print the resulting exception if given exceptional input' do
126
127
  ARGV << '--debug'
127
128
  lines = TestUtils.output_of(Commands::DEBUG, :exec, client, state, 'x')
128
- lines.length.must_equal 1
129
- lines[0].must_match(/^#<.+>$/)
129
+ lines.size.must_equal 1
130
+ lines.first.must_match(/^#<.+>$/)
130
131
  end
131
132
 
132
133
  it 'must fail with UsageError when given no args' do
@@ -145,13 +146,13 @@ describe Commands do
145
146
  it 'must accept multiple arguments' do
146
147
  args = %w(bogus1, bogus2)
147
148
  TestUtils.output_of(Commands::FORGET, :exec, client, state, *args)
148
- .length.must_equal 2
149
+ .size.must_equal 2
149
150
  end
150
151
 
151
152
  it 'must recursively clear contents of directory argument' do
152
153
  Commands::LS.exec(client, state, '/', '/testing')
153
154
  Commands::FORGET.exec(client, state, '/')
154
- state.cache.length.must_equal 1
155
+ state.cache.size.must_equal 1
155
156
  end
156
157
  end
157
158
 
@@ -170,8 +171,8 @@ describe Commands do
170
171
 
171
172
  it 'must give an error message if trying to get a bogus file' do
172
173
  lines = TestUtils.output_of(Commands::GET, :exec, client, state, 'bogus')
173
- lines.length.must_equal 1
174
- lines[0].start_with?('get: ').must_equal true
174
+ lines.size.must_equal 1
175
+ lines.first.start_with?('get: ').must_equal true
175
176
  end
176
177
  end
177
178
 
@@ -235,14 +236,14 @@ describe Commands do
235
236
  TestUtils.exact_structure(client, state, 'test')
236
237
  lines = TestUtils.output_of(Commands::LS, :exec, client, state,
237
238
  '-l', '/testing')
238
- lines.length.must_equal 1
239
- /d +0 \w{3} .\d \d\d:\d\d test/.match(lines[0]).wont_be_nil
239
+ lines.size.must_equal 1
240
+ lines.first[/d +0 \w{3} .\d \d\d:\d\d test/].wont_be_nil
240
241
  end
241
242
 
242
243
  it 'must give an error message if trying to list a bogus file' do
243
244
  lines = TestUtils.output_of(Commands::LS, :exec, client, state, 'bogus')
244
- lines.length.must_equal 1
245
- lines[0].start_with?('ls: ').must_equal true
245
+ lines.size.must_equal 1
246
+ lines.first.start_with?('ls: ').must_equal true
246
247
  end
247
248
  end
248
249
 
@@ -251,15 +252,15 @@ describe Commands do
251
252
  TestUtils.structure(client, state, 'test.txt')
252
253
  path = '/testing/test.txt'
253
254
  lines = TestUtils.output_of(Commands::MEDIA, :exec, client, state, path)
254
- lines.length.must_equal 1
255
- %r{https://.+\..+/}.match(lines[0]).wont_be_nil
255
+ lines.size.must_equal 1
256
+ %r{https://.+\..+/}.match(lines.first).wont_be_nil
256
257
  end
257
258
 
258
259
  it 'must fail with error when given directory path' do
259
260
  lines = TestUtils.output_of(Commands::MEDIA, :exec, client, state,
260
261
  '/testing')
261
- lines.length.must_equal 1
262
- %r{https://.+\..+/}.match(lines[0]).must_be_nil
262
+ lines.size.must_equal 1
263
+ %r{https://.+\..+/}.match(lines.first).must_be_nil
263
264
  end
264
265
 
265
266
  it 'must fail with UsageError when given no args' do
@@ -269,8 +270,8 @@ describe Commands do
269
270
 
270
271
  it 'must give an error message if trying to link a bogus file' do
271
272
  lines = TestUtils.output_of(Commands::MEDIA, :exec, client, state, '%')
272
- lines.length.must_equal 1
273
- lines[0].start_with?('media: ').must_equal true
273
+ lines.size.must_equal 1
274
+ lines.first.start_with?('media: ').must_equal true
274
275
  end
275
276
  end
276
277
 
@@ -327,7 +328,7 @@ describe Commands do
327
328
  it 'must give an error message if trying to move a bogus file' do
328
329
  lines = TestUtils.output_of(Commands::MV, :exec, client, state,
329
330
  'bogus1', 'bogus2', 'bogus3')
330
- lines.length.must_equal 3
331
+ lines.size.must_equal 3
331
332
  lines.all? { |line| line.start_with?('mv: ') }.must_equal true
332
333
  end
333
334
  end
@@ -351,6 +352,15 @@ describe Commands do
351
352
  state.metadata('/testing/dest.txt').wont_be_nil
352
353
  end
353
354
 
355
+ it 'must put file in directory if second arg is directory' do
356
+ TestUtils.not_structure(client, state, 'test.txt')
357
+ state.pwd = '/'
358
+ `touch test.txt`
359
+ Commands::PUT.exec(client, state, 'test.txt', 'testing')
360
+ `rm test.txt`
361
+ state.metadata('/testing/test.txt').wont_be_nil
362
+ end
363
+
354
364
  it 'must fail with UsageError when given no args' do
355
365
  proc { Commands::PUT.exec(client, state) }
356
366
  .must_raise Commands::UsageError
@@ -362,8 +372,8 @@ describe Commands do
362
372
  TestUtils.structure(client, state, 'test.txt')
363
373
  lines = TestUtils.output_of(Commands::SHARE, :exec, client, state,
364
374
  '/testing/test.txt')
365
- lines.length.must_equal 1
366
- %r{https://.+\..+/}.match(lines[0]).wont_be_nil
375
+ lines.size.must_equal 1
376
+ %r{https://.+\..+/}.match(lines.first).wont_be_nil
367
377
  end
368
378
 
369
379
  it 'must fail with UsageError when given no args' do
@@ -373,8 +383,8 @@ describe Commands do
373
383
 
374
384
  it 'must give an error message if trying to share a bogus file' do
375
385
  lines = TestUtils.output_of(Commands::SHARE, :exec, client, state, '%')
376
- lines.length.must_equal 1
377
- lines[0].start_with?('share: ').must_equal true
386
+ lines.size.must_equal 1
387
+ lines.first.start_with?('share: ').must_equal true
378
388
  end
379
389
  end
380
390
 
@@ -400,27 +410,27 @@ describe Commands do
400
410
 
401
411
  it 'must give an error message if trying to remove a bogus file' do
402
412
  lines = TestUtils.output_of(Commands::RM, :exec, client, state, 'bogus')
403
- lines.length.must_equal 1
404
- lines[0].start_with?('rm: ').must_equal true
413
+ lines.size.must_equal 1
414
+ lines.first.start_with?('rm: ').must_equal true
405
415
  end
406
416
  end
407
417
 
408
418
  describe 'when executing the help command' do
409
419
  it 'must print a list of commands when given no args' do
410
420
  TestUtils.output_of(Commands::HELP, :exec, client, state)
411
- .join.split.length.must_equal Commands::NAMES.length
421
+ .join.split.size.must_equal Commands::NAMES.size
412
422
  end
413
423
 
414
424
  it 'must print help for a command when given it as an arg' do
415
425
  lines = TestUtils.output_of(Commands::HELP, :exec, client, state, 'help')
416
- lines.length.must_be :>=, 2
417
- lines[0].must_equal Commands::HELP.usage
426
+ lines.size.must_be :>=, 2
427
+ lines.first.must_equal Commands::HELP.usage
418
428
  lines.drop(1).join(' ').must_equal Commands::HELP.description
419
429
  end
420
430
 
421
431
  it 'must print an error message if given a bogus name as an arg' do
422
432
  TestUtils.output_of(Commands::HELP, :exec, client, state, 'bogus')
423
- .length.must_equal 1
433
+ .size.must_equal 1
424
434
  end
425
435
 
426
436
  it 'must fail with UsageError when given multiple args' do
@@ -1,175 +1,123 @@
1
- require 'dropbox_sdk'
2
1
  require 'minitest/autorun'
3
2
 
4
3
  require_relative 'testutils'
5
4
  require_relative '../lib/droxi/commands'
6
5
  require_relative '../lib/droxi/complete'
7
- require_relative '../lib/droxi/settings'
8
- require_relative '../lib/droxi/state'
9
6
 
10
7
  describe Complete do
11
8
  CHARACTERS = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
12
9
 
10
+ _, state = TestUtils.create_client_and_state
11
+
13
12
  def random_string(length)
14
13
  rand(length).times.map { CHARACTERS.sample }.join
15
14
  end
16
15
 
17
- describe 'when resolving a local search path' do
18
- it 'must resolve unqualified string to working directory' do
19
- Complete.local_search_path('').must_equal Dir.pwd
20
- Complete.local_search_path('f').must_equal Dir.pwd
21
- end
22
-
23
- it 'must resolve / to root directory' do
24
- Complete.local_search_path('/').must_equal '/'
25
- Complete.local_search_path('/f').must_equal '/'
26
- end
27
-
28
- it 'must resolve directory name to named directory' do
29
- Complete.local_search_path('/home/').must_equal '/home'
30
- Complete.local_search_path('/home/f').must_equal '/home'
31
- end
32
-
33
- it 'must resolve ~/ to home directory' do
34
- Complete.local_search_path('~/').must_equal Dir.home
35
- Complete.local_search_path('~/f').must_equal Dir.home
36
- end
37
-
38
- it 'must resolve ./ to working directory' do
39
- Complete.local_search_path('./').must_equal Dir.pwd
40
- Complete.local_search_path('./f').must_equal Dir.pwd
41
- end
42
-
43
- it 'must resolve ../ to parent directory' do
44
- Complete.local_search_path('../').must_equal File.dirname(Dir.pwd)
45
- Complete.local_search_path('../f').must_equal File.dirname(Dir.pwd)
46
- end
47
-
48
- it 'must resolve a bogus string to working directory' do
49
- Complete.local_search_path('~bogus/bogus').must_equal Dir.pwd
16
+ def remote_contents(state, path)
17
+ state.contents(path).map do |entry|
18
+ entry += (state.directory?(entry) ? '/' : ' ')
19
+ entry[1, entry.size].gsub(' ', '\\ ').sub(/\\ $/, ' ')
50
20
  end
51
21
  end
52
22
 
53
- describe 'when finding potential local tab completions' do
54
- def check(path)
55
- 100.times.all? do
56
- prefix = path + random_string(5)
57
- Complete.local(prefix).all? { |match| match.start_with?(prefix) }
58
- end.must_equal true
59
- 1000.times.any? do
60
- prefix = path + random_string(5)
61
- !Complete.local(prefix).empty?
62
- end.must_equal true
63
- end
64
-
65
- it 'seed must prefix results for unqualified string' do
66
- check('')
67
- end
68
-
69
- it 'seed must prefix results for /' do
70
- check('/')
71
- end
72
-
73
- it 'seed must prefix results for named directory' do
74
- check('/home/')
75
- end
76
-
77
- it 'seed must prefix results for ~/' do
78
- check('~/')
79
- end
80
-
81
- it 'seed must prefix results for ./' do
82
- check('./')
23
+ def local_contents
24
+ files = Dir.entries(Dir.pwd).map do |entry|
25
+ entry << (File.directory?(entry) ? '/' : ' ')
83
26
  end
27
+ files.reject { |file| file[/^\.\.?\/$/] }
28
+ end
84
29
 
85
- it 'seed must prefix results for ../' do
86
- check('../')
30
+ describe 'when given an empty string or whitespace' do
31
+ it 'lists all command names' do
32
+ names = Commands::NAMES.map { |n| "#{n} " }
33
+ Complete.complete('', state).must_equal names
34
+ Complete.complete(' ', state).must_equal names
87
35
  end
36
+ end
88
37
 
89
- it "won't raise an exception on a bogus string" do
90
- Complete.local('~bogus')
38
+ describe 'when given a letter' do
39
+ it 'lists all command names starting with that letter' do
40
+ letter = 'c'
41
+ names = Commands::NAMES.map { |n| "#{n} " }
42
+ matches = names.select { |n| n.start_with?(letter) }
43
+ Complete.complete(letter, state).sort.must_equal matches.sort
91
44
  end
92
45
  end
93
46
 
94
- describe 'when finding local directory tab completions' do
95
- it 'must include all directories and only directories' do
96
- entries = Dir.entries(Dir.pwd).select do |entry|
97
- File.directory?(entry) && !/^..?$/.match(entry)
98
- end
99
- matches = Complete.local_dir('').map { |match| match.chomp('/') }
100
- matches.sort.must_equal entries.sort
47
+ describe 'when given a context for local files' do
48
+ it 'lists all local files except . and .. and end with correct char' do
49
+ Complete.complete('put ', state).sort.must_equal local_contents.sort
101
50
  end
51
+ end
102
52
 
103
- it 'must append a / to the end of options' do
104
- Complete.local_dir('').all? { |option| option.end_with?('/') }
53
+ describe 'when given a context for local directories' do
54
+ it 'lists all local dirs except . and .. and end with correct char' do
55
+ dirs = local_contents.reject { |entry| entry.end_with?(' ') }
56
+ Complete.complete('lcd ', state).sort.must_equal dirs.sort
105
57
  end
106
58
  end
107
59
 
108
- describe 'when resolving a remote search path' do
109
- client = DropboxClient.new(Settings[:access_token])
110
- TestUtils.ignore(DropboxError) { client.file_create_folder('/testing') }
111
- state = State.new(client)
112
- state.pwd = '/testing'
113
-
114
- it 'must resolve unqualified string to working directory' do
115
- Complete.remote_search_path('', state).must_equal state.pwd
116
- Complete.remote_search_path('f', state).must_equal state.pwd
60
+ describe 'when given local context and faulty path' do
61
+ it 'must return empty list' do
62
+ Complete.complete('put bogus/', state).must_equal [] # fictional
63
+ Complete.complete('put ~bogus/', state).must_equal [] # malformed
117
64
  end
65
+ end
118
66
 
119
- it 'must resolve / to root directory' do
120
- Complete.remote_search_path('/', state).must_equal '/'
121
- Complete.remote_search_path('/f', state).must_equal '/'
67
+ describe 'when given an implicit context for remote files' do
68
+ it 'lists all remote files and end with correct char' do
69
+ state.pwd = '/'
70
+ entries = remote_contents(state, '/')
71
+ Complete.complete('put thing ', state).sort.must_equal entries.sort
122
72
  end
73
+ end
123
74
 
124
- it 'must resolve directory name to named directory' do
125
- Complete.remote_search_path('/testing/', state).must_equal '/testing'
126
- Complete.remote_search_path('/testing/f', state).must_equal '/testing'
127
- end
75
+ describe 'when given an explicit, absolute context for remote files' do
76
+ it 'lists all remote files in path and end with correct char' do
77
+ state.pwd = '/'
128
78
 
129
- it 'must resolve ./ to working directory' do
130
- Complete.remote_search_path('./', state).must_equal state.pwd
131
- Complete.remote_search_path('./f', state).must_equal state.pwd
132
- end
79
+ entries = remote_contents(state, '/testing').map { |e| "/#{e}" }
80
+ Complete.complete('ls /testing/', state).sort.must_equal entries.sort
133
81
 
134
- it 'must resolve ../ to parent directory' do
135
- parent = File.dirname(state.pwd)
136
- Complete.remote_search_path('../', state).must_equal parent
137
- Complete.remote_search_path('../f', state).must_equal parent
82
+ entries.map! { |e| e.sub('/testing/', '/testing/../testing/./') }
83
+ Complete.complete('ls /testing/../testing/./', state).sort
84
+ .must_equal entries.sort
138
85
  end
139
86
  end
140
87
 
141
- describe 'when finding remote tab completions' do
142
- client = DropboxClient.new(Settings[:access_token])
143
- state = State.new(client)
144
- state.pwd = '/testing'
145
- Commands::RM.exec(client, state, '/testing/*')
146
- %w(/testing /testing/one /testing/two).each do |dir|
147
- Commands::MKDIR.exec(client, state, dir) unless state.metadata(dir)
148
- end
149
- `echo hello > test.txt`
150
- Commands::PUT.exec(client, state, 'test.txt')
151
- `rm test.txt`
88
+ describe 'when given an explicit, relative context for remote files' do
89
+ it 'lists all remote files in path and end with correct char' do
90
+ state.pwd = '/'
152
91
 
153
- it 'must return only matches of which the string is a prefix' do
154
- Complete.remote('t', state).must_equal ['two/', 'test.txt ']
155
- end
92
+ entries = remote_contents(state, '/testing')
93
+ Complete.complete('ls testing/', state).sort.must_equal entries.sort
156
94
 
157
- it 'must return only directories if requested' do
158
- Complete.remote_dir('', state).must_equal %w(one/ two/)
95
+ entries.map! { |e| e.sub('testing/', 'testing/../testing/./') }
96
+ Complete.complete('ls testing/../testing/./', state).sort
97
+ .must_equal entries.sort
159
98
  end
160
99
  end
161
100
 
162
- describe 'when resolving command names' do
163
- before do
164
- @words = %w(plank plague plonk lake lag lock)
101
+ describe 'when given a context for remote directories' do
102
+ it 'lists all remote dirs and end with correct char' do
103
+ state.pwd = '/'
104
+ dirs = remote_contents(state, '/').select { |e| e.end_with?('/') }
105
+ Complete.complete('cd ', state).sort.must_equal dirs.sort
165
106
  end
107
+ end
166
108
 
167
- it 'must return matches if and only if the string is a prefix' do
168
- Complete.command('pla', @words).length.must_equal 2
109
+ describe 'when given name with spaces' do
110
+ it 'must continue to match correctly' do
111
+ `touch a\\ b\\ c`
112
+ matches = local_contents.select { |entry| entry.start_with?('a\\ ') }
113
+ Complete.complete('lcd a\\ ', state).sort.must_equal matches.sort
114
+ `rm a\\ b\\ c`
169
115
  end
116
+ end
170
117
 
171
- it 'must return matches that end with a space' do
172
- Complete.command('plank', @words).must_equal ['plank ']
118
+ describe 'when given an unworkable context' do
119
+ it 'lists nothing' do
120
+ Complete.complete('debug ', state).must_equal []
173
121
  end
174
122
  end
175
123
  end
data/spec/state_spec.rb CHANGED
@@ -65,18 +65,18 @@ describe State do
65
65
  end
66
66
 
67
67
  it 'must yield an error for a bogus path' do
68
- TestUtils.output_of(state, :forget_contents, 'bogus').length.must_equal 1
68
+ TestUtils.output_of(state, :forget_contents, 'bogus').size.must_equal 1
69
69
  end
70
70
 
71
71
  it 'must yield an error for a non-directory path' do
72
72
  TestUtils.output_of(state, :forget_contents, '/dir/file0')
73
- .length.must_equal 1
73
+ .size.must_equal 1
74
74
  end
75
75
 
76
76
  it 'must yield an error for an already forgotten path' do
77
77
  state.forget_contents('/dir')
78
78
  TestUtils.output_of(state, :forget_contents, '/dir')
79
- .length.must_equal 1
79
+ .size.must_equal 1
80
80
  end
81
81
 
82
82
  it 'must forget contents of given directory' do
@@ -91,7 +91,7 @@ describe State do
91
91
  state.forget_contents('/')
92
92
  state.cache['/'].include?('contents').must_equal false
93
93
  state.cache.keys.any? do |key|
94
- key.length > 1
94
+ key.size > 1
95
95
  end.must_equal false
96
96
  end
97
97
  end
data/spec/testutils.rb CHANGED
@@ -1,4 +1,8 @@
1
+ require 'dropbox_sdk'
2
+
1
3
  require_relative '../lib/droxi/commands'
4
+ require_relative '../lib/droxi/settings'
5
+ require_relative '../lib/droxi/state'
2
6
 
3
7
  # Module of helper methods for testing.
4
8
  module TestUtils
@@ -6,13 +10,6 @@ module TestUtils
6
10
  # take place.
7
11
  TEST_ROOT = '/testing'
8
12
 
9
- # Run the attached block, rescuing the given +Exception+ class.
10
- def self.ignore(error_class)
11
- yield
12
- rescue error_class
13
- nil
14
- end
15
-
16
13
  # Call the method on the reciever with the given args and return an +Array+
17
14
  # of lines of output from the method.
18
15
  def self.output_of(receiver, method, *args)
@@ -52,6 +49,12 @@ module TestUtils
52
49
  Commands::RM.exec(client, state, *dead_paths)
53
50
  end
54
51
 
52
+ # Returns a new +DropboxClient+ and +State+.
53
+ def self.create_client_and_state
54
+ client = DropboxClient.new(Settings[:access_token])
55
+ [client, State.new(client)]
56
+ end
57
+
55
58
  private
56
59
 
57
60
  # Creates a remote file at the given path.
data/spec/text_spec.rb CHANGED
@@ -23,19 +23,19 @@ describe Text do
23
23
  describe 'when wrapping text' do
24
24
  it "won't return any line larger than the screen width if unnecessary" do
25
25
  Text.wrap(@paragraph).all? do |line|
26
- line.length <= @columns
26
+ line.size <= @columns
27
27
  end.must_equal true
28
28
  end
29
29
 
30
30
  it "won't split a word larger than the screen width" do
31
- Text.wrap(@big_word).length.must_equal 1
31
+ Text.wrap(@big_word).size.must_equal 1
32
32
  end
33
33
  end
34
34
 
35
35
  describe 'when tabulating text' do
36
36
  it 'must space items equally' do
37
37
  lines = Text.table(@paragraph.split)
38
- lines = lines[0, lines.length - 1]
38
+ lines = lines[0, lines.size - 1]
39
39
 
40
40
  space_positions = [0]
41
41
  while lines.first.index(/ \S/, space_positions.last + 3)
@@ -43,18 +43,18 @@ describe Text do
43
43
  end
44
44
 
45
45
  space_positions.drop(1).all? do |position|
46
- lines.all? { |line| / \S/.match(line[position, 3]) }
46
+ lines.all? { |line| line[position, 3][/ \S/] }
47
47
  end.must_equal true
48
48
  end
49
49
 
50
50
  it "won't return any line larger than the screen width if unnecessary" do
51
51
  Text.table(@paragraph.split).all? do |line|
52
- line.length <= @columns
52
+ line.size <= @columns
53
53
  end.must_equal true
54
54
  end
55
55
 
56
56
  it "won't split a word larger than the screen width" do
57
- Text.table([@big_word]).length.must_equal 1
57
+ Text.table([@big_word]).size.must_equal 1
58
58
  end
59
59
  end
60
60
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: droxi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Mulcahy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-05 00:00:00.000000000 Z
11
+ date: 2014-06-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dropbox-sdk
@@ -45,6 +45,7 @@ files:
45
45
  - droxi.1.template
46
46
  - droxi.gemspec
47
47
  - lib/droxi.rb
48
+ - lib/droxi/cache.rb
48
49
  - lib/droxi/commands.rb
49
50
  - lib/droxi/complete.rb
50
51
  - lib/droxi/settings.rb