howzit 2.1.45 → 2.1.46
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/lib/howzit/config.rb +1 -0
- data/lib/howzit/script_support.rb +29 -0
- data/lib/howzit/stringutils.rb +66 -13
- data/lib/howzit/task.rb +29 -6
- data/lib/howzit/version.rb +1 -1
- data/spec/shell_variables_spec.rb +177 -0
- metadata +3 -2
- data/nested-topics-for-howzit.md +0 -100
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: daae85713f05d780640619a8ada2291aaa69c0eb3f2d941f6fb9e2964c9247b4
|
|
4
|
+
data.tar.gz: 5773440b0a9718af5dd0b3e66929bfa74023061b99a85c61d65558b76ac57d4d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f688d2e403997a3cf6f3b91c0ebd3746ed51b1d0bc4dfea2be5fd1297a22715d2a7b5672482b2f243dc26b38b56eee2880c9e57df98c55c16178b84eddeb8da
|
|
7
|
+
data.tar.gz: 934e5ef9e7b71bc0323475ae4c54596d7a6813f552d63fd08dd358f51805c6950780d4435b9cf830aed7712392a5a38d52077dfbcceb69293adf02f9b0970794
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
### 2.1.46
|
|
2
|
+
|
|
3
|
+
2026-09-27 12:52
|
|
4
|
+
|
|
5
|
+
#### CHANGED
|
|
6
|
+
|
|
7
|
+
- **Shell run blocks** (sh, bash, zsh, dash, ksh, or no hashbang) now receive Howzit variables as environment variables instead of having ${VAR} replaced in the script text, so scripts can use their own variables, parameter expansions, and ENV without Howzit clobbering them.
|
|
8
|
+
- **Command-line arguments** after -- are passed to shell run blocks as script arguments, so $1, $#, and "$@" work like any shell script (including inside shell functions).
|
|
9
|
+
|
|
10
|
+
#### NEW
|
|
11
|
+
|
|
12
|
+
- **:shell_variables: config option** (env or substitute) to switch shell run blocks back to text substitution; defaults to env.
|
|
13
|
+
- **$${VAR} escape** passes ${VAR} through to the shell untouched in @run, @copy, and text-substituted blocks.
|
|
14
|
+
|
|
15
|
+
#### IMPROVED
|
|
16
|
+
|
|
17
|
+
- **Howzit-style defaults** (${name:default}) in shell run blocks are converted to the shell's ${name:-default} so they keep working with environment variables.
|
|
18
|
+
|
|
19
|
+
#### FIXED
|
|
20
|
+
|
|
21
|
+
- **Shell parameter expansions** like ${VAR:-default}, ${VAR:=x}, ${VAR:+x}, ${VAR:?x}, and ${VAR:0:3} are no longer mangled into Howzit defaults when VAR isn't a Howzit variable.
|
|
22
|
+
- **Positional placeholders** ($1, ${2}) without a matching argument are left intact instead of being replaced with an empty string, which broke shell functions and awk '{print $1}'.
|
|
23
|
+
- **$@ and $*** are only replaced when arguments were passed after --, instead of always being replaced with an empty string.
|
|
24
|
+
- **Default values containing colons** (e.g. ${url:http://localhost:3000}) are no longer cut off at the second colon.
|
|
25
|
+
|
|
1
26
|
### 2.1.45
|
|
2
27
|
|
|
3
28
|
2026-09-27 12:16
|
data/lib/howzit/config.rb
CHANGED
|
@@ -153,6 +153,35 @@ module Howzit
|
|
|
153
153
|
end
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
+
##
|
|
157
|
+
## Whether a run block is a POSIX-style shell script (sh, bash,
|
|
158
|
+
## zsh, dash, ksh). Blocks without a hashbang run with sh.
|
|
159
|
+
##
|
|
160
|
+
## @param script_content [String] The script content
|
|
161
|
+
##
|
|
162
|
+
## @return [Boolean]
|
|
163
|
+
##
|
|
164
|
+
def shell_script?(script_content)
|
|
165
|
+
first_line = script_content.to_s.lines.first&.strip
|
|
166
|
+
return true unless first_line&.start_with?('#!')
|
|
167
|
+
|
|
168
|
+
first_line.match?(%r{\A#!\s*(?:/usr/bin/env\s+)?(?:\S*/)?(?:sh|bash|zsh|dash|ksh)(?:\s|\z)})
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
##
|
|
172
|
+
## Howzit variables as environment variables for shell run blocks
|
|
173
|
+
##
|
|
174
|
+
## @return [Hash] name => string value
|
|
175
|
+
##
|
|
176
|
+
def variables_env
|
|
177
|
+
(Howzit.named_arguments || {}).each_with_object({}) do |(key, value), env|
|
|
178
|
+
name = key.to_s
|
|
179
|
+
next if value.nil? || !name.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
180
|
+
|
|
181
|
+
env[name] = value.to_s
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
156
185
|
##
|
|
157
186
|
## Get the injection line for a given interpreter
|
|
158
187
|
##
|
data/lib/howzit/stringutils.rb
CHANGED
|
@@ -383,29 +383,64 @@ module Howzit
|
|
|
383
383
|
str = dup
|
|
384
384
|
str.render_named_placeholders
|
|
385
385
|
str.render_numeric_placeholders
|
|
386
|
-
|
|
386
|
+
args = Howzit.arguments
|
|
387
|
+
return str if args.nil? || args.empty?
|
|
388
|
+
|
|
389
|
+
str.gsub(/(?<!\$)\$[@*]/, Shellwords.join(args))
|
|
387
390
|
end
|
|
388
391
|
|
|
392
|
+
##
|
|
393
|
+
## Replace ${NAME} and ${NAME:default} with Howzit variable
|
|
394
|
+
## values. Names Howzit doesn't know, shell parameter
|
|
395
|
+
## expansions (${VAR:-x}, ${VAR:=x}, ${VAR:0:3}, etc.), and
|
|
396
|
+
## escaped $${NAME} placeholders are left for the shell.
|
|
397
|
+
##
|
|
389
398
|
def render_named_placeholders
|
|
390
|
-
gsub!(
|
|
399
|
+
gsub!(/(?<!\$)\$\{(?<name>[A-Z0-9_]+)(?<rest>:[^}]*)?\}/i) do
|
|
391
400
|
m = Regexp.last_match
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
401
|
+
original = m[0]
|
|
402
|
+
rest = m['rest'].to_s
|
|
403
|
+
value = placeholder_value(m['name'])
|
|
404
|
+
|
|
405
|
+
if rest.empty?
|
|
406
|
+
value.nil? ? original : value
|
|
407
|
+
elsif rest.start_with?(':-')
|
|
408
|
+
value.nil? || value.empty? || rest.include?('$') ? original : value
|
|
409
|
+
elsif rest.match?(/\A:[=+?\s\d]/)
|
|
410
|
+
original
|
|
397
411
|
else
|
|
398
|
-
|
|
399
|
-
m[0]
|
|
412
|
+
value.nil? ? rest[1..].strip : value
|
|
400
413
|
end
|
|
401
414
|
end
|
|
402
415
|
end
|
|
403
416
|
|
|
404
417
|
def render_numeric_placeholders
|
|
405
|
-
gsub!(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
418
|
+
gsub!(/(?<!\$)\$(?:\{(\d+)\}|(\d+))/) do
|
|
419
|
+
m = Regexp.last_match
|
|
420
|
+
placeholder_value(m[1] || m[2]) || m[0]
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
##
|
|
425
|
+
## Remove the escape from $${NAME} placeholders, leaving
|
|
426
|
+
## ${NAME} for the shell. Applied right before execution.
|
|
427
|
+
##
|
|
428
|
+
## @return [String] unescaped string
|
|
429
|
+
##
|
|
430
|
+
def unescape_placeholders
|
|
431
|
+
gsub(/\$\$(?=\{)/, '$')
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
##
|
|
435
|
+
## Convert Howzit-style ${NAME:default} to the shell's
|
|
436
|
+
## ${NAME:-default} so defaults work when variables are
|
|
437
|
+
## passed to a shell script through the environment
|
|
438
|
+
##
|
|
439
|
+
## @return [String] converted string
|
|
440
|
+
##
|
|
441
|
+
def shellify_defaults
|
|
442
|
+
gsub(/(?<!\$)\$\{([A-Za-z_][A-Za-z0-9_]*|\d+):(?![-=+?\s\d$(])([^}]*)\}/) do
|
|
443
|
+
"${#{Regexp.last_match(1)}:-#{Regexp.last_match(2)}}"
|
|
409
444
|
end
|
|
410
445
|
end
|
|
411
446
|
|
|
@@ -560,6 +595,24 @@ module Howzit
|
|
|
560
595
|
Color.template("\n\n#{title}#{tail}{x}\n\n")
|
|
561
596
|
end
|
|
562
597
|
end
|
|
598
|
+
|
|
599
|
+
private
|
|
600
|
+
|
|
601
|
+
##
|
|
602
|
+
## Look up a placeholder name: digits are positional
|
|
603
|
+
## arguments, anything else is a named variable
|
|
604
|
+
##
|
|
605
|
+
## @return [String, nil] value, or nil if not defined
|
|
606
|
+
##
|
|
607
|
+
def placeholder_value(name)
|
|
608
|
+
if name.match?(/\A\d+\z/)
|
|
609
|
+
idx = name.to_i - 1
|
|
610
|
+
args = Howzit.arguments || []
|
|
611
|
+
return idx >= 0 && idx < args.length ? args[idx].to_s : nil
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
Howzit.named_arguments&.[](name)&.to_s
|
|
615
|
+
end
|
|
563
616
|
end
|
|
564
617
|
end
|
|
565
618
|
|
data/lib/howzit/task.rb
CHANGED
|
@@ -32,7 +32,8 @@ module Howzit
|
|
|
32
32
|
@title = attributes[:title]&.to_s
|
|
33
33
|
@parent = attributes[:parent] || nil
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
action = attributes[:action]
|
|
36
|
+
@action = shell_env_block?(action) ? action : action.render_arguments
|
|
36
37
|
@log_level = attributes[:log_level]
|
|
37
38
|
# Get source_file from parent topic if available, or from attributes
|
|
38
39
|
parent_obj = attributes[:parent]
|
|
@@ -61,15 +62,37 @@ module Howzit
|
|
|
61
62
|
@title
|
|
62
63
|
end
|
|
63
64
|
|
|
65
|
+
##
|
|
66
|
+
## Whether this is a shell run block that receives Howzit
|
|
67
|
+
## variables through the environment instead of text substitution
|
|
68
|
+
##
|
|
69
|
+
## @param content [String] The block content
|
|
70
|
+
##
|
|
71
|
+
def shell_env_block?(content = @action)
|
|
72
|
+
@type == :block &&
|
|
73
|
+
Howzit.options[:shell_variables].to_s != 'substitute' &&
|
|
74
|
+
ScriptSupport.shell_script?(content)
|
|
75
|
+
end
|
|
76
|
+
|
|
64
77
|
##
|
|
65
78
|
## Execute a block type
|
|
66
79
|
##
|
|
67
80
|
def run_block
|
|
68
81
|
Howzit.console.info "#{@prefix}{bg}Running block {bw}#{@title}{x}".c if Howzit.options[:log_level] < 2
|
|
69
82
|
block = @action
|
|
83
|
+
env_mode = shell_env_block?(block)
|
|
70
84
|
# Apply variable substitution to block content at execution time
|
|
71
85
|
# (variables from previous run blocks are now available)
|
|
72
|
-
|
|
86
|
+
if env_mode
|
|
87
|
+
block = block.shellify_defaults
|
|
88
|
+
env = ScriptSupport.variables_env
|
|
89
|
+
script_args = Howzit.arguments || []
|
|
90
|
+
else
|
|
91
|
+
block = block.render_arguments if block && !block.empty?
|
|
92
|
+
env = {}
|
|
93
|
+
script_args = []
|
|
94
|
+
end
|
|
95
|
+
block = block.unescape_placeholders
|
|
73
96
|
script = Tempfile.new('howzit_script')
|
|
74
97
|
comm_file = ScriptComm.setup
|
|
75
98
|
old_log_level = apply_log_level
|
|
@@ -117,9 +140,9 @@ module Howzit
|
|
|
117
140
|
cmd = ScriptSupport.execution_command_for(script.path, interpreter)
|
|
118
141
|
# If interpreter is nil, execute directly (will respect hashbang)
|
|
119
142
|
res = if interpreter.nil?
|
|
120
|
-
system(script.path)
|
|
143
|
+
system(env, script.path, *script_args)
|
|
121
144
|
else
|
|
122
|
-
system(cmd)
|
|
145
|
+
system(env, [cmd, *script_args.map { |arg| Shellwords.escape(arg) }].join(' '))
|
|
123
146
|
end
|
|
124
147
|
ensure
|
|
125
148
|
# Restore original directory
|
|
@@ -241,7 +264,7 @@ module Howzit
|
|
|
241
264
|
Dir.chdir(expanded_exec_dir) if expanded_exec_dir != expanded_original
|
|
242
265
|
end
|
|
243
266
|
|
|
244
|
-
res = system(@action)
|
|
267
|
+
res = system(@action.unescape_placeholders)
|
|
245
268
|
ensure
|
|
246
269
|
# Restore original directory
|
|
247
270
|
if exec_dir && Dir.exist?(exec_dir)
|
|
@@ -274,7 +297,7 @@ module Howzit
|
|
|
274
297
|
@title && !@title.empty? ? @title : @action
|
|
275
298
|
end
|
|
276
299
|
Howzit.console.info("#{@prefix}{bg}Copied {bw}#{display_title}{bg} to clipboard{x}".c)
|
|
277
|
-
Util.os_copy(@action)
|
|
300
|
+
Util.os_copy(@action.unescape_placeholders)
|
|
278
301
|
@last_status = 0
|
|
279
302
|
true
|
|
280
303
|
end
|
data/lib/howzit/version.rb
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
describe 'shell variable handling' do
|
|
8
|
+
before do
|
|
9
|
+
Howzit.arguments = []
|
|
10
|
+
Howzit.named_arguments = { 'file' => 'README.md', 'version' => '1.2.3' }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
after do
|
|
14
|
+
Howzit.arguments = []
|
|
15
|
+
Howzit.named_arguments = {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe 'String#render_arguments' do
|
|
19
|
+
it 'substitutes Howzit variables' do
|
|
20
|
+
expect('v${version}'.render_arguments).to eq('v1.2.3')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'leaves unknown variables for the shell' do
|
|
24
|
+
expect('echo "${HOME}"'.render_arguments).to eq('echo "${HOME}"')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'uses Howzit-style defaults for undefined variables' do
|
|
28
|
+
expect('${missing:fallback}'.render_arguments).to eq('fallback')
|
|
29
|
+
expect('${url:http://localhost:3000}'.render_arguments).to eq('http://localhost:3000')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'leaves shell parameter expansions alone' do
|
|
33
|
+
%w[${OUT:-/tmp} ${OUT:=/tmp} ${OUT:+set} ${OUT:?missing} ${NAME:0:3} ${#NAME} ${NAME/a/b}].each do |expansion|
|
|
34
|
+
expect(expansion.render_arguments).to eq(expansion)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'substitutes a defined Howzit variable in ${VAR:-default}' do
|
|
39
|
+
expect('${version:-0.0.0}'.render_arguments).to eq('1.2.3')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'leaves positional placeholders without a matching argument' do
|
|
43
|
+
expect('f() { echo "$1 ${2}"; }'.render_arguments).to eq('f() { echo "$1 ${2}"; }')
|
|
44
|
+
expect("awk '{print $1}'".render_arguments).to eq("awk '{print $1}'")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'leaves $@ and $* alone when no arguments were passed' do
|
|
48
|
+
expect('run() { "$@"; }'.render_arguments).to eq('run() { "$@"; }')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'substitutes positional arguments when passed' do
|
|
52
|
+
Howzit.arguments = ['one', 'two words']
|
|
53
|
+
expect('$1 ${2} ${3:three}'.render_arguments).to eq('one two words three')
|
|
54
|
+
expect('cmd $@'.render_arguments).to eq("cmd one two\\ words")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'leaves escaped $${VAR} placeholders until execution' do
|
|
58
|
+
rendered = 'echo $${file}'.render_arguments
|
|
59
|
+
expect(rendered).to eq('echo $${file}')
|
|
60
|
+
expect(rendered.unescape_placeholders).to eq('echo ${file}')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe 'String#shellify_defaults' do
|
|
65
|
+
it 'converts Howzit defaults to shell defaults' do
|
|
66
|
+
expect('${name:world} ${1:first}'.shellify_defaults).to eq('${name:-world} ${1:-first}')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'leaves shell expansions alone' do
|
|
70
|
+
%w[${name} ${name:-x} ${name:0:3} ${name:$offset} ${name: -2}].each do |expansion|
|
|
71
|
+
expect(expansion.shellify_defaults).to eq(expansion)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe 'ScriptSupport.shell_script?' do
|
|
77
|
+
it 'detects shell scripts' do
|
|
78
|
+
expect(Howzit::ScriptSupport.shell_script?("echo hi\n")).to be true
|
|
79
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/bin/bash\necho hi\n")).to be true
|
|
80
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env zsh\n")).to be true
|
|
81
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/bin/sh -e\n")).to be true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'excludes other interpreters' do
|
|
85
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env ruby\n")).to be false
|
|
86
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env fish\n")).to be false
|
|
87
|
+
expect(Howzit::ScriptSupport.shell_script?("#!/usr/bin/env python3\n")).to be false
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe 'running blocks' do
|
|
92
|
+
let(:temp_dir) { File.expand_path(Dir.mktmpdir('howzit_shell_vars_test')) }
|
|
93
|
+
let(:out_file) { File.join(temp_dir, 'out.txt') }
|
|
94
|
+
|
|
95
|
+
before do
|
|
96
|
+
File.write(
|
|
97
|
+
File.join(temp_dir, 'buildnotes.md'),
|
|
98
|
+
<<~NOTE
|
|
99
|
+
# Shell Variables
|
|
100
|
+
|
|
101
|
+
## Shell Block (target:prod)
|
|
102
|
+
|
|
103
|
+
```run
|
|
104
|
+
#!/bin/bash
|
|
105
|
+
greet() { echo "arg=$1"; }
|
|
106
|
+
for file in a.rb; do echo "loop=${file}"; done > "#{out_file}"
|
|
107
|
+
echo "howzit=${target}" >> "#{out_file}"
|
|
108
|
+
echo "default=${undefined_var:fallback}" >> "#{out_file}"
|
|
109
|
+
echo "shell_default=${UNSET_THING:-shellval}" >> "#{out_file}"
|
|
110
|
+
echo "env=${HOWZIT_SPEC_ENV}" >> "#{out_file}"
|
|
111
|
+
greet hello >> "#{out_file}"
|
|
112
|
+
echo "script_arg=${1:-none}" >> "#{out_file}"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Escaped Run
|
|
116
|
+
|
|
117
|
+
@run(echo "$${HOWZIT_SPEC_ENV}" > "#{out_file}") Escaped
|
|
118
|
+
NOTE
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
ENV['HOWZIT_SPEC_ENV'] = 'from_env'
|
|
122
|
+
Dir.chdir(temp_dir)
|
|
123
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
124
|
+
Howzit.options[:stack] = false
|
|
125
|
+
Howzit.options[:include_upstream] = false
|
|
126
|
+
Howzit.options[:log_level] = 3
|
|
127
|
+
Howzit.named_arguments = { 'file' => 'README.md' }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
after do
|
|
131
|
+
ENV.delete('HOWZIT_SPEC_ENV')
|
|
132
|
+
Howzit.options[:shell_variables] = 'env'
|
|
133
|
+
Howzit.options[:log_level] = 1
|
|
134
|
+
Dir.chdir(Dir.tmpdir)
|
|
135
|
+
FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
|
|
136
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def output_lines
|
|
140
|
+
File.read(out_file).lines.map(&:strip)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'passes Howzit variables to shell blocks through the environment' do
|
|
144
|
+
Howzit::BuildNote.new.find_topic('Shell Block')[0].run
|
|
145
|
+
|
|
146
|
+
expect(output_lines).to eq([
|
|
147
|
+
'loop=a.rb',
|
|
148
|
+
'howzit=prod',
|
|
149
|
+
'default=fallback',
|
|
150
|
+
'shell_default=shellval',
|
|
151
|
+
'env=from_env',
|
|
152
|
+
'arg=hello',
|
|
153
|
+
'script_arg=none'
|
|
154
|
+
])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'passes positional arguments to the script' do
|
|
158
|
+
Howzit.arguments = ['cli_arg']
|
|
159
|
+
Howzit::BuildNote.new.find_topic('Shell Block')[0].run
|
|
160
|
+
|
|
161
|
+
expect(output_lines).to include('arg=hello', 'script_arg=cli_arg')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'uses text substitution when shell_variables is substitute' do
|
|
165
|
+
Howzit.options[:shell_variables] = 'substitute'
|
|
166
|
+
Howzit::BuildNote.new.find_topic('Shell Block')[0].run
|
|
167
|
+
|
|
168
|
+
expect(output_lines).to include('loop=README.md', 'howzit=prod')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'passes escaped placeholders to the shell in @run commands' do
|
|
172
|
+
Howzit::BuildNote.new.find_topic('Escaped Run')[0].run
|
|
173
|
+
|
|
174
|
+
expect(output_lines).to eq(['from_env'])
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: howzit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.46
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -314,7 +314,6 @@ files:
|
|
|
314
314
|
- lib/howzit/topic.rb
|
|
315
315
|
- lib/howzit/util.rb
|
|
316
316
|
- lib/howzit/version.rb
|
|
317
|
-
- nested-topics-for-howzit.md
|
|
318
317
|
- scripts/runtests.sh
|
|
319
318
|
- spec/buildnote_spec.rb
|
|
320
319
|
- spec/cli_spec.rb
|
|
@@ -330,6 +329,7 @@ files:
|
|
|
330
329
|
- spec/script_comm_spec.rb
|
|
331
330
|
- spec/sequential_conditional_spec.rb
|
|
332
331
|
- spec/set_var_spec.rb
|
|
332
|
+
- spec/shell_variables_spec.rb
|
|
333
333
|
- spec/spec_helper.rb
|
|
334
334
|
- spec/stack_mode_spec.rb
|
|
335
335
|
- spec/stringutils_spec.rb
|
|
@@ -378,6 +378,7 @@ test_files:
|
|
|
378
378
|
- spec/script_comm_spec.rb
|
|
379
379
|
- spec/sequential_conditional_spec.rb
|
|
380
380
|
- spec/set_var_spec.rb
|
|
381
|
+
- spec/shell_variables_spec.rb
|
|
381
382
|
- spec/spec_helper.rb
|
|
382
383
|
- spec/stack_mode_spec.rb
|
|
383
384
|
- spec/stringutils_spec.rb
|
data/nested-topics-for-howzit.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Nested topics for Howzit
|
|
3
|
-
layout: post
|
|
4
|
-
tags: [howzit, ruby, markdown, cli, automation]
|
|
5
|
-
categories: [Blog,Code]
|
|
6
|
-
post_class: 'code'
|
|
7
|
-
comments: true
|
|
8
|
-
---
|
|
9
|
-
[Howzit](https://github.com/ttscoff/howzit) has always split build notes into topics using Markdown headers. What it didn't do was care *which* header level you used. An `##` and a `###` were both just "a topic," and they all got flattened into one long list. As of version 2.1.44, that's changed: deeper headers now nest under the header above them, and that nesting affects both how notes display and what happens when you run them.
|
|
10
|
-
|
|
11
|
-
### The short version
|
|
12
|
-
|
|
13
|
-
A `###` following a `##` is now a subtopic of that `##`. A `####` under the `###` is a subtopic of the `###`, and so on down. The next header at the same level (or shallower) closes out the parent.
|
|
14
|
-
|
|
15
|
-
```markdown
|
|
16
|
-
## Deploy
|
|
17
|
-
|
|
18
|
-
@run(./scripts/preflight.sh) Preflight checks
|
|
19
|
-
|
|
20
|
-
### Build assets
|
|
21
|
-
|
|
22
|
-
@run(gulp) Build
|
|
23
|
-
|
|
24
|
-
### Upload
|
|
25
|
-
|
|
26
|
-
@run(gulp sync) Sync to server
|
|
27
|
-
|
|
28
|
-
## Package management
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Here "Build assets" and "Upload" belong to "Deploy." "Package management" is back at the top level.
|
|
32
|
-
|
|
33
|
-
### Viewing a parent topic
|
|
34
|
-
|
|
35
|
-
When you pull up a parent topic, you now get the whole thing: the parent, followed by all of its subtopics.
|
|
36
|
-
|
|
37
|
-
```console
|
|
38
|
-
$ howzit deploy
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Subtopic headers get a slightly lighter treatment (a dotted rule and a cyan title) so you can see where the nesting happens without it getting noisy. You can still ask for a subtopic directly:
|
|
42
|
-
|
|
43
|
-
```console
|
|
44
|
-
$ howzit upload
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
...and you'll get just that section.
|
|
48
|
-
|
|
49
|
-
### Running a parent topic
|
|
50
|
-
|
|
51
|
-
This is the part I actually wanted. Running a parent now runs everything under it:
|
|
52
|
-
|
|
53
|
-
```console
|
|
54
|
-
$ howzit -r deploy
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
That runs Deploy's own tasks first, then each subtopic's tasks in order, depth-first. So the example above runs preflight, then the build, then the sync. You get one combined summary at the end instead of a "Ran 1 task" line for every section.
|
|
58
|
-
|
|
59
|
-
A few details worth knowing:
|
|
60
|
-
|
|
61
|
-
- **Errors stop the run.** If a task fails, the remaining subtopics are skipped. Add `--force` if you want it to push through anyway.
|
|
62
|
-
- **`@before` and `@after` still work per topic.** Each topic shows its prerequisites before its own tasks run. A parent's `@after` block shows up once the parent *and* all of its subtopics are done, which is usually where you want that "don't forget to..." reminder.
|
|
63
|
-
- **Parents don't need tasks of their own.** A parent that's just an umbrella for a few subtopics runs fine, and it won't complain about "No @directive found."
|
|
64
|
-
- **Subtopics still run on their own.** `howzit -r upload` runs just the upload step.
|
|
65
|
-
|
|
66
|
-
### Includes get the whole tree too
|
|
67
|
-
|
|
68
|
-
If another topic does `@include(Deploy)`, it now runs Deploy and all of its subtopics, same as running it directly. Task counts in include notes and menus reflect the full tree, so you're not surprised by how much is about to happen.
|
|
69
|
-
|
|
70
|
-
### Listing and "show everything"
|
|
71
|
-
|
|
72
|
-
Running `howzit` with no arguments shows every topic. It now only prints top-level topics, since subtopics come along with their parents. No more seeing the same section twice.
|
|
73
|
-
|
|
74
|
-
`howzit -l` indents subtopics under their parents, so the list reflects the structure of your note:
|
|
75
|
-
|
|
76
|
-
```console
|
|
77
|
-
$ howzit -l
|
|
78
|
-
Topics:
|
|
79
|
-
|
|
80
|
-
- Deploy
|
|
81
|
-
- Build assets
|
|
82
|
-
- Upload
|
|
83
|
-
- Package management
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Shell completions stay flat, so you can still tab-complete straight to a subtopic.
|
|
87
|
-
|
|
88
|
-
### One thing to check in your notes
|
|
89
|
-
|
|
90
|
-
If you've been using `###` headers under `##` purely as visual formatting, those are now real subtopics. Viewing the parent will include them (probably what you wanted anyway), and running the parent will run their tasks too. If you had a `###` section with tasks you *don't* want run as part of its parent, bump it up to `##`.
|
|
91
|
-
|
|
92
|
-
### Grab it
|
|
93
|
-
|
|
94
|
-
It's in 2.1.44:
|
|
95
|
-
|
|
96
|
-
```console
|
|
97
|
-
$ gem install howzit
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
The details are in the [build notes anatomy page](https://github.com/ttscoff/howzit/wiki/Anatomy-of-a-build-notes-file#subtopics) on the wiki. I've already started restructuring a few of my own notes around this. Having a single "Release" topic that fans out into build, test, and publish steps is a lot nicer than keeping them all at the same level and chaining them together with `@include`.
|