droxi 0.3.1 → 0.4.0

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: 1c51dbec9e627b8b8b2e07e4ac8bf4464bee33dd
4
- data.tar.gz: 480c18b2b62a8ec434cd6c8a8a481a40976f884b
3
+ metadata.gz: ad346e735c5106bff58480298a50a5c296e15542
4
+ data.tar.gz: f565510a170c8ad38066048d4cf7fe10bf09d51e
5
5
  SHA512:
6
- metadata.gz: 5af1c7d1ab268567fa44508dad484090c6da945f1861e022c025e34dff7527525d81af89d03812b613c1d8aa008e9e33e806656d47832f95fa9ae37654b16655
7
- data.tar.gz: bd890a7e5aa0c14d912181f7d80643a46c1d41dbf914b4556919f660fa1e67edec98d8e18f48c25d01b9b6d71c4555e3c4e34a18553a6a613838410fba431906
6
+ metadata.gz: d14ddec3bd6d7cb7bbd3ad82ece43f7f9cb3811868fa1ef82525fb3dcbf853aaecf30365c960ded970be82fbf26455d454a334e34c27a6b599b7eca50f56cbda
7
+ data.tar.gz: 337b45f2741a59ac04422264ce0452abcd69e62eea9e2ee38dea7a97dca309fc56e14d4c90b62e2244c7ce11a4e36c82ed9b4541e2505e5ce8a8e5a49a54dcc1
data/README.md CHANGED
@@ -26,7 +26,24 @@ Features
26
26
  - Context-sensitive tab completion and path globbing
27
27
  - Upload, download, organize, search, and share files
28
28
  - File revision control
29
- - Man page and interactive help
29
+ - Interactive help
30
+
31
+ Usage
32
+ -----
33
+ Usage: droxi [OPTION ...] [COMMAND [ARGUMENT ...]]
34
+
35
+ If invoked without arguments, run in interactive mode. If invoked with
36
+ arguments, parse the arguments as a command invocation, execute the
37
+ command, and exit.
38
+
39
+ For a list of commands, run `droxi help` or use the 'help' command in
40
+ interactive mode.
41
+
42
+ Options:
43
+ --debug Enable debug command
44
+ -f, --file FILE Specify path of config file
45
+ -h, --help Print help information and exit
46
+ --version Print version information and exit
30
47
 
31
48
  Examples
32
49
  --------
data/Rakefile CHANGED
@@ -37,7 +37,7 @@ task :doc do
37
37
  sh 'rdoc `find lib -name *.rb`'
38
38
  end
39
39
 
40
- desc 'build executable and man page'
40
+ desc 'build executable'
41
41
  task :build do
42
42
  def build_exe
43
43
  filenames = `find lib -name *.rb`.split + ['bin/droxi']
@@ -50,64 +50,29 @@ task :build do
50
50
  File.chmod(0755, 'build/droxi')
51
51
  end
52
52
 
53
- def date(gemspec)
54
- require 'time'
55
- Time.parse(gemspec[/\d{4}-\d{2}-\d{2}/]).strftime('%B %Y')
56
- end
57
-
58
- def commands
59
- require_relative 'lib/droxi/commands'
60
- Commands::NAMES.sort.map do |name|
61
- cmd = Commands.const_get(name.upcase.to_sym)
62
- ".TP\n#{cmd.usage}\n#{cmd.description}\n"
63
- end.join.strip
64
- end
65
-
66
- def build_page
67
- gemspec = IO.read('droxi.gemspec')
68
- main = IO.read('lib/droxi.rb')
69
-
70
- contents = format(IO.read('droxi.1.template'),
71
- date: date(gemspec),
72
- version: main[/\d+\.\d+\.\d+/],
73
- commands: commands)
74
-
75
- IO.write('build/droxi.1', contents)
76
- end
77
-
78
53
  Dir.mkdir('build') unless Dir.exist?('build')
79
54
  build_exe
80
- build_page
81
- end
82
-
83
- desc 'build html version of man page'
84
- task :html do
85
- sh 'groff -man -T html build/droxi.1 > build/droxi.html'
86
55
  end
87
56
 
88
57
  PREFIX = ENV['PREFIX'] || ENV['prefix'] || '/usr/local'
89
58
  BIN_PATH = "#{PREFIX}/bin"
90
- MAN_PATH = "#{PREFIX}/share/man/man1"
91
59
 
92
- desc 'install executable and man page'
60
+ desc 'install executable'
93
61
  task :install do
94
62
  require 'fileutils'
95
63
  begin
96
64
  FileUtils.mkdir_p(BIN_PATH)
97
65
  FileUtils.cp('build/droxi', BIN_PATH)
98
- FileUtils.mkdir_p(MAN_PATH)
99
- FileUtils.cp('build/droxi.1', MAN_PATH)
100
66
  rescue => error
101
67
  puts error
102
68
  end
103
69
  end
104
70
 
105
- desc 'uninstall executable and man page'
71
+ desc 'uninstall executable'
106
72
  task :uninstall do
107
73
  require 'fileutils'
108
74
  begin
109
75
  FileUtils.rm("#{BIN_PATH}/droxi")
110
- FileUtils.rm("#{MAN_PATH}/droxi.1")
111
76
  rescue => error
112
77
  puts error
113
78
  end
data/bin/droxi CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
2
  require_relative '../lib/droxi'
3
- Droxi.run(ARGV.dup)
3
+ Droxi.run
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'droxi'
3
3
  s.version = IO.read('lib/droxi.rb')[/VERSION = '(.+)'/, 1]
4
- s.date = '2015-05-09'
4
+ s.date = '2015-05-31'
5
5
  s.summary = 'An ftp-like command-line interface to Dropbox'
6
6
  s.description = "A command-line Dropbox interface based on GNU coreutils, \
7
7
  GNU ftp, and lftp. Features include smart tab completion, \
@@ -1,10 +1,11 @@
1
1
  begin
2
2
  require 'dropbox_sdk'
3
3
  rescue LoadError
4
- puts "droxi requires the dropbox-sdk gem."
5
- puts "Run `gem install dropbox-sdk` to install it."
4
+ puts 'droxi requires the dropbox-sdk gem.'
5
+ puts 'Run `gem install dropbox-sdk` to install it.'
6
6
  exit
7
7
  end
8
+ require 'optparse'
8
9
  require 'readline'
9
10
 
10
11
  require_relative 'droxi/commands'
@@ -16,24 +17,34 @@ require_relative 'droxi/text'
16
17
  # Command-line Dropbox client module.
17
18
  module Droxi
18
19
  # Version number of the program.
19
- VERSION = '0.3.1'
20
+ VERSION = '0.4.0'
20
21
 
21
22
  # Message to display when invoked with the --help option.
22
- HELP_TEXT =
23
- "If you've installed this program using Rake or the AUR package, you " \
24
- 'should also have the man page installed on your system. If you do not ' \
25
- 'have the man page, you can access it at http://jangler.info/man/droxi ' \
26
- 'in HTML form.'
23
+ HELP_TEXT = [
24
+ 'If invoked without arguments, run in interactive mode. If invoked with ' \
25
+ 'arguments, parse the arguments as a command invocation, execute the ' \
26
+ 'command, and exit.',
27
+ "For a list of commands, run `droxi help` or use the 'help' command in " \
28
+ 'interactive mode.'
29
+ ]
27
30
 
28
31
  # Run the client.
29
- def self.run(args)
30
- client = DropboxClient.new(access_token)
31
- state = State.new(client)
32
+ def self.run
33
+ reenter = ARGV[0] == 'REENTER'
34
+ ARGV.delete_if { |arg| arg == 'REENTER' }
35
+ original_argv = ARGV.dup
36
+ options = handle_options
37
+ Settings.init
32
38
 
33
- options = handle_options(args)
34
- args.shift(options.size)
39
+ client = DropboxClient.new(access_token)
40
+ state = State.new(client, ARGV.empty?, original_argv)
41
+ state.debug_enabled = options[:debug]
35
42
 
36
- args.empty? ? run_interactive(client, state) : invoke(args, client, state)
43
+ if ARGV.empty?
44
+ run_interactive(client, state, reenter)
45
+ else
46
+ invoke(ARGV, client, state)
47
+ end
37
48
  rescue DropboxAuthError => error
38
49
  warn error
39
50
  Settings.delete(:access_token)
@@ -43,15 +54,45 @@ module Droxi
43
54
 
44
55
  private
45
56
 
46
- # Handles command-line options extracted from an +Array+ and returns an
47
- # +Array+ of the extracted options.
48
- def self.handle_options(args)
49
- options = args.take_while { |s| s.start_with?('-') }
50
- puts "droxi v#{VERSION}" if options.include?('--version')
51
- if options.include?('-h') || options.include?('--help')
52
- Text.wrap(HELP_TEXT).each { |s| puts s }
57
+ # Handles command-line options and returns a +Hash+ of the extracted options.
58
+ def self.handle_options
59
+ options = { debug: false }
60
+
61
+ parser = OptionParser.new do |opts|
62
+ opts.banner = 'Usage: droxi [OPTION ...] [COMMAND [ARGUMENT ...]]'
63
+
64
+ opts.separator ''
65
+ HELP_TEXT.each do |text|
66
+ Text.wrap(text).each { |s| opts.separator(s) }
67
+ opts.separator ''
68
+ end
69
+ opts.separator 'Options:'
70
+
71
+ opts.on('--debug', 'Enable debug command') { options[:debug] = true }
72
+
73
+ opts.on('-f', '--file FILE', String,
74
+ 'Specify path of config file') do |path|
75
+ Settings.config_file_path = path
76
+ end
77
+
78
+ opts.on('-h', '--help', 'Print help information and exit') do
79
+ puts opts
80
+ exit
81
+ end
82
+
83
+ opts.on('--version', 'Print version information and exit') do
84
+ puts "droxi v#{VERSION}"
85
+ exit
86
+ end
53
87
  end
54
- exit if %w(-h --help --version).any? { |s| options.include?(s) }
88
+
89
+ begin
90
+ parser.parse!
91
+ rescue OptionParser::ParseError => err
92
+ warn(err)
93
+ exit(1)
94
+ end
95
+
55
96
  options
56
97
  end
57
98
 
@@ -96,9 +137,13 @@ module Droxi
96
137
  end
97
138
 
98
139
  # Run the client in interactive mode.
99
- def self.run_interactive(client, state)
140
+ def self.run_interactive(client, state, reenter)
100
141
  info = client.account_info
101
- puts "Logged in as #{info['display_name']} (#{info['email']})"
142
+ if reenter
143
+ state.pwd = state.oldpwd
144
+ else
145
+ puts "Logged in as #{info['display_name']} (#{info['email']})"
146
+ end
102
147
 
103
148
  init_readline(state)
104
149
  with_interrupt_handling { do_interaction_loop(client, state, info) }
@@ -51,7 +51,7 @@ module Commands
51
51
  # command, +false+ otherwise.
52
52
  def num_args_ok?(num_args)
53
53
  args = @usage.split.drop(1)
54
- min_args = args.reject { |arg| arg[/[\[\]]/] }.size
54
+ min_args = args.count { |arg| !arg[/[\[\]]/] }
55
55
  max_args = if args.any? { |arg| arg.end_with?('...') }
56
56
  num_args
57
57
  else
@@ -118,12 +118,12 @@ module Commands
118
118
  'debug STRING...',
119
119
  "Evaluates the given string as Ruby code and prints the result. Won't \
120
120
  work unless the program was invoked with the --debug flag.",
121
- # rubocop:disable Lint/UnusedBlockArgument, Lint/Eval
122
- lambda do |client, state, args|
123
- if ARGV.include?('--debug')
121
+ # rubocop:disable Lint/Eval
122
+ lambda do |_client, state, args|
123
+ if state.debug_enabled
124
124
  begin
125
125
  p eval(args.join(' '))
126
- # rubocop:enable Lint/UnusedBlockArgument, Lint/Eval
126
+ # rubocop:enable Lint/Eval
127
127
  rescue SyntaxError => error
128
128
  warn error
129
129
  rescue => error
@@ -135,6 +135,20 @@ module Commands
135
135
  end
136
136
  )
137
137
 
138
+ # Execute shell command.
139
+ EXEC = Command.new(
140
+ 'exec STRING...',
141
+ "Executes the given string in the system shell. ! can be used as \
142
+ shorthand for exec, as in \"!ls\".",
143
+ lambda do |_client, state, args|
144
+ state.pwd = '/'
145
+ Settings.save
146
+ cmd = args.join(' ')
147
+ cmd += '; droxi REENTER ' + state.argv.join(' ') if state.interactive
148
+ Kernel.exec(cmd)
149
+ end
150
+ )
151
+
138
152
  # Terminate the session.
139
153
  EXIT = Command.new(
140
154
  'exit',
@@ -233,7 +247,6 @@ module Commands
233
247
  else
234
248
  try_and_handle(DropboxError) do
235
249
  client.revisions(path).each do |rev|
236
-
237
250
  size = rev['size'].sub(/ (.)B/, '\1').sub(' bytes', '').rjust(7)
238
251
  mtime = Time.parse(rev['modified'])
239
252
  current_year = (mtime.year == Time.now.year)
@@ -285,7 +298,8 @@ module Commands
285
298
  lambda do |_client, state, args|
286
299
  long = extract_flags(LS.usage, args, '-l' => 0).include?('-l')
287
300
 
288
- files, dirs = [], []
301
+ files = []
302
+ dirs = []
289
303
  state.expand_patterns(args, true).each do |path|
290
304
  if path.is_a?(GlobError)
291
305
  warn "ls: #{path}: no such file or directory"
@@ -566,11 +580,11 @@ module Commands
566
580
 
567
581
  # Parse and execute a line of user input in the given context.
568
582
  def self.exec(input, client, state)
569
- if input.start_with?('!')
570
- shell(input[1, input.size - 1]) { |line| puts line }
571
- elsif !input.empty?
583
+ unless input.empty?
584
+ input.sub!(/^!/, 'exec ')
572
585
  tokens = Text.tokenize(input)
573
- cmd, args = tokens.first, tokens.drop(1)
586
+ cmd = tokens.first
587
+ args = tokens.drop(1)
574
588
  try_command(cmd, args, client, state)
575
589
  end
576
590
  end
@@ -700,7 +714,8 @@ module Commands
700
714
  # removed flags. Prints warnings if the flags are not in the given +String+
701
715
  # of valid flags (e.g. '-rf').
702
716
  def self.extract_flags(usage, args, flags)
703
- extracted, index = [], 0
717
+ extracted = []
718
+ index = 0
704
719
  while index < args.size
705
720
  arg = args[index]
706
721
  extracted_flags =
@@ -742,7 +757,6 @@ module Commands
742
757
  end
743
758
 
744
759
  # Continuously try to upload until successful or interrupted.
745
- # rubocop:disable Style/MethodLength
746
760
  def self.loop_upload(uploader, monitor_thread, tries)
747
761
  while tries != 0 && uploader.offset < uploader.total_size
748
762
  begin
@@ -756,7 +770,6 @@ module Commands
756
770
  monitor_thread.kill if monitor_thread
757
771
  raise error
758
772
  end
759
- # rubocop:enable Style/MethodLength
760
773
 
761
774
  # Displays real-time progress for the a being uploaded.
762
775
  def self.monitor_upload(uploader, to_path)
@@ -11,7 +11,7 @@ module Complete
11
11
  completion_options(type, tokens.last, state).map do |option|
12
12
  option.gsub(' ', '\ ').sub(/\\ $/, ' ')
13
13
  .split.drop(tokens.last.count(' ')).join(' ')
14
- .sub(/[^\\\/]$/, '\0 ')
14
+ .sub(%r{[^\\/]$}, '\0 ')
15
15
  end
16
16
  end
17
17
 
@@ -36,6 +36,8 @@ module Complete
36
36
  index = tokens.drop_while { |token| token[/^-\w+$/] }.size
37
37
  if index <= 1
38
38
  'COMMAND'
39
+ elsif tokens[0].start_with?('!') || tokens[0].downcase == 'exec'
40
+ 'LOCAL_FILE'
39
41
  elsif Commands::NAMES.include?(tokens.first)
40
42
  cmd = Commands.const_get(tokens.first.upcase.to_sym)
41
43
  cmd.type_of_arg(index - 2)
@@ -121,7 +123,7 @@ module Complete
121
123
  # Return the name of the directory indicated by a path.
122
124
  def self.strip_filename(path)
123
125
  return path if path == '/'
124
- path.end_with?('/') ? path.sub(/\/$/, '') : File.dirname(path)
126
+ path.end_with?('/') ? path.sub(%r{/$}, '') : File.dirname(path)
125
127
  end
126
128
 
127
129
  # Return a version of a path with .. and . resolved to appropriate
@@ -75,10 +75,14 @@ module Settings
75
75
  # setting data.
76
76
  def self.parse(line)
77
77
  return warn_invalid(line) unless /^(.+?)=(.+)$/ =~ line
78
- key, value = Regexp.last_match[1].to_sym, Regexp.last_match[2]
78
+ key = Regexp.last_match[1].to_sym
79
+ value = Regexp.last_match[2]
79
80
  return warn_invalid(line) unless [:access_token, :oldpwd].include?(key)
80
81
  { key => value }
81
82
  end
82
83
 
83
- self.settings = read
84
+ # Initialize settings by reading rc file.
85
+ def self.init
86
+ self.settings = read
87
+ end
84
88
  end
@@ -15,18 +15,30 @@ class State
15
15
  # The previous remote working directory path.
16
16
  attr_reader :oldpwd
17
17
 
18
+ # The actual/original command-line arguments
19
+ attr_reader :argv
20
+
21
+ # Whether the session is interactive.
22
+ attr_reader :interactive
23
+
18
24
  # The previous local working directory path.
19
25
  attr_accessor :local_oldpwd
20
26
 
21
27
  # +true+ if the client has requested to quit, +false+ otherwise.
22
28
  attr_accessor :exit_requested
23
29
 
30
+ # +true+ if the --debug option was given, +false+ otherwise.
31
+ attr_accessor :debug_enabled
32
+
24
33
  # Return a new application state that uses the given client. Starts at the
25
34
  # Dropbox root and with an empty cache.
26
- def initialize(client)
35
+ def initialize(client, interactive = true, argv = ARGV)
36
+ @interactive = interactive
37
+ @argv = argv
27
38
  @cache = Cache.new
28
39
  @client = client
29
40
  @exit_requested = false
41
+ @debug_enabled = false
30
42
  @pwd = '/'
31
43
  @oldpwd = Settings[:oldpwd] || '/'
32
44
  @local_oldpwd = Dir.pwd
@@ -53,9 +65,10 @@ class State
53
65
  path = resolve_path(path)
54
66
  metadata(path)
55
67
  path = "#{path}/".sub('//', '/')
56
- @cache.keys.select do |key|
68
+ keys = @cache.keys.select do |key|
57
69
  key.start_with?(path) && key != path && !key.sub(path, '').include?('/')
58
- end.map { |key| @cache[key]['path'] }
70
+ end
71
+ keys.map { |key| @cache[key]['path'] }
59
72
  end
60
73
 
61
74
  # Return +true+ if the Dropbox path is a directory, +false+ otherwise.
@@ -80,7 +93,7 @@ class State
80
93
  path.gsub!('//', '/')
81
94
  nil while path.sub!(%r{/([^/]+?)/\.\.}, '')
82
95
  nil while path.sub!('./', '')
83
- path.sub!(/\/\.$/, '')
96
+ path.sub!(%r{/\.$}, '')
84
97
  path.chomp!('/')
85
98
  path.gsub!('//', '/')
86
99
  path.empty? ? '/' : path
@@ -130,8 +143,8 @@ class State
130
143
  path = path.downcase
131
144
  dir = File.dirname(path)
132
145
  matches = contents(dir).select do |entry|
133
- File.fnmatch(path, entry.downcase)
134
- end
146
+ File.fnmatch(path, entry.downcase)
147
+ end
135
148
  return GlobError.new(pattern) if matches.empty?
136
149
  return matches unless preserve_root
137
150
  prefix = pattern.rpartition('/')[0, 2].join
@@ -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.size }.max + 2
11
+ item_width = items.map(&: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
@@ -16,7 +16,8 @@ module Text
16
16
  # Wrap a +String+ to fit the terminal and return an +Array+ of lines in the
17
17
  # result.
18
18
  def self.wrap(text)
19
- width, position = terminal_width, 0
19
+ width = terminal_width
20
+ position = 0
20
21
  lines = []
21
22
  while position < text.size
22
23
  lines << get_wrap_segment(text[position, text.size], width)
@@ -52,7 +53,8 @@ module Text
52
53
 
53
54
  # Return an +Array+ of lines of the given items formatted as a table.
54
55
  def self.format_table(items, item_width, columns)
55
- lines, items = [], items.dup
56
+ lines = []
57
+ items = items.dup
56
58
  until items.empty?
57
59
  lines << items.shift(columns).map { |item| item.ljust(item_width) }.join
58
60
  end
@@ -51,6 +51,7 @@ module TestUtils
51
51
 
52
52
  # Returns a new +DropboxClient+ and +State+.
53
53
  def self.create_client_and_state
54
+ Settings.init
54
55
  client = DropboxClient.new(Settings[:access_token])
55
56
  [client, State.new(client)]
56
57
  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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brandon Mulcahy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-05-09 00:00:00.000000000 Z
11
+ date: 2015-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dropbox-sdk
@@ -42,7 +42,6 @@ files:
42
42
  - README.md
43
43
  - Rakefile
44
44
  - bin/droxi
45
- - droxi.1.template
46
45
  - droxi.gemspec
47
46
  - lib/droxi.rb
48
47
  - lib/droxi/cache.rb
@@ -1,30 +0,0 @@
1
- .TH DROXI 1 "%{date}" "droxi %{version}"
2
- .SH NAME
3
- droxi \- ftp-like command-line interface to Dropbox
4
- .SH SYNOPSIS
5
- droxi [OPTION]... [COMMAND] [ARGUMENT]...
6
- .SH DESCRIPTION
7
- A command-line Dropbox interface inspired by GNU coreutils, GNU ftp, and lftp.
8
- Features include smart tab completion, globbing, and interactive help. If
9
- invoked without arguments, runs in interactive mode. If invoked with arguments,
10
- parses the arguments as a command invocation, executes the command, and exits.
11
- .SH COMMANDS
12
- .TP
13
- !STRING
14
- Pass a string to be executed by the local shell.
15
- %{commands}
16
- .SH OPTIONS
17
- .TP
18
- --debug
19
- Enable the 'debug' command for the session.
20
- .TP
21
- -h, --help
22
- Print help information and exit.
23
- .TP
24
- --version
25
- Print version information and exit.
26
- .SH BUGS
27
- If you find one, please use https://github.com/jangler/droxi/issues to report
28
- it, or contact me via email.
29
- .SH AUTHOR
30
- Written by Brandon Mulcahy (brandon@jangler.info).