exa 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.md +4 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +20 -0
- data/README.md +33 -0
- data/Rakefile +31 -0
- data/bin/exa +20 -0
- data/exa.gemspec +60 -0
- data/features/.gitkeep +0 -0
- data/features/exa.feature +1 -0
- data/features/step_definitions/.gitkeep +0 -0
- data/features/step_definitions/exa_steps.rb +1 -0
- data/gemspec.yml +16 -0
- data/lib/exa.rb +85 -0
- data/lib/exa/shell.rb +107 -0
- data/lib/exa/tree_node.rb +159 -0
- data/lib/exa/version.rb +4 -0
- data/lib/exa/visitor.rb +100 -0
- data/spec/exa_spec.rb +121 -0
- data/spec/spec_helper.rb +4 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f27cfd470e0649009f1628ac416129e508b9f892
|
4
|
+
data.tar.gz: de0b8c876be5db9d1aa5e51f139f675d0fef6fbf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84260a3a82086953e2e99ff908f370609ddbe5a60bf7567f646939ae71137477c7e00b5c29359d8339a458c1cb527dbc1bc14c891b376604c4a5bd78e2bb0737
|
7
|
+
data.tar.gz: 85cca81bd881342d01aa2878127d9469a1f154258285dcefd43794f1b948fdc8398e20a0d3c44d780b3b9069ecf9792842b75491ce43ae655c9337568096b9db
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour --format documentation
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown --title "exa Documentation" --protected
|
data/ChangeLog.md
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2016 Joseph Weissman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# exa
|
2
|
+
|
3
|
+
* [Homepage](https://rubygems.org/gems/exa)
|
4
|
+
* [Documentation](http://rubydoc.info/gems/exa/frames)
|
5
|
+
* [Email](mailto:jweissman1986 at gmail.com)
|
6
|
+
|
7
|
+
[![Code Climate GPA](https://codeclimate.com/github//exa/badges/gpa.svg)](https://codeclimate.com/github//exa)
|
8
|
+
|
9
|
+
## Description
|
10
|
+
|
11
|
+
TODO: Description
|
12
|
+
|
13
|
+
## Features
|
14
|
+
|
15
|
+
## Examples
|
16
|
+
|
17
|
+
require 'exa'
|
18
|
+
|
19
|
+
## Requirements
|
20
|
+
|
21
|
+
## Install
|
22
|
+
|
23
|
+
$ gem install exa
|
24
|
+
|
25
|
+
## Synopsis
|
26
|
+
|
27
|
+
$ exa
|
28
|
+
|
29
|
+
## Copyright
|
30
|
+
|
31
|
+
Copyright (c) 2016 Joseph Weissman
|
32
|
+
|
33
|
+
See {file:LICENSE.txt} for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'bundler/setup'
|
7
|
+
rescue LoadError => e
|
8
|
+
abort e.message
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rake'
|
12
|
+
|
13
|
+
|
14
|
+
require 'rubygems/tasks'
|
15
|
+
Gem::Tasks.new
|
16
|
+
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
RSpec::Core::RakeTask.new
|
19
|
+
|
20
|
+
task :test => :spec
|
21
|
+
task :default => :spec
|
22
|
+
|
23
|
+
require 'yard'
|
24
|
+
YARD::Rake::YardocTask.new
|
25
|
+
task :doc => :yard
|
26
|
+
|
27
|
+
require 'cucumber/rake/task'
|
28
|
+
|
29
|
+
Cucumber::Rake::Task.new do |t|
|
30
|
+
t.cucumber_opts = %w[--format pretty]
|
31
|
+
end
|
data/bin/exa
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
root = File.expand_path(File.join(File.dirname(__FILE__),'..'))
|
4
|
+
if File.directory?(File.join(root,'.git'))
|
5
|
+
Dir.chdir(root) do
|
6
|
+
begin
|
7
|
+
require 'bundler/setup'
|
8
|
+
require 'exa'
|
9
|
+
require 'exa/shell'
|
10
|
+
|
11
|
+
Exa::Shell.repl! do |config|
|
12
|
+
config.prompt = -> shell { shell.pastel.bold(" #{shell.pwd.path} $ ") }
|
13
|
+
end
|
14
|
+
rescue LoadError => e
|
15
|
+
warn e.message
|
16
|
+
warn "Run `gem install bundler` to install Bundler"
|
17
|
+
exit(-1)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/exa.gemspec
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gemspec = YAML.load_file('gemspec.yml')
|
7
|
+
|
8
|
+
gem.name = gemspec.fetch('name')
|
9
|
+
gem.version = gemspec.fetch('version') do
|
10
|
+
lib_dir = File.join(File.dirname(__FILE__),'lib')
|
11
|
+
$LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
|
12
|
+
|
13
|
+
require 'exa/version'
|
14
|
+
Exa::VERSION
|
15
|
+
end
|
16
|
+
|
17
|
+
gem.summary = gemspec['summary']
|
18
|
+
gem.description = gemspec['description']
|
19
|
+
gem.licenses = Array(gemspec['license'])
|
20
|
+
gem.authors = Array(gemspec['authors'])
|
21
|
+
gem.email = gemspec['email']
|
22
|
+
gem.homepage = gemspec['homepage']
|
23
|
+
|
24
|
+
glob = lambda { |patterns| gem.files & Dir[*patterns] }
|
25
|
+
|
26
|
+
gem.files = `git ls-files`.split($/)
|
27
|
+
gem.files = glob[gemspec['files']] if gemspec['files']
|
28
|
+
|
29
|
+
gem.executables = gemspec.fetch('executables') do
|
30
|
+
glob['bin/*'].map { |path| File.basename(path) }
|
31
|
+
end
|
32
|
+
gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
|
33
|
+
|
34
|
+
gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb']
|
35
|
+
gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb']
|
36
|
+
gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}']
|
37
|
+
|
38
|
+
gem.require_paths = Array(gemspec.fetch('require_paths') {
|
39
|
+
%w[ext lib].select { |dir| File.directory?(dir) }
|
40
|
+
})
|
41
|
+
|
42
|
+
gem.requirements = Array(gemspec['requirements'])
|
43
|
+
gem.required_ruby_version = gemspec['required_ruby_version']
|
44
|
+
gem.required_rubygems_version = gemspec['required_rubygems_version']
|
45
|
+
gem.post_install_message = gemspec['post_install_message']
|
46
|
+
|
47
|
+
split = lambda { |string| string.split(/,\s*/) }
|
48
|
+
|
49
|
+
if gemspec['dependencies']
|
50
|
+
gemspec['dependencies'].each do |name,versions|
|
51
|
+
gem.add_dependency(name,split[versions])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
if gemspec['development_dependencies']
|
56
|
+
gemspec['development_dependencies'].each do |name,versions|
|
57
|
+
gem.add_development_dependency(name,split[versions])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/features/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
Feature: Blah blah blah
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
@wip
|
data/gemspec.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
name: exa
|
2
|
+
summary: "recursive in-memory algos and data structures"
|
3
|
+
description: "recursive in-memory structures"
|
4
|
+
license: MIT
|
5
|
+
authors: Joseph Weissman
|
6
|
+
email: jweissman1986@gmail.com
|
7
|
+
homepage: https://rubygems.org/gems/exa
|
8
|
+
|
9
|
+
development_dependencies:
|
10
|
+
bundler: ~> 1.10
|
11
|
+
codeclimate-test-reporter: ~> 0.1
|
12
|
+
cucumber: ~> 0.10.2
|
13
|
+
rake: ~> 10.0
|
14
|
+
rspec: ~> 3.0
|
15
|
+
rubygems-tasks: ~> 0.2
|
16
|
+
yard: ~> 0.8
|
data/lib/exa.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'exa/version'
|
2
|
+
require 'exa/tree_node'
|
3
|
+
require 'exa/visitor'
|
4
|
+
|
5
|
+
module Exa
|
6
|
+
class Process
|
7
|
+
def initialize(title)
|
8
|
+
@title = title
|
9
|
+
end
|
10
|
+
|
11
|
+
def register
|
12
|
+
Process.table += [ self ]
|
13
|
+
end
|
14
|
+
|
15
|
+
def unregister
|
16
|
+
Process.table -= [ self ]
|
17
|
+
end
|
18
|
+
|
19
|
+
def run!
|
20
|
+
register
|
21
|
+
perform!
|
22
|
+
unregister
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.table
|
26
|
+
@table ||= []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Copier < Process
|
31
|
+
def initialize(source, target)
|
32
|
+
@source = source
|
33
|
+
@target = target
|
34
|
+
super("copy #{source} -> #{target}")
|
35
|
+
end
|
36
|
+
|
37
|
+
def perform!
|
38
|
+
@target.update @source.value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class Deleter < Process
|
43
|
+
def initialize(target)
|
44
|
+
@target = target
|
45
|
+
super("delete #{target}")
|
46
|
+
end
|
47
|
+
|
48
|
+
def perform!
|
49
|
+
@target.parent.remove_child(child_name: @target.name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
def remember(path, value)
|
55
|
+
p [ :remember, path: path, value: value ]
|
56
|
+
recall(path).update(value)
|
57
|
+
end
|
58
|
+
alias :[]= :remember
|
59
|
+
|
60
|
+
def recall(path)
|
61
|
+
p [ :recall, path: path ]
|
62
|
+
visitor.seek(path)
|
63
|
+
end
|
64
|
+
alias :[] :recall
|
65
|
+
|
66
|
+
def expand(path)
|
67
|
+
visitor.query(path)
|
68
|
+
end
|
69
|
+
alias :call :expand
|
70
|
+
|
71
|
+
def clean_slate!
|
72
|
+
@root = TreeNode.new(name: '', value: '(root)')
|
73
|
+
# @visitor = Visitor.new(@root)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def visitor
|
78
|
+
Visitor.new(root)
|
79
|
+
end
|
80
|
+
|
81
|
+
def root
|
82
|
+
@root ||= TreeNode.new(name: '', value: '(system root)')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/exa/shell.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'pry'
|
2
|
+
require 'pastel'
|
3
|
+
|
4
|
+
module Exa
|
5
|
+
class ShellConfig
|
6
|
+
attr_accessor :prompt
|
7
|
+
end
|
8
|
+
|
9
|
+
class ShellCommand
|
10
|
+
def initialize(title:,args:)
|
11
|
+
@title = title
|
12
|
+
@args = args
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate(shell)
|
16
|
+
case @title
|
17
|
+
when "ls" then
|
18
|
+
if shell.pwd.children.any?
|
19
|
+
shell.print_collection shell.pwd.children.map(&:name)
|
20
|
+
else
|
21
|
+
shell.print_warning "(no children of #{shell.pwd.path})"
|
22
|
+
end
|
23
|
+
when "cd" then
|
24
|
+
target = Exa.expand(@args.first)
|
25
|
+
if target && !target.empty?
|
26
|
+
shell.change_directory target.first
|
27
|
+
else
|
28
|
+
shell.print_warning "Invalid path for cd: #{@args.first}"
|
29
|
+
end
|
30
|
+
when "mkdir" then
|
31
|
+
target = Exa.recall(@args.first)
|
32
|
+
when "pwd" then
|
33
|
+
shell.print_info shell.pwd.path
|
34
|
+
else
|
35
|
+
shell.print_warning "Unknown command: '#@title'"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.extract(string)
|
40
|
+
tokens = string.split(' ')
|
41
|
+
# p [ :extract, tokens: tokens ]
|
42
|
+
cmd,*args = *tokens
|
43
|
+
# p [ :extract, cmd: cmd, args: args ]
|
44
|
+
new(title: cmd, args: args)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Shell
|
49
|
+
attr_reader :pwd
|
50
|
+
|
51
|
+
def initialize(pwd)
|
52
|
+
@pwd = pwd
|
53
|
+
end
|
54
|
+
|
55
|
+
def configuration
|
56
|
+
@config ||= ShellConfig.new
|
57
|
+
end
|
58
|
+
|
59
|
+
def kickstart!
|
60
|
+
loop do
|
61
|
+
print configuration.prompt.call(self)
|
62
|
+
inp = gets.chomp
|
63
|
+
outp = shell_eval(inp)
|
64
|
+
p outp unless outp.nil? || (outp.respond_to?(:empty?) && outp.empty?)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def shell_eval(cmd_str)
|
69
|
+
cmd = ShellCommand.extract(cmd_str)
|
70
|
+
cmd.evaluate(self)
|
71
|
+
end
|
72
|
+
|
73
|
+
def change_directory(target)
|
74
|
+
@pwd = target
|
75
|
+
end
|
76
|
+
|
77
|
+
def print_collection(elements)
|
78
|
+
puts
|
79
|
+
elements.each do |element|
|
80
|
+
puts " - " + pastel.blue(" #{element}")
|
81
|
+
end
|
82
|
+
puts
|
83
|
+
end
|
84
|
+
|
85
|
+
def print_warning(message)
|
86
|
+
puts
|
87
|
+
puts pastel.red(message)
|
88
|
+
puts
|
89
|
+
end
|
90
|
+
|
91
|
+
def print_info(message)
|
92
|
+
puts
|
93
|
+
puts pastel.green(message)
|
94
|
+
puts
|
95
|
+
end
|
96
|
+
|
97
|
+
def pastel
|
98
|
+
@pastel ||= Pastel.new
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.repl!
|
102
|
+
shell = new(Exa['/'])
|
103
|
+
yield shell.configuration
|
104
|
+
shell.kickstart!
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Exa
|
2
|
+
class TreeNode
|
3
|
+
attr_reader :name, :parent, :overlays, :links
|
4
|
+
def initialize(name:, value: nil, parent: nil, virtual: false, symbolic: false)
|
5
|
+
p [ :tree_node, name: name ]
|
6
|
+
@name = name
|
7
|
+
@value = value
|
8
|
+
@parent = parent
|
9
|
+
@virtual = virtual
|
10
|
+
@symbolic = symbolic
|
11
|
+
@children = []
|
12
|
+
@overlays = []
|
13
|
+
@links = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def value
|
17
|
+
if virtual?
|
18
|
+
constituents.first.value
|
19
|
+
elsif symbolic?
|
20
|
+
dereference_symbolic_link.value
|
21
|
+
else
|
22
|
+
@value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def virtual?
|
27
|
+
@virtual
|
28
|
+
end
|
29
|
+
|
30
|
+
def symbolic?
|
31
|
+
@symbolic
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
"<#{@name}>"
|
36
|
+
end
|
37
|
+
|
38
|
+
def path
|
39
|
+
if symbolic?
|
40
|
+
dereference_symbolic_link.path
|
41
|
+
else
|
42
|
+
if @parent
|
43
|
+
slash_name = "/#@name"
|
44
|
+
if @parent.path == '/'
|
45
|
+
slash_name
|
46
|
+
else
|
47
|
+
@parent.path + slash_name
|
48
|
+
end
|
49
|
+
else
|
50
|
+
'/' #@name
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def update(val)
|
56
|
+
@value = val
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_child(child_name:)
|
61
|
+
child = TreeNode.new(name: child_name, parent: self)
|
62
|
+
@children << child
|
63
|
+
child
|
64
|
+
end
|
65
|
+
|
66
|
+
def remove_child(child_name:)
|
67
|
+
child = @children.detect { |c| c.name == child_name }
|
68
|
+
@children.delete(child)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def unify(overlay:)
|
73
|
+
if overlay.virtual?
|
74
|
+
raise "Won't union mount virtual paths! (Try `link(source: ...)` instead.)"
|
75
|
+
end
|
76
|
+
|
77
|
+
@overlays << overlay
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
def link(source:)
|
82
|
+
@links << source
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def children
|
87
|
+
if virtual?
|
88
|
+
virtualize(constituents.flat_map(&:children))
|
89
|
+
else
|
90
|
+
@children + symbolize(symbolic_children) + virtualize(virtual_children)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def recall(path)
|
95
|
+
Visitor.new(self).seek(path)
|
96
|
+
end
|
97
|
+
alias :[] :recall
|
98
|
+
|
99
|
+
def query(path)
|
100
|
+
Visitor.new(self).query(path)
|
101
|
+
end
|
102
|
+
|
103
|
+
def copy(target)
|
104
|
+
Copier.new(self, target).perform!
|
105
|
+
end
|
106
|
+
|
107
|
+
def delete
|
108
|
+
Deleter.new(self).perform!
|
109
|
+
end
|
110
|
+
|
111
|
+
protected
|
112
|
+
def constituents
|
113
|
+
sources = if parent.virtual?
|
114
|
+
parent.constituents.flat_map(&:children)
|
115
|
+
else
|
116
|
+
parent.overlays.flat_map(&:children)
|
117
|
+
end
|
118
|
+
|
119
|
+
sources.select do |candidate|
|
120
|
+
candidate.name == @name
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def virtual_children
|
125
|
+
@overlays.flat_map(&:children)
|
126
|
+
end
|
127
|
+
|
128
|
+
def symbolic_children
|
129
|
+
# this is really a reference
|
130
|
+
@links.flat_map(&:children)
|
131
|
+
end
|
132
|
+
|
133
|
+
def dereference_symbolic_link
|
134
|
+
# okay, we're symbolic... so our parents created us
|
135
|
+
# and have a link
|
136
|
+
parent.links.flat_map(&:children).detect do |linked_child|
|
137
|
+
linked_child.name == @name
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
def symbolize(schildren)
|
143
|
+
schildren.map(&method(:symbolize_one))
|
144
|
+
end
|
145
|
+
|
146
|
+
def symbolize_one(schild)
|
147
|
+
TreeNode.new(name: schild.name, value: schild.value, parent: self, symbolic: true)
|
148
|
+
end
|
149
|
+
|
150
|
+
def virtualize(vchildren)
|
151
|
+
vchildren.map(&method(:virtualize_one))
|
152
|
+
end
|
153
|
+
|
154
|
+
def virtualize_one(vchild)
|
155
|
+
TreeNode.new(name: vchild.name, value: vchild.value, parent: self, virtual: true)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
data/lib/exa/version.rb
ADDED
data/lib/exa/visitor.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module Exa
|
2
|
+
class Visitor
|
3
|
+
def initialize(root)
|
4
|
+
@root = root
|
5
|
+
end
|
6
|
+
|
7
|
+
def gather_leaves(branch=@root, depth: 10)
|
8
|
+
return [] if depth < 0
|
9
|
+
if branch.children.any?
|
10
|
+
branch.children.flat_map do |child|
|
11
|
+
gather_leaves(child, depth: depth-1)
|
12
|
+
end.uniq
|
13
|
+
else # we are a leaf!
|
14
|
+
[ branch ]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def gather_branches(branch=@root, depth: 10)
|
19
|
+
return [] if depth < 0
|
20
|
+
if branch.children.any?
|
21
|
+
[ branch ] + branch.children.flat_map do |child|
|
22
|
+
gather_branches(child, depth: depth-1)
|
23
|
+
end.uniq
|
24
|
+
else # we are a leaf!
|
25
|
+
[ ]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def seek(path, create_missing: true)
|
30
|
+
path = '/' + path unless path.start_with?('/')
|
31
|
+
_root, *path_segments = path.split('/')
|
32
|
+
current = @root
|
33
|
+
path_segments.each do |segment|
|
34
|
+
next_child = current.children.detect do |child|
|
35
|
+
child.name == segment
|
36
|
+
end
|
37
|
+
|
38
|
+
current = if next_child
|
39
|
+
next_child
|
40
|
+
else
|
41
|
+
return nil unless create_missing
|
42
|
+
current.create_child(child_name: segment)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
current
|
46
|
+
end
|
47
|
+
|
48
|
+
def query(path)
|
49
|
+
path = '/' + path unless path.start_with?('/')
|
50
|
+
_root, next_segment, *remaining_segments = path.split('/')
|
51
|
+
remaining_path = '/' + remaining_segments.join('/')
|
52
|
+
current = @root
|
53
|
+
return [current] unless next_segment
|
54
|
+
if next_segment == '*'
|
55
|
+
# need to multiplex remaining query across *all* children
|
56
|
+
next_children = current.children
|
57
|
+
if remaining_segments.any?
|
58
|
+
next_children.flat_map do |child|
|
59
|
+
child.query(remaining_path).uniq
|
60
|
+
end
|
61
|
+
else
|
62
|
+
next_children
|
63
|
+
end
|
64
|
+
elsif next_segment == '**'
|
65
|
+
# this is more subtle, and really points to:
|
66
|
+
# do we need to be treating the path query as a regular expression?
|
67
|
+
# and matching against *all* filenames?
|
68
|
+
if remaining_segments.any?
|
69
|
+
gather_branches(current).flat_map do |folder|
|
70
|
+
folder.query(remaining_path).uniq
|
71
|
+
end
|
72
|
+
else
|
73
|
+
gather_leaves(current)
|
74
|
+
end
|
75
|
+
elsif next_segment == '..'
|
76
|
+
# need to back up...
|
77
|
+
parent = current.parent
|
78
|
+
if remaining_segments.any?
|
79
|
+
parent.children.flat_map do |child|
|
80
|
+
child.query(remaining_path).uniq
|
81
|
+
end.uniq
|
82
|
+
else
|
83
|
+
[ parent ]
|
84
|
+
end
|
85
|
+
else
|
86
|
+
# need to find just child matching *this* segment
|
87
|
+
next_child = current.children.detect { |c| c.name == next_segment }
|
88
|
+
if next_child
|
89
|
+
if remaining_segments.any?
|
90
|
+
next_child.query(remaining_path).uniq
|
91
|
+
else
|
92
|
+
[ next_child ]
|
93
|
+
end
|
94
|
+
else
|
95
|
+
[]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/spec/exa_spec.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'pry'
|
3
|
+
require 'exa'
|
4
|
+
|
5
|
+
describe Exa do
|
6
|
+
before do
|
7
|
+
Exa.clean_slate!
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should store a value' do
|
11
|
+
Exa.remember('x', 5)
|
12
|
+
Exa.remember('y', 7)
|
13
|
+
expect(Exa.recall('x').value).to eq(5)
|
14
|
+
expect(Exa.recall('y').value).to eq(7)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'has shorthand' do
|
18
|
+
Exa['hi'] = 'hello'
|
19
|
+
expect( Exa['hi'].value ).to eq('hello')
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should store a structured value' do
|
23
|
+
Exa.remember('/hello/world', 5)
|
24
|
+
expect(Exa.recall('/hello/world').value).to eq(5)
|
25
|
+
|
26
|
+
# shorthand
|
27
|
+
expect(Exa['/hello/world'].value).to eq(5)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should navigate' do
|
31
|
+
Exa.remember("/a/b/c", 'd')
|
32
|
+
expect(Exa["/a/b/c"].value).to eq('d')
|
33
|
+
expect(Exa["/a/b/c"].path).to eq('/a/b/c')
|
34
|
+
expect(Exa["/a"].children).to eq([Exa["/a/b"]])
|
35
|
+
expect(Exa["/a"].parent).to eq(Exa["/"])
|
36
|
+
|
37
|
+
expect(Exa['/'].path).to eq('/')
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'filesystem-like structures' do
|
41
|
+
before do
|
42
|
+
Exa["/usr/joe/minutes/alpha"] = "hello"
|
43
|
+
Exa["/usr/joe/books/ch01/sec01/intro"] = "welcome"
|
44
|
+
Exa["/usr/mal/minutes/beta"] = "world"
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should unify/virtualize' do
|
48
|
+
Exa["/usr/mal/friends"].unify(overlay: Exa["/usr/joe"])
|
49
|
+
|
50
|
+
expect( Exa["/usr/mal/friends/minutes"] ).to be_virtual
|
51
|
+
expect( Exa["/usr/mal/friends/minutes/alpha"] ).to be_virtual
|
52
|
+
expect( Exa["/usr/mal/friends/minutes/alpha"].value ).to eq('hello')
|
53
|
+
expect( Exa["/usr/mal/friends/books/ch01/sec01/intro"].value ).to eq('welcome')
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should symlink' do
|
57
|
+
# okay, make a union mount so we have virtual paths...
|
58
|
+
Exa["/usr/joe/friends"].unify(overlay: Exa["/usr/mal"])
|
59
|
+
|
60
|
+
expect( Exa['/usr/joe/friends/minutes'] ).to be_virtual
|
61
|
+
|
62
|
+
# we should be able to *symlink* to virtual paths
|
63
|
+
# which we can't otherwise mount directly (since virtual)
|
64
|
+
Exa['/news'].link(source: Exa['/usr/joe/friends/minutes'])
|
65
|
+
|
66
|
+
expect( Exa['/news/beta'] ).to be_symbolic
|
67
|
+
expect( Exa['/news/beta'].value ).to eq('world')
|
68
|
+
expect( Exa['/news/beta'].path ).to eq('/usr/joe/friends/minutes/beta')
|
69
|
+
end
|
70
|
+
|
71
|
+
describe 'path expansion' do
|
72
|
+
it 'should expand root path' do
|
73
|
+
expect( Exa.expand('/') ).to eq([ Exa['/'] ])
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should expand user path without a slash' do
|
77
|
+
expect( Exa.expand('usr')).to eq([ Exa['/usr'] ])
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should expand double stars' do
|
81
|
+
expect( Exa.expand('**/joe') ).to eq([ Exa['/usr/joe'] ])
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should expand single stars' do
|
85
|
+
expect( Exa.expand('/usr/*') ).to eq([ Exa['/usr/joe'], Exa['/usr/mal'] ])
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'should expand nested single stars' do
|
89
|
+
expect( Exa.expand('/usr/joe/*/*') ).to eq([ Exa['/usr/joe/minutes/alpha'], Exa['/usr/joe/books/ch01'] ])
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should expand double dots' do
|
93
|
+
expect( Exa.expand('/usr/..') ).to eq([ Exa['/'] ])
|
94
|
+
expect( Exa.expand('/usr/joe/..') ).to eq([ Exa['/usr'] ])
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should not find expansions for inexistent paths' do
|
98
|
+
expect( Exa.expand('/does_not_exist') ).to eq([])
|
99
|
+
expect( Exa.expand('/also/does/not/exist') ).to eq([])
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'should create/nav arbitrary structures' do
|
104
|
+
abel_route = '/universes/ea/regions/blessed_isles/cities/numenor/people/abel'
|
105
|
+
cain_route = '/universes/ea/regions/blessed_isles/cities/numenor/people/cain'
|
106
|
+
Exa[abel_route] = 'hi from abel!'
|
107
|
+
Exa[cain_route] = 'hi from cain!'
|
108
|
+
expect( Exa.expand('/**/people/*') ).to eq([ Exa[abel_route], Exa[cain_route] ])
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should perform some fs-like operations' do
|
112
|
+
Exa['/tmp/hello'] = 'world'
|
113
|
+
|
114
|
+
Exa['/tmp/hello'].copy(Exa['/tmp/there'])
|
115
|
+
Exa['/tmp/hello'].delete
|
116
|
+
|
117
|
+
expect( Exa['/tmp/there'].value ).to eq('world')
|
118
|
+
expect( Exa['/tmp/hello'].value ).to eq(nil)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: exa
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Joseph Weissman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-12-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: codeclimate-test-reporter
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.1'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.1'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: cucumber
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.10.2
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.10.2
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubygems-tasks
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.2'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: yard
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.8'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.8'
|
111
|
+
description: recursive in-memory structures
|
112
|
+
email: jweissman1986@gmail.com
|
113
|
+
executables:
|
114
|
+
- exa
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files:
|
117
|
+
- ChangeLog.md
|
118
|
+
- LICENSE.txt
|
119
|
+
- README.md
|
120
|
+
files:
|
121
|
+
- ".document"
|
122
|
+
- ".gitignore"
|
123
|
+
- ".rspec"
|
124
|
+
- ".yardopts"
|
125
|
+
- ChangeLog.md
|
126
|
+
- Gemfile
|
127
|
+
- LICENSE.txt
|
128
|
+
- README.md
|
129
|
+
- Rakefile
|
130
|
+
- bin/exa
|
131
|
+
- exa.gemspec
|
132
|
+
- features/.gitkeep
|
133
|
+
- features/exa.feature
|
134
|
+
- features/step_definitions/.gitkeep
|
135
|
+
- features/step_definitions/exa_steps.rb
|
136
|
+
- gemspec.yml
|
137
|
+
- lib/exa.rb
|
138
|
+
- lib/exa/shell.rb
|
139
|
+
- lib/exa/tree_node.rb
|
140
|
+
- lib/exa/version.rb
|
141
|
+
- lib/exa/visitor.rb
|
142
|
+
- spec/exa_spec.rb
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
homepage: https://rubygems.org/gems/exa
|
145
|
+
licenses:
|
146
|
+
- MIT
|
147
|
+
metadata: {}
|
148
|
+
post_install_message:
|
149
|
+
rdoc_options: []
|
150
|
+
require_paths:
|
151
|
+
- lib
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
version: '0'
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
requirements:
|
159
|
+
- - ">="
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
162
|
+
requirements: []
|
163
|
+
rubyforge_project:
|
164
|
+
rubygems_version: 2.5.1
|
165
|
+
signing_key:
|
166
|
+
specification_version: 4
|
167
|
+
summary: recursive in-memory algos and data structures
|
168
|
+
test_files: []
|