bashly 0.6.7 → 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/bashly/cli.rb +1 -0
  4. data/lib/bashly/commands/add.rb +35 -82
  5. data/lib/bashly/commands/generate.rb +55 -9
  6. data/lib/bashly/commands/init.rb +1 -1
  7. data/lib/bashly/commands/preview.rb +2 -2
  8. data/lib/bashly/commands/validate.rb +19 -0
  9. data/lib/bashly/concerns/asset_helper.rb +4 -0
  10. data/lib/bashly/concerns/completions.rb +5 -1
  11. data/lib/bashly/config_validator.rb +135 -0
  12. data/lib/bashly/extensions/file.rb +13 -0
  13. data/lib/bashly/extensions/string.rb +5 -1
  14. data/lib/bashly/libraries/base.rb +19 -0
  15. data/lib/bashly/libraries/completions.rb +14 -0
  16. data/lib/bashly/libraries/completions_function.rb +38 -0
  17. data/lib/bashly/libraries/completions_script.rb +29 -0
  18. data/lib/bashly/libraries/completions_yaml.rb +27 -0
  19. data/lib/bashly/libraries.yml +39 -0
  20. data/lib/bashly/library.rb +63 -0
  21. data/lib/bashly/refinements/compose_refinements.rb +45 -0
  22. data/lib/bashly/{models → script}/argument.rb +1 -1
  23. data/lib/bashly/{models → script}/base.rb +4 -2
  24. data/lib/bashly/{models → script}/command.rb +11 -22
  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} +21 -3
  28. data/lib/bashly/templates/lib/colors.sh +41 -31
  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 +4 -0
  32. data/lib/bashly/templates/lib/validations/validate_file_exists.sh +4 -0
  33. data/lib/bashly/templates/lib/validations/validate_integer.sh +4 -0
  34. data/lib/bashly/templates/lib/validations/validate_not_empty.sh +4 -0
  35. data/lib/bashly/templates/lib/yaml.sh +12 -15
  36. data/lib/bashly/templates/strings.yml +1 -0
  37. data/lib/bashly/version.rb +1 -1
  38. data/lib/bashly/views/argument/validations.erb +8 -0
  39. data/lib/bashly/views/command/command_filter.erb +1 -1
  40. data/lib/bashly/views/command/default_assignments.erb +2 -2
  41. data/lib/bashly/views/command/default_initialize_script.erb +6 -6
  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 -6
  45. data/lib/bashly/views/command/parse_requirements.erb +1 -1
  46. data/lib/bashly/views/command/parse_requirements_case.erb +2 -1
  47. data/lib/bashly/views/command/required_args_filter.erb +1 -5
  48. data/lib/bashly/views/command/required_flags_filter.erb +1 -4
  49. data/lib/bashly/views/command/run.erb +4 -4
  50. data/lib/bashly/views/command/usage_commands.erb +1 -1
  51. data/lib/bashly/views/flag/case.erb +2 -1
  52. data/lib/bashly/views/flag/validations.erb +8 -0
  53. data/lib/bashly/views/wrapper/bash3_bouncer.erb +5 -0
  54. data/lib/bashly/views/{script → wrapper}/header.erb +1 -0
  55. data/lib/bashly/views/{script → wrapper}/wrapper.erb +0 -0
  56. data/lib/bashly.rb +3 -1
  57. metadata +28 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c47271e43a775ff08c071428540301ed1fa0656c7d58020c9825dfbc492804ef
4
- data.tar.gz: fdc8ac40f1d18c2bc90ae4700fd0757e55132852bbdf8700a2829a02dd3be61c
3
+ metadata.gz: 53a5744ae1746cdd47de191ea91539ab7336aca894f583384ef0b170c03a746e
4
+ data.tar.gz: 0e1e9bc417d3947bcce84c3c6f44df87521dfeeae2329cc736a53816cb66cba3
5
5
  SHA512:
6
- metadata.gz: aff1e0723cdfdc10c409697cb2c715a37382dc7dbef3da3afe3b0e45fc8c91255ffeda153d24a25aa3bdcdf67ff863cfa7bec4558f79fced3d3ff33dc720b4d1
7
- data.tar.gz: 54aa504a25003ddf1331e39de9b68612d2fff39bedc93dfc09bc961254c6b4ba7beacadf90412af6d76f9159bffb48456b6d6e85e5c805864372221f2edfbe2e
6
+ metadata.gz: 72154aafdc3c3787b4d23a2935192a92c21b94b830d7fb01c22057ed839d30120d8a31a8962b7b075110551d034b2c971b92010e55c88a17d4e9041ecb309ad7
7
+ data.tar.gz: a02638101b685d5e9de575fe4537991c558c462dc030355b5e8ab6ffe4f32b8018c510afb516f51c02d4412a20c21a3703b5dc5872a0f377baa343345f045605
data/README.md CHANGED
@@ -40,7 +40,7 @@ a [docker image](https://hub.docker.com/r/dannyben/bashly).
40
40
  file for you ([example](https://github.com/DannyBen/bashly/tree/master/examples/minimal#bashlyyml)).
41
41
  2. Bashly then automatically generates a bash script (when you run
42
42
  `bashly generate`) that can parse and validate user input, provide help
43
- messages, and run your code for each command ([example](https://github.com/DannyBen/bashly/blob/master/examples/minimal/download)).
43
+ messages, and run your code for each command.
44
44
  3. Your code for each command is kept in a separate file, and can be merged
45
45
  again if you change it ([example](https://github.com/DannyBen/bashly/blob/master/examples/minimal/src/root_command.sh)).
46
46
 
@@ -67,11 +67,13 @@ Bashly is responsible for:
67
67
  ## Contributing / Support
68
68
 
69
69
  If you experience any issue, have a question or a suggestion, or if you wish
70
- to contribute, feel free to [open an issue][issues].
70
+ to contribute, feel free to [open an issue][issues] or
71
+ [start a discussion][discussions].
71
72
 
72
73
 
73
74
 
74
75
  [issues]: https://github.com/DannyBen/bashly/issues
76
+ [discussions]: https://github.com/DannyBen/bashly/discussions
75
77
  [docs]: https://bashly.dannyb.co/
76
78
  [examples]: https://github.com/DannyBen/bashly/tree/master/examples#bashly-examples
77
79
 
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
 
@@ -8,7 +8,8 @@ module Bashly
8
8
  usage "bashly add config [--force]"
9
9
  usage "bashly add colors [--force]"
10
10
  usage "bashly add yaml [--force]"
11
- usage "bashly add comp FORMAT [OUTPUT]"
11
+ usage "bashly add validations [--force]"
12
+ usage "bashly add comp FORMAT [OUTPUT --force]"
12
13
  usage "bashly add (-h|--help)"
13
14
 
14
15
  option "-f --force", "Overwrite existing files"
@@ -21,6 +22,7 @@ module Bashly
21
22
  command "config", "Add standard functions for handling INI files to the lib directory."
22
23
  command "colors", "Add standard functions for printing colorful and formatted text to the lib directory."
23
24
  command "yaml", "Add standard functions for reading YAML files to the lib directory."
25
+ command "validations", "Add argument validation functions to the lib directory."
24
26
  command "comp", "Generate a bash completions script or function."
25
27
 
26
28
  example "bashly add strings --force"
@@ -30,119 +32,70 @@ module Bashly
30
32
  environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
31
33
 
32
34
  def strings_command
33
- safe_copy asset("templates/strings.yml"), "#{Settings.source_dir}/bashly-strings.yml"
35
+ add_lib 'strings'
34
36
  end
35
37
 
36
38
  def lib_command
37
- safe_copy_lib "sample_function.sh"
39
+ add_lib 'lib'
38
40
  end
39
41
 
40
42
  def config_command
41
- safe_copy_lib "config.sh"
43
+ add_lib 'config'
42
44
  end
43
45
 
44
46
  def colors_command
45
- safe_copy_lib "colors.sh"
47
+ add_lib 'colors'
46
48
  end
47
49
 
48
50
  def yaml_command
49
- safe_copy_lib "yaml.sh"
51
+ add_lib 'yaml'
52
+ end
53
+
54
+ def validations_command
55
+ add_lib 'validations'
50
56
  end
51
57
 
52
58
  def comp_command
53
59
  format = args['FORMAT']
54
60
  output = args['OUTPUT']
55
-
61
+
56
62
  case format
57
- when "function"
58
- save_comp_function output
59
- when "yaml"
60
- save_comp_yaml output
61
- when "script"
62
- save_comp_script output
63
- else
64
- 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}"
65
67
  end
66
68
 
67
69
  end
68
70
 
69
71
  private
70
72
 
71
- def safe_copy_lib(libfile)
72
- safe_copy asset("templates/lib/#{libfile}"), "#{Settings.source_dir}/lib/#{libfile}"
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
79
+ end
80
+ message = library.post_install_message
81
+ say "\n#{message}" if message and files_created > 0
73
82
  end
74
83
 
75
- def safe_copy(source, target)
84
+ def safe_write(path, content)
76
85
  if !Dir.exist? Settings.source_dir
77
86
  raise InitError, "Directory !txtgrn!#{Settings.source_dir}!txtrst! does not exist\nRun !txtpur!bashly init!txtrst! first"
78
87
  end
79
88
 
80
- if File.exist? target and !args['--force']
81
- say "skipped !txtgrn!#{target}!txtrst! (exists)"
89
+ if File.exist? path and !args['--force']
90
+ say "!txtblu!skipped!txtrst! #{path} (exists)"
91
+ false
92
+
82
93
  else
83
- deep_copy source, target
84
- say "created !txtgrn!#{target}"
85
- end
86
- end
87
-
88
- def deep_copy(source, target)
89
- target_dir = File.dirname target
90
- FileUtils.mkdir_p target_dir unless Dir.exist? target_dir
91
- FileUtils.cp source, target
92
- end
93
-
94
- def config
95
- @config ||= Config.new "#{Settings.source_dir}/bashly.yml"
96
- end
97
-
98
- def command
99
- @command ||= Models::Command.new config
100
- end
101
-
102
- def completions
103
- @completions ||= command.completion_data
104
- end
105
-
106
- def completions_script
107
- @completions_script ||= command.completion_script
108
- end
109
-
110
- def completions_function
111
- @completions_function ||= command.completion_function
112
- end
113
-
114
- def save_comp_yaml(filename = nil)
115
- filename ||= "#{Settings.target_dir}/completions.yml"
116
- File.write filename, completions.to_yaml
117
- say "created !txtgrn!#{filename}"
118
- say ""
119
- say "This file can be converted to a completions script using the !txtgrn!completely!txtrst! gem."
120
- end
121
-
122
- def save_comp_script(filename = nil)
123
- filename ||= "#{Settings.target_dir}/completions.bash"
124
- File.write filename, completions_script
125
- say "created !txtgrn!#{filename}"
126
- say ""
127
- say "In order to enable completions, run:"
128
- say ""
129
- say " !txtpur!$ source #{filename}"
130
- end
131
-
132
- def save_comp_function(name = nil)
133
- name ||= "send_completions"
134
- target_dir = "#{Settings.source_dir}/lib"
135
- filename = "#{target_dir}/#{name}.sh"
94
+ File.deep_write path, content
95
+ say "!txtgrn!created!txtrst! #{path}"
96
+ true
136
97
 
137
- FileUtils.mkdir_p target_dir unless Dir.exist? target_dir
138
- File.write filename, completions_function
139
-
140
- say "created !txtgrn!#{filename}"
141
- say ""
142
- 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."
143
- say "Your users can then run something like this to enable completions:"
144
- say ""
145
- say " !txtpur!$ eval \"$(#{command.name} completions)\""
98
+ end
146
99
  end
147
100
 
148
101
  end
@@ -1,30 +1,76 @@
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 --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"
12
+ option "-q --quiet", "Disable on-screen progress report"
13
+ option "-u --upgrade", "Upgrade all added library functions"
10
14
  option "-w --wrap FUNCTION", "Wrap the entire script in a function so it can also be sourced"
11
15
 
12
16
  environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
13
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)"
14
19
 
15
20
  example "bashly generate --force"
16
21
  example "bashly generate --wrap my_function"
17
22
 
18
23
  def run
19
24
  create_user_files
25
+ upgrade_libs if args['--upgrade']
20
26
  create_master_script
21
- say "run !txtpur!#{master_script_path} --help!txtrst! to test your bash script"
27
+ quiet_say "run !txtpur!#{master_script_path} --help!txtrst! to test your bash script"
22
28
  end
23
29
 
24
30
  private
25
31
 
32
+ def quiet_say(message)
33
+ say message unless args['--quiet']
34
+ end
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
+
26
72
  def create_user_files
27
- say "creating user files in !txtgrn!#{Settings.source_dir}"
73
+ quiet_say "creating user files in !txtgrn!#{Settings.source_dir}"
28
74
 
29
75
  create_file "#{Settings.source_dir}/initialize.sh", command.render(:default_initialize_script)
30
76
 
@@ -50,21 +96,21 @@ module Bashly
50
96
 
51
97
  def create_file(file, content)
52
98
  if File.exist? file and !args['--force']
53
- say "skipped !txtgrn!#{file}!txtrst! (exists)"
99
+ quiet_say "!txtblu!skipped!txtrst! #{file} (exists)"
54
100
  else
55
101
  File.write file, content
56
- say "created !txtgrn!#{file}"
102
+ quiet_say "!txtgrn!created!txtrst! #{file}"
57
103
  end
58
104
  end
59
105
 
60
106
  def create_master_script
61
107
  File.write master_script_path, script.code
62
108
  FileUtils.chmod "+x", master_script_path
63
- say "created !txtgrn!#{master_script_path}"
109
+ quiet_say "!txtgrn!created!txtrst! #{master_script_path}"
64
110
  end
65
111
 
66
112
  def script
67
- @script ||= Models::Script.new(command, args['--wrap'])
113
+ @script ||= Script::Wrapper.new(command, args['--wrap'])
68
114
  end
69
115
 
70
116
  def master_script_path
@@ -72,11 +118,11 @@ module Bashly
72
118
  end
73
119
 
74
120
  def config
75
- @config ||= Config.new "#{Settings.source_dir}/bashly.yml"
121
+ @config ||= Config.new("#{Settings.source_dir}/bashly.yml").compose
76
122
  end
77
123
 
78
124
  def command
79
- @command ||= Models::Command.new config
125
+ @command ||= Script::Command.new config
80
126
  end
81
127
 
82
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
@@ -31,12 +31,16 @@ module Bashly
31
31
  flags.map(&:name) + flags.map(&:short)
32
32
  end
33
33
 
34
+ def completion_allowed_args
35
+ flags.map(&:allowed).flatten + args.map(&:allowed).flatten
36
+ end
37
+
34
38
  def completion_words(with_version: false)
35
39
  trivial_flags = %w[--help -h]
36
40
  trivial_flags += %w[--version -v] if with_version
37
41
  all = (
38
42
  command_names + trivial_flags +
39
- completion_flag_names
43
+ completion_flag_names + completion_allowed_args
40
44
  )
41
45
 
42
46
  all += completions if completions
@@ -0,0 +1,135 @@
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
+ end
85
+
86
+ def assert_flag(key, value)
87
+ assert_hash key, value
88
+ assert value['short'] || value['long'], "#{key} must have at least one of long or short name"
89
+
90
+ assert_optional_string "#{key}.long", value['long']
91
+ assert_optional_string "#{key}.short", value['short']
92
+ assert_optional_string "#{key}.help", value['help']
93
+ assert_optional_string "#{key}.arg", value['arg']
94
+ assert_optional_string "#{key}.default", value['default']
95
+ assert_optional_string "#{key}.validate", value['validate']
96
+
97
+ assert_boolean "#{key}.required", value['required']
98
+ assert_array "#{key}.allowed", value['allowed'], of: :string
99
+ end
100
+
101
+ def assert_env_var(key, value)
102
+ assert_hash key, value
103
+ assert_string "#{key}.name", value['name']
104
+ assert_optional_string "#{key}.help", value['help']
105
+ assert_optional_string "#{key}.default", value['default']
106
+ assert_boolean "#{key}.required", value['required']
107
+ end
108
+
109
+ def assert_command(key, value)
110
+ assert_hash key, value
111
+
112
+ refute value['commands'] && value['args'], "#{key} cannot have both commands and args"
113
+ refute value['commands'] && value['flags'], "#{key} cannot have both commands and flags"
114
+
115
+ assert_string "#{key}.name", value['name']
116
+ assert_optional_string "#{key}.short", value['short']
117
+ assert_optional_string "#{key}.help", value['help']
118
+ assert_optional_string "#{key}.footer", value['footer']
119
+ assert_optional_string "#{key}.group", value['group']
120
+
121
+ assert_boolean "#{key}.default", value['default']
122
+ assert_version "#{key}.version", value['version']
123
+ assert_catch_all "#{key}.catch_all", value['catch_all']
124
+ assert_extensible "#{key}.extensible", value['extensible']
125
+
126
+ assert_array "#{key}.args", value['args'], of: :arg
127
+ assert_array "#{key}.flags", value['flags'] , of: :flag
128
+ assert_array "#{key}.commands", value['commands'], of: :command
129
+ assert_array "#{key}.completions", value['completions'], of: :string
130
+ assert_array "#{key}.dependencies", value['dependencies'], of: :string
131
+ assert_array "#{key}.environment_variables", value['environment_variables'], of: :env_var
132
+ assert_array "#{key}.examples", value['examples'], of: :string
133
+ end
134
+ end
135
+ 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(/\n{2,}/, "\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
@@ -0,0 +1,29 @@
1
+ module Bashly
2
+ module Libraries
3
+ class CompletionsScript < Completions
4
+ def files
5
+ [
6
+ {
7
+ path: target_path,
8
+ content: command.completion_script
9
+ }
10
+ ]
11
+ end
12
+
13
+ def post_install_message
14
+ <<~EOF
15
+ In order to enable completions, run:
16
+
17
+ !txtpur!$ source #{target_path}
18
+ EOF
19
+ end
20
+
21
+ private
22
+
23
+ def target_path
24
+ @target_path ||= args[0] || "#{Settings.target_dir}/completions.bash"
25
+ end
26
+
27
+ end
28
+ end
29
+ end