bashly 0.6.9 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bashly/cli.rb +1 -0
  3. data/lib/bashly/commands/add.rb +29 -88
  4. data/lib/bashly/commands/generate.rb +51 -10
  5. data/lib/bashly/commands/init.rb +1 -1
  6. data/lib/bashly/commands/preview.rb +2 -2
  7. data/lib/bashly/commands/validate.rb +19 -0
  8. data/lib/bashly/concerns/asset_helper.rb +4 -0
  9. data/lib/bashly/concerns/command_scopes.rb +68 -0
  10. data/lib/bashly/config_validator.rb +142 -0
  11. data/lib/bashly/extensions/file.rb +13 -0
  12. data/lib/bashly/extensions/string.rb +5 -1
  13. data/lib/bashly/libraries/base.rb +19 -0
  14. data/lib/bashly/libraries/completions.rb +14 -0
  15. data/lib/bashly/libraries/completions_function.rb +38 -0
  16. data/lib/bashly/libraries/completions_script.rb +29 -0
  17. data/lib/bashly/libraries/completions_yaml.rb +27 -0
  18. data/lib/bashly/libraries.yml +39 -0
  19. data/lib/bashly/library.rb +63 -0
  20. data/lib/bashly/refinements/compose_refinements.rb +45 -0
  21. data/lib/bashly/{models → script}/argument.rb +1 -1
  22. data/lib/bashly/{models → script}/base.rb +5 -4
  23. data/lib/bashly/script/catch_all.rb +49 -0
  24. data/lib/bashly/{models → script}/command.rb +10 -112
  25. data/lib/bashly/{models → script}/environment_variable.rb +1 -1
  26. data/lib/bashly/{models → script}/flag.rb +1 -1
  27. data/lib/bashly/{models/script.rb → script/wrapper.rb} +2 -2
  28. data/lib/bashly/templates/lib/colors.sh +12 -15
  29. data/lib/bashly/templates/lib/config.sh +34 -35
  30. data/lib/bashly/templates/lib/sample_function.sh +10 -10
  31. data/lib/bashly/templates/lib/validations/validate_dir_exists.sh +1 -0
  32. data/lib/bashly/templates/lib/validations/validate_file_exists.sh +1 -0
  33. data/lib/bashly/templates/lib/validations/validate_integer.sh +1 -0
  34. data/lib/bashly/templates/lib/validations/validate_not_empty.sh +1 -0
  35. data/lib/bashly/templates/lib/yaml.sh +12 -15
  36. data/lib/bashly/version.rb +1 -1
  37. data/lib/bashly/views/command/catch_all_filter.erb +2 -2
  38. data/lib/bashly/views/command/command_filter.erb +1 -1
  39. data/lib/bashly/views/command/default_assignments.erb +2 -2
  40. data/lib/bashly/views/command/default_initialize_script.erb +6 -6
  41. data/lib/bashly/views/command/default_root_script.erb +1 -1
  42. data/lib/bashly/views/command/environment_variables_filter.erb +1 -1
  43. data/lib/bashly/views/command/fixed_flags_filter.erb +1 -1
  44. data/lib/bashly/views/command/initialize.erb +1 -1
  45. data/lib/bashly/views/command/parse_requirements.erb +1 -1
  46. data/lib/bashly/views/command/parse_requirements_case.erb +2 -2
  47. data/lib/bashly/views/command/parse_requirements_while.erb +2 -2
  48. data/lib/bashly/views/command/required_args_filter.erb +1 -6
  49. data/lib/bashly/views/command/required_flags_filter.erb +1 -4
  50. data/lib/bashly/views/command/root_command.erb +1 -1
  51. data/lib/bashly/views/command/run.erb +4 -4
  52. data/lib/bashly/views/command/usage.erb +1 -1
  53. data/lib/bashly/views/command/usage_args.erb +3 -3
  54. data/lib/bashly/views/command/usage_commands.erb +1 -1
  55. data/lib/bashly/views/{script → wrapper}/bash3_bouncer.erb +0 -0
  56. data/lib/bashly/views/{script → wrapper}/header.erb +0 -0
  57. data/lib/bashly/views/{script → wrapper}/wrapper.erb +0 -0
  58. data/lib/bashly.rb +3 -1
  59. metadata +27 -14
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c76c3000789b54fa2d9be8f7a61d93556e6a8665f4ea7769588797a25fa78f49
4
- data.tar.gz: a5a0eae36b00cf879082836211f2aa21b38b280993e2dccfc907f3e6fcb99104
3
+ metadata.gz: a68036595993a9c9661c283bcb30d0b89941d15930cba9b821e2500d53087178
4
+ data.tar.gz: d8d50d5c8f91f95a46a94116d3f7ffe5a66f7c2a994e87524036f91292b707f5
5
5
  SHA512:
6
- metadata.gz: ae77cd9b8fa000d9e12f2948fb5c41bd02e34bd6bdcdd91f3a3a627778f66c9472e956e92fb65b502fa78fe2e77ba6c4dfb12c1f35abed36f1012ce502b7cab9
7
- data.tar.gz: 5e14659441abf6b11ca53a5d0d656e5097519acc7a2e842f3cdfb6374fd52a0403c6ddeacf4f193da1ab18687de909b074e4d581ce4700c9fc28e2b589ef961d
6
+ metadata.gz: d7ad12eef82213a5ed61673d205dafb5626028516efe0768986dc64cf527ae346e0fa868019cb7c23b8366a277edc7f9414149ece6f2ab8178f5f0b20beedb77
7
+ data.tar.gz: 3db2a86f70fb882bc63f615f25dc2e70aa818f1c5b9af9c13ce1d4d1d2c681dd1e7c5919c93422ccf1d0d440f018efefc3e8901e4af2b43a092fd652d503fee8
data/lib/bashly/cli.rb CHANGED
@@ -10,6 +10,7 @@ module Bashly
10
10
 
11
11
  runner.route 'init', to: Commands::Init
12
12
  runner.route 'preview', to: Commands::Preview
13
+ runner.route 'validate', to: Commands::Validate
13
14
  runner.route 'generate', to: Commands::Generate
14
15
  runner.route 'add', to: Commands::Add
15
16
 
@@ -9,7 +9,7 @@ module Bashly
9
9
  usage "bashly add colors [--force]"
10
10
  usage "bashly add yaml [--force]"
11
11
  usage "bashly add validations [--force]"
12
- usage "bashly add comp FORMAT [OUTPUT]"
12
+ usage "bashly add comp FORMAT [OUTPUT --force]"
13
13
  usage "bashly add (-h|--help)"
14
14
 
15
15
  option "-f --force", "Overwrite existing files"
@@ -32,129 +32,70 @@ module Bashly
32
32
  environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
33
33
 
34
34
  def strings_command
35
- safe_copy asset("templates/strings.yml"), "#{Settings.source_dir}/bashly-strings.yml"
35
+ add_lib 'strings'
36
36
  end
37
37
 
38
38
  def lib_command
39
- safe_copy_file "sample_function.sh"
39
+ add_lib 'lib'
40
40
  end
41
41
 
42
42
  def config_command
43
- safe_copy_file "config.sh"
43
+ add_lib 'config'
44
44
  end
45
45
 
46
46
  def colors_command
47
- safe_copy_file "colors.sh"
47
+ add_lib 'colors'
48
48
  end
49
49
 
50
50
  def yaml_command
51
- safe_copy_file "yaml.sh"
51
+ add_lib 'yaml'
52
52
  end
53
53
 
54
54
  def validations_command
55
- safe_copy_dir "validations"
55
+ add_lib 'validations'
56
56
  end
57
57
 
58
58
  def comp_command
59
59
  format = args['FORMAT']
60
60
  output = args['OUTPUT']
61
-
61
+
62
62
  case format
63
- when "function"
64
- save_comp_function output
65
- when "yaml"
66
- save_comp_yaml output
67
- when "script"
68
- save_comp_script output
69
- else
70
- raise Error, "Unrecognized format: #{format}"
63
+ when "script" then add_lib 'completions_script', output
64
+ when "function" then add_lib 'completions', output
65
+ when "yaml" then add_lib 'completions_yaml', output
66
+ else raise Error, "Unrecognized format: #{format}"
71
67
  end
72
68
 
73
69
  end
74
70
 
75
71
  private
76
72
 
77
- def safe_copy_dir(dir)
78
- Dir[asset("templates/lib/#{dir}/*.sh")].sort.each do |file|
79
- safe_copy_file "#{dir}/#{File.basename file}"
73
+ def add_lib(name, *args)
74
+ library = Bashly::Library.new name, *args
75
+ files_created = 0
76
+ library.files.each do |file|
77
+ created = safe_write file[:path], file[:content]
78
+ files_created += 1 if created
80
79
  end
80
+ message = library.post_install_message
81
+ say "\n#{message}" if message and files_created > 0
81
82
  end
82
83
 
83
- def safe_copy_file(file)
84
- safe_copy asset("templates/lib/#{file}"), "#{Settings.source_dir}/lib/#{file}"
85
- end
86
-
87
- def safe_copy(source, target)
84
+ def safe_write(path, content)
88
85
  if !Dir.exist? Settings.source_dir
89
86
  raise InitError, "Directory !txtgrn!#{Settings.source_dir}!txtrst! does not exist\nRun !txtpur!bashly init!txtrst! first"
90
87
  end
91
88
 
92
- if File.exist? target and !args['--force']
93
- say "skipped !txtgrn!#{target}!txtrst! (exists)"
89
+ if File.exist? path and !args['--force']
90
+ say "!txtblu!skipped!txtrst! #{path} (exists)"
91
+ false
92
+
94
93
  else
95
- deep_copy source, target
96
- say "created !txtgrn!#{target}"
97
- end
98
- end
99
-
100
- def deep_copy(source, target)
101
- target_dir = File.dirname target
102
- FileUtils.mkdir_p target_dir unless Dir.exist? target_dir
103
- FileUtils.cp source, target
104
- end
105
-
106
- def config
107
- @config ||= Config.new "#{Settings.source_dir}/bashly.yml"
108
- end
109
-
110
- def command
111
- @command ||= Models::Command.new config
112
- end
113
-
114
- def completions
115
- @completions ||= command.completion_data
116
- end
117
-
118
- def completions_script
119
- @completions_script ||= command.completion_script
120
- end
121
-
122
- def completions_function
123
- @completions_function ||= command.completion_function
124
- end
125
-
126
- def save_comp_yaml(filename = nil)
127
- filename ||= "#{Settings.target_dir}/completions.yml"
128
- File.write filename, completions.to_yaml
129
- say "created !txtgrn!#{filename}"
130
- say ""
131
- say "This file can be converted to a completions script using the !txtgrn!completely!txtrst! gem."
132
- end
133
-
134
- def save_comp_script(filename = nil)
135
- filename ||= "#{Settings.target_dir}/completions.bash"
136
- File.write filename, completions_script
137
- say "created !txtgrn!#{filename}"
138
- say ""
139
- say "In order to enable completions, run:"
140
- say ""
141
- say " !txtpur!$ source #{filename}"
142
- end
143
-
144
- def save_comp_function(name = nil)
145
- name ||= "send_completions"
146
- target_dir = "#{Settings.source_dir}/lib"
147
- filename = "#{target_dir}/#{name}.sh"
94
+ File.deep_write path, content
95
+ say "!txtgrn!created!txtrst! #{path}"
96
+ true
148
97
 
149
- FileUtils.mkdir_p target_dir unless Dir.exist? target_dir
150
- File.write filename, completions_function
151
-
152
- say "created !txtgrn!#{filename}"
153
- say ""
154
- say "In order to use it in your script, create a command or a flag (for example: !txtgrn!#{command.name} completions!txtrst! or !txtgrn!#{command.name} --completions!txtrst!) that calls the !txtgrn!#{name}!txtrst! function."
155
- say "Your users can then run something like this to enable completions:"
156
- say ""
157
- say " !txtpur!$ eval \"$(#{command.name} completions)\""
98
+ end
158
99
  end
159
100
 
160
101
  end
@@ -1,23 +1,28 @@
1
1
  module Bashly
2
2
  module Commands
3
3
  class Generate < Base
4
+ using ComposeRefinements
5
+
4
6
  help "Generate the bash script and required files"
5
7
 
6
- usage "bashly generate [--force --quiet --wrap FUNCTION]"
8
+ usage "bashly generate [--force --quiet --upgrade --wrap FUNCTION]"
7
9
  usage "bashly generate (-h|--help)"
8
10
 
9
11
  option "-f --force", "Overwrite existing files"
10
- option "-w --wrap FUNCTION", "Wrap the entire script in a function so it can also be sourced"
11
12
  option "-q --quiet", "Disable on-screen progress report"
13
+ option "-u --upgrade", "Upgrade all added library functions"
14
+ option "-w --wrap FUNCTION", "Wrap the entire script in a function so it can also be sourced"
12
15
 
13
16
  environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
14
17
  environment "BASHLY_TARGET_DIR", "The path to use for creating the bash script [default: .]"
18
+ environment "BASHLY_STRICT", "When not empty, enable bash strict mode (set -euo pipefail)"
15
19
 
16
20
  example "bashly generate --force"
17
21
  example "bashly generate --wrap my_function"
18
22
 
19
23
  def run
20
24
  create_user_files
25
+ upgrade_libs if args['--upgrade']
21
26
  create_master_script
22
27
  quiet_say "run !txtpur!#{master_script_path} --help!txtrst! to test your bash script"
23
28
  end
@@ -28,6 +33,42 @@ module Bashly
28
33
  say message unless args['--quiet']
29
34
  end
30
35
 
36
+ def upgrade_libs
37
+ generated_files.each do |file|
38
+ content = File.read file
39
+
40
+ if content =~ /\[@bashly-upgrade (.+)\]/
41
+ args = $1.split ' '
42
+ library_name = args.shift
43
+ upgrade file, library_name, *args
44
+ end
45
+ end
46
+ end
47
+
48
+ def generated_files
49
+ Dir["#{Settings.source_dir}/**/*.*"].sort
50
+ end
51
+
52
+ def upgrade(existing_file, library_name, *args)
53
+ if Library.exist? library_name
54
+ upgrade! existing_file, library_name, *args
55
+ else
56
+ quiet_say "!txtred!warning!txtrst! not upgrading !txtcyn!#{existing_file}!txtrst!, unknown library '#{library_name}'"
57
+ end
58
+ end
59
+
60
+ def upgrade!(existing_file, library_name, *args)
61
+ library = Bashly::Library.new library_name, *args
62
+ file = library.find_file existing_file
63
+
64
+ if file
65
+ File.deep_write file[:path], file[:content]
66
+ quiet_say "!txtcyn!updated!txtrst! #{file[:path]}"
67
+ else
68
+ quiet_say "!txtred!warning!txtrst! not upgrading !txtcyn!#{existing_file}!txtrst!, path mismatch"
69
+ end
70
+ end
71
+
31
72
  def create_user_files
32
73
  quiet_say "creating user files in !txtgrn!#{Settings.source_dir}"
33
74
 
@@ -41,7 +82,7 @@ module Bashly
41
82
  end
42
83
 
43
84
  def create_root_command_file
44
- create_file "#{Settings.source_dir}/root_command.sh", command.render(:default_root_script)
85
+ create_file "#{Settings.source_dir}/#{command.filename}", command.render(:default_root_script)
45
86
  end
46
87
 
47
88
  def create_all_command_files
@@ -55,21 +96,21 @@ module Bashly
55
96
 
56
97
  def create_file(file, content)
57
98
  if File.exist? file and !args['--force']
58
- quiet_say "skipped !txtgrn!#{file}!txtrst! (exists)"
99
+ quiet_say "!txtblu!skipped!txtrst! #{file} (exists)"
59
100
  else
60
- File.write file, content
61
- quiet_say "created !txtgrn!#{file}"
101
+ File.deep_write file, content
102
+ quiet_say "!txtgrn!created!txtrst! #{file}"
62
103
  end
63
104
  end
64
105
 
65
106
  def create_master_script
66
107
  File.write master_script_path, script.code
67
108
  FileUtils.chmod "+x", master_script_path
68
- quiet_say "created !txtgrn!#{master_script_path}"
109
+ quiet_say "!txtgrn!created!txtrst! #{master_script_path}"
69
110
  end
70
111
 
71
112
  def script
72
- @script ||= Models::Script.new(command, args['--wrap'])
113
+ @script ||= Script::Wrapper.new(command, args['--wrap'])
73
114
  end
74
115
 
75
116
  def master_script_path
@@ -77,11 +118,11 @@ module Bashly
77
118
  end
78
119
 
79
120
  def config
80
- @config ||= Config.new "#{Settings.source_dir}/bashly.yml"
121
+ @config ||= Config.new("#{Settings.source_dir}/bashly.yml").compose
81
122
  end
82
123
 
83
124
  def command
84
- @command ||= Models::Command.new config
125
+ @command ||= Script::Command.new config
85
126
  end
86
127
 
87
128
  end
@@ -17,7 +17,7 @@ module Bashly
17
17
  end
18
18
  Dir.mkdir target_dir unless Dir.exist? target_dir
19
19
  File.write "#{target_dir}/bashly.yml", yaml_content
20
- say "created !txtgrn!#{target_dir}/bashly.yml"
20
+ say "!txtgrn!created!txtrst! #{target_dir}/bashly.yml"
21
21
  say "run !txtpur!bashly generate!txtrst! to create the bash script"
22
22
  end
23
23
 
@@ -10,8 +10,8 @@ module Bashly
10
10
 
11
11
  def run
12
12
  config = Config.new "#{Settings.source_dir}/bashly.yml"
13
- command = Models::Command.new(config)
14
- script = Models::Script.new command
13
+ command = Script::Command.new(config)
14
+ script = Script::Wrapper.new command
15
15
  puts script.code
16
16
  end
17
17
  end
@@ -0,0 +1,19 @@
1
+ module Bashly
2
+ module Commands
3
+ class Validate < Base
4
+ help "Scan the configuration file for errors"
5
+
6
+ usage "bashly validate"
7
+ usage "bashly validate (-h|--help)"
8
+
9
+ environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
10
+
11
+ def run
12
+ config = Config.new "#{Settings.source_dir}/bashly.yml"
13
+ validator = ConfigValidator.new config
14
+ validator.validate
15
+ say "!txtgrn!OK"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -3,5 +3,9 @@ module Bashly
3
3
  def asset(path)
4
4
  File.expand_path "../#{path}", __dir__
5
5
  end
6
+
7
+ def asset_content(path)
8
+ File.read asset(path)
9
+ end
6
10
  end
7
11
  end
@@ -0,0 +1,68 @@
1
+ module Bashly
2
+ # This is a `Command` concern responsible for providing additional scopes.
3
+ module CommandScopes
4
+ # Returns only the names of the Commands
5
+ def command_names
6
+ commands.map &:name
7
+ end
8
+
9
+ # Returns a flat array containing all the commands in this tree.
10
+ # This includes self + children + grandchildres + ...
11
+ def deep_commands
12
+ result = []
13
+ commands.each do |command|
14
+ result << command
15
+ if command.commands.any?
16
+ result += command.deep_commands
17
+ end
18
+ end
19
+ result
20
+ end
21
+
22
+ # If any of this command's subcommands has the default option set to
23
+ # true, this default command will be returned, nil otherwise.
24
+ def default_command
25
+ commands.find { |c| c.default }
26
+ end
27
+
28
+ # Returns an array of all the default Args
29
+ def default_args
30
+ args.select &:default
31
+ end
32
+
33
+ # Returns an array of all the default Environment Variables
34
+ def default_environment_variables
35
+ environment_variables.select &:default
36
+ end
37
+
38
+ # Returns an array of all the default Flags
39
+ def default_flags
40
+ flags.select &:default
41
+ end
42
+
43
+ # Returns an array of all the required Arguments
44
+ def required_args
45
+ args.select &:required
46
+ end
47
+
48
+ # Returns an array of all the required EnvironmentVariables
49
+ def required_environment_variables
50
+ environment_variables.select &:required
51
+ end
52
+
53
+ # Returns an array of all the required Flags
54
+ def required_flags
55
+ flags.select &:required
56
+ end
57
+
58
+ # Returns an array of all the args with a whitelist
59
+ def whitelisted_args
60
+ args.select &:allowed
61
+ end
62
+
63
+ # Returns an array of all the flags with a whitelist arg
64
+ def whitelisted_flags
65
+ flags.select &:allowed
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,142 @@
1
+ module Bashly
2
+ class ConfigValidator
3
+ attr_reader :data
4
+
5
+ def initialize(data)
6
+ @data = data
7
+ end
8
+
9
+ def validate
10
+ assert_command "root", data
11
+ end
12
+
13
+ private
14
+
15
+ def assert(valid, message)
16
+ raise ConfigurationError, message unless valid
17
+ end
18
+
19
+ def refute(invalid, message)
20
+ assert !invalid, message
21
+ end
22
+
23
+ def assert_string(key, value)
24
+ assert value.is_a?(String), "#{key} must be a string"
25
+ end
26
+
27
+ def assert_optional_string(key, value)
28
+ assert_string key, value if value
29
+ end
30
+
31
+ def assert_boolean(key, value)
32
+ assert [true, false, nil].include?(value), "#{key} must be a boolean"
33
+ end
34
+
35
+ def assert_array(key, value, of: nil)
36
+ return unless value
37
+ assert value.is_a?(Array), "#{key} must be an array"
38
+ if of
39
+ value.each_with_index do |val, i|
40
+ send "assert_#{of}".to_sym, "#{key}[#{i}]", val
41
+ end
42
+ end
43
+ end
44
+
45
+ def assert_hash(key, value)
46
+ assert value.is_a?(Hash), "#{key} must be a hash"
47
+ end
48
+
49
+ def assert_version(key, value)
50
+ return unless value
51
+ assert [String, Integer, Float].include?(value.class),
52
+ "#{key} must be a string or a number"
53
+ end
54
+
55
+ def assert_catch_all(key, value)
56
+ return unless value
57
+ assert [TrueClass, String, Hash].include?(value.class),
58
+ "#{key} must be a boolean, a string or a hash"
59
+
60
+ assert_catch_all_hash key, value if value.is_a? Hash
61
+ end
62
+
63
+ def assert_catch_all_hash(key, value)
64
+ assert_string "#{key}.label", value['label']
65
+ assert_optional_string "#{key}.help", value['help']
66
+ assert_boolean "#{key}.required", value['required']
67
+ end
68
+
69
+ def assert_extensible(key, value)
70
+ return unless value
71
+ assert [TrueClass, String].include?(value.class),
72
+ "#{key} must be a boolean or a string"
73
+ end
74
+
75
+ def assert_arg(key, value)
76
+ assert_hash key, value
77
+ assert_string "#{key}.name", value['name']
78
+ assert_optional_string "#{key}.help", value['help']
79
+ assert_optional_string "#{key}.default", value['default']
80
+ assert_optional_string "#{key}.validate", value['validate']
81
+ assert_boolean "#{key}.required", value['required']
82
+
83
+ assert_array "#{key}.allowed", value['allowed'], of: :string
84
+
85
+ refute value['name'].match(/^-/), "#{key}.name must not start with '-'"
86
+ end
87
+
88
+ def assert_flag(key, value)
89
+ assert_hash key, value
90
+ assert value['short'] || value['long'], "#{key} must have at least one of long or short name"
91
+
92
+ assert_optional_string "#{key}.long", value['long']
93
+ assert_optional_string "#{key}.short", value['short']
94
+ assert_optional_string "#{key}.help", value['help']
95
+ assert_optional_string "#{key}.arg", value['arg']
96
+ assert_optional_string "#{key}.default", value['default']
97
+ assert_optional_string "#{key}.validate", value['validate']
98
+
99
+ assert_boolean "#{key}.required", value['required']
100
+ assert_array "#{key}.allowed", value['allowed'], of: :string
101
+
102
+ assert value['long'].match(/^--[a-zA-Z0-9_\-]+$/), "#{key}.long must be in the form of '--name'" if value['long']
103
+ assert value['short'].match(/^-[a-zA-Z0-9]$/), "#{key}.short must be in the form of '-n'" if value['short']
104
+ refute value['arg'].match(/^-/), "#{key}.arg must not start with '-'" if value['arg']
105
+ end
106
+
107
+ def assert_env_var(key, value)
108
+ assert_hash key, value
109
+ assert_string "#{key}.name", value['name']
110
+ assert_optional_string "#{key}.help", value['help']
111
+ assert_optional_string "#{key}.default", value['default']
112
+ assert_boolean "#{key}.required", value['required']
113
+ end
114
+
115
+ def assert_command(key, value)
116
+ assert_hash key, value
117
+
118
+ refute value['commands'] && value['args'], "#{key} cannot have both commands and args"
119
+ refute value['commands'] && value['flags'], "#{key} cannot have both commands and flags"
120
+
121
+ assert_string "#{key}.name", value['name']
122
+ assert_optional_string "#{key}.short", value['short']
123
+ assert_optional_string "#{key}.help", value['help']
124
+ assert_optional_string "#{key}.footer", value['footer']
125
+ assert_optional_string "#{key}.group", value['group']
126
+ assert_optional_string "#{key}.filename", value['filename']
127
+
128
+ assert_boolean "#{key}.default", value['default']
129
+ assert_version "#{key}.version", value['version']
130
+ assert_catch_all "#{key}.catch_all", value['catch_all']
131
+ assert_extensible "#{key}.extensible", value['extensible']
132
+
133
+ assert_array "#{key}.args", value['args'], of: :arg
134
+ assert_array "#{key}.flags", value['flags'] , of: :flag
135
+ assert_array "#{key}.commands", value['commands'], of: :command
136
+ assert_array "#{key}.completions", value['completions'], of: :string
137
+ assert_array "#{key}.dependencies", value['dependencies'], of: :string
138
+ assert_array "#{key}.environment_variables", value['environment_variables'], of: :env_var
139
+ assert_array "#{key}.examples", value['examples'], of: :string
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,13 @@
1
+ require "fileutils"
2
+
3
+ class File
4
+ def self.deep_write(file, content)
5
+ dir = File.dirname file
6
+ FileUtils.mkdir_p dir unless Dir.exist? dir
7
+ File.write file, content
8
+ end
9
+
10
+ def self.append(path, content)
11
+ File.open(path, "a") { |f| f << content }
12
+ end
13
+ end
@@ -24,7 +24,11 @@ class String
24
24
  end
25
25
 
26
26
  def lint
27
- gsub(/\s+\n/m, "\n\n")
27
+ gsub(/\s+\n/m, "\n\n").lines.reject { |l| l =~ /^\s*##/ }.join ""
28
+ end
29
+
30
+ def remove_front_matter
31
+ split(/^---\s*/).last
28
32
  end
29
33
 
30
34
  end
@@ -0,0 +1,19 @@
1
+ module Bashly
2
+ module Libraries
3
+ class Base
4
+ attr_reader :args
5
+
6
+ def initialize(*args)
7
+ @args = args
8
+ end
9
+
10
+ def files
11
+ raise NotImplementedError, "Please implement #files"
12
+ end
13
+
14
+ def post_install_message
15
+ nil
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,14 @@
1
+ module Bashly
2
+ module Libraries
3
+ class Completions < Base
4
+ protected
5
+ def command
6
+ @command ||= Script::Command.new config
7
+ end
8
+
9
+ def config
10
+ @config ||= Bashly::Config.new "#{Settings.source_dir}/bashly.yml"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ module Bashly
2
+ module Libraries
3
+ class CompletionsFunction < Completions
4
+ def files
5
+ [
6
+ {
7
+ path: "#{Settings.source_dir}/lib/#{function_name}.sh",
8
+ content: completions_function_code(function_name)
9
+ }
10
+ ]
11
+ end
12
+
13
+ def post_install_message
14
+ <<~EOF
15
+ In order to enable completions in your script, create a command or a flag (for example: !txtgrn!#{command.name} completions!txtrst! or !txtgrn!#{command.name} --completions!txtrst!) that calls the !txtgrn!#{function_name}!txtrst! function.
16
+
17
+ Your users can then run something like this to enable completions:
18
+
19
+ !txtpur!$ eval \"$(#{command.name} completions)\"
20
+ EOF
21
+ end
22
+
23
+ private
24
+
25
+ def function_name
26
+ @function_name ||= args[0] || 'send_completions'
27
+ end
28
+
29
+ def completions_function_code(function_name)
30
+ [
31
+ "## [@bashly-upgrade completions #{function_name}]",
32
+ command.completion_function(function_name)
33
+ ].join "\n"
34
+ end
35
+
36
+ end
37
+ end
38
+ end