droxi 0.3.1 → 0.4.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 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).