droxi 0.0.5 → 0.1.0
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/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
|