bashly 0.7.7 → 0.7.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bashly/cli.rb +2 -1
  3. data/lib/bashly/commands/add.rb +41 -33
  4. data/lib/bashly/commands/generate.rb +13 -2
  5. data/lib/bashly/concerns/renderable.rb +8 -1
  6. data/lib/bashly/config_validator.rb +19 -6
  7. data/lib/bashly/extensions/string.rb +6 -0
  8. data/lib/bashly/extensions/yaml.rb +2 -0
  9. data/lib/bashly/libraries.yml +13 -9
  10. data/lib/bashly/script/argument.rb +13 -1
  11. data/lib/bashly/script/base.rb +7 -29
  12. data/lib/bashly/script/catch_all.rb +4 -0
  13. data/lib/bashly/script/command.rb +24 -6
  14. data/lib/bashly/script/environment_variable.rb +6 -0
  15. data/lib/bashly/script/flag.rb +9 -0
  16. data/lib/bashly/script/wrapper.rb +11 -7
  17. data/lib/bashly/settings.rb +34 -5
  18. data/lib/bashly/templates/settings.yml +24 -0
  19. data/lib/bashly/version.rb +1 -1
  20. data/lib/bashly/views/argument/usage.erb +2 -2
  21. data/lib/bashly/views/argument/validations.erb +1 -1
  22. data/lib/bashly/views/command/catch_all_filter.erb +1 -1
  23. data/lib/bashly/views/command/command_fallback.erb +1 -1
  24. data/lib/bashly/views/command/command_filter.erb +1 -1
  25. data/lib/bashly/views/command/command_functions.erb +1 -1
  26. data/lib/bashly/views/command/default_assignments.erb +1 -1
  27. data/lib/bashly/views/command/dependencies_filter.erb +1 -1
  28. data/lib/bashly/views/command/environment_variables_filter.erb +1 -1
  29. data/lib/bashly/views/command/fixed_flags_filter.erb +3 -1
  30. data/lib/bashly/views/command/footer.erb +1 -0
  31. data/lib/bashly/views/command/function.erb +1 -1
  32. data/lib/bashly/views/command/initialize.erb +1 -1
  33. data/lib/bashly/views/command/inspect_args.erb +1 -1
  34. data/lib/bashly/views/command/master_script.erb +1 -0
  35. data/lib/bashly/views/command/normalize_input.erb +1 -1
  36. data/lib/bashly/views/command/parse_requirements.erb +1 -1
  37. data/lib/bashly/views/command/parse_requirements_case.erb +5 -23
  38. data/lib/bashly/views/command/parse_requirements_case_catch_all.erb +18 -0
  39. data/lib/bashly/views/command/parse_requirements_case_repeatable.erb +18 -0
  40. data/lib/bashly/views/command/parse_requirements_case_simple.erb +18 -0
  41. data/lib/bashly/views/command/parse_requirements_while.erb +1 -1
  42. data/lib/bashly/views/command/required_args_filter.erb +1 -1
  43. data/lib/bashly/views/command/required_flags_filter.erb +1 -1
  44. data/lib/bashly/views/command/root_command.erb +1 -1
  45. data/lib/bashly/views/command/run.erb +1 -1
  46. data/lib/bashly/views/command/usage.erb +1 -1
  47. data/lib/bashly/views/command/usage_args.erb +1 -1
  48. data/lib/bashly/views/command/usage_commands.erb +1 -1
  49. data/lib/bashly/views/command/usage_environment_variables.erb +1 -1
  50. data/lib/bashly/views/command/usage_examples.erb +1 -1
  51. data/lib/bashly/views/command/usage_fixed_flags.erb +1 -1
  52. data/lib/bashly/views/command/usage_flags.erb +1 -1
  53. data/lib/bashly/views/command/user_filter.erb +1 -1
  54. data/lib/bashly/views/command/user_lib.erb +2 -2
  55. data/lib/bashly/views/command/version_command.erb +1 -1
  56. data/lib/bashly/views/command/whitelist_filter.erb +11 -1
  57. data/lib/bashly/views/environment_variable/usage.erb +1 -1
  58. data/lib/bashly/views/flag/case.erb +2 -28
  59. data/lib/bashly/views/flag/case_arg.erb +19 -0
  60. data/lib/bashly/views/flag/case_no_arg.erb +8 -0
  61. data/lib/bashly/views/flag/conflicts.erb +1 -1
  62. data/lib/bashly/views/flag/usage.erb +1 -1
  63. data/lib/bashly/views/flag/validations.erb +1 -1
  64. data/lib/bashly/views/wrapper/bash3_bouncer.erb +1 -1
  65. data/lib/bashly/views/wrapper/wrapper.erb +1 -1
  66. metadata +9 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 187dd845a74d432185c8c53460836cf856251384c750cc4c0e4f2d2d894946c9
4
- data.tar.gz: 1b4f519c55758ca113be95a0149ca8050481e0a57c64ba5a01ce1596d0aa1035
3
+ metadata.gz: 471a0ae8e462a056c196ebf128f02f3f30ff3a100c10aff2d2c010757d6d942e
4
+ data.tar.gz: d78cf8473f40b241ee412a00d819be124a244acda1f487f20e105dee1cba9842
5
5
  SHA512:
6
- metadata.gz: 92a1416625eeb9d8f8e3d3b5c7b1cfaadf0b43b5d7e329af5f89173f82eb1f5d9bda10fa33d486d5e86262a8858b36ff5a8e794271d10ec57b8191f0ed96dd06
7
- data.tar.gz: f52974cfdabc844b12ba046b155fe77dd598f5dcbfd3d7dbbf6e7b7dd0103cefb006e08cfebc0ecb93a811748e523afe18a57883ce6e71bdb080a7ec0cf138ed
6
+ metadata.gz: 77617bc6668796730c67d33621d497a8a454a6208d7f42d7c28ecb26e940da2e4edee6b3aefd8fc98b301d8480a13f14c1f6b292ff7f4d29b6030e601cdc1b08
7
+ data.tar.gz: d7b851826c9dde19201f2fb48b5f4b5ff0cd9a2421f4770ed3fdbc1c340943b9d0c3b3703b26646e46dfb012dca0c2a10728c568786ac4c381ab1431dc191695
data/lib/bashly/cli.rb CHANGED
@@ -6,7 +6,8 @@ module Bashly
6
6
  class CLI
7
7
  def self.runner
8
8
  runner = MisterBin::Runner.new version: Bashly::VERSION,
9
- header: "Bashly - Bash CLI Generator"
9
+ header: "Bashly - Bash CLI Generator",
10
+ footer: "Help: !txtpur!bashly COMMAND --help!txtrst!\nDocs: !undblu!https://bashly.dannyb.co"
10
11
 
11
12
  runner.route 'init', to: Commands::Init
12
13
  runner.route 'preview', to: Commands::Preview
@@ -3,14 +3,15 @@ module Bashly
3
3
  class Add < Base
4
4
  help "Add extra features and customization to your script"
5
5
 
6
- usage "bashly add strings [--force]"
7
- usage "bashly add lib [--force]"
8
- usage "bashly add config [--force]"
9
6
  usage "bashly add colors [--force]"
10
- usage "bashly add yaml [--force]"
11
- usage "bashly add validations [--force]"
12
- usage "bashly add test [--force]"
13
7
  usage "bashly add comp FORMAT [OUTPUT --force]"
8
+ usage "bashly add config [--force]"
9
+ usage "bashly add lib [--force]"
10
+ usage "bashly add settings [--force]"
11
+ usage "bashly add strings [--force]"
12
+ usage "bashly add test [--force]"
13
+ usage "bashly add validations [--force]"
14
+ usage "bashly add yaml [--force]"
14
15
  usage "bashly add (-h|--help)"
15
16
 
16
17
  option "-f --force", "Overwrite existing files"
@@ -18,14 +19,15 @@ module Bashly
18
19
  param "FORMAT", "Output format, can be one of:\n function : generate a function file to be included in your script.\n script : generate a standalone bash completions script.\n yaml : generate a yaml compatible with completely."
19
20
  param "OUTPUT", "For the 'comp function' command: Name of the generated function.\nFor the 'comp script' or 'comp yaml' commands: path to output file.\nIn all cases, this is optional and will have sensible defaults."
20
21
 
21
- command "strings", "Copy an additional configuration file to your project, allowing you to customize all the tips and error strings."
22
- command "lib", "Create the additional lib directory for additional user scripts. All *.sh scripts in this folder will be included in the final bash script."
23
- command "config", "Add standard functions for handling INI files to the lib directory."
24
22
  command "colors", "Add standard functions for printing colorful and formatted text to the lib directory."
25
- command "yaml", "Add standard functions for reading YAML files to the lib directory."
26
- command "validations", "Add argument validation functions to the lib directory."
27
- command "test", "Add approval testing."
28
23
  command "comp", "Generate a bash completions script or function."
24
+ command "config", "Add standard functions for handling INI files to the lib directory."
25
+ command "lib", "Create the additional lib directory for additional user scripts. All *.sh scripts in this folder will be included in the final bash script."
26
+ command "settings", "Copy a sample settings.yml file to your project, allowing you to customize some bashly options."
27
+ command "strings", "Copy an additional configuration file to your project, allowing you to customize all the tips and error strings."
28
+ command "test", "Add approval testing."
29
+ command "validations", "Add argument validation functions to the lib directory."
30
+ command "yaml", "Add standard functions for reading YAML files to the lib directory."
29
31
 
30
32
  example "bashly add strings --force"
31
33
  example "bashly add comp function"
@@ -34,45 +36,51 @@ module Bashly
34
36
  environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
35
37
  environment "BASHLY_LIB_DIR", "The path to use for creating the library files, relative to the source dir [default: lib]"
36
38
 
37
- def strings_command
38
- add_lib 'strings'
39
+ attr_reader :skip_src_check
40
+
41
+ def colors_command
42
+ add_lib 'colors'
39
43
  end
40
44
 
41
- def lib_command
42
- add_lib 'lib'
45
+ def comp_command
46
+ format = args['FORMAT']
47
+ output = args['OUTPUT']
48
+
49
+ case format
50
+ when "script" then add_lib 'completions_script', output
51
+ when "function" then add_lib 'completions', output
52
+ when "yaml" then add_lib 'completions_yaml', output
53
+ else raise Error, "Unrecognized format: #{format}"
54
+ end
43
55
  end
44
56
 
45
57
  def config_command
46
58
  add_lib 'config'
47
59
  end
48
60
 
49
- def colors_command
50
- add_lib 'colors'
61
+ def settings_command
62
+ @skip_src_check = true
63
+ add_lib 'settings'
51
64
  end
52
65
 
53
- def yaml_command
54
- add_lib 'yaml'
66
+ def strings_command
67
+ add_lib 'strings'
55
68
  end
56
69
 
57
- def validations_command
58
- add_lib 'validations'
70
+ def lib_command
71
+ add_lib 'lib'
59
72
  end
60
73
 
61
74
  def test_command
62
75
  add_lib 'test'
63
76
  end
64
77
 
65
- def comp_command
66
- format = args['FORMAT']
67
- output = args['OUTPUT']
68
-
69
- case format
70
- when "script" then add_lib 'completions_script', output
71
- when "function" then add_lib 'completions', output
72
- when "yaml" then add_lib 'completions_yaml', output
73
- else raise Error, "Unrecognized format: #{format}"
74
- end
78
+ def yaml_command
79
+ add_lib 'yaml'
80
+ end
75
81
 
82
+ def validations_command
83
+ add_lib 'validations'
76
84
  end
77
85
 
78
86
  private
@@ -89,7 +97,7 @@ module Bashly
89
97
  end
90
98
 
91
99
  def safe_write(path, content)
92
- if !Dir.exist? Settings.source_dir
100
+ if !skip_src_check and !Dir.exist? Settings.source_dir
93
101
  raise InitError, "Directory !txtgrn!#{Settings.source_dir}!txtrst! does not exist\nRun !txtpur!bashly init!txtrst! first"
94
102
  end
95
103
 
@@ -3,24 +3,35 @@ module Bashly
3
3
  class Generate < Base
4
4
  help "Generate the bash script and required files"
5
5
 
6
- usage "bashly generate [--force --quiet --upgrade --wrap FUNCTION]"
6
+ usage "bashly generate [options]"
7
7
  usage "bashly generate (-h|--help)"
8
8
 
9
9
  option "-f --force", "Overwrite existing files"
10
10
  option "-q --quiet", "Disable on-screen progress report"
11
11
  option "-u --upgrade", "Upgrade all added library functions"
12
12
  option "-w --wrap FUNCTION", "Wrap the entire script in a function so it can also be sourced"
13
+ option "-e --env ENV", "Force the generation environment (see BASHLY_ENV)"
13
14
 
14
15
  environment "BASHLY_SOURCE_DIR", "The path containing the bashly configuration and source files [default: src]"
15
16
  environment "BASHLY_TARGET_DIR", "The path to use for creating the bash script [default: .]"
16
17
  environment "BASHLY_LIB_DIR", "The path to use for upgrading library files, relative to the source dir [default: lib]"
17
18
  environment "BASHLY_STRICT", "When not empty, enable bash strict mode (set -euo pipefail)"
19
+ environment "BASHLY_TAB_INDENT", "When not empty, the generated script will use tab indentation instead of spaces (every 2 leading spaces will be converted to a tab character)"
20
+ environment "BASHLY_ENV", <<~EOF
21
+ Set to 'production' or 'development':
22
+ - production generate a smaller script, without file markers
23
+ - development generate with file markers
24
+
25
+ Can be overridden with --env [default: development]
26
+ EOF
18
27
 
19
28
  example "bashly generate --force"
20
29
  example "bashly generate --wrap my_function"
21
30
 
22
31
  def run
23
32
  validate_config
33
+ Settings.env = args['--env'] if args['--env']
34
+ quiet_say "creating !txtgrn!production!txtrst! version" if Settings.production?
24
35
  create_user_files
25
36
  upgrade_libs if args['--upgrade']
26
37
  create_master_script
@@ -104,7 +115,7 @@ module Bashly
104
115
  end
105
116
 
106
117
  def create_master_script
107
- File.write master_script_path, script.code
118
+ File.write master_script_path, script.code(tab_indent: Settings.tab_indent)
108
119
  FileUtils.chmod "+x", master_script_path
109
120
  quiet_say "!txtgrn!created!txtrst! #{master_script_path}"
110
121
  end
@@ -4,13 +4,20 @@ module Bashly
4
4
  module Renderable
5
5
  def render(view)
6
6
  template = File.read view_path(view)
7
- ERB.new(template, trim_mode: '%-').result(binding)
7
+ erb = ERB.new(template, trim_mode: '%-')
8
+ erb.filename = "#{views_subfolder}.#{view}"
9
+ erb.result binding
8
10
  end
9
11
 
10
12
  def strings
11
13
  @strings ||= MessageStrings.new
12
14
  end
13
15
 
16
+ def view_marker(id = nil)
17
+ id ||= ":#{caller_locations.first.path}"
18
+ "# #{id}" unless Settings.production?
19
+ end
20
+
14
21
  private
15
22
 
16
23
  def view_path(view)
@@ -42,8 +42,13 @@ module Bashly
42
42
  end
43
43
  end
44
44
 
45
- def assert_hash(key, value)
45
+ def assert_hash(key, value, whitelist = nil)
46
46
  assert value.is_a?(Hash), "#{key} must be a hash"
47
+
48
+ if whitelist
49
+ invalid_keys = value.keys.map(&:to_sym) - whitelist
50
+ assert invalid_keys.empty?, "#{key} contains invalid options: #{invalid_keys.join(', ')}"
51
+ end
47
52
  end
48
53
 
49
54
  def assert_version(key, value)
@@ -61,6 +66,7 @@ module Bashly
61
66
  end
62
67
 
63
68
  def assert_catch_all_hash(key, value)
69
+ assert_hash key, value, Script::CatchAll.option_keys
64
70
  assert_string "#{key}.label", value['label']
65
71
  assert_optional_string "#{key}.help", value['help']
66
72
  assert_boolean "#{key}.required", value['required']
@@ -73,7 +79,7 @@ module Bashly
73
79
  end
74
80
 
75
81
  def assert_arg(key, value)
76
- assert_hash key, value
82
+ assert_hash key, value, Script::Argument.option_keys
77
83
  assert_string "#{key}.name", value['name']
78
84
  assert_optional_string "#{key}.help", value['help']
79
85
  assert_optional_string "#{key}.default", value['default']
@@ -89,7 +95,7 @@ module Bashly
89
95
  end
90
96
 
91
97
  def assert_flag(key, value)
92
- assert_hash key, value
98
+ assert_hash key, value, Script::Flag.option_keys
93
99
  assert value['short'] || value['long'], "#{key} must have at least one of long or short name"
94
100
 
95
101
  assert_optional_string "#{key}.long", value['long']
@@ -99,6 +105,7 @@ module Bashly
99
105
  assert_optional_string "#{key}.default", value['default']
100
106
  assert_optional_string "#{key}.validate", value['validate']
101
107
 
108
+ assert_boolean "#{key}.repeatable", value['repeatable']
102
109
  assert_boolean "#{key}.required", value['required']
103
110
  assert_array "#{key}.allowed", value['allowed'], of: :string
104
111
  assert_array "#{key}.conflicts", value['conflicts'], of: :string
@@ -119,7 +126,7 @@ module Bashly
119
126
  end
120
127
 
121
128
  def assert_env_var(key, value)
122
- assert_hash key, value
129
+ assert_hash key, value, Script::EnvironmentVariable.option_keys
123
130
  assert_string "#{key}.name", value['name']
124
131
  assert_optional_string "#{key}.help", value['help']
125
132
  assert_optional_string "#{key}.default", value['default']
@@ -127,11 +134,11 @@ module Bashly
127
134
  end
128
135
 
129
136
  def assert_command(key, value)
130
- assert_hash key, value
137
+ assert_hash key, value, Script::Command.option_keys
131
138
 
132
139
  refute value['commands'] && value['args'], "#{key} cannot have both commands and args"
133
140
  refute value['commands'] && value['flags'], "#{key} cannot have both commands and flags"
134
-
141
+
135
142
  assert_string "#{key}.name", value['name']
136
143
  assert_optional_string "#{key}.short", value['short']
137
144
  assert_optional_string "#{key}.help", value['help']
@@ -139,6 +146,7 @@ module Bashly
139
146
  assert_optional_string "#{key}.group", value['group']
140
147
  assert_optional_string "#{key}.filename", value['filename']
141
148
 
149
+ assert_boolean "#{key}.private", value['private']
142
150
  assert_boolean "#{key}.default", value['default']
143
151
  assert_version "#{key}.version", value['version']
144
152
  assert_catch_all "#{key}.catch_all", value['catch_all']
@@ -153,6 +161,11 @@ module Bashly
153
161
  assert_array "#{key}.environment_variables", value['environment_variables'], of: :env_var
154
162
  assert_array "#{key}.examples", value['examples'], of: :string
155
163
 
164
+ if value['catch_all'] and value['args']
165
+ repeatable_arg = value['args'].select { |a| a['repeatable'] }.first&.dig 'name'
166
+ refute repeatable_arg, "#{key}.catch_all makes no sense with repeatable arg (#{repeatable_arg})"
167
+ end
168
+
156
169
  if key == "root"
157
170
  refute value['short'], "#{key}.short makes no sense"
158
171
  refute value['group'], "#{key}.group makes no sense"
@@ -31,4 +31,10 @@ class String
31
31
  split(/^---\s*/).last
32
32
  end
33
33
 
34
+ def expand_tabs(tabstop = 2)
35
+ gsub(/^( {#{tabstop}}+)/) do
36
+ "\t" * ($1.size / tabstop)
37
+ end
38
+ end
39
+
34
40
  end
@@ -3,6 +3,8 @@ module YAML
3
3
  def self.properly_load_file(path)
4
4
  YAML.load_file path, aliases: true
5
5
  rescue ArgumentError
6
+ # :nocov:
6
7
  YAML.load_file path
8
+ # :nocov:
7
9
  end
8
10
  end
@@ -3,21 +3,25 @@ colors:
3
3
  - source: "templates/lib/colors.sh"
4
4
  target: "%{user_lib_dir}/colors.sh"
5
5
 
6
+ completions: :CompletionsFunction
7
+ completions_script: :CompletionsScript
8
+ completions_yaml: :CompletionsYAML
9
+
6
10
  config:
7
11
  files:
8
12
  - source: "templates/lib/config.sh"
9
13
  target: "%{user_lib_dir}/config.sh"
10
14
 
11
- yaml:
12
- files:
13
- - source: "templates/lib/yaml.sh"
14
- target: "%{user_lib_dir}/yaml.sh"
15
-
16
15
  lib:
17
16
  files:
18
17
  - source: "templates/lib/sample_function.sh"
19
18
  target: "%{user_lib_dir}/sample_function.sh"
20
19
 
20
+ settings:
21
+ files:
22
+ - source: "templates/settings.yml"
23
+ target: "settings.yml"
24
+
21
25
  strings:
22
26
  files:
23
27
  - source: "templates/strings.yml"
@@ -37,7 +41,6 @@ test:
37
41
 
38
42
  Docs: !undblu!https://github.com/DannyBen/approvals.bash
39
43
 
40
-
41
44
  validations:
42
45
  files:
43
46
  - source: "templates/lib/validations/validate_dir_exists.sh"
@@ -49,6 +52,7 @@ validations:
49
52
  - source: "templates/lib/validations/validate_not_empty.sh"
50
53
  target: "%{user_lib_dir}/validations/validate_not_empty.sh"
51
54
 
52
- completions: :CompletionsFunction
53
- completions_script: :CompletionsScript
54
- completions_yaml: :CompletionsYAML
55
+ yaml:
56
+ files:
57
+ - source: "templates/lib/yaml.sh"
58
+ target: "%{user_lib_dir}/yaml.sh"
@@ -1,8 +1,20 @@
1
1
  module Bashly
2
2
  module Script
3
3
  class Argument < Base
4
+ class << self
5
+ def option_keys
6
+ @option_keys ||= %i[
7
+ allowed default help name repeatable required validate
8
+ ]
9
+ end
10
+ end
11
+
4
12
  def usage_string
5
- required ? name.upcase : "[#{name.upcase}]"
13
+ required ? label : "[#{label}]"
14
+ end
15
+
16
+ def label
17
+ repeatable ? "#{name.upcase}..." : name.upcase
6
18
  end
7
19
  end
8
20
  end
@@ -5,33 +5,11 @@ module Bashly
5
5
 
6
6
  attr_reader :options
7
7
 
8
- OPTION_KEYS = %i[
9
- allowed
10
- arg
11
- catch_all
12
- completions
13
- conflicts
14
- default
15
- dependencies
16
- description
17
- environment_variables
18
- examples
19
- extensible
20
- filters
21
- flags
22
- footer
23
- group
24
- help
25
- long
26
- name
27
- parent_name
28
- private
29
- repeatable
30
- required
31
- short
32
- validate
33
- version
34
- ]
8
+ class << self
9
+ def option_keys
10
+ @option_keys ||= []
11
+ end
12
+ end
35
13
 
36
14
  def initialize(options)
37
15
  raise Error, "Invalid options provided" unless options.respond_to? :keys
@@ -39,7 +17,7 @@ module Bashly
39
17
  end
40
18
 
41
19
  def optional
42
- !required
20
+ !options['required']
43
21
  end
44
22
 
45
23
  def summary
@@ -56,7 +34,7 @@ module Bashly
56
34
  end
57
35
 
58
36
  def respond_to_missing?(method_name, include_private = false)
59
- OPTION_KEYS.include?(method_name) || super
37
+ self.class.option_keys.include?(method_name) || super
60
38
  end
61
39
  end
62
40
  end
@@ -2,6 +2,10 @@ module Bashly
2
2
  module Script
3
3
  class CatchAll
4
4
  class << self
5
+ def option_keys
6
+ @option_keys ||= %i[label help required]
7
+ end
8
+
5
9
  def from_config(config)
6
10
  options = case config
7
11
  when nil
@@ -4,6 +4,18 @@ module Bashly
4
4
  include Completions
5
5
  include CommandScopes
6
6
 
7
+ class << self
8
+ def option_keys
9
+ @option_keys ||= %i[
10
+ args catch_all commands completions
11
+ default dependencies environment_variables examples
12
+ extensible filename filters flags
13
+ footer group help name
14
+ private short version
15
+ ]
16
+ end
17
+ end
18
+
7
19
  # Returns the name to be used as an action.
8
20
  # - If it is the root command, the action is "root"
9
21
  # - Else, it is all the parents, except the first one (root) joined
@@ -28,7 +40,7 @@ module Bashly
28
40
 
29
41
  # Returns a string suitable to be a headline
30
42
  def caption_string
31
- help ? "#{full_name} - #{summary}" : full_name
43
+ help.empty? ? full_name : "#{full_name} - #{summary}"
32
44
  end
33
45
 
34
46
  def catch_all
@@ -82,15 +94,16 @@ module Bashly
82
94
  # If the file is not found, returns a string with a hint.
83
95
  def load_user_file(file, placeholder: true)
84
96
  path = "#{Settings.source_dir}/#{file}"
85
- default_content = placeholder ? "echo \"error: cannot load file\"" : ''
86
97
 
87
98
  content = if File.exist? path
88
99
  File.read(path).remove_front_matter
89
- else
90
- default_content
100
+ elsif placeholder
101
+ %q[echo "error: cannot load file"]
102
+ else
103
+ ''
91
104
  end
92
105
 
93
- "# :#{path}\n#{content}"
106
+ Settings.production? ? content : "#{view_marker path}\n#{content}"
94
107
  end
95
108
 
96
109
  # Returns an array of all parents. For example, the command
@@ -99,7 +112,12 @@ module Bashly
99
112
  options['parents'] || []
100
113
  end
101
114
 
102
- # Returns trus if this is the root command (no parents)
115
+ # Returns true if one of the args is repeatable
116
+ def repeatable_arg_exist?
117
+ args.select(&:repeatable).any?
118
+ end
119
+
120
+ # Returns true if this is the root command (no parents)
103
121
  def root_command?
104
122
  parents.empty?
105
123
  end
@@ -1,6 +1,12 @@
1
1
  module Bashly
2
2
  module Script
3
3
  class EnvironmentVariable < Base
4
+ class << self
5
+ def option_keys
6
+ @option_keys ||= %i[default help name required]
7
+ end
8
+ end
9
+
4
10
  def usage_string(extended: false)
5
11
  result = [name.upcase]
6
12
  result << strings[:required] if required and extended
@@ -1,6 +1,15 @@
1
1
  module Bashly
2
2
  module Script
3
3
  class Flag < Base
4
+ class << self
5
+ def option_keys
6
+ @option_keys ||= %i[
7
+ allowed arg conflicts default help long repeatable required
8
+ short validate
9
+ ]
10
+ end
11
+ end
12
+
4
13
  def aliases
5
14
  if long and short
6
15
  [long, short]
@@ -9,18 +9,22 @@ module Bashly
9
9
  @command, @function_name = command, function_name
10
10
  end
11
11
 
12
- def code
13
- if function_name
14
- result = [header, render('wrapper')].join "\n"
12
+ def code(tab_indent: false)
13
+ tab_indent ? base_code.expand_tabs : base_code
14
+ end
15
+
16
+ private
17
+
18
+ def base_code
19
+ result = if function_name
20
+ [header, render('wrapper')]
15
21
  else
16
- result = [header, body].join "\n"
22
+ [header, body]
17
23
  end
18
24
 
19
- result.lint
25
+ result.join("\n").lint
20
26
  end
21
27
 
22
- private
23
-
24
28
  def header
25
29
  @header ||= header!
26
30
  end
@@ -1,27 +1,56 @@
1
1
  module Bashly
2
2
  class Settings
3
3
  class << self
4
- attr_writer :source_dir, :target_dir, :lib_dir, :strict
4
+ attr_writer :source_dir, :target_dir, :lib_dir, :strict, :tab_indent
5
5
 
6
6
  def source_dir
7
- @source_dir ||= ENV['BASHLY_SOURCE_DIR'] || 'src'
7
+ @source_dir ||= get :source_dir, 'src'
8
8
  end
9
9
 
10
10
  def target_dir
11
- @target_dir ||= ENV['BASHLY_TARGET_DIR'] || '.'
11
+ @target_dir ||= get :target_dir, '.'
12
12
  end
13
13
 
14
14
  def lib_dir
15
- @lib_dir ||= ENV['BASHLY_LIB_DIR'] || 'lib'
15
+ @lib_dir ||= get :lib_dir, 'lib'
16
16
  end
17
17
 
18
18
  def strict
19
- @strict ||= ENV['BASHLY_STRICT']
19
+ @strict ||= get :strict
20
+ end
21
+
22
+ def tab_indent
23
+ @tab_indent ||= get :tab_indent
24
+ end
25
+
26
+ def env
27
+ @env ||= get(:env, :development)&.to_sym
28
+ end
29
+
30
+ def env=(value)
31
+ @env = value&.to_sym
32
+ end
33
+
34
+ def production?
35
+ env == :production
20
36
  end
21
37
 
22
38
  def full_lib_dir
23
39
  "#{source_dir}/#{lib_dir}"
24
40
  end
41
+
42
+ private
43
+
44
+ def get(key, default = nil)
45
+ ENV["BASHLY_#{key.upcase}"] || user_settings[key.to_s] || default
46
+ end
47
+
48
+ def user_settings
49
+ @user_settings ||= begin
50
+ File.exist?('settings.yml') ? Config.new('settings.yml') : {}
51
+ end
52
+ end
53
+
25
54
  end
26
55
  end
27
56
  end
@@ -0,0 +1,24 @@
1
+ # All settings are optional (with their default values provided below), and
2
+ # can also be set with an environment variable with the same name, capitalized
3
+ # and prefixed by `BASHLY_` - for example: BASHLY_SOURCE_DIR
4
+
5
+ # The path containing the bashly configuration and source files
6
+ source_dir: src
7
+
8
+ # The path to use for creating the bash script
9
+ target_dir: .
10
+
11
+ # The path to use for upgrading library files, relative to the source dir
12
+ lib_dir: lib
13
+
14
+ # When true, enable bash strict mode (set -euo pipefail)
15
+ strict: false
16
+
17
+ # When true, the generated script will use tab indentation instead of spaces
18
+ # (every 2 leading spaces will be converted to a tab character)
19
+ tab_indent: false
20
+
21
+ # Set to 'production' or 'development':
22
+ # - production generate a smaller script, without file markers
23
+ # - development generate with file markers
24
+ env: development
@@ -1,3 +1,3 @@
1
1
  module Bashly
2
- VERSION = "0.7.7"
2
+ VERSION = "0.7.10"
3
3
  end
@@ -1,5 +1,5 @@
1
- # :argument.usage
2
- echo " <%= name.upcase %>"
1
+ <%= view_marker %>
2
+ echo " <%= label %>"
3
3
  printf "<%= help.wrap(76).indent(4).sanitize_for_print %>\n"
4
4
  % if allowed
5
5
  printf " <%= strings[:allowed] % { values: allowed.join(', ') } -%>\n"
@@ -1,4 +1,4 @@
1
- # :argument.validations
1
+ <%= view_marker %>
2
2
  % if validate
3
3
  if [[ -n $(validate_<%= validate %> "$1") ]]; then
4
4
  printf "<%= strings[:validation_error] %>\n" "<%= name.upcase %>" "$(validate_<%= validate %> "$1")"
@@ -1,4 +1,4 @@
1
- # :command.catch_all_filter
1
+ <%= view_marker %>
2
2
  % if catch_all.required?
3
3
  if [[ ${#other_args[@]} -eq 0 ]]; then
4
4
  printf "<%= strings[:missing_required_argument] % { arg: catch_all.label, usage: usage_string } %>\n"
@@ -1,4 +1,4 @@
1
- # :command.command_fallback
1
+ <%= view_marker %>
2
2
  % if default_command
3
3
  "" )
4
4
  <%= function_name %>_usage
@@ -1,4 +1,4 @@
1
- # :command.command_filter
1
+ <%= view_marker %>
2
2
  % if commands.any?
3
3
  action=${1:-}
4
4
 
@@ -1,4 +1,4 @@
1
- # :command.command_functions
1
+ <%= view_marker %>
2
2
  % deep_commands.each do |command|
3
3
  <%= command.render :function unless command.commands.any? %>
4
4
  % end
@@ -1,4 +1,4 @@
1
- # :command.default_assignments
1
+ <%= view_marker %>
2
2
  % default_args.each do |arg|
3
3
  [[ -n ${args[<%= arg.name %>]:-} ]] || args[<%= arg.name %>]="<%= arg.default %>"
4
4
  % end
@@ -1,4 +1,4 @@
1
- # :command.dependencies_filter
1
+ <%= view_marker %>
2
2
  % if dependencies
3
3
  % dependencies.each do |dependency|
4
4
  if ! [[ -x "$(command -v <%= dependency %>)" ]]; then
@@ -1,4 +1,4 @@
1
- # :command.environment_variables_filter
1
+ <%= view_marker %>
2
2
  % if default_environment_variables.any?
3
3
  % default_environment_variables.each do |env_var|
4
4
  export <%= env_var.name.upcase %>="${<%= env_var.name.upcase %>:-<%= env_var.default %>}"
@@ -1,5 +1,6 @@
1
- # :command.fixed_flag_filter
1
+ <%= view_marker %>
2
2
  case "${1:-}" in
3
+ % if root_command?
3
4
  % if short_flag_exist? "-v"
4
5
  --version )
5
6
  % else
@@ -9,6 +10,7 @@ case "${1:-}" in
9
10
  exit
10
11
  ;;
11
12
 
13
+ % end
12
14
  % if short_flag_exist? "-h"
13
15
  --help )
14
16
  % else
@@ -1,2 +1,3 @@
1
+ <%= view_marker %>
1
2
  printf "<%= footer.gsub("\n", '\n').gsub('"', '\"') %>\n"
2
3
  echo
@@ -1,4 +1,4 @@
1
- # :command.function
1
+ <%= view_marker %>
2
2
  <%= function_name %>_command() {
3
3
  <%= load_user_file(filename).indent 2 %>
4
4
  }
@@ -1,4 +1,4 @@
1
- # :command.initialize
1
+ <%= view_marker %>
2
2
  initialize() {
3
3
  version="<%= version %>"
4
4
  long_usage=''
@@ -1,4 +1,4 @@
1
- # :command.inspect_args
1
+ <%= view_marker %>
2
2
  inspect_args() {
3
3
  readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort)
4
4
  if (( ${#args[@]} )); then
@@ -1,3 +1,4 @@
1
+ <%= view_marker %>
1
2
  <%= render :root_command if commands.empty? %>
2
3
  <%= render :version_command %>
3
4
  <%= render :usage %>
@@ -1,4 +1,4 @@
1
- # :command.normalize_input
1
+ <%= view_marker %>
2
2
  normalize_input() {
3
3
  local arg flags
4
4
 
@@ -1,4 +1,4 @@
1
- # :command.parse_requirements
1
+ <%= view_marker %>
2
2
  % if root_command?
3
3
  parse_requirements() {
4
4
  % else
@@ -1,26 +1,8 @@
1
- # :command.parse_requirements_case
2
- % if args.any?
3
- % condition = "if"
4
- % args.each do |arg|
5
- <%= condition %> [[ -z ${args[<%= arg.name %>]+x} ]]; then
6
- <%= arg.render(:validations).indent 2 %>
7
- args[<%= arg.name %>]=$1
8
- shift
9
- % condition = "elif"
10
- % end
11
- else
1
+ <%= view_marker %>
12
2
  % if catch_all.enabled?
13
- other_args+=("$1")
14
- shift
15
- % else
16
- printf "<%= strings[:invalid_argument] %>\n" "$key"
17
- exit 1
18
- % end
19
- fi
20
- % elsif catch_all.enabled?
21
- other_args+=("$1")
22
- shift
3
+ <%= render(:parse_requirements_case_catch_all) %>
4
+ % elsif repeatable_arg_exist?
5
+ <%= render(:parse_requirements_case_repeatable) %>
23
6
  % else
24
- printf "<%= strings[:invalid_argument] %>\n" "$key"
25
- exit 1
7
+ <%= render(:parse_requirements_case_simple) %>
26
8
  % end
@@ -0,0 +1,18 @@
1
+ <%= view_marker %>
2
+ % if args.any?
3
+ % condition = "if"
4
+ % args.each do |arg|
5
+ <%= condition %> [[ -z ${args[<%= arg.name %>]+x} ]]; then
6
+ <%= arg.render(:validations).indent 2 %>
7
+ args[<%= arg.name %>]=$1
8
+ shift
9
+ % condition = "elif"
10
+ % end
11
+ else
12
+ other_args+=("$1")
13
+ shift
14
+ fi
15
+ % else
16
+ other_args+=("$1")
17
+ shift
18
+ % end
@@ -0,0 +1,18 @@
1
+ <%= view_marker %>
2
+ % condition = "if"
3
+ % args.each do |arg|
4
+ <%= condition %> [[ -z ${args[<%= arg.name %>]+x} ]]; then
5
+ <%= arg.render(:validations).indent 2 %>
6
+ % if arg.repeatable
7
+ args[<%= arg.name %>]="\"$1\""
8
+ shift
9
+ else
10
+ args[<%= arg.name %>]="${args[<%= arg.name %>]} \"$1\""
11
+ shift
12
+ % else
13
+ args[<%= arg.name %>]=$1
14
+ shift
15
+ % end
16
+ % condition = "elif"
17
+ % end
18
+ fi
@@ -0,0 +1,18 @@
1
+ <%= view_marker %>
2
+ % if args.any?
3
+ % condition = "if"
4
+ % args.each do |arg|
5
+ <%= condition %> [[ -z ${args[<%= arg.name %>]+x} ]]; then
6
+ <%= arg.render(:validations).indent 2 %>
7
+ args[<%= arg.name %>]=$1
8
+ shift
9
+ % condition = "elif"
10
+ % end
11
+ else
12
+ printf "<%= strings[:invalid_argument] %>\n" "$key"
13
+ exit 1
14
+ fi
15
+ % else
16
+ printf "<%= strings[:invalid_argument] %>\n" "$key"
17
+ exit 1
18
+ % end
@@ -1,4 +1,4 @@
1
- # :command.parse_requirements_while
1
+ <%= view_marker %>
2
2
  while [[ $# -gt 0 ]]; do
3
3
  key="$1"
4
4
  case "$key" in
@@ -1,4 +1,4 @@
1
- # :command.required_args_filter
1
+ <%= view_marker %>
2
2
  % required_args.each do |arg|
3
3
  if [[ -z ${args[<%= arg.name %>]+x} ]]; then
4
4
  printf "<%= strings[:missing_required_argument] % { arg: arg.name.upcase, usage: usage_string } %>\n"
@@ -1,4 +1,4 @@
1
- # :command.required_flags_filter
1
+ <%= view_marker %>
2
2
  % required_flags.each do |flag|
3
3
  if [[ -z ${args[<%= flag.long %>]+x} ]]; then
4
4
  printf "<%= strings[:missing_required_flag] % { usage: flag.usage_string } %>\n"
@@ -1,4 +1,4 @@
1
- # :command.root_command
1
+ <%= view_marker %>
2
2
  root_command() {
3
3
  <%= load_user_file(filename).indent 2 %>
4
4
  }
@@ -1,4 +1,4 @@
1
- # :command.run
1
+ <%= view_marker %>
2
2
  run() {
3
3
  declare -A args=()
4
4
  declare -a other_args=()
@@ -1,4 +1,4 @@
1
- # :command.usage
1
+ <%= view_marker %>
2
2
  <%= function_name %>_usage() {
3
3
  if [[ -n $long_usage ]]; then
4
4
  <%- if summary == help -%>
@@ -1,4 +1,4 @@
1
- # :command.usage_args
1
+ <%= view_marker %>
2
2
  printf "<%= strings[:arguments] %>\n"
3
3
  % if args.any?
4
4
 
@@ -1,4 +1,4 @@
1
- # :command.usage_commands
1
+ <%= view_marker %>
2
2
  % unless commands.first.group
3
3
  printf "<%= strings[:commands] %>\n"
4
4
  % end
@@ -1,4 +1,4 @@
1
- # :command.usage_environment_variables
1
+ <%= view_marker %>
2
2
  printf "<%= strings[:environment_variables] %>\n"
3
3
 
4
4
  % environment_variables.each do |env_var|
@@ -1,4 +1,4 @@
1
- # :command.usage_examples
1
+ <%= view_marker %>
2
2
  printf "<%= strings[:examples] %>\n"
3
3
 
4
4
  % examples.each do |example|
@@ -1,4 +1,4 @@
1
- # :command.usage_fixed_flags
1
+ <%= view_marker %>
2
2
  % if short_flag_exist? "-h"
3
3
  echo " --help"
4
4
  % else
@@ -1,4 +1,4 @@
1
- # :command.usage_flags
1
+ <%= view_marker %>
2
2
  % flags.each do |flag|
3
3
  <%= flag.render(:usage) %>
4
4
  % end
@@ -1,4 +1,4 @@
1
- # :command.user_filter
1
+ <%= view_marker %>
2
2
  % if filters
3
3
  % filters.each do |filter|
4
4
  filter_error=$(filter_<%= filter %>)
@@ -1,6 +1,6 @@
1
- # :command.user_lib
1
+ <%= view_marker %>
2
2
  % user_lib.each do |file|
3
- # <%= ":#{file}" %>
3
+ <%= view_marker file %>
4
4
  <%= File.read file %>
5
5
 
6
6
  % end
@@ -1,4 +1,4 @@
1
- # :command.version_command
1
+ <%= view_marker %>
2
2
  version_command() {
3
3
  echo "$version"
4
4
  }
@@ -1,10 +1,20 @@
1
- # :command.whitelist_filter
1
+ <%= view_marker %>
2
2
  % whitelisted_args.each do |arg|
3
+ % if arg.repeatable
4
+ eval "input_array=(${args[<%= arg.name %>]})"
5
+ for i in "${input_array[@]}"; do
6
+ if [[ ! $i =~ ^(<%= arg.allowed.join '|' %>)$ ]]; then
7
+ printf "%s\n" "<%= strings[:disallowed_argument] % { name: arg.name, allowed: arg.allowed.join(', ') } %>"
8
+ exit 1
9
+ fi
10
+ done
11
+ % else
3
12
  if [[ ! ${args[<%= arg.name %>]} =~ ^(<%= arg.allowed.join '|' %>)$ ]]; then
4
13
  printf "%s\n" "<%= strings[:disallowed_argument] % { name: arg.name, allowed: arg.allowed.join(', ') } %>"
5
14
  exit 1
6
15
  fi
7
16
  % end
17
+ % end
8
18
  % whitelisted_flags.each do |flag|
9
19
  % if flag.repeatable
10
20
  eval "input_array=(${args[<%= flag.name %>]})"
@@ -1,4 +1,4 @@
1
- # :environment_variable.usage
1
+ <%= view_marker %>
2
2
  echo " <%= usage_string extended: true %>"
3
3
  printf "<%= help.wrap(76).indent(4).sanitize_for_print %>\n"
4
4
  % if default
@@ -1,30 +1,4 @@
1
- # :flag.case
1
+ <%= view_marker %>
2
2
  <%= aliases.join " | " %> )
3
3
  <%= render(:conflicts).indent 2 %>
4
- % if arg
5
- if [[ -n ${2+x} ]]; then
6
- <%= render(:validations).indent 4 %>
7
- % if repeatable
8
- if [[ -z ${args[<%= name %>]+x} ]]; then
9
- args[<%= name %>]="\"$2\""
10
- else
11
- args[<%= name %>]="${args[<%= name %>]} \"$2\""
12
- fi
13
- % else
14
- args[<%= name %>]="$2"
15
- % end
16
- shift
17
- shift
18
- else
19
- printf "%s\n" "<%= strings[:flag_requires_an_argument] % { name: name, usage: usage_string } %>"
20
- exit 1
21
- fi
22
- % else
23
- % if repeatable
24
- (( args[<%= name %>]+=1 ))
25
- % else
26
- args[<%= name %>]=1
27
- % end
28
- shift
29
- % end
30
- ;;
4
+ <%= render(arg ? :case_arg : :case_no_arg).indent 2 %>
@@ -0,0 +1,19 @@
1
+ <%= view_marker %>
2
+ if [[ -n ${2+x} ]]; then
3
+ <%= render(:validations).indent 2 %>
4
+ % if repeatable
5
+ if [[ -z ${args[<%= name %>]+x} ]]; then
6
+ args[<%= name %>]="\"$2\""
7
+ else
8
+ args[<%= name %>]="${args[<%= name %>]} \"$2\""
9
+ fi
10
+ % else
11
+ args[<%= name %>]="$2"
12
+ % end
13
+ shift
14
+ shift
15
+ else
16
+ printf "%s\n" "<%= strings[:flag_requires_an_argument] % { name: name, usage: usage_string } %>"
17
+ exit 1
18
+ fi
19
+ ;;
@@ -0,0 +1,8 @@
1
+ <%= view_marker %>
2
+ % if repeatable
3
+ (( args[<%= name %>]+=1 ))
4
+ % else
5
+ args[<%= name %>]=1
6
+ % end
7
+ shift
8
+ ;;
@@ -1,4 +1,4 @@
1
- # :flag.conflicts
1
+ <%= view_marker %>
2
2
  % if conflicts
3
3
  % if conflicts.count == 1
4
4
  if [[ -n "${args[<%= conflicts.first %>]:-}" ]]; then
@@ -1,4 +1,4 @@
1
- # :flag.usage
1
+ <%= view_marker %>
2
2
  echo " <%= usage_string extended: true %>"
3
3
  printf "<%= help.wrap(76).indent(4).sanitize_for_print %>\n"
4
4
  % if allowed
@@ -1,4 +1,4 @@
1
- # :flag.validations
1
+ <%= view_marker %>
2
2
  % if validate
3
3
  if [[ -n $(validate_<%= validate %> "$2") ]]; then
4
4
  printf "<%= strings[:validation_error] %>\n" "<%= usage_string %>" "$(validate_<%= validate %> "$2")"
@@ -1,4 +1,4 @@
1
- # :script.bash3_bouncer
1
+ <%= view_marker %>
2
2
  if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then
3
3
  printf "<%= strings[:unsupported_bash_version] -%>\n"
4
4
  exit 1
@@ -1,4 +1,4 @@
1
- # :script.wrapper
1
+ <%= view_marker %>
2
2
  <%= function_name %>() {
3
3
  <%= body.indent 2 %>
4
4
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bashly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.7
4
+ version: 0.7.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Ben Shitrit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-02-20 00:00:00.000000000 Z
11
+ date: 2022-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colsole
@@ -121,6 +121,7 @@ files:
121
121
  - lib/bashly/templates/lib/validations/validate_not_empty.sh
122
122
  - lib/bashly/templates/lib/yaml.sh
123
123
  - lib/bashly/templates/minimal.yml
124
+ - lib/bashly/templates/settings.yml
124
125
  - lib/bashly/templates/strings.yml
125
126
  - lib/bashly/templates/test/approvals.bash
126
127
  - lib/bashly/version.rb
@@ -145,6 +146,9 @@ files:
145
146
  - lib/bashly/views/command/normalize_input.erb
146
147
  - lib/bashly/views/command/parse_requirements.erb
147
148
  - lib/bashly/views/command/parse_requirements_case.erb
149
+ - lib/bashly/views/command/parse_requirements_case_catch_all.erb
150
+ - lib/bashly/views/command/parse_requirements_case_repeatable.erb
151
+ - lib/bashly/views/command/parse_requirements_case_simple.erb
148
152
  - lib/bashly/views/command/parse_requirements_while.erb
149
153
  - lib/bashly/views/command/required_args_filter.erb
150
154
  - lib/bashly/views/command/required_flags_filter.erb
@@ -163,6 +167,8 @@ files:
163
167
  - lib/bashly/views/command/whitelist_filter.erb
164
168
  - lib/bashly/views/environment_variable/usage.erb
165
169
  - lib/bashly/views/flag/case.erb
170
+ - lib/bashly/views/flag/case_arg.erb
171
+ - lib/bashly/views/flag/case_no_arg.erb
166
172
  - lib/bashly/views/flag/conflicts.erb
167
173
  - lib/bashly/views/flag/usage.erb
168
174
  - lib/bashly/views/flag/validations.erb
@@ -192,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
198
  - !ruby/object:Gem::Version
193
199
  version: '0'
194
200
  requirements: []
195
- rubygems_version: 3.2.15
201
+ rubygems_version: 3.3.3
196
202
  signing_key:
197
203
  specification_version: 4
198
204
  summary: Bash Command Line Tool Generator