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 +4 -4
- data/bin/droxi +1 -1
- data/droxi.1.template +8 -1
- data/droxi.gemspec +2 -2
- data/lib/droxi/commands.rb +169 -92
- data/lib/droxi/complete.rb +1 -1
- data/lib/droxi.rb +24 -8
- data/spec/commands_spec.rb +273 -113
- data/spec/testutils.rb +6 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: db93fae1cb4f2b8945d7bac19ad38940817f2f6e
|
4
|
+
data.tar.gz: 8a8d36e14900fefebba6a090dae94df16cbe7c64
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8212f5b0d45da0538697258becc8d3f7e6971c6e9b72e66b2fee4aa001c78a4831fc3fe1358384e1a0b574c423a46bc5ebf4264ef647dc359150035a28aaeddb
|
7
|
+
data.tar.gz: 99355ccb042b1f05b784d19e4c8ba723c248ce51c292514be89575d1d6ba2a34a3ec468e05c3eafcc77b5ff8af79b3986ed52931b9333b9beea14a51cf031833
|
data/bin/droxi
CHANGED
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 = '
|
4
|
-
s.date = '2014-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 \
|
data/lib/droxi/commands.rb
CHANGED
@@ -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
|
24
|
-
# +DropboxClient+, the +State+, an +Array+ of command-line arguments
|
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
|
33
|
-
#
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
97
|
-
|
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
|
106
|
+
lambda do |client, state, args|
|
108
107
|
if ARGV.include?('--debug')
|
109
108
|
begin
|
110
|
-
|
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
|
-
|
114
|
+
warn error.inspect
|
114
115
|
end
|
115
116
|
else
|
116
|
-
|
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,
|
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
|
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|
|
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
|
-
|
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
|
-
|
160
|
+
warn "get: #{path}: no such file or directory"
|
156
161
|
else
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
182
|
+
lambda do |_client, _state, args|
|
183
|
+
extract_flags('help', args, '')
|
174
184
|
if args.empty?
|
175
|
-
Text.table(NAMES).each { |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
|
-
|
181
|
-
Text.wrap(cmd.description).each { |line|
|
190
|
+
puts cmd.usage
|
191
|
+
Text.wrap(cmd.description).each { |line| puts line }
|
182
192
|
else
|
183
|
-
|
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
|
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
|
-
|
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
|
226
|
-
long = args.
|
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
|
-
|
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|
|
242
|
-
|
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
|
-
|
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|
|
250
|
-
|
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
|
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
|
-
|
275
|
+
warn "media: #{path}: no such file or directory"
|
264
276
|
else
|
265
|
-
try_and_handle(DropboxError
|
277
|
+
try_and_handle(DropboxError) do
|
266
278
|
url = client.media(path)['url']
|
267
|
-
|
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
|
290
|
+
lambda do |client, state, args|
|
291
|
+
extract_flags('mkdir', args, '')
|
279
292
|
args.each do |arg|
|
280
|
-
try_and_handle(DropboxError
|
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
|
-
|
297
|
-
|
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
|
307
|
-
|
308
|
-
|
309
|
-
|
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(
|
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
|
-
|
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
|
-
|
329
|
-
|
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
|
-
|
378
|
+
warn "rmdir: #{path}: no such file or directory"
|
333
379
|
else
|
334
|
-
|
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
|
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
|
-
|
410
|
+
warn "share: #{path}: no such file or directory"
|
355
411
|
else
|
356
|
-
try_and_handle(DropboxError
|
412
|
+
try_and_handle(DropboxError) do
|
357
413
|
url = client.shares(path)['url']
|
358
|
-
|
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
|
389
|
-
def self.try_and_handle(exception_class
|
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
|
-
|
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
|
-
|
459
|
+
warn "Usage: #{error}"
|
404
460
|
end
|
405
461
|
else
|
406
|
-
|
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,
|
442
|
-
#
|
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
|
-
|
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
|
-
|
455
|
-
|
456
|
-
|
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
|
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
|
-
|
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,
|
468
|
-
|
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
|
536
|
+
copy_move(method, [sources.first, args.last], flags, client, state)
|
474
537
|
else
|
475
|
-
cp_mv_to_dir(args, client, state, cmd
|
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
|
481
|
-
sources = expand(state, args.take(args.size - 1), true,
|
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
|
549
|
+
copy_move(method, [source, to_path], flags, client, state)
|
487
550
|
end
|
488
551
|
else
|
489
|
-
|
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
|
data/lib/droxi/complete.rb
CHANGED
@@ -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(
|
15
|
+
def self.run(args)
|
13
16
|
client = DropboxClient.new(access_token)
|
14
17
|
state = State.new(client)
|
15
18
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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)
|