bashly 0.6.9 → 0.7.3

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.
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