Skip to content

Add ability to choose a different formatter #646

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Aug 5, 2025
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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: Install pandoc
run: rush get pandoc

- name: Install shfmt
run: rush get shfmt

# libyaml needed for Ruby's YAML library
- name: Install OS dependencies
run: sudo apt-get -y install libyaml-dev
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" "July 2025" "Version 0.1.0" "Sample application"
.TH "download" "1" "August 2025" "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
% July 2025
% August 2025

NAME
==================================================
Expand Down
2 changes: 1 addition & 1 deletion lib/bashly.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module Bashly
module Script
autoloads 'bashly/script', %i[
Argument Base CatchAll Command Dependency EnvironmentVariable Flag
Variable Wrapper
Formatter Variable Wrapper
]

module Introspection
Expand Down
4 changes: 2 additions & 2 deletions lib/bashly/extensions/string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ def wrap(length = 80)
end * "\n"
end

def lint
gsub(/\s+\n/m, "\n\n").lines.grep_v(/^\s*##/).join
def remove_private_comments
lines.grep_v(/^\s*##/).join
end

def remove_front_matter
Expand Down
26 changes: 18 additions & 8 deletions lib/bashly/libraries/settings/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,26 @@ partials_extension: sh
#-------------------------------------------------------------------------------

# Configure the bash options that will be added to the initialize function:
# strict: true Bash strict mode (set -euo pipefail)
# strict: false Only exit on errors (set -e)
# strict: '' Do not add any 'set' directive
# strict: <string> Add any other custom 'set' directive
# strict: true # Bash strict mode (set -euo pipefail)
# strict: false # Only exit on errors (set -e)
# strict: '' # Do not add any 'set' directive
# strict: <string> # Add any other custom 'set' directive
strict: false

# When true, the generated script will use tab indentation instead of spaces
# (every 2 leading spaces will be converted to a tab character)
tab_indent: false

# Choose a post-processor for the generated script:
# formatter: internal # Use Bashly's internal formatter (compacts newlines)
# formatter: external # Run the external command `shfmt --case-indent --indent 2`
# formatter: none # Disable formatting entirely
# formatter: <string> # Use a custom shell command to format the script.
# # The command will receive the script via stdin and
# # must output the result to stdout.
# # Example: shfmt --minify
formatter: internal


#-------------------------------------------------------------------------------
# INTERFACE OPTIONS
Expand Down Expand Up @@ -100,10 +110,10 @@ env: development

# Tweak the script output by enabling or disabling some script output.
# These options accept one of the following strings:
# - production render this feature only when env == production
# - development render this feature only when env == development
# - always render this feature in any environment
# - never do not render this feature
# - production # render this feature only when env == production
# - development # render this feature only when env == development
# - always # render this feature in any environment
# - never # do not render this feature
enable_header_comment: always
enable_bash3_bouncer: always
enable_view_markers: development
Expand Down
44 changes: 44 additions & 0 deletions lib/bashly/script/formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'open3'
require 'shellwords'

module Bashly
module Script
class Formatter
attr_reader :script, :mode

def initialize(script, mode: nil)
@script = script
@mode = mode&.to_s || 'internal'
end

def formatted_script
case mode
when 'internal' then script.gsub(/\s+\n/m, "\n\n")
when 'external' then shfmt_result
when 'none' then script
else custom_formatter_result mode
end
end

private

def shfmt_result
custom_formatter_result %w[shfmt --case-indent --indent 2]
end

def custom_formatter_result(command)
command = Shellwords.split command if command.is_a? String

begin
output, error, status = Open3.capture3(*command, stdin_data: script)
rescue Errno::ENOENT
raise Error, "Command not found: g`#{command.first}`"
end

raise Error, "Failed running g`#{Shellwords.join command}`:\n\n#{error}" unless status.success?

output
end
end
end
end
8 changes: 7 additions & 1 deletion lib/bashly/script/wrapper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ def base_code
[header, body]
end

result.join("\n").lint
clean_code result.join("\n")
end

def clean_code(script)
result = script.remove_private_comments
formatter = Formatter.new result, mode: Settings.formatter
formatter.formatted_script
end

def header
Expand Down
5 changes: 5 additions & 0 deletions lib/bashly/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class << self
:enable_inspect_args,
:enable_sourcing,
:enable_view_markers,
:formatter,
:function_names,
:lib_dir,
:partials_extension,
Expand Down Expand Up @@ -86,6 +87,10 @@ def env=(value)
@env = value&.to_sym
end

def formatter
@formatter ||= get :formatter
end

def full_lib_dir
"#{source_dir}/#{lib_dir}"
end
Expand Down
19 changes: 19 additions & 0 deletions schemas/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,25 @@
],
"default": "development"
},
"formatter": {
"title": "formatter",
"description": "Choose how to post-process the generated script\nhttps://bashly.dev/usage/settings/#formatter",
"anyOf": [
{
"type": "string",
"enum": [
"internal",
"external",
"none"
]
},
{
"type": "string",
"minLength": 1
}
],
"default": "internal"
},
"partials_extension": {
"title": "partials extension",
"description": "The extension to use when reading/writing partial script snippets\nhttps://bashly.dev/usage/settings/#partials_extension",
Expand Down
2 changes: 1 addition & 1 deletion spec/approvals/examples/render-mandoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,4 @@ ISSUE TRACKER
AUTHORS
Lana Lang.

Version 0.1.0 July 2025 download(1)
Version 0.1.0 August 2025 download(1)
4 changes: 2 additions & 2 deletions spec/approvals/examples/stacktrace
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,5 @@ Examples:

Stack trace:
from ./download:15 in `root_command`
from ./download:260 in `run`
from ./download:266 in `main`
from ./download:259 in `run`
from ./download:265 in `main`
1 change: 1 addition & 0 deletions spec/approvals/formatter/error-failure
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#<Bashly::Error:"Failed running g`shfmt --diff`:\n\n">
1 change: 1 addition & 0 deletions spec/approvals/formatter/error-not-found
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#<Bashly::Error: Command not found: g`my_formatter`>
14 changes: 14 additions & 0 deletions spec/approvals/formatter/internal
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash

funk() {
cat <<EOF
start multiline heredoc test

end multiline heredoc test
EOF

echo "unnecessary multiline test"

echo "unnecessary multiline test complete"
echo "rogue indentation"
}
16 changes: 16 additions & 0 deletions spec/approvals/formatter/shfmt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash

funk() {
cat <<EOF
start multiline heredoc test



end multiline heredoc test
EOF

echo "unnecessary multiline test"

echo "unnecessary multiline test complete"
echo "rogue indentation"
}
17 changes: 17 additions & 0 deletions spec/approvals/formatter/shfmt-custom
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash

funk()
{
cat <<EOF
start multiline heredoc test



end multiline heredoc test
EOF

echo "unnecessary multiline test"

echo "unnecessary multiline test complete"
echo "rogue indentation"
}
18 changes: 4 additions & 14 deletions spec/bashly/extensions/string_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,11 @@
end
end

describe '#lint' do
context 'with a string that contains multiple consecutive newlines' do
subject { "one\n two\n \n three\n \n \nfour\n\n\n\n" }
describe '#remove_private_comments' do
subject { "this is important\n## SECRET\n ## ANOTHER SECRET\n also important\n" }

it 'replaces two or more newlines with two newlines' do
expect(subject.lint).to eq "one\n two\n\n three\n\nfour\n\n"
end
end

context 'with a string that contains double-hash comments' do
subject { "this is important\n## SECRET\n ## ANOTHER SECRET\n also important\n" }

it 'removes these comments' do
expect(subject.lint).to eq "this is important\n also important\n"
end
it 'removes these comments' do
expect(subject.remove_private_comments).to eq "this is important\n also important\n"
end
end

Expand Down
53 changes: 53 additions & 0 deletions spec/bashly/script/formatter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
describe Script::Formatter do
subject { described_class.new script, mode: mode }

let(:script) { File.read "spec/fixtures/formatter/#{script_id}.sh" }
let(:script_id) { :simple }
let(:mode) { nil }

describe '#formatted_script' do
it 'formats the script using the internal formatter' do
expect(subject.formatted_script).to match_approval 'formatter/internal'
end

context 'with mode: none' do
let(:mode) { 'none' }

it 'returns the original script' do
expect(subject.formatted_script).to eq script
end
end

context 'with mode: external' do
let(:mode) { 'external' }

it 'uses shfmt to format the script' do
expect(subject.formatted_script).to match_approval 'formatter/shfmt'
end
end

context 'with mode: shfmt (string)' do
let(:mode) { 'shfmt --func-next-line' }

it 'uses the given command shfmt to format the script' do
expect(subject.formatted_script).to match_approval 'formatter/shfmt-custom'
end
end

context 'when the external command does not exist' do
let(:mode) { 'my_formatter' }

it 'raises a Bashly::Error' do
expect { subject.formatted_script }.to raise_approval 'formatter/error-not-found'
end
end

context 'when the external command fails' do
let(:mode) { 'shfmt --diff' }

it 'raises a Bashly::Error' do
expect { subject.formatted_script }.to raise_approval 'formatter/error-failure'
end
end
end
end
18 changes: 18 additions & 0 deletions spec/fixtures/formatter/simple.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash

funk() {
cat <<EOF
start multiline heredoc test



end multiline heredoc test
EOF

echo "unnecessary multiline test"



echo "unnecessary multiline test complete"
echo "rogue indentation"
}
14 changes: 14 additions & 0 deletions support/schema/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ properties:
type: string
enum: *feature_toggles
default: development
formatter:
title: formatter
description: |-
Choose how to post-process the generated script
https://bashly.dev/usage/settings/#formatter
anyOf:
- type: string
enum:
- internal
- external
- none
- type: string
minLength: 1
default: internal
partials_extension:
title: partials extension
description: |-
Expand Down