timeless 0.1.0 → 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: a45d84d4290e39bba5b6fe3a99fd101cefd7f97c
4
- data.tar.gz: 761ab4cf1594beb69534198f556a11f936806801
3
+ metadata.gz: 06ba9bc95d407ecb088dc4c016fc7a66c78417cf
4
+ data.tar.gz: 560ac4abb81ef3f80476a0b7baa154ff3e3257e2
5
5
  SHA512:
6
- metadata.gz: 5b97c16e7cc2fc1b1b919c93518a11c7ae6a115e11ff1138251bf26bd699da7d18b11f59b0a222c4ca242105d596528b52e1bb6149abd4f1df8f4c37adeffd3f
7
- data.tar.gz: 72160f36426d2659db29cb1cfc57deb81948e1a24d483639197f881c0068c65b7b06455fc684d018793e091d7d85b0bd5e22d3c96d9b485d116d175affe9fddd
6
+ metadata.gz: f7b8f73506f3c399c5f17b0ce60620cfb31d63b09d87fb1bf89f3bf0babeb16cda0fae91d383405e9d848929837517e5567f186c5197c9b6cea2e49363c30a46
7
+ data.tar.gz: d44c950c14ba467675f0d0cb9c8f4a58b6ffd8ea93bba6329709944cc39aa4b9f3b45455f478a5aa39ec20c00d2cb17826e34dc5b70a2a23e235c002302f2dee
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
2
4
 
3
5
  # Specify your gem's dependencies in timeless.gemspec
4
6
  gemspec
@@ -1,22 +1,21 @@
1
- Copyright (c) 2015 Adolfo Villafiorita
1
+ The MIT License (MIT)
2
2
 
3
- MIT License
3
+ Copyright (c) 2015-2017 Adolfo Villafiorita
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
12
11
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
15
14
 
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,152 @@
1
+ Timeless
2
+ ========
3
+
4
+ Timeless is a simple command line time tracker.
5
+
6
+ Installation
7
+ ------------
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ruby
12
+ gem 'timeless'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install timeless
21
+
22
+ Usage
23
+ -----
24
+
25
+ timeless is a simple command line time tracker. Entries store the
26
+ following information:
27
+
28
+ 1. start time
29
+ 2. end time
30
+ 3. notes
31
+
32
+ timeless allows one to
33
+
34
+ - run a pomodoro timer
35
+ - clock time using a "stopwatch" (with start and stop commands)
36
+ - manually entering data
37
+ - print a text report, possibly filtered by date ranges of notes'
38
+ content
39
+ - export to csv
40
+
41
+ Timeless has also some basic support for key-pair values in the notes
42
+ form. They can be used to assign a special meaning to some strings and
43
+ improve filtering and exporting. Two special keys are `p` for projects
44
+ and `c` for clients. These two keys are exported in dedicated columns
45
+ when exporting data to csv.
46
+
47
+ Some non nominal conditions are also handled (e.g., starting a clock
48
+ twice, trying to stop when no clock was started).
49
+
50
+ If you do not want to type `timeless` every time, you can invoke a console:
51
+
52
+ timeless console
53
+ timeless:000> start
54
+ timeless:001> stop
55
+ timeless:002>
56
+
57
+ More information with:
58
+
59
+ timeless help # get list of available commands
60
+ timeless help command # help about command
61
+ timeless man # output this README file
62
+
63
+
64
+ Examples
65
+ --------
66
+
67
+ Get information about command syntax:
68
+
69
+ timeless -h
70
+
71
+ Run a pomodoro timer:
72
+
73
+ timeless pom p:project meeting with c:john
74
+ timeless pom --duration 60 p:prj2
75
+ timeless pom --long # default to 50 minutes
76
+
77
+ Start clocking using a stopwatch:
78
+
79
+ timeless start requirements doc
80
+ timeless start --at 'thirty minutes ago' fixing bug 182 for c:tim
81
+ timeless start --force --at 'five minutes ago' requirements document for p:prj1
82
+
83
+ Stop clocking:
84
+
85
+ timeless stop
86
+ timeless stop forgot notes on start
87
+ timeless stop --last # reuse the notes of last entry
88
+ timeless stop --at 'five minutes ago'
89
+ timeless stop --start '1 hour ago' --at 'now' clocked a full entry
90
+
91
+ Enter a full entry:
92
+
93
+ timeless clock activity on p:project # from end of last entry to now
94
+ timeless clock --start 'three hours ago' --stop 'five minutes ago'
95
+
96
+ What was i doing?
97
+
98
+ timeless current # print current entry
99
+ timeless forget # forget pending clock
100
+
101
+ What did I do?
102
+
103
+ timeless last # print last entry
104
+
105
+ Reporting and exporting
106
+
107
+ timeless statement
108
+ timeless balance --from yesterday --filter p:project
109
+ timeless export
110
+
111
+ Notice that even though `timeless` uses CSV as its native format, the
112
+ export command exports to a CSV file which is more easily parsed by a
113
+ Spreadsheet such as LibreOffice.
114
+
115
+ If something goes wrong
116
+ -----------------------
117
+
118
+ Data is stored in the CSV file `~/.timeless.csv`. You can edit the file
119
+ to manually fix entries if you make some mistake.
120
+
121
+ Version History
122
+ ---------------
123
+
124
+ **0.4.0**
125
+
126
+ - second public release
127
+ - new `balance` command
128
+ - the `report` command has been renamed `statement`
129
+ - interactive `console`
130
+ - more detailed help for commands
131
+ - fixed a bug in the last command: now it returns the last activity in
132
+ chronological order (rather than the last activity which has been clocked)
133
+
134
+ **0.2.0**
135
+ - initial public release
136
+
137
+
138
+
139
+ License
140
+ -------
141
+
142
+ Licensed under the terms of the MIT License.
143
+
144
+ Contributing
145
+ ------------
146
+
147
+ 1. Fork it ( https://github.com/\[my-github-username\]/timeless/fork )
148
+ 2. Create your feature branch (\`git checkout -b my-new-feature\`)
149
+ 3. Commit your changes (\`git commit -am 'Add some feature'\`)
150
+ 4. Push to the branch (\`git push origin my-new-feature\`)
151
+ 5. Create a new Pull Request
152
+
data/Rakefile CHANGED
@@ -1,2 +1,2 @@
1
1
  require "bundler/gem_tasks"
2
-
2
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "timeless"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,22 @@
1
+ ;; forms-mode for timeless -*- emacs-lisp -*-
2
+
3
+ (setq forms-file (expand-file-name "~/.timeless.csv"))
4
+
5
+ ;; Use `forms-enumerate' to set field names and number thereof.
6
+ (setq forms-number-of-fields
7
+ (forms-enumerate
8
+ '(start-datetime
9
+ end-datetime
10
+ description)))
11
+
12
+ (setq forms-field-sep ",")
13
+
14
+ ;; The format list.
15
+ (setq forms-format-list
16
+ (list
17
+ "====== Timeless Record ======\n\n"
18
+ "Start: " start-datetime "\n"
19
+ "End: " end-datetime "\n"
20
+ "Description: " description "\n"
21
+ ))
22
+
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'timeless'
4
+
5
+ Timeless::CommandSemantics.reps Timeless::CommandSyntax.commands, ARGV
@@ -1,7 +1,8 @@
1
1
  require "timeless/version"
2
2
 
3
- module Timeless
4
- require "timeless/pomodoro"
5
- require "timeless/stopwatch"
6
- require "timeless/storage"
7
- end
3
+ require "timeless/storage"
4
+ require "timeless/stopwatch"
5
+ require "timeless/pomodoro"
6
+
7
+ require "timeless/cli/command_syntax"
8
+ require "timeless/cli/command_semantics"
@@ -0,0 +1,352 @@
1
+ require 'timeless'
2
+
3
+ require 'chronic'
4
+ require 'readline'
5
+ require 'fileutils'
6
+
7
+ module Timeless
8
+ module CommandSemantics
9
+ APPNAME = 'timeless'
10
+ VERSION = Timeless::VERSION
11
+
12
+ #
13
+ # Main App Starts Here!
14
+ #
15
+ def self.version opts = nil, argv = []
16
+ puts "#{APPNAME} version #{VERSION}"
17
+ end
18
+
19
+ def self.man opts = nil, argv = []
20
+ path = File.join(File.dirname(__FILE__), "/../../../README.md")
21
+ file = File.open(path, "r")
22
+ contents = file.read
23
+ puts contents
24
+ end
25
+
26
+ def self.help opts = nil, argv = []
27
+ all_commands = CommandSyntax.commands
28
+
29
+ if argv != []
30
+ argv.map { |x| puts all_commands[x.to_sym][2] }
31
+ else
32
+ puts "#{APPNAME} command [options] [args]"
33
+ puts ""
34
+ puts "Available commands:"
35
+ puts ""
36
+ all_commands.keys.each do |key|
37
+ puts " " + all_commands[key][0].banner
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.console opts, argv = []
43
+ all_commands = CommandSyntax.commands
44
+ all_commands.delete(:console)
45
+
46
+ i = 0
47
+ while true
48
+ string = Readline.readline("#{APPNAME}:%03d> " % i, true)
49
+ string.gsub!(/^#{APPNAME} /, "") # as a courtesy, remove any leading appname string
50
+ if string == "exit" or string == "quit" or string == "." then
51
+ exit 0
52
+ end
53
+ reps all_commands, string.split(' ')
54
+ i = i + 1
55
+ end
56
+ end
57
+
58
+ # read-eval-print step
59
+ def self.reps all_commands, argv
60
+ if argv == [] or argv[0] == "--help" or argv[0] == "-h"
61
+ CommandSemantics.help
62
+ exit 0
63
+ else
64
+ command = argv[0]
65
+ syntax_and_semantics = all_commands[command.to_sym]
66
+ if syntax_and_semantics
67
+ opts = syntax_and_semantics[0]
68
+ function = syntax_and_semantics[1]
69
+
70
+ begin
71
+ parser = Slop::Parser.new(opts)
72
+
73
+ result = parser.parse(argv[1..-1])
74
+ options = result.to_hash
75
+ arguments = result.arguments
76
+
77
+ eval "CommandSemantics::#{function}(options, arguments)"
78
+ rescue Slop::Error => e
79
+ puts "#{APPNAME}: #{e}"
80
+ rescue Exception => e
81
+ puts e
82
+ end
83
+ else
84
+ puts "#{APPNAME}: '#{command}' is not a valid command. See '#{APPNAME} help'"
85
+ end
86
+ end
87
+ end
88
+
89
+ #
90
+ # APP SPECIFIC COMMANDS
91
+ #
92
+ def self.pom opts, argv
93
+ duration = opts.to_hash[:duration] ||
94
+ (opts.to_hash[:long] ? Timeless::Pomodoro::WORKING_LONG : Timeless::Pomodoro::WORKING)
95
+
96
+ start, stop, notes = Timeless::Pomodoro.run_pomodoro_timer(duration, argv.join(" "))
97
+ Timeless::Storage.store(start, stop, notes)
98
+ puts last_entry
99
+ end
100
+
101
+
102
+ def self.start opts, argv
103
+ start = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
104
+ notes = argv.join(" ") # empty string is no args specified
105
+ force = opts.to_hash[:force]
106
+
107
+ if Timeless::Stopwatch.clocking? and not force
108
+ start, notes = Timeless::Stopwatch.get_start # for information purposes only
109
+ puts "There is a clock started at #{start} (notes: \"#{notes}\"). Use --force to override."
110
+ else
111
+ Timeless::Stopwatch.start(start, notes)
112
+ puts "Clock started at #{start ? start : Time.now}."
113
+ puts "You may want to specify notes for the entry when you stop clocking." if notes == ""
114
+ end
115
+ end
116
+
117
+ def self.forget opts, argv
118
+ if File.exists?(Timeless::Stopwatch::START_FILENAME)
119
+ entry = Timeless::Storage.last
120
+ Timeless::Stopwatch.forget
121
+ puts "Forgotten timer started at #{entry[0]} (#{entry[2]})."
122
+ else
123
+ puts "You don't have an active timer"
124
+ end
125
+ end
126
+
127
+ def self.stop opts, argv
128
+ start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
129
+ stop = Chronic.parse(opts.to_hash[:at]) # nil if no option specified
130
+ notes = argv.join(" ") # empty string if no args specified
131
+
132
+ manage_stop start, stop, notes, opts[:last]
133
+ end
134
+
135
+ def self.clock opts, argv
136
+ start = Chronic.parse(opts.to_hash[:start]) # nil if no option specified
137
+ stop_opt = Chronic.parse(opts.to_hash[:stop]) # nil if no option specified
138
+ end_opt = Chronic.parse(opts.to_hash[:end]) # nil if no option specified
139
+ stop = stop_opt ? stop_opt : end_opt # stop_opt if --stop, end_opt if --end, nil otherwise
140
+ notes = argv.join(" ") # empty string if no args specified
141
+
142
+ if stop_opt and end_opt then
143
+ puts "Timeless error: specify end time with either --end or --stop (not both)"
144
+ exit
145
+ end
146
+
147
+ manage_stop start, stop, notes, opts[:last]
148
+ end
149
+
150
+ def self.current opts, argv
151
+ if Timeless::Stopwatch.clocking? then
152
+ start, notes = Timeless::Stopwatch.get_start
153
+ puts "Timeless: you have been clocking #{"%.0d" % ((Time.now- Time.parse(start)) / 60)} minutes"
154
+ puts "on: #{notes}" if notes
155
+ else
156
+ puts "There is no clock started."
157
+ end
158
+ end
159
+
160
+ def self.last opts, argv
161
+ puts last_entry
162
+ end
163
+
164
+ def self.list opts, argv
165
+ values = Timeless::Storage.get_key argv[0]
166
+ puts "Timeless: list of keys #{argv[0]} found in timesheets:"
167
+ values.each do |value|
168
+ puts value
169
+ end
170
+ end
171
+
172
+ def self.export opts, argv
173
+ Timeless::Storage.export
174
+ end
175
+
176
+ def self.gaps opts, argv
177
+ from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
178
+ to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
179
+ interval = opts.to_hash[:interval] || 1
180
+
181
+ entries = Timeless::Storage.get(from, to)
182
+ previous_stop = from || Time.parse(entries[0][0])
183
+ day = ""
184
+
185
+ printf "%-18s %-5s - %-5s %-6s %-s\n", "Day", "Start", "End", "Gap", "Command to fix"
186
+ entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
187
+ current_start = Time.parse(entry[0])
188
+
189
+ current_day = current_start.strftime("%a %b %d, %Y")
190
+
191
+ if (current_start.day == previous_stop.day and current_start - previous_stop > interval * 60) then
192
+
193
+ if current_day != day then
194
+ day = current_day
195
+ else
196
+ current_day = "" # avoid printing the day if same as before
197
+ end
198
+
199
+ duration = (current_start - previous_stop) / 60
200
+ printf "%-18s %5s - %5s %02i:%02i %s\n",
201
+ current_day,
202
+ previous_stop.strftime("%H:%M"),
203
+ current_start.strftime("%H:%M"),
204
+ duration / 60, duration % 60,
205
+ "timeless clock --start '#{previous_stop}' --end '#{current_start}'"
206
+
207
+ end
208
+ previous_stop = Time.parse(entry[1])
209
+ end
210
+ end
211
+
212
+
213
+ def self.statement opts, argv
214
+ from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
215
+ to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
216
+
217
+ entries = Timeless::Storage.get(from, to, opts.to_hash[:filter])
218
+ # it could become a function of a reporting module
219
+ printf "%-18s %-5s - %-5s %-8s %-s\n", "Day", "Start", "End", "Duration", "Notes"
220
+ total = 0
221
+ day = ""
222
+ entries.sort { |x,y| Time.parse(x[0]) <=> Time.parse(y[0]) }.each do |entry|
223
+ current_day = Time.parse(entry[0]).strftime("%a %b %d, %Y")
224
+ if current_day != day then
225
+ day = current_day
226
+ else
227
+ current_day = "" # avoid printing the day if same as before
228
+ end
229
+
230
+ duration = (Time.parse(entry[1]) - Time.parse(entry[0])) / 60
231
+ total = total + duration
232
+
233
+ printf "%-18s %5s - %5s %5s %s\n",
234
+ current_day,
235
+ Time.parse(entry[0]).strftime("%H:%M"),
236
+ Time.parse(entry[1]).strftime("%H:%M"),
237
+ in_hours(duration),
238
+ entry[2]
239
+ end
240
+ puts "----------------------------------------------------------------------"
241
+ printf "Total %02i:%02i\n", total / 60, total % 60
242
+ end
243
+
244
+ def self.balance opts, argv
245
+ from = opts.to_hash[:from] ? Chronic.parse(opts.to_hash[:from]) : nil
246
+ to = opts.to_hash[:to] ? Chronic.parse(opts.to_hash[:to]) : nil
247
+
248
+ # {key1 => {value1 => total .. } , ... }
249
+ hash = Timeless::Storage::balance from, to, opts.to_hash[:filter]
250
+
251
+ total = 0
252
+ puts "Projects"
253
+ puts "========"
254
+ hash["p"].each do |key, value|
255
+ printf "%-20s%5s\n", key, in_hours(value)
256
+ total += value
257
+ end
258
+ puts "-------------------------"
259
+ printf "%-20s%5s\n", "Total", in_hours(total)
260
+
261
+ total = 0
262
+ puts ""
263
+ puts "Clients"
264
+ puts "======="
265
+ hash["c"].each do |key, value|
266
+ printf "%-20s%5s\n", key, in_hours(value)
267
+ total += value
268
+ end
269
+ puts "-------------------------"
270
+ printf "%-20s%5s\n", "Total", in_hours(total)
271
+
272
+ total = 0
273
+ puts ""
274
+ puts "Activities"
275
+ puts "=========="
276
+ hash["a"].each do |key, value|
277
+ printf "%-20s%5s\n", key, in_hours(value)
278
+ total += value
279
+ end
280
+ puts "-------------------------"
281
+ printf "%-20s%5s\n", "Total", in_hours(total)
282
+ end
283
+
284
+
285
+ private
286
+
287
+ def self.in_hours value
288
+ sprintf "%02i:%02i", value / 60, value % 60
289
+ end
290
+
291
+ def self.last_entry
292
+ start, stop, notes = Timeless::Storage.last
293
+
294
+ interval = Time.parse(stop) - Time.parse(start)
295
+ seconds = interval % 60
296
+ minutes = (interval / 60) % 60
297
+ hours = interval / 3600
298
+
299
+ sprintf "Timeless: you clocked %02d:%02d:%02d on %s\nYou stopped clocking at: %s", hours, minutes, seconds, notes, stop
300
+ end
301
+
302
+ #
303
+ # code shared by stop and clock (which accept slightly different options,
304
+ # but behave in the same way
305
+ #
306
+ def self.manage_stop start, stop, notes, reuse_last
307
+ # syntax check:
308
+ # - --start is illegal if there is a running clock
309
+ # - --last is illegal if there are notes
310
+ #
311
+ # however:
312
+ # - --last and notes prevail over notes stored when starting the clock
313
+ if Timeless::Stopwatch.clocking? and start
314
+ puts "Timeless error: you specified --start with a running clock. Use 'timeless forget' or drop --start"
315
+ end
316
+ if reuse_last and notes != "" then
317
+ puts "Timeless error: you specified both --last and notes. Choose one or the other"
318
+ end
319
+
320
+ if reuse_last then
321
+ _, _, notes = Timeless::Storage.last
322
+ end
323
+
324
+ if Timeless::Stopwatch.clocking?
325
+ start, stop, notes = Timeless::Stopwatch.stop(start, stop, notes) # merge passed with data in running clock
326
+ Timeless::Storage.store(start, stop, notes)
327
+
328
+ puts "Clock stopped at #{stop}."
329
+ puts last_entry
330
+ else
331
+ start = start ? start : Timeless::Storage.last[1]
332
+ stop = stop ? stop : Time.now
333
+ Timeless::Storage.store(start, stop, notes)
334
+
335
+ puts "Clock stopped at #{stop}."
336
+ puts last_entry
337
+ end
338
+ end
339
+
340
+ # GENERAL UTILITIES
341
+
342
+ def self.backup filename
343
+ FileUtils::cp filename, filename + "~"
344
+ puts "Backup copy #{filename} created in #{filename}~."
345
+ end
346
+
347
+ def self.backup_and_write filename, content
348
+ backup(filename) if File.exist?(filename)
349
+ File.open(filename, "w") { |f| f.puts content }
350
+ end
351
+ end
352
+ end