rubyshelltools 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,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: