droxi 0.0.5 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +9 -1
- data/Rakefile +18 -5
- data/bin/droxi +2 -3
- data/droxi.gemspec +2 -2
- data/lib/droxi.rb +45 -33
- data/lib/droxi/commands.rb +117 -97
- data/lib/droxi/complete.rb +36 -31
- data/lib/droxi/settings.rb +51 -52
- data/lib/droxi/state.rb +72 -69
- data/lib/droxi/text.rb +27 -37
- data/spec/all.rb +10 -0
- data/spec/commands_spec.rb +285 -114
- data/spec/complete_spec.rb +93 -28
- data/spec/settings_spec.rb +37 -4
- data/spec/state_spec.rb +51 -23
- data/spec/testutils.rb +65 -0
- data/spec/text_spec.rb +5 -5
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 244170f34242a9088ac104e483683219845ac012
|
4
|
+
data.tar.gz: eb1dab3e763e6749171ae587ef288dd466d74700
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92865a868baf0e3efbbf0bf57cb844c12f89c58f500aa2151eea276d3cf3cd899aade5c80e4c10468e6e6c8bc542141bba88bea7786efcf2e924643ac944f426
|
7
|
+
data.tar.gz: eeda04ec25143e42c8e185aa2e88b71aee7d7be4facbe49e5bd3b2f2c80256a5e44e82e01d1963e05566498fe73368ea9ed8f09f3b71cb737c722a569cdbc1e2
|
data/README.md
CHANGED
@@ -28,5 +28,13 @@ features
|
|
28
28
|
[GNU ftp](http://www.gnu.org/software/inetutils/), and
|
29
29
|
[lftp](http://lftp.yar.ru/)
|
30
30
|
- context-sensitive tab completion and path globbing
|
31
|
-
- upload, download, and share files
|
31
|
+
- upload, download, organize, and share files
|
32
32
|
- man page and interactive help
|
33
|
+
|
34
|
+
developer features
|
35
|
+
------------------
|
36
|
+
|
37
|
+
- extensive spec-style unit tests using
|
38
|
+
[MiniTest](https://github.com/seattlerb/minitest)
|
39
|
+
- [RuboCop](https://github.com/bbatsov/rubocop)-approved
|
40
|
+
- fully [RDoc](http://rdoc.sourceforge.net/) documented
|
data/Rakefile
CHANGED
@@ -5,10 +5,24 @@ task :test do
|
|
5
5
|
sh 'ruby spec/all.rb'
|
6
6
|
end
|
7
7
|
|
8
|
+
desc 'run unit tests in verbose mode'
|
9
|
+
task :verbose_test do
|
10
|
+
sh 'ruby -w spec/all.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'check code with rubocop'
|
14
|
+
task :cop do
|
15
|
+
sh 'rubocop bin lib spec'
|
16
|
+
end
|
17
|
+
|
8
18
|
desc 'run program'
|
9
19
|
task :run do
|
10
|
-
|
11
|
-
|
20
|
+
sh 'ruby bin/droxi'
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'run program in debug mode'
|
24
|
+
task :debug do
|
25
|
+
sh 'ruby bin/droxi --debug'
|
12
26
|
end
|
13
27
|
|
14
28
|
desc 'install gem'
|
@@ -30,8 +44,7 @@ task :build do
|
|
30
44
|
|
31
45
|
contents = "#!/usr/bin/env ruby\n\n"
|
32
46
|
contents << `cat -s #{filenames.join(' ')} \
|
33
|
-
| grep -v require_relative
|
34
|
-
| grep -v "require 'droxi'"`
|
47
|
+
| grep -v require_relative`
|
35
48
|
|
36
49
|
IO.write('build/droxi', contents)
|
37
50
|
File.chmod(0755, 'build/droxi')
|
@@ -96,5 +109,5 @@ end
|
|
96
109
|
|
97
110
|
desc 'remove files generated by other targets'
|
98
111
|
task :clean do
|
99
|
-
sh 'rm -rf build doc droxi-*.gem'
|
112
|
+
sh 'rm -rf build coverage doc droxi-*.gem'
|
100
113
|
end
|
data/bin/droxi
CHANGED
data/droxi.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'droxi'
|
3
|
-
s.version = '0.0
|
4
|
-
s.date = '2014-06-
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.date = '2014-06-05'
|
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
@@ -8,18 +8,16 @@ require_relative 'droxi/state'
|
|
8
8
|
|
9
9
|
# Command-line Dropbox client module.
|
10
10
|
module Droxi
|
11
|
-
|
12
11
|
# Run the client.
|
13
12
|
def self.run(*args)
|
14
|
-
client = DropboxClient.new(
|
13
|
+
client = DropboxClient.new(access_token)
|
15
14
|
state = State.new(client)
|
16
15
|
|
17
16
|
if args.empty?
|
18
17
|
run_interactive(client, state)
|
19
18
|
else
|
20
19
|
with_interrupt_handling do
|
21
|
-
|
22
|
-
Commands.exec(cmd, client, state)
|
20
|
+
Commands.exec(join_cmd(args), client, state)
|
23
21
|
end
|
24
22
|
end
|
25
23
|
|
@@ -28,6 +26,12 @@ module Droxi
|
|
28
26
|
|
29
27
|
private
|
30
28
|
|
29
|
+
# Return a +String+ of joined command-line args, adding backslash escapes for
|
30
|
+
# spaces.
|
31
|
+
def self.join_cmd(args)
|
32
|
+
args.map { |arg| arg.gsub(' ', '\ ') }.join(' ')
|
33
|
+
end
|
34
|
+
|
31
35
|
# Attempt to authorize the user for app usage.
|
32
36
|
def self.authorize
|
33
37
|
app_key = '5sufyfrvtro9zp7'
|
@@ -35,7 +39,7 @@ module Droxi
|
|
35
39
|
|
36
40
|
flow = DropboxOAuth2FlowNoRedirect.new(app_key, app_secret)
|
37
41
|
|
38
|
-
authorize_url = flow.start
|
42
|
+
authorize_url = flow.start
|
39
43
|
code = get_auth_code(authorize_url)
|
40
44
|
|
41
45
|
begin
|
@@ -47,8 +51,8 @@ module Droxi
|
|
47
51
|
|
48
52
|
# Return the access token for the user, requesting authorization if no saved
|
49
53
|
# token exists.
|
50
|
-
def self.
|
51
|
-
authorize
|
54
|
+
def self.access_token
|
55
|
+
authorize until Settings.include?(:access_token)
|
52
56
|
Settings[:access_token]
|
53
57
|
end
|
54
58
|
|
@@ -69,37 +73,45 @@ module Droxi
|
|
69
73
|
state.pwd = '/'
|
70
74
|
end
|
71
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 []
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
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
|
101
|
+
end
|
102
|
+
|
72
103
|
# Set up the Readline library's completion capabilities.
|
73
104
|
def self.init_readline(state)
|
74
105
|
Readline.completion_proc = proc do |word|
|
75
|
-
|
76
|
-
|
77
|
-
index += 1 if Readline.line_buffer.end_with?(' ')
|
78
|
-
if index <= 1
|
79
|
-
type = 'COMMAND'
|
80
|
-
elsif Commands::NAMES.include?(words[0])
|
81
|
-
cmd = Commands.const_get(words[0].upcase.to_sym)
|
82
|
-
type = cmd.type_of_arg(index - 2)
|
83
|
-
end
|
84
|
-
|
85
|
-
options = case type
|
86
|
-
when 'COMMAND'
|
87
|
-
Commands::NAMES.select { |name| name.start_with? word }.map do |name|
|
88
|
-
name + ' '
|
89
|
-
end
|
90
|
-
when 'LOCAL_FILE' then Complete.local(word)
|
91
|
-
when 'LOCAL_DIR' then Complete.local_dir(word)
|
92
|
-
when 'REMOTE_FILE' then Complete.remote(word, state)
|
93
|
-
when 'REMOTE_DIR' then Complete.remote_dir(word, state)
|
94
|
-
else []
|
106
|
+
completion_options(completion_type, word, state).map do |option|
|
107
|
+
option.gsub(' ', '\ ').sub(/\\ $/, ' ')
|
95
108
|
end
|
96
|
-
|
97
|
-
options.map { |option| option.gsub(' ', '\ ').sub(/\\ $/, ' ') }
|
98
109
|
end
|
99
110
|
|
100
111
|
begin
|
101
112
|
Readline.completion_append_character = nil
|
102
113
|
rescue NotImplementedError
|
114
|
+
nil
|
103
115
|
end
|
104
116
|
end
|
105
117
|
|
@@ -114,11 +126,12 @@ module Droxi
|
|
114
126
|
# Run the main loop of the program, getting user input and executing it as a
|
115
127
|
# command until an getting input fails or an exit is requested.
|
116
128
|
def self.do_interaction_loop(client, state, info)
|
117
|
-
|
118
|
-
|
129
|
+
until state.exit_requested
|
130
|
+
line = Readline.readline(prompt(info, state), true)
|
131
|
+
break unless line
|
119
132
|
with_interrupt_handling { Commands.exec(line.chomp, client, state) }
|
120
133
|
end
|
121
|
-
puts
|
134
|
+
puts unless line
|
122
135
|
end
|
123
136
|
|
124
137
|
# Instruct the user to enter an authorization code and return the code. If
|
@@ -131,5 +144,4 @@ module Droxi
|
|
131
144
|
code = $stdin.gets
|
132
145
|
code ? code.strip! : exit
|
133
146
|
end
|
134
|
-
|
135
147
|
end
|
data/lib/droxi/commands.rb
CHANGED
@@ -4,7 +4,6 @@ require_relative 'text'
|
|
4
4
|
|
5
5
|
# Module containing definitions for client commands.
|
6
6
|
module Commands
|
7
|
-
|
8
7
|
# Exception indicating that a client command was given the wrong number of
|
9
8
|
# arguments.
|
10
9
|
class UsageError < ArgumentError
|
@@ -12,7 +11,6 @@ module Commands
|
|
12
11
|
|
13
12
|
# A client command. Contains metadata as well as execution procedure.
|
14
13
|
class Command
|
15
|
-
|
16
14
|
# A +String+ specifying the usage of the command in the style of a man page
|
17
15
|
# synopsis. Optional arguments are enclosed in brackets; varargs-style
|
18
16
|
# arguments are suffixed with an ellipsis.
|
@@ -36,12 +34,9 @@ module Commands
|
|
36
34
|
# given. Raises a +UsageError+ if an invalid number of command-line
|
37
35
|
# arguments is given.
|
38
36
|
def exec(client, state, *args)
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
else
|
43
|
-
fail UsageError, @usage
|
44
|
-
end
|
37
|
+
fail UsageError, @usage unless num_args_ok?(args.length)
|
38
|
+
block = proc { |line| yield line if block_given? }
|
39
|
+
@procedure.yield(client, state, args, block)
|
45
40
|
end
|
46
41
|
|
47
42
|
# Return a +String+ describing the type of argument at the given index.
|
@@ -49,12 +44,9 @@ module Commands
|
|
49
44
|
# the +Command+ takes no arguments, return +nil+.
|
50
45
|
def type_of_arg(index)
|
51
46
|
args = @usage.split.drop(1).reject { |arg| arg.include?('-') }
|
52
|
-
if args.empty?
|
53
|
-
|
54
|
-
|
55
|
-
index = [index, args.length - 1].min
|
56
|
-
args[index].tr('[].', '')
|
57
|
-
end
|
47
|
+
return nil if args.empty?
|
48
|
+
index = [index, args.length - 1].min
|
49
|
+
args[index].tr('[].', '')
|
58
50
|
end
|
59
51
|
|
60
52
|
private
|
@@ -64,13 +56,11 @@ module Commands
|
|
64
56
|
def num_args_ok?(num_args)
|
65
57
|
args = @usage.split.drop(1)
|
66
58
|
min_args = args.reject { |arg| arg.start_with?('[') }.length
|
67
|
-
if args.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
max_args = args.length
|
73
|
-
end
|
59
|
+
max_args = if args.any? { |arg| arg.end_with?('...') }
|
60
|
+
num_args
|
61
|
+
else
|
62
|
+
args.length
|
63
|
+
end
|
74
64
|
(min_args..max_args).include?(num_args)
|
75
65
|
end
|
76
66
|
end
|
@@ -82,17 +72,16 @@ module Commands
|
|
82
72
|
Dropbox root. With a remote directory name as the argument, changes to \
|
83
73
|
that directory. With - as the argument, changes to the previous working \
|
84
74
|
directory.",
|
85
|
-
lambda do |
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
state.pwd = state.oldpwd
|
75
|
+
lambda do |_client, state, args, output|
|
76
|
+
case
|
77
|
+
when args.empty? then state.pwd = '/'
|
78
|
+
when args[0] == '-' then state.pwd = state.oldpwd
|
90
79
|
else
|
91
80
|
path = state.resolve_path(args[0])
|
92
81
|
if state.directory?(path)
|
93
82
|
state.pwd = path
|
94
83
|
else
|
95
|
-
output.call(
|
84
|
+
output.call("cd: #{args[0]}: no such directory")
|
96
85
|
end
|
97
86
|
end
|
98
87
|
end
|
@@ -106,15 +95,35 @@ module Commands
|
|
106
95
|
final argument is a directory, copies each remote file or folder into \
|
107
96
|
that directory.",
|
108
97
|
lambda do |client, state, args, output|
|
109
|
-
cp_mv(client, state, args, output, 'cp'
|
98
|
+
cp_mv(client, state, args, output, 'cp')
|
99
|
+
end
|
100
|
+
)
|
101
|
+
|
102
|
+
# Execute arbitrary code.
|
103
|
+
DEBUG = Command.new(
|
104
|
+
'debug STRING...',
|
105
|
+
"Evaluates the given string as Ruby code and prints the result. Won't \
|
106
|
+
work unless the program was invoked with the --debug flag.",
|
107
|
+
# rubocop:disable Lint/UnusedBlockArgument, Lint/Eval
|
108
|
+
lambda do |client, state, args, output|
|
109
|
+
if ARGV.include?('--debug')
|
110
|
+
begin
|
111
|
+
output.call(eval(args.join(' ')).inspect)
|
112
|
+
# rubocop:enable Lint/UnusedBlockArgument, Lint/Eval
|
113
|
+
rescue => error
|
114
|
+
output.call(error.inspect)
|
115
|
+
end
|
116
|
+
else
|
117
|
+
output.call('Debug not enabled.')
|
118
|
+
end
|
110
119
|
end
|
111
120
|
)
|
112
121
|
|
113
122
|
# Terminate the session.
|
114
123
|
EXIT = Command.new(
|
115
124
|
'exit',
|
116
|
-
|
117
|
-
lambda do |
|
125
|
+
'Exit the program.',
|
126
|
+
lambda do |_client, state, _args, _output|
|
118
127
|
state.exit_requested = true
|
119
128
|
end
|
120
129
|
)
|
@@ -125,7 +134,7 @@ module Commands
|
|
125
134
|
"Clear the client-side cache of remote filesystem metadata. With no \
|
126
135
|
arguments, clear the entire cache. If given directories as arguments, \
|
127
136
|
(recursively) clear the cache of those directories only.",
|
128
|
-
lambda do |
|
137
|
+
lambda do |_client, state, args, output|
|
129
138
|
if args.empty?
|
130
139
|
state.cache.clear
|
131
140
|
else
|
@@ -144,14 +153,13 @@ module Commands
|
|
144
153
|
lambda do |client, state, args, output|
|
145
154
|
state.expand_patterns(args).each do |path|
|
146
155
|
if path.is_a?(GlobError)
|
147
|
-
output.call("get: #{path}:
|
156
|
+
output.call("get: #{path}: no such file or directory")
|
148
157
|
else
|
149
158
|
try_and_handle(DropboxError, output) do
|
150
159
|
contents = client.get_file(path)
|
151
|
-
File.
|
152
|
-
|
153
|
-
|
154
|
-
output.call("#{File.basename(path)} <- #{path}")
|
160
|
+
basename = File.basename(path)
|
161
|
+
File.open(basename, 'wb') { |file| file.write(contents) }
|
162
|
+
output.call("#{basename} <- #{path}")
|
155
163
|
end
|
156
164
|
end
|
157
165
|
end
|
@@ -163,7 +171,7 @@ module Commands
|
|
163
171
|
'help [COMMAND]',
|
164
172
|
"Print usage and help information about a command. If no command is \
|
165
173
|
given, print a list of commands instead.",
|
166
|
-
lambda do |
|
174
|
+
lambda do |_client, _state, args, output|
|
167
175
|
if args.empty?
|
168
176
|
Text.table(NAMES).each { |line| output.call(line) }
|
169
177
|
else
|
@@ -186,20 +194,23 @@ module Commands
|
|
186
194
|
home directory. With a local directory name as the argument, changes to \
|
187
195
|
that directory. With - as the argument, changes to the previous working \
|
188
196
|
directory.",
|
189
|
-
lambda do |
|
190
|
-
path =
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
197
|
+
lambda do |_client, state, args, output|
|
198
|
+
path = case
|
199
|
+
when args.empty? then File.expand_path('~')
|
200
|
+
when args[0] == '-' then state.local_oldpwd
|
201
|
+
else
|
202
|
+
begin
|
203
|
+
File.expand_path(args[0])
|
204
|
+
rescue ArgumentError
|
205
|
+
args[0]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
if Dir.exist?(path)
|
199
210
|
state.local_oldpwd = Dir.pwd
|
200
211
|
Dir.chdir(path)
|
201
212
|
else
|
202
|
-
output.call("lcd: #{args[0]}:
|
213
|
+
output.call("lcd: #{args[0]}: no such file or directory")
|
203
214
|
end
|
204
215
|
end
|
205
216
|
)
|
@@ -212,13 +223,13 @@ module Commands
|
|
212
223
|
arguments, list the contents of the directories. When given remote files \
|
213
224
|
as arguments, list the files. If the -l option is given, display \
|
214
225
|
information about the files.",
|
215
|
-
lambda do |
|
216
|
-
long = args.delete('-l')
|
226
|
+
lambda do |_client, state, args, output|
|
227
|
+
long = args.delete('-l')
|
217
228
|
|
218
229
|
files, dirs = [], []
|
219
230
|
state.expand_patterns(args, true).each do |path|
|
220
231
|
if path.is_a?(GlobError)
|
221
|
-
output.call("ls: #{path}:
|
232
|
+
output.call("ls: #{path}: no such file or directory")
|
222
233
|
else
|
223
234
|
type = state.directory?(path) ? dirs : files
|
224
235
|
type << path
|
@@ -229,7 +240,7 @@ module Commands
|
|
229
240
|
|
230
241
|
# First list files
|
231
242
|
list(state, files, files, long) { |line| output.call(line) }
|
232
|
-
output.call('')
|
243
|
+
output.call('') unless dirs.empty? || files.empty?
|
233
244
|
|
234
245
|
# Then list directory contents
|
235
246
|
dirs.each_with_index do |dir, i|
|
@@ -250,7 +261,7 @@ module Commands
|
|
250
261
|
lambda do |client, state, args, output|
|
251
262
|
state.expand_patterns(args).each do |path|
|
252
263
|
if path.is_a?(GlobError)
|
253
|
-
output.call("media: #{path}:
|
264
|
+
output.call("media: #{path}: no such file or directory")
|
254
265
|
else
|
255
266
|
try_and_handle(DropboxError, output) do
|
256
267
|
url = client.media(path)['url']
|
@@ -264,12 +275,13 @@ module Commands
|
|
264
275
|
# Create a remote directory.
|
265
276
|
MKDIR = Command.new(
|
266
277
|
'mkdir REMOTE_DIR...',
|
267
|
-
|
278
|
+
'Create remote directories.',
|
268
279
|
lambda do |client, state, args, output|
|
269
280
|
args.each do |arg|
|
270
281
|
try_and_handle(DropboxError, output) do
|
271
282
|
path = state.resolve_path(arg)
|
272
|
-
|
283
|
+
metadata = client.file_create_folder(path)
|
284
|
+
state.cache.add(metadata)
|
273
285
|
end
|
274
286
|
end
|
275
287
|
end
|
@@ -283,7 +295,7 @@ module Commands
|
|
283
295
|
final argument is a directory, moves each remote file or folder into \
|
284
296
|
that directory.",
|
285
297
|
lambda do |client, state, args, output|
|
286
|
-
cp_mv(client, state, args, output, 'mv'
|
298
|
+
cp_mv(client, state, args, output, 'mv')
|
287
299
|
end
|
288
300
|
)
|
289
301
|
|
@@ -296,17 +308,13 @@ module Commands
|
|
296
308
|
remote working directory.",
|
297
309
|
lambda do |client, state, args, output|
|
298
310
|
from_path = args[0]
|
299
|
-
|
300
|
-
to_path = args[1]
|
301
|
-
else
|
302
|
-
to_path = File.basename(from_path)
|
303
|
-
end
|
311
|
+
to_path = (args.length == 2) ? args[1] : File.basename(from_path)
|
304
312
|
to_path = state.resolve_path(to_path)
|
305
313
|
|
306
|
-
try_and_handle(Exception, output) do
|
314
|
+
try_and_handle(Exception, output) do
|
307
315
|
File.open(File.expand_path(from_path), 'rb') do |file|
|
308
316
|
data = client.put_file(to_path, file)
|
309
|
-
state.cache
|
317
|
+
state.cache.add(data)
|
310
318
|
output.call("#{from_path} -> #{data['path']}")
|
311
319
|
end
|
312
320
|
end
|
@@ -316,15 +324,15 @@ module Commands
|
|
316
324
|
# Remove remote files.
|
317
325
|
RM = Command.new(
|
318
326
|
'rm REMOTE_FILE...',
|
319
|
-
|
327
|
+
'Remove each specified remote file or directory.',
|
320
328
|
lambda do |client, state, args, output|
|
321
329
|
state.expand_patterns(args).each do |path|
|
322
330
|
if path.is_a?(GlobError)
|
323
|
-
output.call("rm: #{path}:
|
331
|
+
output.call("rm: #{path}: no such file or directory")
|
324
332
|
else
|
325
333
|
try_and_handle(DropboxError, output) do
|
326
334
|
client.file_delete(path)
|
327
|
-
state.
|
335
|
+
state.cache.remove(path)
|
328
336
|
end
|
329
337
|
end
|
330
338
|
end
|
@@ -342,7 +350,7 @@ module Commands
|
|
342
350
|
lambda do |client, state, args, output|
|
343
351
|
state.expand_patterns(args).each do |path|
|
344
352
|
if path.is_a?(GlobError)
|
345
|
-
output.call("share: #{path}:
|
353
|
+
output.call("share: #{path}: no such file or directory")
|
346
354
|
else
|
347
355
|
try_and_handle(DropboxError, output) do
|
348
356
|
url = client.shares(path)['url']
|
@@ -353,16 +361,20 @@ module Commands
|
|
353
361
|
end
|
354
362
|
)
|
355
363
|
|
364
|
+
# Return an +Array+ of all command names.
|
365
|
+
def self.names
|
366
|
+
symbols = constants.select { |sym| const_get(sym).is_a?(Command) }
|
367
|
+
symbols.map { |sym| sym.to_s.downcase }
|
368
|
+
end
|
369
|
+
|
356
370
|
# +Array+ of all command names.
|
357
|
-
NAMES =
|
358
|
-
const_get(sym).is_a?(Command)
|
359
|
-
end.map { |sym| sym.to_s.downcase }
|
371
|
+
NAMES = names
|
360
372
|
|
361
373
|
# Parse and execute a line of user input in the given context.
|
362
374
|
def self.exec(input, client, state)
|
363
375
|
if input.start_with?('!')
|
364
376
|
shell(input[1, input.length - 1]) { |line| puts line }
|
365
|
-
elsif
|
377
|
+
elsif !input.empty?
|
366
378
|
tokens = tokenize(input)
|
367
379
|
cmd, args = tokens[0], tokens.drop(1)
|
368
380
|
try_command(cmd, args, client, state)
|
@@ -399,10 +411,10 @@ module Commands
|
|
399
411
|
def self.tokenize(string)
|
400
412
|
string.split.reduce([]) do |list, token|
|
401
413
|
list << if !list.empty? && list.last.end_with?('\\')
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
414
|
+
"#{list.pop.chop} #{token}"
|
415
|
+
else
|
416
|
+
token
|
417
|
+
end
|
406
418
|
end
|
407
419
|
end
|
408
420
|
|
@@ -432,16 +444,17 @@ module Commands
|
|
432
444
|
pipe.each_line { |line| yield line.chomp if block_given? }
|
433
445
|
end
|
434
446
|
rescue Interrupt
|
435
|
-
|
447
|
+
yield ''
|
448
|
+
rescue Errno::ENOENT => error
|
436
449
|
yield error.to_s if block_given?
|
437
450
|
end
|
438
451
|
|
439
452
|
# Return an +Array+ of paths from an +Array+ of globs, passing error messages
|
440
453
|
# to the output +Proc+ for non-matches.
|
441
|
-
def self.expand(state, paths, preserve_root, output,
|
442
|
-
state.expand_patterns(paths,
|
454
|
+
def self.expand(state, paths, preserve_root, output, cmd)
|
455
|
+
state.expand_patterns(paths, preserve_root).map do |item|
|
443
456
|
if item.is_a?(GlobError)
|
444
|
-
output.call("#{
|
457
|
+
output.call("#{cmd}: #{item}: no such file or directory") if output
|
445
458
|
nil
|
446
459
|
else
|
447
460
|
item
|
@@ -449,41 +462,48 @@ module Commands
|
|
449
462
|
end.compact
|
450
463
|
end
|
451
464
|
|
452
|
-
# Copies or moves
|
453
|
-
#
|
454
|
-
def self.copy_move(method,
|
455
|
-
from_path, to_path =
|
465
|
+
# Copies or moves a file and passes a description of the operation to the
|
466
|
+
# output +Proc+.
|
467
|
+
def self.copy_move(method, args, client, state, output)
|
468
|
+
from_path, to_path = args.map { |p| state.resolve_path(p) }
|
456
469
|
try_and_handle(DropboxError, output) do
|
457
470
|
metadata = client.send(method, from_path, to_path)
|
458
|
-
state.
|
459
|
-
state.
|
460
|
-
output.call("#{
|
471
|
+
state.cache.remove(from_path) if method == :file_move
|
472
|
+
state.cache.add(metadata)
|
473
|
+
output.call("#{args[0]} -> #{args[1]}")
|
461
474
|
end
|
462
475
|
end
|
463
476
|
|
464
477
|
# Execute a 'mv' or 'cp' operation depending on arguments given.
|
465
|
-
def self.cp_mv(client, state, args, output, cmd
|
478
|
+
def self.cp_mv(client, state, args, output, cmd)
|
466
479
|
sources = expand(state, args.take(args.length - 1), true, output, cmd)
|
480
|
+
method = (cmd == 'cp') ? :file_copy : :file_move
|
467
481
|
dest = state.resolve_path(args.last)
|
468
482
|
|
469
483
|
if sources.length == 1 && !state.directory?(dest)
|
470
|
-
copy_move(method, sources[0], args.last, client, state, output)
|
484
|
+
copy_move(method, [sources[0], args.last], client, state, output)
|
471
485
|
else
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
486
|
+
cp_mv_to_dir(args, client, state, cmd, output)
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# Copies or moves files into a directory.
|
491
|
+
def self.cp_mv_to_dir(args, client, state, cmd, output)
|
492
|
+
sources = expand(state, args.take(args.length - 1), true, nil, cmd)
|
493
|
+
method = (cmd == 'cp') ? :file_copy : :file_move
|
494
|
+
if state.metadata(state.resolve_path(args.last))
|
495
|
+
sources.each do |source|
|
496
|
+
to_path = args.last.chomp('/') + '/' + File.basename(source)
|
497
|
+
copy_move(method, [source, to_path], client, state, output)
|
479
498
|
end
|
499
|
+
else
|
500
|
+
output.call("#{cmd}: #{args.last}: no such directory")
|
480
501
|
end
|
481
502
|
end
|
482
503
|
|
483
504
|
# If the remote working directory does not exist, move up the directory
|
484
505
|
# tree until at a real location.
|
485
506
|
def self.check_pwd(state)
|
486
|
-
state.pwd = File.dirname(state.pwd) until state.metadata(state.pwd)
|
507
|
+
(state.pwd = File.dirname(state.pwd)) until state.metadata(state.pwd)
|
487
508
|
end
|
488
|
-
|
489
509
|
end
|