fled 0.0.1

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.
data/README.md ADDED
@@ -0,0 +1,230 @@
1
+
2
+ # FlEd
3
+
4
+ `fled` lets you organise your files and folders in your favourite editor
5
+
6
+ ## Disclaimer
7
+
8
+ Warning: This is a very dangerous tool. The author recommends you do not
9
+ use it. The author cannot be held responsible in any case.
10
+
11
+ ## Introduction
12
+
13
+ `fled` enumerates a folder and its files, and generates a text listing.
14
+ You can then edit that listing in your favourite editor, and save changes.
15
+ `fled` then reloads those changes, and prints a shell script that would move
16
+ your files and folders around as-per your edits.
17
+
18
+ **You should review that shell script very carefully before running it.**
19
+
20
+ ### Philosophy
21
+
22
+ `fled` only generates text, it does not perform any operation directly.
23
+
24
+ The design optimises for making the edits very simple. The consequence of
25
+ this is that very small edits can have large consequences, which makes
26
+ this a **very dangerous** tool. But so is `rm` and the rest of the shell anyway...
27
+
28
+ ### Caveats
29
+
30
+ `fled` is only aware of files it scanned. It will not warn for overwrites,
31
+ nor use temporary files in those cases, etc.
32
+
33
+ `fled`'s editing model is rather complex and fuzzy. While there are some test
34
+ cases defined, any help is much appreciated.
35
+
36
+ You should be scared when using `fled`.
37
+
38
+ ### Examples
39
+
40
+ Print help text and option list
41
+
42
+ fled --help
43
+
44
+ Edit current folder
45
+
46
+ fled
47
+
48
+ Edit all files directly in `path` folder
49
+
50
+ fled -a path -d 0
51
+
52
+ Save default options
53
+
54
+ fled --options > fled.config.yaml
55
+
56
+ Edit current folder using options
57
+
58
+ fled --load fled.config.yaml
59
+
60
+ Add options to a command (`mkdir`, `mv`, `rm` or `rmdir`)
61
+
62
+ fled | sed 's/^mv/mv -i/'
63
+
64
+ ## Listing Format
65
+
66
+ folder/ :0
67
+ file_one :1
68
+ folder_two/ :2
69
+ file_three :3
70
+
71
+ Each line of the listing is in the format *`[indentation]`* *`[name]`* `:`*`[uid]`*
72
+
73
+ - The *indentation* must consist of only spaces, and is used to indicate the parent folder
74
+ - The *name* must not use colons (`:`). If it is cleared, it is assumed the file/folder is to be deleted
75
+ - The *uid* is used by FlEd to recognise the original of the edited line. Do not assume a *uid* does not
76
+ change between runs. It is valid only once.
77
+
78
+ ## Operations
79
+ ### Creating a new folder
80
+
81
+ Add a new line (therefore with no uid):
82
+
83
+ folder/ :0
84
+ new_folder
85
+ folder_two/ :2
86
+
87
+ Generates:
88
+
89
+ mkdir folder/new_folder
90
+
91
+ ### Moving
92
+
93
+ Change the indentation and/or line order to change the parent of a file or folder:
94
+
95
+ folder/ :0
96
+ folder_two/ :2
97
+ file_one :1
98
+ file_three :3
99
+
100
+ Generates:
101
+
102
+ mv folder/file_one folder/folder_two/file_one
103
+
104
+ *Moving an item below itself or its children is not recommended, as the listing may not be exhaustive*
105
+
106
+ ### Renaming
107
+
108
+ Edit the name while preserving the uid to rename the item
109
+
110
+ folder_renamed/ :0
111
+ file_one :1
112
+ folder_two/ :2
113
+ file_changed :3
114
+
115
+ Generates:
116
+
117
+ mv folder folder_renamed
118
+ mv folder_renamed/folder_two/file_three folder_renamed/folder_two/file_changed
119
+
120
+ *Swapping file names may not work in cases where the generated intermediary file exists but was not included in the listing*
121
+
122
+ ### Deleting
123
+
124
+ Clear a name but leave the uid to delete that item
125
+
126
+ folder_renamed/ :0
127
+ :1
128
+ :2
129
+ :3
130
+
131
+ Generates:
132
+
133
+ mv folder folder_renamed
134
+ rm folder_renamed/folder_two/file_three
135
+ rm folder_renamed/file_one
136
+ rmdir folder_renamed/folder_two
137
+
138
+ ### No-op
139
+
140
+ If a line (and all child-lines) is removed from the listing, it will have no operation.
141
+
142
+ folder/ :0
143
+
144
+ Generates:
145
+ *No operation*
146
+
147
+ *Note that removing a folder without removing its children will move its children:*
148
+
149
+ folder/ :0
150
+ file_one :1
151
+ file_three :3
152
+
153
+ Generates:
154
+
155
+ mv folder/folder_two/file_three folder/file_three
156
+
157
+
158
+ If an indent is forgotten:
159
+
160
+ folder/ :0
161
+ file_one :1
162
+ file_three :3
163
+
164
+ Generates:
165
+
166
+ mv folder/folder_two/file_three folder/file_one/file_three
167
+
168
+ ### All together
169
+
170
+ folder_new/ :0
171
+ new_folder/
172
+ first :1
173
+ second :3
174
+ :2
175
+
176
+ Generates:
177
+
178
+ mv folder folder_new
179
+ mkdir folder_new/new_folder
180
+ mv folder_new/file_one folder_new/new_folder/first
181
+ mv folder_new/folder_two/file_three folder_new/new_folder/second
182
+ rmdir folder_new/folder_two
183
+
184
+ ## Edge cases
185
+
186
+ These sort-of work, but are still rather experimental
187
+
188
+ ### Swapping files
189
+
190
+ folder/ :0
191
+ file_one :1
192
+ file_two :2
193
+
194
+ When applying
195
+
196
+ folder/ :0
197
+ file_two :1
198
+ file_one :2
199
+
200
+ Generates:
201
+
202
+ mv folder/file_two folder/file_one.tmp
203
+ mv folder/file_one folder/file_two
204
+ mv folder/file_one.tmp folder/file_one
205
+
206
+ ### Tree swapping
207
+
208
+ folder/ :0
209
+ sub_folder/ :1
210
+ sub_sub_folder/ :2
211
+ file.txt :3
212
+
213
+ When applying
214
+
215
+ sub_sub_folder/ :2
216
+ sub_folder/ :1
217
+ folder/ :0
218
+ file.txt :3
219
+
220
+ Generates:
221
+
222
+ mv folder/sub_folder/sub_sub_folder sub_sub_folder
223
+ mv folder/sub_folder sub_sub_folder/sub_folder
224
+ mv folder sub_sub_folder/sub_folder/folder
225
+ mv sub_sub_folder/file.txt sub_sub_folder/sub_folder/folder/file.txt
226
+
227
+ ## Contributors
228
+
229
+ - [Eric Doughty-Papassideris](http://github.com/ddlsmurf)
230
+
data/bin/fled ADDED
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env ruby
2
+ require 'yaml'
3
+ require 'optparse'
4
+ require 'shellwords'
5
+ begin
6
+ require 'fled'
7
+ rescue LoadError => e
8
+ $:.unshift File.join(File.dirname(__FILE__),'../lib')
9
+ require 'fled'
10
+ end
11
+
12
+ class App
13
+ DEFAULT_SKIP_FOLDERS_RX = ['^\..*', '.*\.app$']
14
+ DEFAULT_SKIP_FOLDERS = ['.svn', '_svn', '.git', 'CVS', '.hg']
15
+ DEFAULT_SKIP_FILES_RX = ['^\..*', '.*~$']
16
+ DEFAULT_SKIP_FILES = [".DS_Store", "Thumbs.db", "Temporary Items"]
17
+
18
+ attr_reader :options
19
+
20
+ def initialize(arguments = ARGV)
21
+ @arguments = arguments.dup
22
+ @options = {}
23
+ @options[:verbose] = false
24
+ end
25
+
26
+ def run
27
+ if parsed_options?
28
+ if options[:output_options]
29
+ output_options
30
+ exit 0
31
+ end
32
+ output_version if @options[:verbose]
33
+ $stderr.puts "Starting on path #{@base_path} at #{DateTime.now}" if @options[:verbose]
34
+ builder = FlEd::ListingBuilder.new()
35
+ filter = DTC::Utils::FilteringFileVisitor.new builder, @options
36
+ DTC::Utils::FileVisitor.browse @base_path, filter
37
+ content = builder.listing.to_s
38
+ if content == ""
39
+ $stderr.puts "No files/folders found"
40
+ exit 1
41
+ end
42
+ result = DTC::Utils::InteractiveEditor::edit(content, ".yaml")
43
+ if result.nil? || result.strip == "" || result == content
44
+ $stderr.puts "No changes - aborting" ; exit 2
45
+ end
46
+ target_listing = FlEd::FileListing.parse(result)
47
+ ops = target_listing.operations_from!(builder.listing)
48
+ if ops.empty?
49
+ $stderr.puts "No changes - aborting" ; exit 2
50
+ end
51
+ ops = [[:pushd, @base_path]] + ops + [[:popd]] unless @options[:no_pushd]
52
+ puts FlEd::operation_list_to_bash(ops).join("\n")
53
+ $stderr.puts "\nFinished at #{DateTime.now}" if @options[:verbose]
54
+ else
55
+ output_usage
56
+ end
57
+ rescue SystemExit => e
58
+ raise
59
+ rescue Exception => e
60
+ puts e
61
+ output_usage
62
+ exit 127
63
+ end
64
+ protected
65
+
66
+ def self.default_options
67
+ ({
68
+ :max_depth => -1,
69
+ :excluded_files =>
70
+ DEFAULT_SKIP_FILES_RX +
71
+ DEFAULT_SKIP_FILES.map { |e| '\A' + Regexp.escape(e) + '\z' },
72
+ :excluded_directories =>
73
+ DEFAULT_SKIP_FOLDERS_RX +
74
+ DEFAULT_SKIP_FOLDERS.map { |e| '\A' + Regexp.escape(e) + '\z' },
75
+ })
76
+ end
77
+
78
+ def parsed_options?
79
+ options = {}
80
+ @optionparser = OptionParser.new do |opts|
81
+ opts.banner = "Usage: #{$0} [options] [base_path]"
82
+ opts.separator ""
83
+ opts.separator "Selection"
84
+
85
+ opts.on('-i', '--include RX', String, "Only include files and folders that match any such regexp") { |rx| (options[:included] ||= []) << rx }
86
+ opts.on('--include-dirs RX', String, "Only include folders that match any such regexp") { |rx| (options[:included_directories] ||= []) << rx }
87
+ opts.on('--include-files RX', String, "Only include files that match any such regexp") { |rx| (options[:included_files] ||= []) << rx }
88
+ opts.on('-x', '--exclude RX', String, "Exclude files and folders that match any such regexp") { |rx| (options[:excluded] ||= []) << rx }
89
+ opts.on('--exclude-dirs RX', String, "Exclude folders that match any such regexp") { |rx| (options[:excluded_directories] ||= []) << rx }
90
+ opts.on('--exclude-files RX', String, "Exclude files that match any such regexp") { |rx| (options[:excluded_files] ||= []) << rx }
91
+ opts.on('-a', '--no-exclude', "Empties all the lists of exclusions") do
92
+ options[:excluded] = []
93
+ options[:excluded_files] = []
94
+ options[:excluded_directories] = []
95
+ end
96
+ opts.on('-r', '--recursive', "Scan directories recursively") { options[:max_depth] ||= -1 }
97
+ opts.on("-d", "--depth N", Integer, "Set maximum recursion to N subfolders. (0=no recursion)") { |n| options[:max_depth] = n.to_i }
98
+
99
+ opts.separator ""
100
+ opts.separator "Script"
101
+ opts.on('--no-pushd', "Do not include pushd/popd pair in script") { options[:no_pushd] = true }
102
+
103
+ opts.separator ""
104
+ opts.separator "General"
105
+ opts.on_tail('-l', '--load PATH.YAML', "Merge in the specified yaml files options") { |file|
106
+ File.open(file) { |file| options = options.merge(YAML.load(file)) }
107
+ }
108
+ opts.on_tail('--options', "Show options as interpreted and exit without doing anything") { options[:output_options] = true }
109
+ opts.on('-v', '--verbose', "Display more information about what the tool is doing...") { options[:verbose] = true }
110
+ opts.on_tail('--version', "Show version of this tool") { output_version ; exit 0 }
111
+ opts.on_tail('-h', '--help', "Show this help message") { output_help ; exit 0 }
112
+ end
113
+ @optionparser.parse!(@arguments)
114
+ @base_path = File.expand_path(@arguments.first || options[:base_path] || ".")
115
+ raise RuntimeError, "No more than one argument should be present" if @arguments.count > 1
116
+ @options = self.class.default_options.merge(options)
117
+ true
118
+ end
119
+
120
+ def output_options out = $stdout
121
+ opts = @options.dup
122
+ opts.delete(:output_options)
123
+ out.puts opts.to_yaml
124
+ end
125
+
126
+ def output_help
127
+ output_version
128
+ $stderr.puts ""
129
+ $stderr.puts " Disclaimer: The author is not responsible for anything related"
130
+ $stderr.puts " to this tool. This is quite powerful and slight mistakes can"
131
+ $stderr.puts " lead to loss of data or worse. The author recommends you not"
132
+ $stderr.puts " use this."
133
+ $stderr.puts ""
134
+ $stderr.puts " Operation:"
135
+ $stderr.puts " - Generate list of files and folder"
136
+ $stderr.puts " - Open in your favorite ($EDITOR) text editor"
137
+ $stderr.puts " - You edit file names in the editor, then save and close"
138
+ $stderr.puts " - The file list is reloaded and compared to the original"
139
+ $stderr.puts " - A shell script to re-organise the files/folders is printed"
140
+ $stderr.puts ""
141
+ output_usage
142
+ end
143
+
144
+ def output_usage
145
+ $stderr.puts @optionparser
146
+ end
147
+
148
+ def output_version
149
+ $stderr.puts "#{File.basename(__FILE__)} version #{VERSION}"
150
+ end
151
+ end
152
+ App.new.run
@@ -0,0 +1,259 @@
1
+ module DTC
2
+ module Utils
3
+ module DSLDSL
4
+ class DSLVisitor
5
+ # very approximate visitor =)
6
+ def enter key, *args ; end
7
+ def leave ; end
8
+ def add key, *args ; end
9
+ def visit_dsl &blk
10
+ builder = self.class.delegate_klass.new(self)
11
+ self.class.context_klass.new(builder).__instance_exec(&blk)
12
+ builder.flush
13
+ end
14
+ protected
15
+ def self.delegate_klass ; StaticTreeDSLDelegate ; end
16
+ def self.context_klass ; StaticTreeDSLContextBlank ; end
17
+ end
18
+
19
+ class DSLArrayWriter < DSLVisitor
20
+ attr_reader :stack
21
+ def initialize
22
+ @stack = [[]]
23
+ end
24
+ def enter sym, *args
25
+ @stack.push [[sym, *args]]
26
+ end
27
+ def leave
28
+ @stack.pop
29
+ end
30
+ def add sym, *args
31
+ @stack.last << [sym, *args]
32
+ end
33
+ def each &blk
34
+ return enum_for(:each) unless block_given?
35
+ each_call_of @stack.first, &blk
36
+ end
37
+ protected
38
+ def each_call_of parent, &block
39
+ parent.each do |item|
40
+ if item.first.is_a?(Symbol)
41
+ yield item
42
+ else
43
+ each_call_of item, &block
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ class DSLHashWriter < DSLVisitor
50
+ def initialize target
51
+ @stack = [target]
52
+ end
53
+ def enter key, *args
54
+ container = add_container(key, *args)
55
+ @stack.push(container) if container
56
+ container
57
+ end
58
+ def leave
59
+ @stack.pop
60
+ end
61
+ def add key, *args
62
+ add_key key, args
63
+ end
64
+ def self.write_static_tree_dsl target = {}, &blk
65
+ visitor = self.new(result = target)
66
+ visitor.visit_dsl(&blk)
67
+ result
68
+ end
69
+ def add_container key, *args
70
+ container = {}
71
+ container[:options] = args unless args.empty?
72
+ add_key key, container
73
+ end
74
+ def add_key key, val
75
+ container = @stack.last
76
+ key = key.to_s
77
+ raise RuntimeError, "Key #{key.inspect} already defined" if container[key]
78
+ container[key] = val
79
+ val
80
+ end
81
+ end
82
+
83
+ class StaticTreeDSLDelegate
84
+ def initialize visitor, prefix = nil
85
+ @visitor = visitor
86
+ @prefix = prefix
87
+ @pending_prefix = nil
88
+ @called = false
89
+ end
90
+ def prefix sym
91
+ @called = true
92
+ flush
93
+ @pending_prefix = self.class.new(@visitor, with_prefix(sym))
94
+ end
95
+ def flush
96
+ if @pending_prefix
97
+ @pending_prefix.add_unless_called
98
+ @pending_prefix = nil
99
+ end
100
+ end
101
+ def add sym, *args
102
+ @called = true
103
+ @visitor.add(with_prefix(sym), *args)
104
+ end
105
+ def enter sym, *args
106
+ flush
107
+ @called = true
108
+ @visitor.enter(with_prefix(sym), *args)
109
+ end
110
+ def leave
111
+ flush
112
+ @visitor.leave
113
+ end
114
+ protected
115
+ def with_prefix sym
116
+ @prefix ? (sym ? "#{@prefix}.#{sym}".to_sym : @prefix) : sym
117
+ end
118
+ def add_unless_called
119
+ flush
120
+ @visitor.add(@prefix) unless @called
121
+ end
122
+ end
123
+
124
+ class StaticTreeDSLContextBlank
125
+ alias_method :__instance_exec, :instance_exec
126
+ alias_method :__class, :class
127
+ instance_methods.each { |meth| undef_method(meth) unless meth =~ /\A__/ || meth == :object_id }
128
+ def initialize delegate, unprefixed = delegate
129
+ @delegate = delegate
130
+ @unprefixed = unprefixed
131
+ end
132
+ def method_missing(meth, *args, &block)
133
+ if block
134
+ @delegate.enter meth, *args
135
+ __class.new(@unprefixed, @unprefixed).__instance_exec(&block)
136
+ @unprefixed.flush
137
+ @delegate.leave
138
+ else
139
+ if args.empty?
140
+ return __class.new(@delegate.prefix(meth), @unprefixed)
141
+ else
142
+ @delegate.add(meth, *args)
143
+ end
144
+ end
145
+ self
146
+ end
147
+ end
148
+
149
+ if __FILE__ == $0
150
+
151
+ class DebugStaticTreeDSLDelegate < StaticTreeDSLDelegate
152
+ def prefix sym
153
+ p [:prefix, sym, @prefix]
154
+ super
155
+ end
156
+ def flush
157
+ p [:flush, @prefix] if @pending_prefix
158
+ super
159
+ end
160
+ def add sym, *args
161
+ p [:add, sym, @prefix]
162
+ super
163
+ end
164
+ def enter sym, *args
165
+ p [:enter, sym, @prefix]
166
+ super
167
+ end
168
+ def leave
169
+ p [:leave, @prefix]
170
+ super
171
+ end
172
+ def add_unless_called
173
+ p [:flush_self, @prefix] unless @called
174
+ super
175
+ end
176
+ end
177
+
178
+ class DebugDSLHashWriter < DSLHashWriter
179
+ def enter sym, *args
180
+ p [:wenter, sym]
181
+ super
182
+ end
183
+ def leave
184
+ p [:wleave]
185
+ super
186
+ end
187
+ def add sym, *args
188
+ p [:wadd, sym, args]
189
+ super
190
+ end
191
+ def self.delegate_klass ; DebugStaticTreeDSLDelegate ; end
192
+ end
193
+
194
+ module Examples
195
+ module Simple
196
+ result = DSLHashWriter.write_static_tree_dsl do
197
+ fichier "value"
198
+ fichier2.txt "one", "two"
199
+ end
200
+ result # => {"fichier"=>["value"], "fichier2.txt"=>["one", "two"]}
201
+
202
+ result = DSLHashWriter.write_static_tree_dsl do
203
+ dossier {
204
+ sous.dossier "valeur" do
205
+ file.txt
206
+ end
207
+ }
208
+ end
209
+ result # => {"dossier"=>{"sous.dossier"=>{:options=>["valeur"], "file.txt"=>[]}}}
210
+
211
+ result = DSLHashWriter.write_static_tree_dsl do
212
+ %w[hdpi mdpi ldpi].each do |resolution|
213
+ image.__send__(resolution.to_sym).png
214
+ end
215
+ end
216
+ result # => {"image.ldpi.png"=>[], "image.hdpi.png"=>[], "image.mdpi.png"=>[]}
217
+
218
+ result = DSLHashWriter.write_static_tree_dsl do
219
+ user {
220
+ name :string
221
+ email :string
222
+ address(:json) {
223
+ line :string
224
+ country
225
+ }
226
+ }
227
+ end
228
+ result # => {"user"=>{"address"=>{"country"=>[], "line"=>[:string], :options=>[:json]}, "name"=>[:string], "email"=>[:string]}}
229
+
230
+ result = DSLHashWriter.write_static_tree_dsl do
231
+ def base_file
232
+ yes.this.is.base
233
+ end
234
+ base_file.txt
235
+ base_file.png
236
+ end
237
+ result # => {"yes.this.is.base.png"=>[], "yes.this.is.base.txt"=>[]}
238
+ end
239
+ module CustomBuilder
240
+ class EscapableStaticTreeDSLContextBlank < StaticTreeDSLContextBlank
241
+ def method_missing(meth, *args, &block)
242
+ meth = args.shift if meth == :raw
243
+ super(meth, *args, &block)
244
+ end
245
+ end
246
+ class EscapableDSLHashWriter < DSLHashWriter
247
+ def self.context_klass ; EscapableStaticTreeDSLContextBlank ; end
248
+ end
249
+ result = EscapableDSLHashWriter.write_static_tree_dsl do
250
+ 3.times { |i| raw(i).png }
251
+ image.raw("%!") { file }
252
+ end
253
+ result # => {"0.png"=>[], "1.png"=>[], "image.%!"=>{"file"=>[]}, "2.png"=>[]}
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end