rubyshelltools 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,101 @@
1
+ Ruby Shell Tools
2
+ ================
3
+
4
+ RST Version 0.0.1 is an experimental Ruby2-gem by Andreas Altendorfer <andreas@altendorfer.at>
5
+
6
+ Install from rubygems.org
7
+ -------------------------
8
+
9
+ gem install rubyshelltools
10
+
11
+
12
+ Clone from Github
13
+ -----------------
14
+
15
+ You can clone the project from **[Github](https://github.com/iboard/rst)**
16
+
17
+ git clone git://github.com/iboard/rst.git
18
+
19
+ ### First steps ...
20
+
21
+ After downloading the project you can do
22
+
23
+ cd rst
24
+ bundle # install required Gems
25
+ rake # Run all specs
26
+ rake build # build the gem
27
+ rake install # build and install gem
28
+
29
+
30
+ Usage
31
+ -----
32
+
33
+ see file EXAMPLES.md:
34
+
35
+ * [At Github](https://github.com/iboard/rst/blob/master/assets/docs/examples.md#examples)
36
+ * [loacal copy](./file.examples.html)
37
+
38
+ or run
39
+
40
+ bin/rst --examples
41
+
42
+ if you have done `rake install` you don't have to use `bin/rst` in
43
+ the project-directory but you can use `rst ...` directly from the shell.
44
+
45
+ Documentation
46
+ -------------
47
+
48
+ run `yard; open doc/index.html` to build the rdocs and open
49
+ it in your browser.
50
+
51
+ The documentation can be found also at
52
+ [dav.iboard.cc](http://dav.iboard.cc/container/rst-doc)
53
+
54
+ TDD
55
+ ---
56
+
57
+ And as always we are **green**
58
+
59
+ You can find the current output of [simplecov][] at [dav.iboard.cc](http://dav.iboard.cc/container/rst-coverage)
60
+
61
+
62
+ > $ rake
63
+ >
64
+ > Finished in _a few_ seconds
65
+ >
66
+ > _n_ examples, 0 failures
67
+ >
68
+ > Coverage report generated for RSpec to rst/coverage. _n_ / _n+-0_ LOC (**100.0%**) covered.
69
+ >
70
+ > $ yard
71
+ >
72
+ > Files: _n_
73
+ >
74
+ > Modules: _n_ ( 0 undocumented)
75
+ >
76
+ > Classes: _n_ ( 0 undocumented)
77
+ >
78
+ > Constants: _n_ ( 0 undocumented)
79
+ >
80
+ > Methods: _n_ ( 0 undocumented)
81
+ >
82
+ > **100.00% documented**
83
+
84
+
85
+ License
86
+ =======
87
+
88
+ This is free software
89
+ ---------------------
90
+
91
+ Use it without restrications and on your own risk.
92
+ Leaving the copyright is appreciated, though.
93
+
94
+
95
+ Copyright
96
+ ---------
97
+
98
+ (c) 2013 by Andreas Altendorfer
99
+
100
+
101
+ [simplecov]: http://github.com/colszowka/simplecov
@@ -0,0 +1,92 @@
1
+ EXAMPLES
2
+ ========
3
+
4
+ rst --help
5
+ ----------
6
+
7
+ Print out available options and commands
8
+
9
+ $ rst --help
10
+ Usage: rst [COMMAND [COMMAND ....]] [options] [FILES..]
11
+
12
+ Options:
13
+ -v, --[no-]verbose Run verbosely
14
+ --examples Show some usage examples
15
+ -h, --help Print help
16
+ -f, --from DATE Set from-date
17
+ -t, --to DATE Set to-date
18
+ -n, --name NAME Use this name for the command
19
+ -e, --new-event DATE,STRING Add an event
20
+ --list-calendars List available calendars
21
+ --delete-calendar CALENDARNAME
22
+ Delete an calendar and all it's entries!
23
+ --[no-]empty Show empty entries
24
+ Commands:
25
+ nil .......... no command. Interpret options only (useful in combination with -v)
26
+ ls ........... list directory and files
27
+ cal[endar] ... print a calendar --from --to
28
+
29
+ use --example for a more detailed list of commands.
30
+
31
+ rst ls *
32
+ --------
33
+
34
+ List directory and files
35
+
36
+ $ rst ls *
37
+ Gemfile Gemfile.lock README.md Rakefile assets bin lib
38
+ rst-0.0.0.gem rst.gemspec rst.rb specs
39
+
40
+
41
+ rst [nil] -v SOME FILES HERE
42
+ -----------------------------
43
+
44
+ show the arguments and options parsed from command-line
45
+
46
+ $ rst -v SOME FILES HERE
47
+ Binary : /Users/aa/.rvm/gems/ruby-head@ruby2/bin/rst
48
+ Command:
49
+ Options: [:verbose, true]
50
+ Files: SOME, FILES, HERE
51
+
52
+ rst calendar
53
+ ------------
54
+
55
+ RST supports a (very) simple calendar-function. You can store (all-day)events for a given date.
56
+ The calendar can be stored persistently in a PStore file. The default
57
+ location of the file is within the GEM-path/data/development. You can
58
+ overwrite this by defining env-vars for RST_DATA and RST_ENV
59
+
60
+ $ export RST_DATA=$HOME/.rst/data
61
+ $ export RST_ENV=production
62
+ $ rst cal --new-event='1964-08-31,Birthday Andreas Altendorfer'
63
+ $ rst cal -e 'today,Win the jackpot' # e is an abbreviation for --new-event
64
+ $ rst cal -f 1964/1 # f is an abbreviation for --from
65
+ Mon, Aug 31 1964: Birthday Andreas Altendorfer
66
+ Fri, Mar 15 2013: Win the jackpot
67
+ $ find $HOME/.rst
68
+ /Users/you/.rst
69
+ /Users/you/.rst/data
70
+ /Users/you/.rst/data/production
71
+ /Users/you/.rst/data/production/CALENDAR_FILE
72
+
73
+ $ rst --list-calendars
74
+ birthdays : 5 entries
75
+ work : 2 entries
76
+
77
+
78
+ The full set of parameters is
79
+
80
+ $ rst calendar --from='1964-01-01' --to='today' --empty
81
+ Wed, Jan 01 1964:
82
+ Thu, Jan 02 1964:
83
+ Fri, Jan 03 1964:
84
+ Sat, Jan 04 1964:
85
+ #.... some other empty lines
86
+ Mon, Aug 31 1964: Birthday Andreas Altendorfer
87
+ # .... some thousand other empty lines
88
+ Fri, Mar 15 2013: Win the jackpot
89
+
90
+ * --empty ...... show empty lines
91
+ * --no-empty ... show no empty lines (default)
92
+
data/bin/rst ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ require File.expand_path('../../rst', __FILE__)
5
+ require_relative '../lib/rst'
6
+ require_relative '../lib/load'
7
+ include RST
8
+
9
+
10
+ # Run rst from commandline
11
+ puts RstCommand.new(ARGV).run
@@ -0,0 +1,75 @@
1
+ # Monkey-patch the numeric class to support n.years,days,... as Rails do
2
+ class Numeric
3
+
4
+ #:nodoc:
5
+ SECOND = 1
6
+ #:nodoc:
7
+ SECONDS = SECOND
8
+ #:nodoc:
9
+ MINUTES = 60
10
+ #:nodoc:
11
+ MINUTE = MINUTES
12
+ #:nodoc:
13
+ HOURS = MINUTES * 60
14
+ #:nodoc:
15
+ HOUR = HOURS
16
+ #:nodoc:
17
+ DAYS = HOUR * 24
18
+ #:nodoc:
19
+ DAY = DAYS
20
+ #:nodoc:
21
+ WEEKS = DAY * 7
22
+ #:nodoc:
23
+ WEEK = WEEKS
24
+ #:nodoc:
25
+ MONTHS = DAY * 30.5
26
+ #:nodoc:
27
+ MONTH = MONTHS
28
+ #:nodoc:
29
+ YEAR = DAY * 365.25
30
+ #:nodoc:
31
+ YEARS = YEAR
32
+
33
+ #:nodoc:
34
+ def years
35
+ (self * YEAR).to_f
36
+ end
37
+ alias :year :years
38
+
39
+ #:nodoc:
40
+ def months
41
+ (self * MONTH).to_f
42
+ end
43
+ alias :month :months
44
+
45
+ #:nodoc:
46
+ def weeks
47
+ (self * WEEK).to_f
48
+ end
49
+ alias :week :weeks
50
+
51
+ #:nodoc:
52
+ def days
53
+ (self * DAYS).to_f
54
+ end
55
+ alias :day :days
56
+
57
+ #:nodoc:
58
+ def hours
59
+ (self * HOUR).to_f
60
+ end
61
+ alias :hour :hours
62
+
63
+ #:nodoc:
64
+ def minutes
65
+ (self * MINUTE).to_f
66
+ end
67
+ alias :minute :minutes
68
+
69
+ #:nodoc:
70
+ def seconds
71
+ (self * SECONDS).to_f
72
+ end
73
+ alias :second :seconds
74
+
75
+ end
@@ -0,0 +1,6 @@
1
+ module RST
2
+
3
+ # Exception thrown when an abstract method is called
4
+ class AbstractMethodCallError < NotImplementedError; end
5
+
6
+ end
data/lib/load.rb ADDED
@@ -0,0 +1,37 @@
1
+ unless defined? LIB_LOADED
2
+
3
+ # prevent from double loading
4
+ LIB_LOADED = true
5
+
6
+ # Used with strftime(DEFAULT_DATE_FORMAT) when printing dates
7
+ DEFAULT_DATE_FORMAT = '%a, %b %d %Y'
8
+
9
+ # Filename for the calendar-store
10
+ CALENDAR_FILE = 'calendars.data'
11
+
12
+ require File.expand_path('../../rst',__FILE__)
13
+
14
+ # Load all necessary files
15
+ $LOAD_PATH.unshift(File.expand_path('..',__FILE__))
16
+
17
+ $LOAD_PATH.unshift(File.expand_path('../core_extensions',__FILE__))
18
+ require 'numeric'
19
+
20
+ $LOAD_PATH.unshift(File.expand_path('../errors',__FILE__))
21
+ require 'store_errors'
22
+
23
+ $LOAD_PATH.unshift(File.expand_path('../modules/calendar',__FILE__))
24
+ require 'calendar'
25
+ require 'eventable'
26
+ require 'calendar_event'
27
+
28
+ $LOAD_PATH.unshift(File.expand_path('../modules/persistent',__FILE__))
29
+ require 'persistent'
30
+ require 'store'
31
+ require 'memory_store'
32
+ require 'disk_store'
33
+
34
+
35
+ include RST
36
+ end
37
+
@@ -0,0 +1,155 @@
1
+ require 'date'
2
+ require 'modules/persistent/persistent'
3
+
4
+ module RST
5
+
6
+ # Calendar-module provides the Calendar-class which is supposed to hold
7
+ # 'Eventable'-objects. Where Eventable is a module you can include to any class.
8
+ # A Calendar has a start- and ending-date, and a name. The name is used
9
+ # to store the calendar in a store using this name.
10
+ #
11
+ # @example
12
+ #
13
+ # class Person
14
+ # include Eventable
15
+ #
16
+ # def initialize name, dob
17
+ # @name = name
18
+ # schedule! dob
19
+ # end
20
+ #
21
+ # # override abstract method to format the output
22
+ # def event_headline
23
+ # '#{name}\'s Birthday'
24
+ # end
25
+ # end
26
+ #
27
+ # birthdays = Calendar.new('birthdays')
28
+ # birthdays << Person.new('Andi', '31. Aug. 1964')
29
+ # birthdays << Person.new('Heidi', '28. Aug. 1969')
30
+ # birthdays << Person.new('Julian', '17. Feb. 1995')
31
+ # birthdays << Person.new('Tina', '22. May. 1997')
32
+ #
33
+ # puts birthdays.list_days('31.8.1963','today')
34
+ # # => Mon, Aug 31 1964: Andi's Birthday
35
+ # # => Thu, Aug 28 1969: Heidi's Birthday
36
+ # # => Fri, Feb 17 1995: Julian's Birthday
37
+ # # => Thu, May 22 1997: Tina's Birthday
38
+ #
39
+ module Calendar
40
+
41
+ # Methods useful when dealing with Dates
42
+ module CalendarHelper
43
+
44
+ # You can use 'today' or any format which Date.parse can handle.
45
+ # @param [nil|String|Time|Date] param
46
+ # @return [Date] always returns a Date regardless of the type of input
47
+ def ensure_date(param)
48
+ if param.is_a?(Date) || param.is_a?(::Time)
49
+ param
50
+ elsif param =~ /today/i || param.nil?
51
+ Date.today
52
+ else
53
+ Date.parse(param)
54
+ end
55
+ end
56
+ end
57
+
58
+ # Calendar has a name and will be stored in Persistent::DiskStore(CALENDAR_FILE)
59
+ # with it's name as the id. Thus you can save different calendars in
60
+ # the same file. If no name is given 'unnamed' will be the default.
61
+ class Calendar
62
+
63
+ include Persistent::Persistentable
64
+ include CalendarHelper
65
+
66
+ attr_reader :name, :start_date, :end_date, :events
67
+
68
+ # @param [String] _name Name and persistent-id of this calendar.
69
+ # @param [Date|Time|String] _start The date when the calendar starts
70
+ # @param [Date|Time|String] _end The date when the calendar ends
71
+ def initialize(_name='unnamed', _start=nil, _end=nil, _events=[])
72
+ @name = _name
73
+ @start_date = parse_date_param(_start)
74
+ @end_date = parse_date_param(_end)
75
+ @events = _events
76
+ end
77
+
78
+
79
+ # Setter for from_date
80
+ # @param [Date|String] _from - new starting date
81
+ def from=(_from)
82
+ @start_date = parse_date_param(_from)
83
+ end
84
+
85
+ # Setter for to-date
86
+ # @param [Date|String] _to - new ending date
87
+ def to=(_to)
88
+ @end_date = parse_date_param(_to)
89
+ end
90
+
91
+ # Override Persistentable's id-method
92
+ # @return [String] - the calendar-name is it's id
93
+ def id
94
+ @name || super
95
+ end
96
+
97
+ # Add Eventables to the calendar
98
+ # @param [Eventable] add - the Object to add
99
+ def <<(add)
100
+ events << add
101
+ end
102
+
103
+ # Calculate the span between start and end in seconds
104
+ # @return [Float]
105
+ def span
106
+ ((end_date - start_date)*Numeric::DAYS).to_f
107
+ end
108
+
109
+ # Array of strings for each day with events on it.
110
+ #
111
+ # @example
112
+ # Mon, Aug 31 1964: Birthday Andreas Altendorfer
113
+ #
114
+ # @param [String|Date] start_on - output begins on this day
115
+ # @param [String|Date] end_on - output ends on this day
116
+ # @param [Boolean] show_empty - output days with no events
117
+ # @return [Array] of Strings DATE: EVENT + EVENT + ....
118
+ def list_days(start_on=start_date,end_on=end_date,show_empty=false)
119
+ (parse_date_param(start_on)..parse_date_param(end_on)).to_a.map { |_date|
120
+ format_events_for(_date,show_empty)
121
+ }.compact
122
+ end
123
+
124
+
125
+ private
126
+ # List Event-headlines for a given date
127
+ # @param [Date] date
128
+ # @return [Array] of CalendarEvents
129
+ def events_on(date)
130
+ events.select { |event| event.event_date == date }
131
+ end
132
+
133
+ # Convert strings to a date
134
+ # @param [Date|Time|String] param - default is 'today'
135
+ # @return [Date|Time]
136
+ def parse_date_param(param=Date.today)
137
+ ensure_date(param)
138
+ end
139
+
140
+ # Output date and Events on this date in one line
141
+ # @param [Date] _date
142
+ # @param [Boolean] show_empty - do not output lines with no events
143
+ # @return [String]
144
+ def format_events_for(_date,show_empty=false)
145
+ if show_empty ||
146
+ (_events = events_on(_date).map(&:event_headline).join(' + ').strip) != ''
147
+ [_date.strftime(DEFAULT_DATE_FORMAT), _events].compact.join(": ")
148
+ end
149
+ end
150
+
151
+ end
152
+
153
+ end
154
+
155
+ end
@@ -0,0 +1,30 @@
1
+ module RST
2
+
3
+ module Calendar
4
+
5
+ # A CalendarEvent happens on a given date and
6
+ # has a label. It's supposed to be appended to a Calendar-object
7
+ # @see Calendar
8
+ class CalendarEvent
9
+
10
+ attr_reader :event_date, :label
11
+
12
+ include Eventable
13
+ include CalendarHelper
14
+
15
+ # @param [String|Date] _date - Date of the event (only all-day-events are possible yet)
16
+ # @param [String] _label - Events name
17
+ def initialize(_date,_label)
18
+ @event_date = ensure_date(_date)
19
+ @label = _label
20
+ end
21
+
22
+ # override abstract method for an Eventable
23
+ def event_headline
24
+ self.label.to_s.strip
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,50 @@
1
+ module RST
2
+ module Calendar
3
+
4
+ # Inject Eventable to any Class to make it event-able in a Calendar
5
+ # You have to define a method :event_headline for the class
6
+ # in order to list the event in a Calendar
7
+ # @example
8
+ #
9
+ # class Birthday < Struct.new(:name,:dob)
10
+ # include Eventable
11
+ # def initialize(*args)
12
+ # super
13
+ # schedule!(dob)
14
+ # end
15
+ #
16
+ # protected
17
+ # def event_headline
18
+ # "It's #{name}'s Birthday"
19
+ # end
20
+ # end
21
+ #
22
+ # @see RST::Calendar::Calendar
23
+ module Eventable
24
+
25
+ # @return [Date] the date when the event is scheduled
26
+ def event_date
27
+ @event_date
28
+ end
29
+
30
+ # @return [Boolean] true if the object is scheduled (has an event_date)
31
+ def scheduled?
32
+ !event_date.nil?
33
+ end
34
+
35
+ # @param [Date|String] date schedule this object for the given date
36
+ # @return [Eventable] - self
37
+ def schedule!(date)
38
+ @event_date = date.is_a?(Date) ? date : Date.parse(date)
39
+ self
40
+ end
41
+
42
+ # used in calendar-output as a short entry
43
+ # @return [String]
44
+ def event_headline
45
+ "(untitled event)"
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,90 @@
1
+ module RST
2
+ module Persistent
3
+
4
+ # #DiskStore is responsible to save an Array of objects
5
+ # persistently to the disk - PStore is used to effort this.
6
+ # @see RST::Persistent::Store
7
+ # @api persistent
8
+ class DiskStore < Store
9
+
10
+ attr_reader :filename
11
+
12
+ # The first argument is the filename
13
+ # Following args are passed to base-class
14
+ # @example
15
+ #
16
+ # DiskStore.new('myfile')
17
+ # DiskStore.new(filename: 'myfile', other_option: '...')
18
+ #
19
+ # @param [String] _filename
20
+ # @param [Array] args - arguments passed to the super-class
21
+ def initialize(_filename='store.data',args={})
22
+ @filename = _filename
23
+ super(args)
24
+ end
25
+
26
+ # @return [String] full path of PStore-file
27
+ def path
28
+ @store.path
29
+ end
30
+
31
+ # @todo This is bad for performance and memory - refactor!
32
+ # @return [Array]
33
+ def all
34
+ _all = []
35
+ @store.transaction(true) do |s|
36
+ s.roots.each do |_r|
37
+ _all << s[_r]
38
+ end
39
+ end
40
+ _all
41
+ end
42
+
43
+ # Delete the store
44
+ def delete!
45
+ File.unlink(store_path)
46
+ end
47
+
48
+ private
49
+
50
+ # Initialize a PStore-instance
51
+ def setup_backend
52
+ @store = PStore.new(store_path)
53
+ end
54
+
55
+
56
+ # Determine the store-path from RST::STOREPATH and
57
+ # environment. Creates the path if not exists.
58
+ #
59
+ # @example
60
+ # .../data/development/store.data
61
+ #
62
+ # @return [String]
63
+ def store_path
64
+ prefix = ENV['RST_DATA'] || RST::STOREPATH
65
+ env = ENV['RST_ENV'] || 'development'
66
+ _dir = File.join( prefix, env )
67
+ FileUtils.mkdir_p(_dir)
68
+ File.join(_dir, filename)
69
+ end
70
+
71
+ # Find and update or add object to the store.
72
+ # @param [Object] object
73
+ def update_or_add(object)
74
+ @store.transaction do |s|
75
+ s[object.id] = object
76
+ end
77
+ end
78
+
79
+ # @param [Object] object - the object to be removed.
80
+ def remove_object(object)
81
+ @store.transaction do |s|
82
+ s.delete(object.id)
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ end
89
+ end
90
+
@@ -0,0 +1,37 @@
1
+ module RST
2
+
3
+ module Persistent
4
+
5
+ # # MemoryStore
6
+ # doesn't really store the objects but holds them in
7
+ # memory, in a simple Array. Perfect for testing
8
+ # @api persistent
9
+ class MemoryStore < Store
10
+
11
+ # @return [Array]
12
+ def all
13
+ @objects || []
14
+ end
15
+
16
+ private
17
+
18
+ # Initialize an empty array as an in-memory-store
19
+ def setup_backend
20
+ @objects = []
21
+ end
22
+
23
+ # Find and update or add an object to the store
24
+ # @param [Object] object
25
+ def update_or_add(object)
26
+ @objects -= [object]
27
+ @objects << object
28
+ end
29
+
30
+ # @param [Object] object - the object to be removed
31
+ def remove_object(object)
32
+ @objects -= [object]
33
+ end
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,36 @@
1
+ require 'securerandom'
2
+ require 'fileutils'
3
+
4
+ module RST
5
+
6
+ # The Persistent-module injects Store-functions
7
+ # to any Object including it.
8
+ # @api persistent
9
+ module Persistent
10
+
11
+ KEY_LENGTH=8 # Length of Store-IDs
12
+
13
+ # The interface for persistent-able objects
14
+ module Persistentable
15
+
16
+ # Save the object to Store
17
+ def save
18
+ end
19
+
20
+ # Remove the object from Store
21
+ def delete
22
+ end
23
+
24
+ # If the object doesn't provide an id-method
25
+ # define one
26
+ unless respond_to?(:id)
27
+ # Set and return an unique ID
28
+ def id
29
+ @id ||= SecureRandom::hex(KEY_LENGTH)
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,118 @@
1
+ require 'pstore'
2
+ require 'store_errors'
3
+
4
+ module RST
5
+
6
+ module Persistent
7
+
8
+ # # The abstract Store-base-class
9
+ #
10
+ # Store provides the interface for all store-able classes
11
+ #
12
+ # ## Usage:
13
+ #
14
+ # Derive a concrete store-class from 'Store' and overwrite the
15
+ # following methods:
16
+ #
17
+ # * setup_backend
18
+ # * sync_store
19
+ #
20
+ # @abstract
21
+ #
22
+ class Store
23
+
24
+ # Sets options-hash and calls the abstract
25
+ # 'setup_backend'-callback
26
+ def initialize(args={})
27
+ @options = {}.merge(args)
28
+ setup_backend
29
+ end
30
+
31
+ # Add an object and sync store
32
+ # @param [Persistentable] object - object including the Persistent-module
33
+ def <<(object)
34
+ update_or_add(object)
35
+ end
36
+
37
+ # Remove an object from the store
38
+ # @param [Persistentable] object - the object to be removed from store
39
+ # @return [Store] self
40
+ def -(object)
41
+ remove_object(object)
42
+ self
43
+ end
44
+
45
+ # @return Enumerable
46
+ # @abstract
47
+ def all
48
+ raise AbstractMethodCallError.new("Please, overwrite #{__callee__} in #{self.class.to_s}")
49
+ end
50
+
51
+ # @return [Object|nil] the first object in the store
52
+ def first
53
+ all.first
54
+ end
55
+
56
+ # @param [Array] ids to search
57
+ # @return [nil] if nothing found
58
+ # @return [Object] if exactly one object found
59
+ # @return [Array] of objects with matching ids if more than one matched
60
+ def find(*ids)
61
+ flatten all.select { |obj| ids.include?(obj.id) }
62
+ end
63
+
64
+ # Delete the store
65
+ # @abstract - override this for real persistent store-classes
66
+ def delete!
67
+ @objects = []
68
+ end
69
+
70
+ private
71
+
72
+ # callback called from initializer and aimed to initialize the
73
+ # objects-array, PStore, database-connection,...
74
+ # @abstract
75
+ def setup_backend
76
+ raise AbstractMethodCallError.new("Please override method :setup_backend in class #{self.class.to_s}")
77
+ end
78
+
79
+ # Make sure the current state of objects is stored
80
+ # @abstract
81
+ def sync_store
82
+ # RST.logger.warn("Store class #{self.class.to_s} should overwrite method :sync_store" )
83
+ end
84
+
85
+ # Flatten the result of a select
86
+ # @param [Array] result
87
+ # @return [Array] if result contains more than one element
88
+ # @return [Object] if result contains exactly one element
89
+ # @return [nil] if result is empty
90
+ def flatten result
91
+ if result.count == 1
92
+ result.first
93
+ elsif result.nil? || result.empty?
94
+ nil
95
+ else
96
+ result
97
+ end
98
+ end
99
+
100
+
101
+ # Find and update or add an object to the store
102
+ # @param [Object] object
103
+ # @abstract - override in other StoreClasses
104
+ def update_or_add(object)
105
+ raise AbstractMethodCallError.new("Please, overrwrite #{__callee__} in #{self.class.to_s}")
106
+ end
107
+
108
+ # @param [Object] object
109
+ # @abstract - override in concrete StoreClasses
110
+ def remove_object(object)
111
+ raise AbstractMethodCallError.new("Please, overrwrite #{__callee__} in #{self.class.to_s}")
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+
118
+ end
data/lib/rst.rb ADDED
@@ -0,0 +1,295 @@
1
+ require 'optparse'
2
+ require 'ostruct'
3
+
4
+ # # Ruby Shell Tools main namespace RST
5
+ #
6
+ # By now only two commands/features are available:
7
+ #
8
+ # * **ls** - List files from the filesystem (OS-like ls)
9
+ # * **cal[endar]** - Stores and lists events in one calendar-file,
10
+ # separated in different named calendars
11
+ #
12
+ # See [EXAMPLES.md](./file.examples.html) for detailed instruction.
13
+ #
14
+ #
15
+ # @author Andi Altendorfer <andreas@altendorfer.at>
16
+ # @see https://github.com/iboard/rst
17
+ # @see http://altendorfer.at
18
+ module RST
19
+
20
+ # Interprets and runs options and commands.
21
+ # @example
22
+ # runner = RstCommand.new(ARGV)
23
+ # puts runner.run
24
+ # See [EXAMPLES.md](../file.examples.html)
25
+ class RstCommand
26
+
27
+ # Default options are always present, even if the user will not provide them.
28
+ # They can be overwritten, though.
29
+ # @see #parse_options
30
+ DEFAULT_OPTIONS = { name: 'unnamed', from: 'today', to: 'today', show_empty: false }
31
+
32
+ # @group PUBLIC INTERFACE
33
+
34
+ # the hash stores options parsed in mehtod parse_options.
35
+ # @see #parse_options
36
+ attr_reader :options
37
+
38
+ # the first argument of ARGV is the command. It's extracted in run_command
39
+ # @see #run_command
40
+ attr_reader :command
41
+
42
+ # Initialize the Command-runner with arguments and parse them.
43
+ def initialize(args)
44
+ parse_options(args)
45
+ end
46
+
47
+ # Call 'run_options' and 'run_command', reject empty returns and join
48
+ # output with CR
49
+ # @return [String]
50
+ def run
51
+ [run_options, run_command].compact.reject{|l| l.strip == '' }.join("\n")
52
+ end
53
+
54
+ private
55
+
56
+ # @group PREPARE OPTIONS AND COMMAND
57
+
58
+ # Interpret options from ARGV using OptionParser. No action here. The parsed
59
+ # options get stored in '@options' and then will be used in run_options.
60
+ # @see #options
61
+ # @see #DEFAULT_OPTIONS
62
+ # @see #run_options
63
+ # @see #run_option
64
+ def parse_options(args)
65
+ @options = DEFAULT_OPTIONS
66
+ OptionParser.new do |opts|
67
+ opts.banner = 'Usage: rst [COMMAND [COMMAND ....]] [options] [FILES..]'
68
+
69
+ opts.separator "\nOptions:"
70
+
71
+ opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
72
+ @options[:verbose] = v
73
+ end
74
+
75
+ opts.on('--examples', 'Show some usage examples') do |x|
76
+ @options[:examples] = x
77
+ end
78
+
79
+ opts.on('-h', '--help', 'Print help') do |h|
80
+ puts opts
81
+ end
82
+
83
+ opts.on('-f', '--from DATE', String, 'Set from-date') do |from|
84
+ @options[:from] = from
85
+ end
86
+
87
+ opts.on('-t', '--to DATE', String, 'Set to-date') do |to|
88
+ @options[:to] = to
89
+ end
90
+
91
+ opts.on('-n', '--name NAME', String, 'Use this name for the command') do |name|
92
+ @options[:name] = name
93
+ end
94
+
95
+ opts.on('-e', '--new-event DATE,STRING', Array, 'Add an event') do |date,name|
96
+ @options[:new_event] = {date: date, label: name}
97
+ end
98
+
99
+ opts.on('--list-calendars', 'List available calendars') do
100
+ @options[:list_calendars] = true
101
+ end
102
+
103
+ opts.on('--delete-calendar CALENDARNAME', String, 'Delete an calendar and all it\'s entries!') do |name|
104
+ @options[:delete_calendar] = name
105
+ end
106
+
107
+ opts.on('--[no-]empty', 'Show empty entries') do |show|
108
+ @options[:show_empty] = show
109
+ end
110
+
111
+ opts.separator 'Commands:'
112
+
113
+ opts.separator <<-EOT
114
+ nil .......... no command. Interpret options only (useful in combination with -v)
115
+ ls ........... list directory and files
116
+ cal[endar] ... print a calendar --from --to
117
+ EOT
118
+ .gsub(/^\s+/,' ')
119
+
120
+ opts.separator "\n use --example for a more detailed list of commands."
121
+ end
122
+ .parse!(args)
123
+ rescue => e
124
+ puts "#{e.class} => #{e.message}"
125
+ puts "Try #{File.basename($0)} --help"
126
+ end
127
+
128
+ # Iterate over all given options and call run_option for each
129
+ # @see #run_option
130
+ # @return [String]
131
+ def run_options
132
+ options.map { |k,_v| run_option(k) }.compact.join("\n")
133
+ end
134
+
135
+ # Run the 'command', the first argument of ARGV.
136
+ # If a command is not known/invalid print out a hint to --help
137
+ # Known commands are:
138
+ # @see #directory_listing
139
+ # @see #print_calendar
140
+ # @see #command
141
+ # @return [String]
142
+ def run_command
143
+ case @command=ARGV.shift
144
+ when nil, 'nil', 'null'
145
+ ''
146
+ when 'ls'
147
+ directory_listing(ARGV.empty? ? ['*'] : ARGV)
148
+ when 'cal', 'calendar'
149
+ print_calendar
150
+ else
151
+ "unknown command '#{cmd.inspect}' - try --help"
152
+ end
153
+ end
154
+
155
+
156
+ # @group EXECUTE COMMANDS
157
+
158
+ # Do a Dir for each file-mask given.
159
+ # Duplicated files are stripped.
160
+ # @example
161
+ # rst ls lib/*rb
162
+ # @param [Array] files - a list of files/wildcards
163
+ # @return [String] - tab-delimited files found
164
+ def directory_listing(files)
165
+ files.map { |f| Dir[f].join("\t") }.uniq.join("\t")
166
+ end
167
+
168
+ # Output one line per day between start and end-date
169
+ # Start- and end-date are given with options --from, and --to
170
+ # Option --empty will output dates without events too.
171
+ # default is --no-empty which will suppress those lines in output
172
+ # @example
173
+ # rst cal --from 1.1.1990 --to today --name Birthdays
174
+ # @return [Array] - one string Date: Event + Event + ... for each day.
175
+ def print_calendar
176
+ cal = find_calendar( Persistent::DiskStore.new(CALENDAR_FILE) )
177
+ cal.list_days(options[:from], options[:to], options[:show_empty]).compact.join("\n")
178
+ end
179
+
180
+ # @group EXECUTE ACTION FROM OPTIONS
181
+
182
+ # Execute a single option
183
+ # @see #parse_options
184
+ # @see #run_options
185
+ # @param [String] option (examples, verbose, new_event,...)
186
+ def run_option(option)
187
+ case option.to_s
188
+ when 'examples'
189
+ File.read(File.join(DOCS,'examples.md')).strip
190
+ when 'verbose'
191
+ print_arguments
192
+ when 'new_event'
193
+ new_event = add_event
194
+ "Added: %s: %s" % [new_event.event_date.strftime(DEFAULT_DATE_FORMAT), new_event.event_headline.strip]
195
+ when 'list_calendars'
196
+ list_calendars
197
+ when 'delete_calendar'
198
+ delete_calendar
199
+ else
200
+ nil #noop ignore unknown options likely it's a param for an argument
201
+ end
202
+ end
203
+
204
+ # Add an event to the calendar.
205
+ # @example
206
+ # --add-event DATE,LABEL
207
+ # @return [CalendarEvent] - the just created event
208
+ def add_event
209
+ event_environment do |ee|
210
+ ee[:calendar] << ee[:event]
211
+ ee[:store] << ee[:calendar]
212
+ ee[:event]
213
+ end
214
+ end
215
+
216
+ # remove a calendar from calendar-file named in options[:delete_calendar]
217
+ # @example
218
+ # --delete-calendar CALENDARNAME
219
+ def delete_calendar
220
+ store = Persistent::DiskStore.new(CALENDAR_FILE)
221
+ store -= OpenStruct.new(id: options[:delete_calendar])
222
+ nil
223
+ end
224
+
225
+ # List available calendars and count events of each one.
226
+ # @return [Array] One line/String for each calendar
227
+ def list_calendars
228
+ store = Persistent::DiskStore.new(CALENDAR_FILE)
229
+ store.all.map { |calendar|
230
+ cnt = calendar.events.count
231
+ "%-20.20s: %d %s" % [calendar.id, cnt > 0 ? cnt : 'no', cnt > 1 ? 'entries' : 'entry']
232
+ }
233
+ end
234
+
235
+ # Output the previous interpreted and parsed arguments
236
+ # Called when --verbose is given as an option
237
+ # @example
238
+ # rst --verbose
239
+ def print_arguments
240
+ puts "Binary : #{$0}"
241
+ puts "Command: #{command}"
242
+ puts "Options: #{options.map(&:inspect).join(', ')}"
243
+ puts "Files : #{ARGV.any? ? ARGV[1..-1].join(', ') : ''}"
244
+ end
245
+
246
+ # @group HELPERS
247
+
248
+ # Find or initialize a calendar-object. The name comes from option --name.
249
+ # @param [Store] store - the Store-object where to search for calendar
250
+ # @return [Calendar]
251
+ def find_calendar(store)
252
+ store.find(options[:name]) || Calendar::Calendar.new(options[:name], options[:from], options[:to])
253
+ end
254
+
255
+ # Find the date, label, and calendar_name for an event from options
256
+ # @yield [EventOptions]
257
+ def get_event_options
258
+ date = options[:new_event].fetch(:date) { Date.today.strftime('%Y-%m-%d') }
259
+ label = options[:new_event].fetch(:label) { 'unnamed event' }
260
+ calendar_name = options.fetch(:name) { 'calendar' }
261
+ yield(EventOptions.new(date, label, calendar_name))
262
+ end
263
+
264
+
265
+ # Initialize an EventEnvironment used to operate events in stores.
266
+ # @see #add_event
267
+ # @yield [EventEnvironment]
268
+ def event_environment
269
+ get_event_options do |eo|
270
+ event = Calendar::CalendarEvent.new(eo[:date], eo[:label])
271
+ store = Persistent::DiskStore.new(CALENDAR_FILE)
272
+ calendar = store.find(eo[:calendar_name]) || Calendar::Calendar.new(eo[:calendar_name])
273
+ yield(EventEnvironment.new(event,store,calendar))
274
+ end
275
+ end
276
+
277
+ end
278
+
279
+ # @group HELPER CLASSES
280
+
281
+ # EventEnvironment holds the CalendarEvent and the DiskStore, and Calendar to which the event belongs
282
+ # @api private
283
+ # @see RstCommand#event_environment
284
+ class EventEnvironment < Struct.new(:event,:store,:calendar)
285
+ end
286
+
287
+ # EventOptions holds the options date,label, and calendar_name
288
+ # @api private
289
+ # @see RstCommand#get_event_options
290
+ class EventOptions < Struct.new(:date,:label,:calendar_name,)
291
+ end
292
+
293
+
294
+
295
+ end
data/rst.rb ADDED
@@ -0,0 +1,31 @@
1
+ require 'logger'
2
+
3
+ # # Ruby Shell Tools Top-level file
4
+ module RST
5
+ # Gem-version
6
+ VERSION = '0.0.1'
7
+
8
+ # Path to the docs used by the software
9
+ DOCS = File.expand_path('../assets/docs', __FILE__)
10
+
11
+ # Default DataStore-path. You can overwrite this by
12
+ # defining `ENV[RST_DATA]`
13
+ STOREPATH = File.expand_path('../data/', __FILE__)
14
+
15
+
16
+ # intialize the logger
17
+ # @example Usage
18
+ # RST.logger.info('This will output to STDERR')
19
+ def logger
20
+ @logger ||= Logger.new(STDERR)
21
+ end
22
+
23
+ # initialize a new logger. Necessary from within specs to capture the
24
+ # output for testing.
25
+ # @param [File|Stream] _output
26
+ def logger!(_output)
27
+ @logger = Logger.new(_output)
28
+ end
29
+
30
+
31
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubyshelltools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andi Altendorfer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redcarpet
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: yard
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description: Tools for the unix shell
63
+ email: andreas@altendorfer.at
64
+ executables:
65
+ - rst
66
+ extensions: []
67
+ extra_rdoc_files:
68
+ - README.md
69
+ - assets/docs/examples.md
70
+ files:
71
+ - ./rst.rb
72
+ - lib/core_extensions/numeric.rb
73
+ - lib/errors/store_errors.rb
74
+ - lib/load.rb
75
+ - lib/modules/calendar/calendar.rb
76
+ - lib/modules/calendar/calendar_event.rb
77
+ - lib/modules/calendar/eventable.rb
78
+ - lib/modules/persistent/disk_store.rb
79
+ - lib/modules/persistent/memory_store.rb
80
+ - lib/modules/persistent/persistent.rb
81
+ - lib/modules/persistent/store.rb
82
+ - lib/rst.rb
83
+ - bin/rst
84
+ - README.md
85
+ - assets/docs/examples.md
86
+ homepage: http://rubygems.org/gems/rubyshelltools
87
+ licenses: []
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 1.8.25
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: Ruby Shell Tools
110
+ test_files: []
111
+ has_rdoc: