⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
LC_ALL: en_US.UTF-8

strategy:
matrix: { ruby: ['3.2', '3.3', '3.4'] }
matrix: { ruby: ['3.2', '3.3', '3.4', '4.0'] }

steps:
- name: Checkout code
Expand Down
11 changes: 6 additions & 5 deletions bashly.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ Gem::Specification.new do |s|

s.add_dependency 'colsole', '~> 1.0'
s.add_dependency 'completely', '~> 0.7.0'
s.add_dependency 'filewatcher', '~> 2.0'
s.add_dependency 'gtx', '~> 0.1.1'
s.add_dependency 'listen', '~> 3.9'
s.add_dependency 'lp', '~> 0.2.0'
s.add_dependency 'mister_bin', '~> 0.8.1'
s.add_dependency 'mister_bin', '~> 0.9.0'
s.add_dependency 'requires', '~> 1.1'
s.add_dependency 'tty-markdown', '~> 0.7.2'

# Sub-dependenceis (Ruby 3.3.5 warnings)
s.add_dependency 'logger', '>= 1', '< 3' # required by filewatcher
s.add_dependency 'ostruct', '>= 0', '< 2' # required by json
# Missing sub-dependencies
# logger: required and not bundled by `listen` 3.9.0
# ref: https://github.com/guard/listen/issues/591
s.add_dependency 'logger', '~> 1.7'

s.metadata = {
'bug_tracker_uri' => 'https://github.com/bashly-framework/bashly/issues',
Expand Down
2 changes: 1 addition & 1 deletion examples/render-mandoc/docs/download.1
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.\" Automatically generated by Pandoc 3.2
.\"
.TH "download" "1" "December 2025" "Version 0.1.0" "Sample application"
.TH "download" "1" "January 2026" "Version 0.1.0" "Sample application"
.SH NAME
\f[B]download\f[R] \- Sample application
.SH SYNOPSIS
Expand Down
2 changes: 1 addition & 1 deletion examples/render-mandoc/docs/download.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
% download(1) Version 0.1.0 | Sample application
% Lana Lang
% December 2025
% January 2026

NAME
==================================================
Expand Down
2 changes: 1 addition & 1 deletion lib/bashly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module Bashly

autoloads 'bashly', %i[
CLI Config ConfigValidator Library LibrarySource LibrarySourceConfig
MessageStrings RenderContext RenderSource Settings VERSION
MessageStrings RenderContext RenderSource Settings VERSION Watch
]

autoloads 'bashly/concerns', %i[
Expand Down
4 changes: 1 addition & 3 deletions lib/bashly/commands/generate.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require 'filewatcher'

module Bashly
module Commands
class Generate < Base
Expand Down Expand Up @@ -41,7 +39,7 @@ def run
def watch
quiet_say "g`watching` #{Settings.source_dir}\n"

Filewatcher.new([Settings.source_dir]).watch do
Watch.new(Settings.source_dir).on_change do
reset
generate
rescue Bashly::ConfigurationError => e
Expand Down
3 changes: 1 addition & 2 deletions lib/bashly/commands/render.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
require 'filewatcher'
require 'tty-markdown'

module Bashly
Expand Down Expand Up @@ -75,7 +74,7 @@ def render
def watch
say "g`watching`\n"

Filewatcher.new(watchables).watch do
Watch.new(*watchables).on_change do
render
say "g`waiting`\n"
end
Expand Down
52 changes: 52 additions & 0 deletions lib/bashly/watch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require 'listen'

module Bashly
# File system watcher - an ergonomic wrapper around the Listen gem
class Watch
attr_reader :dirs, :options

DEFAULT_OPTIONS = {
force_polling: true,
latency: 1.0,
}.freeze

def initialize(*dirs, **options)
@options = DEFAULT_OPTIONS.merge(options).freeze
@dirs = dirs.empty? ? ['.'] : dirs
end

def on_change(&)
start(&)
wait
ensure
stop
end

private

def build_listener
listen.to(*dirs, **options) do |modified, added, removed|
yield changes(modified, added, removed)
end
end

def start(&block)
raise ArgumentError, 'block required' unless block

@listener = build_listener(&block)
@listener.start
end

def stop
@listener&.stop
@listener = nil
end

def changes(modified, added, removed)
{ modified:, added:, removed: }
end

def listen = Listen
def wait = sleep
end
end
2 changes: 1 addition & 1 deletion spec/approvals/examples/dependencies-alt
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ args: none
deps:
- ${deps[git]} = /usr/bin/git
- ${deps[http_client]} = /usr/bin/curl
- ${deps[ruby]} = /home/vagrant/.rbenv/versions/3.4.1/bin/ruby
- ${deps[ruby]} = /home/vagrant/.rbenv/versions/4.0.0/bin/ruby
10 changes: 5 additions & 5 deletions spec/bashly/commands/generate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -229,20 +229,20 @@

let(:bashly_config_path) { "#{source_dir}/bashly.yml" }
let(:bashly_config) { YAML.load_file bashly_config_path }
let(:watcher_double) { instance_double Filewatcher, watch: nil }
let(:watch_double) { instance_double Watch, on_change: nil }

it 'generates immediately and on change' do
allow(Filewatcher).to receive(:new).and_return(watcher_double)
allow(watcher_double).to receive(:watch).and_yield
allow(Watch).to receive(:new).and_return(watch_double)
allow(watch_double).to receive(:on_change).and_yield

expect { subject.execute %w[generate --watch] }
.to output_approval('cli/generate/watch')
end

context 'when ConfigurationError is raised during watch' do
it 'shows the error gracefully and continues to watch' do
allow(Filewatcher).to receive(:new).and_return(watcher_double)
allow(watcher_double).to receive(:watch) do |&block|
allow(Watch).to receive(:new).and_return(watch_double)
allow(watch_double).to receive(:on_change) do |&block|
bashly_config['invalid_option'] = 'error this'
File.write bashly_config_path, bashly_config.to_yaml
block.call
Expand Down
6 changes: 3 additions & 3 deletions spec/bashly/commands/render_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@
describe 'SOURCE TARGET --watch' do
let(:bashly_config_path) { "#{source_dir}/bashly.yml" }
let(:bashly_config) { YAML.load_file bashly_config_path }
let(:watcher_double) { instance_double Filewatcher, watch: nil }
let(:watch_double) { instance_double Watch, on_change: nil }

it 'generates immediately and on change' do
allow(Filewatcher).to receive(:new).and_return(watcher_double)
allow(watcher_double).to receive(:watch).and_yield
allow(Watch).to receive(:new).and_return(watch_double)
allow(watch_double).to receive(:on_change).and_yield

expect(subject).to receive(:render).twice

Expand Down
2 changes: 1 addition & 1 deletion spec/bashly/commands/shell_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
describe 'in-terminal commands' do
before do
ENV['BASHLY_SHELL'] = nil
allow(Readline).to receive(:readline).and_return(*input)
allow(Reline).to receive(:readline).and_return(*input)
end

context 'with exit command' do
Expand Down
3 changes: 1 addition & 2 deletions spec/bashly/concerns/completions_command_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
describe Script::Command do
fixtures = load_fixture 'script/commands'

subject { described_class.new fixtures[fixture] }

let(:fixtures) { load_fixture('script/commands') }
let(:fixture) { :completions_simple }

describe '#completion_data' do
Expand Down
3 changes: 1 addition & 2 deletions spec/bashly/concerns/completions_flag_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
describe Script::Flag do
fixtures = load_fixture 'script/flags'

subject { described_class.new fixtures[fixture] }

let(:fixtures) { load_fixture 'script/flags' }
let(:fixture) { :basic_flag }
let(:command) { 'some command' }

Expand Down
9 changes: 5 additions & 4 deletions spec/bashly/config_validator_spec.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
describe ConfigValidator do
fixtures = load_fixture 'script/validations'
fixtures = load_fixture('script/validations')

describe '#validate' do
fixtures.each do |fixture, options|
validator = described_class.new options

context "with :#{fixture}" do
let(:validator) { described_class.new(options) }

it 'raises an error' do
expect { validator.validate }.to raise_approval("validations/#{fixture}")
expect { validator.validate }
.to raise_approval("validations/#{fixture}")
end
end
end
Expand Down
25 changes: 14 additions & 11 deletions spec/bashly/integration/examples_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,44 @@
#
# To only test examples containing a certain string in their path, run:
# EXAMPLE=yaml bundle exec run spec examples

describe 'generated bash scripts', :slow do
# Make sure all examples are generated with strict mode
before { ENV['BASHLY_STRICT'] = 'yes' }
after { ENV['BASHLY_STRICT'] = nil }

# Test public examples from the examples folder...
examples = Dir['examples/*'].select { |f| File.directory? f }
examples = Dir['examples/*'].select { |f| File.directory?(f) }

# ...as well as internal examples, not suitable for public view
fixtures = Dir['spec/fixtures/workspaces/*'].select do |f|
File.directory? f and File.exist? "#{f}/test.sh"
File.directory?(f) && File.exist?("#{f}/test.sh")
end

test_cases = fixtures + examples

# Allow up to a certain string distance from the approval text in CI
leeway = ENV['CI'] ? 40 : 0
let(:leeway) { ENV['CI'] ? 40 : 0 }

# For certain examples, allow some exceptions (replacements) since they
# are too volatile (e.g. line number changes)
exceptions = {
'examples/stacktrace' => [/download:\d+/, 'download:<line>'],
'examples/render-mandoc' => [/Version 0.1.0.*download\(1\)/, '<footer>'],
}
let(:exceptions) do
{
'examples/stacktrace' => [/download:\d+/, 'download:<line>'],
'examples/render-mandoc' => [/Version 0.1.0.*download\(1\)/, '<footer>'],
}
end

test_cases.each do |example|
approval_name = example.gsub 'spec/fixtures/workspaces', 'fixtures'

next if ENV['EXAMPLE'] && !example.include?(ENV['EXAMPLE'])

describe example do
let(:approval_name) do
example.gsub('spec/fixtures/workspaces', 'fixtures')
end

it 'is executed properly' do
output = 'not executed'
Dir.chdir example do
Dir.chdir(example) do
output = `bash test.sh 2>&1`
end

Expand Down
6 changes: 2 additions & 4 deletions spec/bashly/integration/rendering_mandoc_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
subject { Commands::Render.new }

let(:source_dir) { Settings.source_dir }
let(:leeway) { ENV['CI'] ? 20 : 0 }
let(:target) { 'spec/tmp' }

target = 'spec/tmp'
examples = %w[
catch-all-advanced
dependencies-alt
Expand All @@ -15,9 +16,6 @@
render-mandoc
]

# Allow up to a certain string distance from the approval text in CI
leeway = ENV['CI'] ? 20 : 0

examples.each do |example|
describe example do
before do
Expand Down
12 changes: 6 additions & 6 deletions spec/bashly/integration/rendering_markdown_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
subject { Commands::Render.new }

let(:source_dir) { Settings.source_dir }
let(:target) { 'spec/tmp' }
let(:leeway) { ENV['CI'] ? 20 : 0 }

target = 'spec/tmp'
examples = %w[
catch-all-advanced
dependencies-alt
Expand All @@ -15,9 +16,6 @@
render-markdown
]

# Allow up to a certain string distance from the approval text in CI
leeway = ENV['CI'] ? 20 : 0

examples.each do |example|
describe example do
before do
Expand All @@ -31,8 +29,10 @@

Dir["#{target}/*.md"].each do |file|
puts " => #{file}"
basename = File.basename file
expect(File.read file).to match_approval("rendering/markdown/#{example}/#{basename}")
basename = File.basename(file)

expect(File.read(file))
.to match_approval("rendering/markdown/#{example}/#{basename}")
.diff(leeway)
end
end
Expand Down
9 changes: 5 additions & 4 deletions spec/bashly/library_source_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@

describe '#validate' do
fixtures.each do |path|
fixture_name = File.basename path, '.yml'
config = described_class.new path
context "with #{File.basename(path)}" do
let(:fixture_name) { File.basename(path, '.yml') }
let(:config) { described_class.new(path) }

context "with #{fixture_name}.yml" do
it 'raises an error' do
expect { config.validate }.to raise_approval("libraries_validation/#{fixture_name}")
expect { config.validate }
.to raise_approval("libraries_validation/#{fixture_name}")
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/bashly/settings_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
end

context 'when its corresponding env var is set' do
original_value = ENV['BASHLY_TAB_INDENT']
let(:original_value) { ENV['BASHLY_TAB_INDENT'] }

before { described_class.tab_indent = nil }
after { ENV['BASHLY_TAB_INDENT'] = original_value }
Expand Down
Loading