clir 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,237 @@
1
+ =begin
2
+
3
+ A command line is composed by:
4
+
5
+ - app command (for example: 'rake')
6
+ - main command (main_command): firts element not leading with '-'
7
+ dans without '=' (for example: 'build' in 'rake build')
8
+ - options: elements that lead with '-' or '--'
9
+ - params : elments that contain '=' (key=value pairs)
10
+ - components: all other elements
11
+
12
+ We can get this elments with:
13
+
14
+ CLI.main_command
15
+ CLI.options[:<key>]
16
+ CLI.option(:key)
17
+ CLI.params[:<key>]
18
+ CLI.components[<index>]
19
+
20
+ App can define its own short options (to long options) with:
21
+
22
+ CLI.set_options_table({short: long, short: long...})
23
+
24
+ Note : il faut obligatoirement mettre la version courte (souvent
25
+ une seule lettre) en Symbol :
26
+ CLI.set_options_table({e: :edition})
27
+
28
+
29
+ =end
30
+ module CLI
31
+ MARKER_TESTS_FILE = File.expand_path(File.join('.','.MARKER_TESTS'))
32
+ class << self
33
+
34
+ ##
35
+ # First class method
36
+ # (call it at start-up)
37
+ #
38
+ def init
39
+ parse(ARGV)
40
+ Q.init if Q.respond_to?(:init)
41
+ end
42
+
43
+ ##
44
+ # @return command line options
45
+ # @options is set while parse method. If it is not set, we
46
+ # CLI.init first.
47
+ #
48
+ def options
49
+ defined?(@options) || self.init
50
+ @options
51
+ end
52
+
53
+ # @return option of key +key+
54
+ def option(key)
55
+ options[key.to_sym]
56
+ end
57
+
58
+ ##
59
+ # @main command (after command name)
60
+ def main_command
61
+ defined?(@main_command) || self.init
62
+ @main_command
63
+ end
64
+
65
+ ##
66
+ # @return command line parameters
67
+ #
68
+ def params
69
+ defined?(@params) || self.init
70
+ @params
71
+ end
72
+
73
+ def param(key)
74
+ @params[key.to_sym]
75
+ end
76
+
77
+ ##
78
+ # @return command line components
79
+ # 'components' are elements of command line that are not options
80
+ # (with leading '-'), that are not parameters (key=value paire) and
81
+ # that are not main_command
82
+ #
83
+ def components
84
+ defined?(@components) || self.init
85
+ @components
86
+ end
87
+
88
+
89
+ ##
90
+ # Command name
91
+ #
92
+ # Don't confuse with 'main command' which is the very first
93
+ # argument in command line
94
+ #
95
+ def command_name
96
+ @command_name ||= begin
97
+ File.basename($PROGRAM_NAME,File.extname($PROGRAM_NAME))
98
+ end
99
+ end
100
+
101
+ ##
102
+ # Main method which parse command line to get:
103
+ # - main command
104
+ # - options (leadings with -/--)
105
+ # - parameters (key=value pairs)
106
+ #
107
+ def parse(argv)
108
+ argv = argv.split(' ') if argv.is_a?(String)
109
+ if replay_it?(argv)
110
+ #
111
+ # Replay last command (if unable)
112
+ #
113
+ puts "Je dois apprendre à replayer la commande précédente".jaune
114
+ puts "Pour ça, je dois enregistrer les inputs précédents.".jaune
115
+ else
116
+ #
117
+ # Regular run
118
+ #
119
+ reset
120
+ @raw_command_line = ([command_name]+argv).join(' ')
121
+ argv.each do |arg|
122
+ if arg.start_with?('--')
123
+ arg, val = key_and_value_in(arg[2..-1])
124
+ @options.merge!(arg.to_sym => val)
125
+ elsif arg.start_with?('-')
126
+ arg, val = key_and_value_in(arg[1..-1])
127
+ arg = long_option_for(arg)
128
+ @options.merge!(arg.to_sym => val)
129
+ elsif arg.match?('.=.')
130
+ key, val = key_and_value_in(arg)
131
+ @params.merge!(key.to_sym => val)
132
+ elsif @main_command.nil?
133
+ @main_command = arg
134
+ else
135
+ @components << arg
136
+ end
137
+ end
138
+ end
139
+ end
140
+
141
+ def set_options_table(table)
142
+ @_app_options_table = table
143
+ end
144
+
145
+ ##
146
+ # For Replayer, return data
147
+ def get_command_line_data
148
+ {
149
+ raw_command_line: @raw_command_line,
150
+ command_name: command_name,
151
+ main_command: main_command,
152
+ components: components,
153
+ options: options,
154
+ params: params,
155
+ table_short2long_options: table_short2long_options
156
+ }
157
+ end
158
+
159
+ def set_command_line_data(data)
160
+ data.each do |k, v| instance_variable_set("@#{k}", v) end
161
+ end
162
+
163
+ # --- Tests Methods ---
164
+
165
+ def set_tests_on_with_marker
166
+ File.write(MARKER_TESTS_FILE, "#{Time.now}")
167
+ end
168
+
169
+ def unset_tests_on_with_marker
170
+ if File.exist?(MARKER_TESTS_FILE)
171
+ File.delete(MARKER_TESTS_FILE)
172
+ else
173
+ puts "Weirdly, the test marker file (CLI::MARKER_TESTS_FILE) doesn't exist…".rouge
174
+ end
175
+ end
176
+
177
+
178
+ private
179
+
180
+ def reset
181
+ @main_command = nil
182
+ @components = []
183
+ @options = {}
184
+ @params = {}
185
+ Clir::State.reset
186
+ CLI::Replayer.init_for_recording
187
+ @table_short2long_options = nil
188
+ end
189
+
190
+ ##
191
+ # @return the long option for +short+
192
+ #
193
+ def long_option_for(short)
194
+ short = short.to_sym
195
+ if table_short2long_options.key?(short)
196
+ table_short2long_options[short]
197
+ else
198
+ short
199
+ end
200
+ end
201
+
202
+ ##
203
+ # @return conversion table from short option (pe 'v') to
204
+ # long option (pe 'verbose').
205
+ # App can define its own table in CLI.set_options_table
206
+ def table_short2long_options
207
+ @table_short2long_options ||= begin
208
+ {
209
+ :h => :help,
210
+ :q => :quiet,
211
+ :v => :verbose,
212
+ :x => :debug,
213
+ }.merge(app_options_table)
214
+ end
215
+ end
216
+
217
+ ##
218
+ # @return the app table of options conversions
219
+ def app_options_table
220
+ @_app_options_table ||= {}
221
+ end
222
+
223
+ ##
224
+ # @return [key, value] in +foo+ if +foo+ contains '='
225
+ # [foo, default] otherwise
226
+ def key_and_value_in(foo, default = true)
227
+ foo.match?('=') ? foo.split('=') : [foo, default]
228
+ end
229
+
230
+ ##
231
+ # @return TRUE if replay character is used and only
232
+ # replay character
233
+ def replay_it?(argv)
234
+ argv.count == 1 && argv[0] == ::Config[:replay_character]
235
+ end
236
+ end #/<< self CLI
237
+ end #/module CLI
@@ -0,0 +1,177 @@
1
+ require 'csv'
2
+ class CSV
3
+ class << self
4
+
5
+ # Pour pouvoir tester l'entête dans les tests
6
+ attr_reader :headers_for_test
7
+
8
+ # Lecture d'un fichier CSV à partir de la fin
9
+ #
10
+ # Les arguments sont les mêmes que pour readlines
11
+ #
12
+ # Pour rappel :
13
+ # +options+ peut contenir
14
+ # :headers À true, on tient compte des entêtes et on
15
+ # retourne des CSV::Row. Sinon, on retourne une
16
+ # Array simple.
17
+ # :headers_converters
18
+ # Convertisseur pour les noms des colonnes.
19
+ # :downcase ou :symbol
20
+ # :converters
21
+ # Liste de convertisseurs pour les données.
22
+ # Principalement :numeric, :date
23
+ # :col_sep
24
+ # Séparateur de colonne. Par défaut une virgule.
25
+ #
26
+ def readlines_backward(path, **options, &block)
27
+
28
+ options ||= {}
29
+
30
+ file = File.new(path)
31
+ size = File.size(file)
32
+
33
+ if size < ( 1 << 16 )
34
+ return readlines_backward_in_small_file(path, **options, &block)
35
+ end
36
+
37
+ #
38
+ # Si options[:headers] est true, il faut récupérer la première
39
+ # ligne et la transformer en entête
40
+ #
41
+ if options[:headers]
42
+ begin
43
+ fileh = File.open(path,'r')
44
+ header = fileh.gets.chomp
45
+ ensure
46
+ fileh.close
47
+ end
48
+ table = CSV.parse(header, **options)
49
+ headers = table.headers
50
+ @headers_for_test = headers # pour les tests
51
+ end
52
+
53
+
54
+ if block_given?
55
+ #
56
+ # Les options pour CSV.parse
57
+ # On garde seulement les convertisseurs de données et on met
58
+ # toujours headers à false (puisque c'est seulement la ligne
59
+ # de données qui sera parser)
60
+ #
61
+ # line_csv_options = {headers:false, converters: options[:converters]}
62
+ line_csv_options = options
63
+ #
64
+ # Avec un bloc fourni, on va lire ligne par ligne en partant
65
+ # de la fin.
66
+ #
67
+ buffer_size = 10000 # disons que c'est la longueur maximale d'une ligne
68
+
69
+ #
70
+ # On se positionne de la fin - la longueur du tampo dans le
71
+ # fichier à lire
72
+ #
73
+ file.seek(-buffer_size, IO::SEEK_END)
74
+ #
75
+ # Le tampon courant (il contient tout jusqu'à ce qu'il y ait
76
+ # au moins une ligne)
77
+ #
78
+ buffer = ""
79
+ #
80
+ # On boucle tant qu'on n'interrompt pas (pour le moment)
81
+ #
82
+ # QUESTIONS
83
+ # 1. Comment repère-t-on la fin ? En comparant la position
84
+ # actuelle du pointeur avec 0 ?
85
+ # 2. Comment met-on fin à la recherche (c'est presque la
86
+ # même question)
87
+ while true
88
+ #
89
+ # On lit la longueur du tampon en l'ajoutant à ce qu'on a
90
+ # déjà lu ou ce qui reste.
91
+ # Celui pourra contenir une ou plusieurs lignes, la première
92
+ # pourra être tronquée
93
+ #
94
+ buffer = file.read(buffer_size) + buffer
95
+ #
96
+ # Nombre de lignes
97
+ # (utile ?)
98
+ nombre_lignes = buffer.count("\n")
99
+ #
100
+ # On traite les lignes du buffer (en gardant ce qui dépasse)
101
+ #
102
+ if nombre_lignes > 0
103
+ # puts "Position dans le fichier : #{file.pos}".bleu
104
+ # puts "Nombre de lignes : #{nombre_lignes}".bleu
105
+ lines = buffer.split("\n").reverse
106
+ #
107
+ # On laisse la dernière ligne, certainement incomplète, dans
108
+ # le tampon. Elle sera ajoutée à la fin de ce qui précède
109
+ #
110
+ buffer = lines.pop
111
+ #
112
+ # Boucle sur les lignes
113
+ #
114
+ # @note : un break, dans le &block, interrompra la boucle
115
+ #
116
+ lines.each do |line|
117
+ line = line.chomp
118
+ line = "#{header}\n#{line}\n" if options[:headers]
119
+ # puts "line parsée : #{line.inspect}".bleu
120
+ line_csv = CSV.parse(line, **line_csv_options)
121
+ # puts "line_csv: #{line_csv.inspect}::#{line_csv.class}".orange
122
+ yield line_csv[0]
123
+ end
124
+ end
125
+ #
126
+ # On remonte de deux fois la longueur du tampon. Une fois pour
127
+ # revenir au point de départ, une fois pour remonter à la
128
+ # portion suivante, à partir de la position courante, évide-
129
+ # ment
130
+ #
131
+ new_pos = file.pos - 2 * buffer_size
132
+ if new_pos < 0
133
+ file.seek(new_pos)
134
+ else
135
+ file.seek(- 2 * buffer_size, IO::SEEK_CUR)
136
+ end
137
+ #
138
+ # Si on se trouve à 0, on doit s'arrêter
139
+ #
140
+ break if file.pos <= 0
141
+ # puts "Nouvelle position dans le fichier : #{file.pos}".bleu
142
+ end
143
+ else
144
+ #
145
+ # Sans bloc fourni, on renvoie tout le code du fichier
146
+ #
147
+ # À vos risques et périls
148
+ # self.readlines(path, **options).to_a.reverse
149
+ self.foreach(path, **options).reverse
150
+ end
151
+
152
+ end
153
+ alias :readlines_backwards :readlines_backward
154
+ alias :foreach_backward :readlines_backward
155
+ alias :foreach_backwards :readlines_backward
156
+
157
+ # Lecture à l'envers dans un petit fichier
158
+ #
159
+ def readlines_backward_in_small_file(path, **options, &block)
160
+ if block_given?
161
+ self.foreach(path, **options).reverse_each do |row|
162
+ yield row
163
+ end
164
+ # liste2reverse = []
165
+ # self.readlines(path, **options).each { |row| liste2reverse << row }
166
+ # liste2reverse.reverse.each do |row|
167
+ # yield row
168
+ # end
169
+ else
170
+ # Lecture toute simple de la table
171
+ # return self.readlines(path, **options).to_a.reverse
172
+ return self.foreach(path, **options).reverse
173
+ end
174
+ end
175
+
176
+ end #/<< self class CSV
177
+ end #/class CSV
@@ -0,0 +1,25 @@
1
+ =begin
2
+
3
+ Pour le moment, on met la configuration comme ça, en dur, mais
4
+ plus tard on pourra la modifier pour chaque application.
5
+
6
+ =end
7
+ module Clir
8
+ class Configuration
9
+
10
+ def [](key)
11
+ data[key]
12
+ end
13
+
14
+ # TODO
15
+ def data
16
+ {
17
+ replay_character: '_'
18
+ }
19
+ end
20
+
21
+ end #/class Configuration
22
+ end #/module CLIR
23
+
24
+ # Expose outside
25
+ Config = Clir::Configuration.new
@@ -0,0 +1,161 @@
1
+ =begin
2
+
3
+ Usefull methods for date & time
4
+
5
+ @author: Philippe Perret <philippe.perret@yahoo.fr>
6
+
7
+ =end
8
+ require 'date'
9
+
10
+ MOIS = {
11
+ 1 => {court: 'jan', long: 'janvier'},
12
+ 2 => {court: 'fév', long: 'février'},
13
+ 3 => {court: 'mars', long: 'mars'},
14
+ 4 => {court: 'avr', long: 'avril'},
15
+ 5 => {court: 'mai', long: 'mai'},
16
+ 6 => {court: 'juin', long: 'juin'},
17
+ 7 => {court: 'juil', long: 'juillet'},
18
+ 8 => {court: 'aout', long: 'aout'},
19
+ 9 => {court: 'sept', long: 'septembre'},
20
+ 10 => {court: 'oct', long: 'octobre'},
21
+ 11 => {court: 'nov', long: 'novembre'},
22
+ 12 => {court: 'déc', long: 'décembre'}
23
+ }
24
+
25
+ DAYNAMES = [
26
+ 'Dimanche', 'Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi', 'Samedi'
27
+ ]
28
+
29
+
30
+ # @return [String] Une date formatée avec le moins verbal
31
+ #
32
+ # @param [Time|Date|NIl] La date. Si non fournie, on prend maintenant
33
+ # @param [Hash] options Les options de formatage
34
+ # @option lenght [Symbol] :court (pour le mois court)
35
+ def human_date(ladate = nil, **options)
36
+ ladate ||= Time.now
37
+ options.key?(:length) || options.merge!(length: :long)
38
+ lemois = MOIS[ladate.month][options[:length]]
39
+ lemois = "#{lemois}." if options[:length] == :court
40
+ "#{ladate.day} #{lemois} #{ladate.year}"
41
+ end
42
+ alias :date_humaine :human_date
43
+
44
+
45
+ # @return A date for a file, now
46
+ # @example
47
+ # date_for_file # => "2022-12-14"
48
+ # date_for_file(nil, true) # => "2022-12-14-23-11"
49
+ def date_for_file(time = nil, with_hour = false, del = '-')
50
+ time ||= Time.now
51
+ fmt = "%Y#{del}%m#{del}%d"
52
+ fmt = "#{fmt}#{del}%H#{del}%M" if with_hour
53
+ time.strftime(fmt)
54
+ end
55
+
56
+ # @reçoit une date et la retourne sous la forme "YYYY-MM-DD"
57
+ def ymd(time = nil, delimitor = '-')
58
+ time ||= Time.now
59
+ time.strftime("%Y#{delimitor}%m#{delimitor}%d")
60
+ end
61
+
62
+
63
+ # @return Date corresponding to +foo+
64
+ # @param [String|Integer|Time|Date] foo
65
+ # - [Time] Return itself
66
+ # - [Date] Return itself.to_time
67
+ # - [String] JJ/MM/AAAA or 'YYYY/MM/DD'
68
+ # - [Integer] Number of seconds
69
+ # @note
70
+ # Alias :time_from (more semanticaly correct)
71
+ #
72
+ def date_from(foo)
73
+ case foo
74
+ when Time then foo
75
+ when Date then foo.to_time
76
+ when Integer then Time.at(foo)
77
+ when String
78
+ a, b, c = foo.split('/')
79
+ if c.length == 4
80
+ Time.new(c.to_i, b.to_i, a.to_i)
81
+ else
82
+ Time.new(a.to_i, b.to_i, c.to_i)
83
+ end
84
+ else
85
+ raise "Unable to transform #{foo.inspect} to Time."
86
+ end
87
+ end
88
+ alias :time_from :date_from
89
+
90
+ ##
91
+ # Formate de date as JJ MM AAAA (or MM JJ AAAA in english)
92
+ # @param {Time} date
93
+ # @param [Hash] options table:
94
+ # @option options [Boolean] :verbal If true, the format will be "month the day-th etc."
95
+ # @option options [Boolean] :no_time If true, only day, without time
96
+ # @option options [Boolean] :seconds If true, add seconds with time
97
+ # @option options [Boolean] :update_format If true, the format is updated. Otherwise, the last format is used for all next date
98
+ # @option options [Boolean] :sentence If true, on met "le ... à ...."
99
+ #
100
+ def formate_date(date, options = nil)
101
+ options ||= {}
102
+ @last_format = nil if options[:update_format] || options[:template]
103
+ @last_format ||= begin
104
+ as_verbal = options[:verbal]||options[:sentence]
105
+ if options[:template]
106
+ options[:template]
107
+ else
108
+ fmt = []
109
+ fmt << 'le ' if options[:sentence]
110
+ if as_verbal
111
+ forday = date.day == 1 ? '1er' : '%-d'
112
+ fmt << "#{forday} #{MOIS[date.month][:long]} %Y"
113
+ else
114
+ fmt << '%d %m %Y'
115
+ end
116
+ delh = options[:sentence] ? 'à' : '-'
117
+ unless options[:no_time]
118
+ fmt << (as_verbal ? " à %H h %M" : " #{delh} %H:%M")
119
+ end
120
+ if options[:seconds]
121
+ fmt << (as_verbal ? ' mn et %S s' : ':%S' )
122
+ end
123
+ fmt.join('')
124
+ end
125
+ end
126
+ date.strftime(@last_format)
127
+ end
128
+
129
+ class Time
130
+
131
+ # @return [String] French date with separator
132
+ # @example
133
+ # Time.now.jj_mm_yyyy # => "28/12/2022"
134
+ # @param separator [String] Separator to use (default: '/')
135
+ def jj_mm_aaaa(separator = '/')
136
+ self.strftime(['%d','%m','%Y'].join(separator))
137
+ end
138
+
139
+ # @return [String] English date with separator
140
+ # @example
141
+ # Time.now.mm_dd_yyyy # => "12/28/2022"
142
+ #
143
+ # @param separator [String] Separator to use (default: '/')
144
+ def mm_dd_yyyy(separator = '/')
145
+ self.strftime(['%m','%d','%Y'].join(separator))
146
+ end
147
+
148
+ end #/class Time
149
+
150
+ class Integer
151
+
152
+ def ago
153
+ (Time.now - self).mm_dd_yyyy
154
+ end
155
+
156
+ end #/class Integer
157
+
158
+ def ilya(laps, options = nil)
159
+ options ||= {}
160
+ (Time.now - laps).jj_mm_aaaa(options[:separator] || '/')
161
+ end