fled 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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