workEnv 0.1.0

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.
@@ -0,0 +1,608 @@
1
+ #!/usr/bin/ruby
2
+ if !ENV["WENV_WITHSQL"].nil? then
3
+ begin
4
+ # Load MySQL2 module
5
+ require 'mysql2'
6
+ #Ugly hack not to laod MySQL2 if we really want to test MySQL1
7
+ raise LoadError if !ENV["WENV_FORCE_SQL1"].nil?
8
+ raise ("Version 0.4.1 of Mysql2 is broken. Please use another version") if Mysql2::VERSION == "0.4.1"
9
+ rescue LoadError
10
+ begin
11
+ require 'mysql'
12
+ rescue LoadError
13
+ raise("Neither ruby-mysql nor ruby-mysql2 are available on this system")
14
+ end
15
+ end
16
+ begin
17
+ require 'sqlite3'
18
+ rescue LoadError
19
+ end
20
+ end
21
+ require 'uri'
22
+ require_relative 'PackageDownloader'
23
+
24
+ module WorkEnvs
25
+ # Empty 2D array to to return when select provided no results
26
+ WORK_EMPTY_QUERY = [[nil]]
27
+
28
+ # Max number of retrying a query. Used to handle bad connections
29
+ MAX_ERROR_RETRY = 3
30
+
31
+ # Maximum duration of a SQL query before trigging a warning when #DEBUG_QUERIES is true
32
+ WENV_SLOW_QUERY_THRESHOLD = 1e-0
33
+
34
+ # Flag to enable tracking of slow queries
35
+ DEBUG_QUERIES = ((ENV["DEBUG_QUERIES"] != nil) ? true: false)
36
+
37
+ # Exception thrown when executing a query on a DB object failed
38
+ class QueryErrorException < StandardError
39
+ # Constructor
40
+ #
41
+ # - query is the query string that caused the failure
42
+ # - table is the table on which the query was executed
43
+ # - msg is an additional error message to print (default = nil)
44
+ def initialize(query, table, msg=nil)
45
+ super("\nERROR: Error while executing query:\n" + query.to_s(table) + "\n" + msg.to_s)
46
+ end
47
+ end
48
+
49
+ # Exception thrown when executing a select query did not returned any results
50
+ class EmptyQueryException < StandardError
51
+ # Constructor
52
+ #
53
+ # - query is the query string that caused the failure
54
+ # - table is the table on which the query was executed
55
+ def initialize(query, table)
56
+ super("\nERROR: Found no match in the database for query:\n" + query.to_s(table))
57
+ end
58
+ end
59
+
60
+ # Interface to execute DBQuery on a SQL database
61
+ class DBInterface
62
+ # Array of table names to search into (default extracted from settings)
63
+ attr_reader :table
64
+ # Object to connect to database. Abstract ruby-mysql, ruby-mysql2 or sqlite3
65
+ attr_reader :db
66
+ # When false, enable verbose mode (default = true)
67
+ attr_accessor :be_silent
68
+
69
+ # Convert a table string issue from the command line to an array of table
70
+ #
71
+ # - If nil or false, get the default table string from settings
72
+ # - If true, only use the "releases" table (legacy from --release option)
73
+ # - If a string, convert to an array containing the string
74
+ def self.toTable(table)
75
+ case table
76
+ when nil, false
77
+ return WorkEnvs.settings[:db][:package_default_table].split(":").
78
+ inject([]){|glob, t| glob + DBInterface::toTable(t)}
79
+ when true
80
+ return [ "releases" ]
81
+ else
82
+ return [ table ]
83
+ end
84
+ end
85
+
86
+ # Connect to a MySQL database
87
+ #
88
+ # Tries to connect with ruby-mysql2 (unless WENV_FORCE_SQL1)
89
+ # fallback to ruby-mysql
90
+ #
91
+ # Returns:
92
+ # - a DB connection object (nil on error)
93
+ # - a label with the db type (:mysql, :mysql2, :http on error)
94
+ def _connectMySQL(db_server, db_user, db_passwd, db_name)
95
+ begin
96
+ raise("Skip") if !ENV["WENV_FORCE_SQL1"].nil?
97
+ return Mysql2::Client.new(:host => db_server,
98
+ :username => db_user,
99
+ :password => db_passwd,
100
+ :database => db_name), :mysql2
101
+ rescue => e
102
+ end
103
+ begin
104
+ return Mysql.new(db_server, db_user, db_passwd, db_name), :mysql
105
+ rescue => e
106
+ end
107
+ return nil, :http
108
+ end
109
+ private :_connectMySQL
110
+
111
+
112
+ # Connect to a MySQL database
113
+ #
114
+ # Extract the proper settings from the settings args and call _connectMySQL
115
+ #
116
+ # Returns:
117
+ # - a DB connection object (nil on error)
118
+ # - a label with the db type (:mysql, :mysql2, :http on error)
119
+ def connectMySQL(settings)
120
+ return _connectMySQL(settings[:package_db], settings[:package_db_user], settings[:package_db_passwd],
121
+ settings[:package_db_db])
122
+ end
123
+ private :connectMySQL
124
+
125
+ # Default constructor
126
+ def initialize(machine = nil, table=nil, silent=true)
127
+ @settings = WorkEnvs::settings[:db]
128
+
129
+ @table = []
130
+ if table.instance_of?(Array) then
131
+ @table = table
132
+ elsif table.to_s == "" then
133
+ @table = self.class::toTable(nil)
134
+ else
135
+ @table = table.to_s.split(":").inject([]){|glob, t| glob + DBInterface::toTable(t)}
136
+ end
137
+ @be_silent = silent
138
+ @admin = false
139
+ @dbType = @settings[:package_db_proto]
140
+
141
+ case @settings[:package_db_proto]
142
+ when :mysql
143
+ @db,@dbType = connectMySQL(@settings)
144
+ raise("Unable to connect to MySQL Database #{@settings[:package_db]}") if @db == nil
145
+ when :http,:https
146
+ @db,@dbType = connectMySQL(@settings)
147
+ if @dbType == :http
148
+ STDERR.puts "HTTP Query are not supported any more"
149
+ STDERR.puts "Please make sure you have ruby mysql or mysql2 installed"
150
+ raise("Protocol Fix")
151
+ end
152
+ when :sqlite
153
+ raise("SQLite DB '#{@settings[:package_db]}' not found." +
154
+ " Use --package-db option") if !File.exist?(@settings[:package_db])
155
+ begin
156
+ @db = SQLite3::Database.open(@settings[:package_db])
157
+ @admin = true
158
+ rescue => e
159
+ raise("Unable to connect to SQLite Database #{@settings[:package_db]}: #{e.to_s}")
160
+ end
161
+ else
162
+ raise("Unsupported DB protocol #{@settings[:package_db_proto].to_s}")
163
+ end
164
+ puts "Connected to DB '#{@settings[:package_db]}' using #{@dbType.to_s}" if @be_silent != true
165
+ end
166
+
167
+ # Switch the DB connection to an "admin" mode where insert are possible
168
+ #
169
+ # Note does not work with SQLite DB
170
+ def doAdminConnect(db_user = @settings[:package_db_user],
171
+ db_passwd = @settings[:package_db_passwd],
172
+ db_server = @settings[:package_db],
173
+ db_name = @settings[:package_db_db])
174
+ @db, @dbType = _connectMySQL(db_server, db_user, db_passwd, db_name)
175
+ raise("Failed to connect as Admin to the database") if @db == nil
176
+ @admin = true
177
+ end
178
+
179
+ # Execute a DBQuery insert query
180
+ #
181
+ # Throw EmptyQueryException on error
182
+ # Returns an error string if DBInterface is not in admin mode
183
+ def doInsert(query)
184
+ return "Admin mode must be enable before inserting..." if @admin != true
185
+ hasInfo = false
186
+ query.cond.each(){|el| hasInfo == true if el[:field] == "info" && el[:value].to_s != "" }
187
+ query.cond << { :field => "info", :value => `date`.chomp()} if hasInfo == false
188
+ begin
189
+ doQueryInternal(query, @table)
190
+ rescue EmptyQueryException
191
+ end
192
+ end
193
+
194
+ # Execute a DBDepsQuery insert query
195
+ #
196
+ # Admin mode is not required
197
+ #
198
+ # Returns an error string if DBInterface is not in admin mode
199
+ def doInsertDep(query)
200
+ begin
201
+ doQueryInternal(query, [ "dependencies" ])
202
+ rescue EmptyQueryException
203
+ end
204
+ end
205
+
206
+ # Convert results generated by the @db object while executing a query into
207
+ # an 2 dimensional array [result_id][field]
208
+ #
209
+ # Throws EmptyQueryException if there are no results
210
+ # Throws QueryErrorException on internal error
211
+ def extractResults(query, table, result, dbType)
212
+ case dbType
213
+ when :mysql
214
+ raise EmptyQueryException.new(query, table) if result.nil? or result.num_rows < 1
215
+ fieldPos={}; idx = 0
216
+ result.result_metadata.fetch_fields.each() { |field|
217
+ fieldPos[field.name] = idx
218
+ idx +=1
219
+ }
220
+ resultTable = []
221
+ result.each(){|line|
222
+ resultEntry = []
223
+ query.qtype.map{ |x|
224
+ resultEntry << "#{line[fieldPos[x]]}"
225
+ }
226
+ resultTable << resultEntry
227
+ }
228
+ return resultTable
229
+ when :sqlite
230
+ raise EmptyQueryException.new(query, table) if result.nil?
231
+
232
+ hasResult = false
233
+
234
+ fieldPos={}; idx = 0
235
+ resultTable = []
236
+ result.each(){|line|
237
+ hasResult = true
238
+ resultTable << line
239
+ }
240
+ raise EmptyQueryException.new(query, table) if !hasResult
241
+ return resultTable
242
+ when :mysql2
243
+ if result.nil? or result.count < 1
244
+ raise EmptyQueryException.new(query, table)
245
+ else
246
+ resultTable=[]
247
+ result.each(){|row|
248
+ resultEntry=[]
249
+ query.qtype.each(){|field|
250
+ resultEntry << row[field]
251
+ }
252
+ resultTable << resultEntry
253
+ }
254
+ return resultTable
255
+ end
256
+ when :http
257
+ file = result
258
+ breakExpr = "-e 's/<[bB][rR][[:space:]]*\\/>/\\n/g'"
259
+ result = runCmd("sed -e 's/.*<BODY>//' -e 's/<\\/BODY>.*//' #{breakExpr} #{file}", @be_silent)
260
+ runCmd("rm -f #{file}", @be_silent)
261
+ if result == "Error querying DB"
262
+ raise QueryErrorException.new(query, table)
263
+ elsif result == "Error not found"
264
+ raise EmptyQueryException.new(query, table)
265
+ end
266
+ resultTable = []
267
+ result.split(/[\n,]/).each(){|line|
268
+ resultEntry = line.split("&nbsp;")
269
+ resultTable << resultEntry
270
+ }
271
+ return resultTable
272
+ else
273
+ raise("Internal error")
274
+ end
275
+ end
276
+ private :extractResults
277
+
278
+ # Internal function to execute a DBQuery (or any class inherting it)
279
+ #
280
+ # Convert the DBQuery into string (+ arg) and run the query.
281
+ #
282
+ # Returns result process by extractResults (two dimensional array)
283
+ # Throw QueryErrorException @db failed to execute the query
284
+ #
285
+ # Note: by setting the env variable DEBUG_QUERIES, it will print a message
286
+ # if queries are slower than WENV_SLOW_QUERY_THRESHOLD
287
+ def doQueryInternal(query, tables)
288
+ result=nil
289
+ startTime=Time.now()
290
+ case @dbType
291
+ when :mysql
292
+ queryStr, args = query.to_sql(tables)
293
+ begin
294
+ db_query = @db.prepare(queryStr)
295
+ puts query.to_s(tables) if @be_silent != true
296
+ result = db_query.execute(*args)
297
+ rescue => e
298
+ raise QueryErrorException.new(query, tables, e.to_s)
299
+ end
300
+ when :sqlite
301
+ queryStr, args = query.to_sql(tables)
302
+ begin
303
+ db_query = @db.prepare(queryStr)
304
+ puts query.to_s(tables) if @be_silent != true
305
+ result = db_query.execute(*args)
306
+ rescue => e
307
+ raise QueryErrorException.new(query, tables, e.to_s)
308
+ end
309
+ when :mysql2
310
+ queryStr = query.to_sql2(tables)
311
+ begin
312
+ puts query.to_s(tables) if @be_silent != true
313
+ result = @db.query(queryStr)
314
+ rescue => e
315
+ p e
316
+ raise QueryErrorException.new(query, tables, e.to_s)
317
+ end
318
+ else
319
+ raise("Internal error")
320
+ end
321
+ endTime = Time.now()
322
+ if DEBUG_QUERIES and (endTime - startTime) > WENV_SLOW_QUERY_THRESHOLD then
323
+ STDERR.puts "# Warning slow query (#{endTime - startTime})"
324
+ puts "#\t" + query.to_s(tables)
325
+ end
326
+ return extractResults(query, tables, result, @dbType)
327
+ end
328
+ private :doQueryInternal
329
+
330
+ # Execute a DBQuery select query
331
+ #
332
+ # Calls doQueryInternal
333
+ #
334
+ # - If all_tables is true, query is ran on the provided tables and
335
+ # all the table referenced as valid in the settings
336
+ # - If all_tables is false the query is only ran on the provided tables
337
+ #
338
+ # Throws EmptyQueryException if required = true and no results were returned
339
+ # Throws QueryErrorException if the query failed
340
+ #
341
+ # Returns a two dimensionnal array containing the results
342
+ def doQuery(query, required = true, all_tables = true, tables = @table)
343
+ raise("Invalid query object") if query.class != DBQuery && query.class != DBDepsQuery
344
+ e = nil
345
+ result = nil
346
+
347
+ if all_tables == true then
348
+ tables += @settings[:package_db_tables]
349
+ tables.uniq!
350
+ end
351
+
352
+ count = 0
353
+ begin
354
+ begin
355
+ count = count + 1
356
+ result = doQueryInternal(query, tables)
357
+ puts "Result: #{result.join("\n")}" if @be_silent != true
358
+
359
+ rescue EmptyQueryException => e
360
+ puts "Result: <none>" if @be_silent != true
361
+ rescue QueryErrorException => e
362
+ puts "Query Error" if @be_silent != true
363
+ end
364
+ end while e.class == QueryErrorException && count < MAX_ERROR_RETRY
365
+ raise e if e.class == QueryErrorException
366
+
367
+ return result if result != nil
368
+
369
+ if required != false
370
+ raise EmptyQueryException.new(query, table)
371
+ else
372
+ return WORK_EMPTY_QUERY
373
+ end
374
+ end
375
+
376
+ # Wrapper around doQuery to query DBDepsQuery
377
+ #
378
+ # Calls doQuery with the appropriate settings
379
+ def doDepQuery(query)
380
+ return doQuery(query, true, false, [ "dependencies" ])
381
+ end
382
+ end
383
+
384
+ # Database query
385
+ class DBQuery
386
+ # Array of DB fields allowed in conditions
387
+ Fields = ["sha1", "project", "name", "version", "branch", "arch"]
388
+
389
+ # Array of fields (String) to be queryed (default Fields)
390
+ attr_reader :qtype
391
+ # Hash describing matching conditions (default {})
392
+ # - :field => field to match in the condition
393
+ # - :not => IF true, checks field value is NOT value (default false)
394
+ # - :value => value to check against
395
+ # - :sub_query => DBQuery to run and check field against its results
396
+ #
397
+ # :sub_query and :value cannot be specified at the same time
398
+ attr_reader :cond
399
+ # Group result by this field (default nil)
400
+ attr_reader :group_by
401
+ # - :limit => Max number of results (default 1)
402
+ attr_reader :limit
403
+ # - :order => Field to use for sorting (default "id")
404
+ attr_reader :order
405
+ # - :orderType => Sorting order (default "ASC")
406
+ attr_reader :orderType
407
+ # - :insert => query is an insert, not a select (default false)
408
+ attr_reader :insert
409
+
410
+ # Constructor
411
+ #
412
+ # queryHash describe the request content
413
+ #
414
+ # Mapping is bascially @<field> = queryHash [ :<field> ]. See attributes for more infos.
415
+ #
416
+ # The constructor convert the input value into SQL compliant ones
417
+ def initialize(queryHash = {})
418
+ @qtype = Fields
419
+ @qtype = queryHash[:qtype] if queryHash[:qtype] != nil
420
+ @cond = {}
421
+ @cond = queryHash[:cond] if queryHash[:cond] != nil
422
+ @group_by = ""
423
+ @group_by = queryHash[:group_by] if queryHash[:group_by] != nil
424
+ @limit = "LIMIT 1"
425
+ if queryHash[:limit] == false then
426
+ @limit = ""
427
+ elsif queryHash[:limit] != nil then
428
+ @limit = "LIMIT " + queryHash[:limit].to_s()
429
+ end
430
+ @order = "ORDER BY id"
431
+ @orderType = "ASC"
432
+ @orderType = queryHash[:orderType] if queryHash[:orderType] != nil
433
+
434
+ if queryHash[:order] == false
435
+ @order = ""
436
+ @orderType = ""
437
+ elsif queryHash[:order] != nil
438
+ @order = "ORDER BY " + queryHash[:order].to_s()
439
+ end
440
+ @insert = false
441
+ @insert = queryHash[:insert] if queryHash[:insert] != nil
442
+ end
443
+
444
+ # Internal method to convert a select DBQuery to a SQL String usable with ruby-mysql or sqlite
445
+ #
446
+ # Used by to_sql
447
+ #
448
+ # Returns:
449
+ # - a query String
450
+ # - an array of arguments to be inserted in the string when executing the query
451
+ def to_sql_select(tables, inplace)
452
+ condition = ''
453
+ args=[]
454
+ if @cond != nil && @cond.length != 0 then
455
+ condition = 'WHERE ' + @cond.map(){|x|
456
+ vals = x[:value]
457
+
458
+ if vals != nil then
459
+ vals = [ vals ] if !vals.kind_of?(Array)
460
+ args += vals
461
+
462
+ (x[:not] == true ? "NOT" : "") +
463
+ " (" + vals.map(){|v|
464
+ x[:field] + " LIKE " + (inplace ? "'#{v}'" : "?")
465
+ }.join(" OR ") + ")"
466
+ else
467
+ "#{x[:not] == true ? "NOT" : ""} (" +
468
+ x[:field] + " IN #{x[:sub_query]})"
469
+ end
470
+ }.join(" AND ")
471
+
472
+ end
473
+
474
+ groupBy = ''
475
+ groupBy = "GROUP BY #{@group_by}" if @group_by.to_s != ""
476
+
477
+
478
+ queryStr= tables.map(){|table|
479
+ "( select #{@qtype.join(',')} from #{table} "+
480
+ "#{condition} #{groupBy} #{@order} #{@orderType} " + @limit + " )"
481
+ }.join(" UNION ") +
482
+ (tables.length > 1 && @limit.to_s != "" ? " #{@limit}" : "")
483
+ queryArgs = tables.inject([]){|x, y| x + args }
484
+ return queryStr, queryArgs
485
+ end
486
+ private :to_sql_select
487
+
488
+ # Internal method to convert an insert DBQuery to a SQL String usable with ruby-mysql or sqlite
489
+ #
490
+ # Used by to_sql
491
+ #
492
+ # Returns:
493
+ # - a query String
494
+ # - an array of arguments to be inserted in the string when executing the query
495
+ def to_sql_insert(tables, inplace)
496
+ raise("No content to insert") if @cond.empty?
497
+
498
+ queryArgs=[]
499
+
500
+ queryStr = "INSERT INTO #{tables[0]} (" +
501
+ @cond.map(){|el| queryArgs << el[:value]; el[:field].to_s}.join(",") + ") VALUES (" +
502
+ @cond.map(){|el| inplace == true ? el[:value].to_s : "?"}.join(",") + ")"
503
+ return queryStr, queryArgs
504
+ end
505
+ private :to_sql_insert
506
+
507
+ # Convert any DBQuery to a SQL String usable with ruby-mysql or sqlite
508
+ #
509
+ # Uses to_sql_insert or to_sql_select depending on the #insert attribute
510
+ #
511
+ # Returns:
512
+ # - a query String
513
+ # - an array of arguments to be inserted in the string when executing the query
514
+ def to_sql(tables, inplace=false)
515
+ if @insert == true then
516
+ to_sql_insert(tables, inplace)
517
+ else
518
+ to_sql_select(tables, inplace)
519
+ end
520
+ end
521
+
522
+ # Internal method to convert a select DBQuery to a SQL String usable with ruby-mysql2
523
+ #
524
+ # Used by to_sql2
525
+ #
526
+ # Returns a query String with arguments inlines
527
+ def to_sql2_select(tables)
528
+ condition = ''
529
+ if @cond != nil && @cond.length != 0 then
530
+ condition = 'WHERE ' + @cond.map(){|x|
531
+
532
+ vals = x[:value]
533
+ if vals != nil then
534
+ vals = [ vals ] if !vals.kind_of?(Array)
535
+
536
+ (x[:not] == true ? "NOT" : "") +
537
+ " (" + vals.map(){|v|
538
+ x[:field] +" LIKE '#{Mysql2::Client.escape(v.to_s)}'"
539
+ }.join(" OR ") + ")"
540
+ else
541
+ "#{x[:not] == true ? "NOT" : ""} (" +
542
+ x[:field] + " IN #{x[:sub_query]})"
543
+ end
544
+ }.join(" AND ")
545
+ end
546
+
547
+ groupBy = ''
548
+ groupBy = "GROUP BY #{@group_by}" if @group_by.to_s != ""
549
+
550
+ return tables.map(){|table|
551
+ queryStr= "select #{@qtype.join(',')} from #{table} "+
552
+ "#{condition} #{groupBy} #{@order} #{@orderType} " + @limit
553
+ "( " + queryStr + " )"
554
+ }.join(" UNION ") +
555
+ (tables.length > 1 && @limit.to_s != "" ? " #{@limit}" : "")
556
+ end
557
+ private :to_sql2_select
558
+
559
+ # Internal method to convert an insert DBQuery to a SQL String usable with ruby-mysql2
560
+ #
561
+ # Used by to_sql2
562
+ #
563
+ # Returns a query String with arguments inlines
564
+ def to_sql2_insert(tables)
565
+ raise("No content to insert") if @cond.empty?
566
+
567
+
568
+ queryStr = "INSERT INTO #{tables[0]}(" +
569
+ @cond.map(){|el| el[:field].to_s}.join(",") + ") VALUES (" +
570
+ @cond.map(){|el| "'" + Mysql2::Client.escape(el[:value].to_s) + "'"}.join(",") + ")"
571
+ return queryStr
572
+ end
573
+ private :to_sql2_insert
574
+
575
+ # Convert any DBQuery to a SQL String usable with ruby-mysql2
576
+ #
577
+ # Uses to_sql2_insert or to_sql2_select depending on the #insert attribute
578
+ #
579
+ # Returns a query String with arguments inlines
580
+ def to_sql2(tables)
581
+ if @insert == true then
582
+ to_sql2_insert(tables)
583
+ else
584
+ to_sql2_select(tables)
585
+ end
586
+ end
587
+
588
+ # Convert a query to a string for logging and exceptions
589
+ def to_s(tables=["??"])
590
+ return "Query: " + self.to_sql(tables, true)[0]
591
+ end
592
+ end
593
+
594
+ # Variation of DBQuery
595
+ #
596
+ # Override the Fields attribute to fir the dependency DB
597
+ class DBDepsQuery < DBQuery
598
+ # Array of Dependency DB fields allowed in conditions
599
+ Fields = ["name", "arch", "dependencies"]
600
+ # Default constructor
601
+ def initialize(queryHash = {})
602
+ super(queryHash)
603
+ @qtype = Fields
604
+ @qtype = queryHash[:qtype] if queryHash[:qtype] != nil
605
+ end
606
+
607
+ end
608
+ end
@@ -0,0 +1,20 @@
1
+ module WorkEnvs
2
+ # Hostname of the current machine
3
+ WORK_ENV_HOSTNAME= ENV["HOSTNAME"].to_s != "" ? ENV["HOSTNAME"] :
4
+ (File.exist?("/usr/bin/hostname") ? `/usr/bin/hostname`.chomp() : "")
5
+
6
+ # Dir where workEnv scripts are
7
+ WORK_ENV_SCRIPTS_DIR=File.dirname(File.dirname(File.dirname(File.dirname(__FILE__))))
8
+
9
+ # Directory for global configuration
10
+ WORK_ENV_GLOBAL_DIR = (ENV["XDG_CONFIG_HOME"] != nil) ?
11
+ File.expand_path("#{ENV["XDG_CONFIG_HOME"]}/workEnv/"):
12
+ File.expand_path("~/.config/workEnv/")
13
+
14
+ # Per host cache directory within the global config directory
15
+ WORK_ENV_CACHE_DIR = WORK_ENV_GLOBAL_DIR + "/" + WORK_ENV_HOSTNAME
16
+
17
+ # Global flag to enable verbosity
18
+ VERBOSE = (ENV["VERBOSE"].to_s != "") ? true: false
19
+
20
+ end