//! Integration coverage for formatter behavior formerly embedded in `src/lib.rs`.
use std::fs;
use std::path::{Path, PathBuf};
use shuck_ast::{AssignmentValue, Command};
use shuck_linter::{AnalysisRequest, Diagnostic, LinterSettings};
use shuck_parser::{ShellDialect as ParseShellDialect, parser::Parser};
use shuck_formatter::{
FormatError, FormattedSource, IndentStyle, ShellDialect, ShellFormatOptions, format_file_ast,
format_source, source_is_formatted,
};
fn parse_for_ast_format(
source: &str,
path: Option<&Path>,
options: &ShellFormatOptions,
) -> shuck_parser::parser::ParseResult {
let dialect = options.resolve(source, path).dialect();
Parser::with_dialect(source, dialect).parse().unwrap()
}
fn assert_source_and_ast_paths_match(
source: &str,
path: Option<&Path>,
options: &ShellFormatOptions,
) {
let parsed = parse_for_ast_format(source, path, options);
let from_source = format_source(source, path, options).unwrap();
let from_ast = format_file_ast(source, parsed.file, path, options).unwrap();
assert_eq!(from_source, from_ast);
assert_eq!(
source_is_formatted(source, path, options).unwrap(),
matches!(from_source, FormattedSource::Unchanged)
);
}
fn format_to_string(source: &str, path: Option<&Path>, options: &ShellFormatOptions) -> String {
match format_source(source, path, options).unwrap() {
FormattedSource::Unchanged => source.to_string(),
FormattedSource::Formatted(formatted) => formatted,
}
}
fn assert_idempotent(source: &str, path: Option<&Path>, options: &ShellFormatOptions) {
let once = format_to_string(source, path, options);
let twice = format_to_string(&once, path, options);
assert_eq!(once, twice);
}
#[track_caller]
fn assert_default_idempotent(source: &str, filename: &str) {
assert_idempotent(
source,
Some(Path::new(filename)),
&ShellFormatOptions::default(),
);
}
#[track_caller]
fn assert_formats_to(
source: &str,
path: Option<&Path>,
options: &ShellFormatOptions,
expected: impl Into<String>,
) {
assert_eq!(
format_source(source, path, options).unwrap(),
FormattedSource::Formatted(expected.into())
);
}
#[track_caller]
fn assert_formats(source: &str, expected: impl Into<String>) {
assert_formats_to(source, None, &ShellFormatOptions::default(), expected);
}
#[track_caller]
fn assert_formats_to_with_ast(
source: &str,
path: Option<&Path>,
options: &ShellFormatOptions,
expected: impl Into<String>,
) {
assert_formats_to(source, path, options, expected);
assert_source_and_ast_paths_match(source, path, options);
}
#[track_caller]
fn assert_unchanged(source: &str, path: Option<&Path>, options: &ShellFormatOptions) {
assert_eq!(
format_source(source, path, options).unwrap(),
FormattedSource::Unchanged
);
}
#[track_caller]
fn assert_unchanged_default(source: &str) {
assert_unchanged(source, None, &ShellFormatOptions::default());
}
#[track_caller]
fn assert_unchanged_with_ast(source: &str, path: Option<&Path>, options: &ShellFormatOptions) {
assert_unchanged(source, path, options);
assert_source_and_ast_paths_match(source, path, options);
}
macro_rules! formatter_cases {
($($name:ident $body:block)+) => {
$(
#[test]
fn $name() $body
)+
};
}
macro_rules! default_format_ast_cases {
($($name:ident: $source:expr => $expected:expr;)+) => {
formatter_cases! { $($name { assert_formats_to_with_ast($source, None, &ShellFormatOptions::default(), $expected); })+ }
};
}
macro_rules! bash_format_ast_cases {
($($name:ident: $source:expr => $expected:expr;)+) => {
formatter_cases! { $($name {
assert_formats_to_with_ast($source, None, &ShellFormatOptions::default().with_dialect(ShellDialect::Bash), $expected);
})+ }
};
}
macro_rules! default_format_cases {
($($name:ident: $source:expr => $expected:expr;)+) => {
formatter_cases! { $($name { assert_formats($source, $expected); })+ }
};
}
macro_rules! default_unchanged_ast_cases {
($($name:ident: $source:expr;)+) => {
formatter_cases! { $($name { assert_unchanged_with_ast($source, None, &ShellFormatOptions::default()); })+ }
};
}
macro_rules! bash_unchanged_ast_cases {
($($name:ident: $source:expr;)+) => {
formatter_cases! { $($name {
assert_unchanged_with_ast($source, None, &ShellFormatOptions::default().with_dialect(ShellDialect::Bash));
})+ }
};
}
macro_rules! default_unchanged_cases {
($($name:ident: $source:expr;)+) => {
formatter_cases! { $($name { assert_unchanged_default($source); })+ }
};
}
macro_rules! default_idempotent_cases {
($($name:ident: $source:expr, $filename:expr;)+) => {
formatter_cases! { $($name { assert_default_idempotent($source, $filename); })+ }
};
}
macro_rules! format_cases_with_options {
($($name:ident: $options:expr, $source:expr $(,)? => $expected:expr;)+) => {
formatter_cases! {
$($name {
let options = $options;
assert_formats_to($source, None, &options, $expected);
})+
}
};
}
macro_rules! unchanged_cases_with_options {
($($name:ident: $options:expr, $source:expr $(,)?;)+) => {
formatter_cases! {
$($name {
let options = $options;
assert_unchanged($source, None, &options);
})+
}
};
}
fn lint_source_posix_strict(source: &str, path: &Path) -> Vec<Diagnostic> {
let parse_result = Parser::with_dialect(source, ParseShellDialect::Posix).parse();
assert!(
!parse_result.is_err(),
"strict parse failed for {}: {}",
path.display(),
parse_result.strict_error()
);
let settings = LinterSettings::default().with_analyzed_paths([path.to_path_buf()]);
AnalysisRequest::from_parse_result(&parse_result, source, &settings)
.with_source_path(path)
.lint()
}
fn diagnostic_count(diagnostics: &[Diagnostic], code: &str) -> usize {
diagnostics
.iter()
.filter(|diagnostic| diagnostic.code() == code)
.count()
}
fn stable_formatter_fixture_cases() -> Vec<(&'static str, &'static str, ShellFormatOptions)> {
let same_name = |fixture, options| (fixture, fixture, options);
let with_name = |fixture, filename, options| (fixture, filename, options);
vec![
same_name(
"function_next_line.sh",
ShellFormatOptions::default().with_function_next_line(true),
),
same_name("case_default.sh", ShellFormatOptions::default()),
same_name(
"space_redirects.sh",
ShellFormatOptions::default().with_space_redirects(true),
),
same_name(
"keep_padding.sh",
ShellFormatOptions::default().with_keep_padding(true),
),
same_name(
"never_split.sh",
ShellFormatOptions::default().with_never_split(true),
),
same_name("nested_heredoc.sh", ShellFormatOptions::default()),
with_name(
"simplify.sh",
"simplify.bash",
ShellFormatOptions::default().with_simplify(true),
),
same_name("minify.sh", ShellFormatOptions::default().with_minify(true)),
with_name(
"mksh_select.sh",
"script.mksh",
ShellFormatOptions::default().with_dialect(ShellDialect::Mksh),
),
]
}
const TONKA_PROMPT_ASSIGNMENT: &str = r##"PS1="$TITLEBAR$YELLOW-$LIGHT_BLUE-(\
$YELLOW\u$LIGHT_BLUE@$YELLOW\h\
$LIGHT_BLUE)-(\
$YELLOW\$PWD\
$LIGHT_BLUE)-$YELLOW-\
\n\
$YELLOW-$LIGHT_BLUE-(\
$(__tonka_clock)\
$WHITE\$ $LIGHT_BLUE)-$YELLOW-$NO_COLOUR "
"##;
#[test]
fn format_file_ast_requires_explicit_clone_for_ast_reuse() {
let source = "echo $(( $a + ${b} ))\n";
let path = Some(Path::new("reuse.bash"));
let options = ShellFormatOptions::default().with_simplify(true);
let parsed = parse_for_ast_format(source, path, &options);
let first = format_file_ast(source, parsed.file.clone(), path, &options).unwrap();
let second = format_file_ast(source, parsed.file, path, &options).unwrap();
assert_eq!(first, second);
assert_eq!(first, format_source(source, path, &options).unwrap());
}
#[test]
fn formats_simple_command_with_tabs_by_default() {
let source = "#!/bin/bash\n echo hi\n";
assert_formats(source, "#!/bin/bash\necho hi\n");
assert!(!source_is_formatted(source, None, &ShellFormatOptions::default()).unwrap());
}
default_format_cases! {
preserves_inline_comments:
"echo hi # note\n"
=> "echo hi # note\n";
keeps_if_close_suffix_comment_on_outer_close:
"if outer; then\n if inner; then\n :\n fi\nfi # outer\n"
=> "if outer; then\n\tif inner; then\n\t\t:\n\tfi\nfi # outer\n";
keeps_inline_if_close_suffix_comment_on_fi:
"if ok; then good; fi # done\n"
=> "if ok; then good; fi # done\n";
keeps_loop_and_case_close_suffix_comments_on_close_keywords:
"while ok; do\n case $cmd in\n run) : ;;\n esac # command\n :\ndone # loop\n"
=> "while ok; do\n\tcase $cmd in\n\trun) : ;;\n\tesac # command\n\t:\ndone # loop\n";
keeps_loop_close_comments_after_literal_done_arguments:
"while ok; do\n echo done\n # close note\ndone\n"
=> "while ok; do\n\techo done\n\t# close note\ndone\n";
keeps_if_close_comments_after_literal_fi_arguments:
"if ok; then\n echo fi\n # close note\nfi\n"
=> "if ok; then\n\techo fi\n\t# close note\nfi\n";
keeps_inline_case_close_suffix_comment_on_esac:
"case \"$IP\" in fe80::*) exit 0 ;; esac\t# ignore IPv6 linklocal, ip2dev() does not work here reliable anyway\n"
=> "case \"$IP\" in fe80::*) exit 0 ;; esac # ignore IPv6 linklocal, ip2dev() does not work here reliable anyway\n";
aligns_nested_close_suffix_comments_by_column:
"if outer; then\n\tif inner; then\n\t\tcase $cmd in\n\t\t*) : ;;\n\t\tesac # case\n\tfi # inner\nfi # outer\n"
=> "if outer; then\n\tif inner; then\n\t\tcase $cmd in\n\t\t*) : ;;\n\t\tesac # case\n\tfi # inner\nfi # outer\n";
aligns_space_indented_close_suffix_comments_by_column:
"if outer; then\n if inner; then\n :\n fi # inner\nfi # outer\n"
=> "if outer; then\n\tif inner; then\n\t\t:\n\tfi # inner\nfi # outer\n";
}
#[test]
fn check_path_reports_already_formatted_sources() {
assert!(
source_is_formatted(
"echo hi\n",
Some(Path::new("script.sh")),
&ShellFormatOptions::default()
)
.unwrap()
);
}
default_format_cases! {
formats_heredoc_command_heads_structurally:
"cat<<EOF\nhello\nEOF\n"
=> "cat <<EOF\nhello\nEOF\n";
formats_nested_heredoc_commands_without_indenting_body:
"if true; then\ncat<<EOF\nhello\nEOF\nfi\n"
=> "if true; then\n\tcat <<EOF\nhello\nEOF\nfi\n";
}
default_format_ast_cases! {
preserves_tab_stripped_heredoc_body_source:
"if true; then\n cat >run <<-EOF\n\t\t#!/bin/sh\n\n\t\texec 2>&1\n\tEOF\nfi\n"
=> "if true; then\n\tcat >run <<-EOF\n\t\t#!/bin/sh\n\n\t\texec 2>&1\n\tEOF\nfi\n";
indents_tab_stripped_heredoc_body_to_context_depth:
"if true; then\n\tcat >&2 <<-EOF\n\t* package moved\n\tEOF\nfi\n"
=> "if true; then\n\tcat >&2 <<-EOF\n\t\t* package moved\n\tEOF\nfi\n";
tab_stripped_heredoc_closer_follows_context_indent:
"if true; then\n if ok; then\n\tcat <<-EOF\n\tbody\n\tEOF\n fi\nfi\n"
=> "if true; then\n\tif ok; then\n\t\tcat <<-EOF\n\t\t\tbody\n\t\tEOF\n\tfi\nfi\n";
preserves_relative_tabs_inside_tab_stripped_heredoc_body:
"build() {\n\tcat <<-EOF >./prerm\n\t#!$PREFIX/bin/bash\n\tif [ -d $PREFIX/etc ]; then\n\t\techo ok\n\t\trm -f file\n\tfi\n\texit 0\n\tEOF\n}\n"
=> "build() {\n\tcat <<-EOF >./prerm\n\t\t#!$PREFIX/bin/bash\n\t\tif [ -d $PREFIX/etc ]; then\n\t\t\techo ok\n\t\t\trm -f file\n\t\tfi\n\t\texit 0\n\tEOF\n}\n";
}
default_format_cases! {
preserves_multiline_if_body_comments:
"if true; then\n# note\necho hi\nfi\n"
=> "if true; then\n\t# note\n\techo hi\nfi\n";
}
default_unchanged_ast_cases! {
keeps_simple_if_else_inline:
"if [ -n \"$REPORTFILE\" ]; then PREQS_MET=\"YES\"; else PREQS_MET=\"NO\"; fi\n";
}
default_format_ast_cases! {
keeps_inline_then_arm_before_multiline_else:
"if [ -n \"$REPORTFILE\" ]; then PREQS_MET=\"YES\"; else\n PREQS_MET=\"NO\"\nfi\n"
=> "if [ -n \"$REPORTFILE\" ]; then PREQS_MET=\"YES\"; else\n\tPREQS_MET=\"NO\"\nfi\n";
}
default_format_cases! {
preserves_comments_inside_elif_bodies:
"foo() {\nif a; then\none\nelif b; then\n# note\n two\nfi\n}\n"
=> "foo() {\n\tif a; then\n\t\tone\n\telif b; then\n\t\t# note\n\t\ttwo\n\tfi\n}\n";
}
bash_unchanged_ast_cases! {
multiline_if_conditions_do_not_capture_later_body_comments:
"f() {\n\tif\n\t\t[[ -n \"${GEM_HOME:-}\" ]]\n\tthen\n\t\tcase \"$PATH:\" in\n\t\t$GEM_HOME/bin:*) true ;; # all fine\n\t\t*)\n\t\t\t# body note\n\t\t\twarn\n\t\t\t;;\n\t\tesac\n\tfi\n}\n\n# marker\ng() { :; }\n";
}
bash_format_ast_cases! {
keeps_inline_else_arm_after_multiline_then:
"if [ $size != scalable ]; then\n ex=png\n size=${size}x${size}\nelse ex=svg; fi\n"
=> "if [ $size != scalable ]; then\n\tex=png\n\tsize=${size}x${size}\nelse ex=svg; fi\n";
aligns_else_suffix_comments_with_nested_multiline_header_suffix_comments:
"if foo; then\n :\nelse # branch\n if [[ \"$x\" =~ y ]]\n then # nested\n :\n fi\nfi\n"
=> "if foo; then\n\t:\nelse # branch\n\tif [[ \"$x\" =~ y ]]; then # nested\n\t\t:\n\tfi\nfi\n";
}
default_format_ast_cases! {
aligns_comments_before_elif_and_else_with_branch_keywords:
"if a; then\none\n# next branch\n# still next branch\nelif b; then\ntwo\n# final branch\nelse\nthree\nfi\n"
=> "if a; then\n\tone\n# next branch\n# still next branch\nelif b; then\n\ttwo\n# final branch\nelse\n\tthree\nfi\n";
ignores_commented_branch_keywords_when_finding_else:
"if a; then\n one\nelse\n# disabled pre\n#if b; then\n#else\n two\nfi\n"
=> "if a; then\n\tone\nelse\n\t# disabled pre\n\t#if b; then\n\t#else\n\ttwo\nfi\n";
keeps_body_indented_comments_before_elif_inside_previous_branch:
"if a; then\none\n # still body context\nelif b; then\ntwo\nfi\n"
=> "if a; then\n\tone\n\t# still body context\nelif b; then\n\ttwo\nfi\n";
keeps_disabled_elif_comment_block_before_real_elif:
"if a; then\none\n\n#elif disabled; then\n #cmd one\n # note\n #cmd two\n\nelif b; then\ntwo\nfi\n"
=> "if a; then\n\tone\n\n\t#elif disabled; then\n\t#cmd one\n\t# note\n\t#cmd two\n\nelif b; then\n\ttwo\nfi\n";
keeps_explanatory_if_comment_before_elif_at_branch_indent:
"if [ -d \"$source_dir\" ]; then\n if ! mkdir -p \"$target_dir\"; then\n return 1\n fi\n# if instead it is a file\nelif [ -f \"$source_dir\" ]; then\n touch \"$target_dir\"\nfi\n"
=> "if [ -d \"$source_dir\" ]; then\n\tif ! mkdir -p \"$target_dir\"; then\n\t\treturn 1\n\tfi\n# if instead it is a file\nelif [ -f \"$source_dir\" ]; then\n\ttouch \"$target_dir\"\nfi\n";
}
bash_format_ast_cases! {
preserves_comments_between_elif_and_condition:
"if a; then\none\nelif\n# explain\n [[ b ]]; then\ntwo\nfi\n"
=> "if a; then\n\tone\nelif\n\t# explain\n\t[[ b ]]\nthen\n\ttwo\nfi\n";
}
default_format_cases! {
preserves_comments_after_if_blocks:
"if true; then\necho hi\nfi\n# after\necho bye\n"
=> "if true; then\n\techo hi\nfi\n# after\necho bye\n";
}
default_format_ast_cases! {
preserves_dangling_comment_inside_binary_brace_group_once:
"if true; then\n ls today && {\n log done\n#\t\tcontinue\n }\n\n rm next\nfi\n"
=> "if true; then\n\tls today && {\n\t\tlog done\n\t\t#\t\tcontinue\n\t}\n\n\trm next\nfi\n";
}
bash_format_ast_cases! {
binary_brace_group_does_not_gain_blank_before_next_command:
"main() {\n [[ ! -f $ok ]] && {\n err missing\n }\n next\n}\n"
=> "main() {\n\t[[ ! -f $ok ]] && {\n\t\terr missing\n\t}\n\tnext\n}\n";
preserves_leading_comments_inside_redirected_brace_group:
"if [[ -n $DEBUG ]]; then\n {\n # one\n # two\n echo hi\n } >&2\nfi\n"
=> "if [[ -n $DEBUG ]]; then\n\t{\n\t\t# one\n\t\t# two\n\t\techo hi\n\t} >&2\nfi\n";
}
default_format_cases! {
preserves_comments_after_function_blocks:
"foo() {\necho hi\n}\n# after\nbar\n"
=> "foo() {\n\techo hi\n}\n# after\nbar\n";
}
default_format_ast_cases! {
normalizes_opening_brace_comment_spacing:
"[ $ok ] && {\t\t# ready\n echo hi\n}\n"
=> "[ $ok ] && { # ready\n\techo hi\n}\n";
preserves_trailing_function_header_comment_when_brace_moves_up:
"foo() # header comment\n{\n echo hi\n}\n"
=> "foo() { # header comment\n\techo hi\n}\n";
preserves_function_header_comment_spacing_when_brace_moves_up:
"foo()\t\t# header comment\n{\n echo hi\n}\n"
=> "foo() { # header comment\n\techo hi\n}\n";
aligns_moved_function_header_comments_with_first_body_comment:
"_olsr_uptime()\t\t\t# in seconds\n{\n local option=\"$1\"\t# string option\n local funcname='olsr_uptime'\n}\n"
=> "_olsr_uptime() { # in seconds\n\tlocal option=\"$1\" # string option\n\tlocal funcname='olsr_uptime'\n}\n";
body_comment_stops_moved_function_header_comment_alignment:
"foo()\t\t# header\n{\n#\tlocal mac=\"$1\"\n\tlocal minute=\"${MINUTE:-$( date +%H )}\"\t\t# built during taskplanner: 00...23\n\tlocal hour=\"${HOUR:-$( date +%M )}\"\t\t# built during taskplanner: 00...59\n}\n"
=> "foo() { # header\n\t#\tlocal mac=\"$1\"\n\tlocal minute=\"${MINUTE:-$(date +%H)}\" # built during taskplanner: 00...23\n\tlocal hour=\"${HOUR:-$(date +%M)}\" # built during taskplanner: 00...59\n}\n";
aligns_old_style_function_header_comments_like_shfmt:
"foo () # header\n{\n a=1 # body\n}\n"
=> "foo() { # header\n\ta=1 # body\n}\n";
preserves_header_and_opening_brace_comments_when_brace_moves_up:
"foo() # header comment\n{ # body comment\n echo hi\n}\n"
=> "foo() { # header comment\n\t# body comment\n\techo hi\n}\n";
preserves_inline_function_keyword_opening_brace_comments:
"function is_integer() { # helper function for todo-txt-count\n [ \"$1\" -eq \"$1\" ] > /dev/null 2>&1\n return $?\n}\n"
=> "function is_integer() { # helper function for todo-txt-count\n\t[ \"$1\" -eq \"$1\" ] >/dev/null 2>&1\n\treturn $?\n}\n";
opening_brace_comment_stops_following_body_comment_alignment:
"foo() # header\n{ # body comment\n local FILE='/tmp/OLSR/LINKS.sh' # see build_tables()\n local json=\"$TMPDIR/links.json\" # FIXME! add _speedtest_stats()\n}\n"
=> "foo() { # header\n\t# body comment\n\tlocal FILE='/tmp/OLSR/LINKS.sh' # see build_tables()\n\tlocal json=\"$TMPDIR/links.json\" # FIXME! add _speedtest_stats()\n}\n";
preserves_blank_line_before_trailing_file_comment:
"foo() {\necho hi\n}\n\n# ex: filetype=sh\n"
=> "foo() {\n\techo hi\n}\n\n# ex: filetype=sh\n";
}
default_unchanged_cases! {
preserves_heredoc_trailing_comments_without_duplication:
"cat <<EOF # note\nhi\nEOF\n";
}
bash_format_ast_cases! {
formats_heredoc_pipeline_with_trailing_comment_structurally:
"f(){\n cat <<EOF |\nbody\n# heredoc comment\nEOF\n python #|\n #sed x\n}\n"
=> "f() {\n\tcat <<EOF |\nbody\n# heredoc comment\nEOF\n\t\tpython #|\n\t#sed x\n}\n";
defers_heredoc_body_until_continued_pipeline_head_finishes:
"if true; then\ncat << EOF | openssl req -new -key \"$key\" \\\n -x509 \\\n -out \"$cert\"\nbody\nEOF\nfi\n"
=> "if true; then\n\tcat <<EOF | openssl req -new -key \"$key\" \\\n\t\t-x509 \\\n\t\t-out \"$cert\"\nbody\nEOF\nfi\n";
}
default_idempotent_cases! {
preserves_quoted_heredoc_delimiters_idempotently:
"cat <<'EOF_264'\ndelta\nEOF_264\n",
"quoted_heredoc.sh";
}
default_format_cases! {
formats_decl_heredoc_heads_structurally:
"declare -x foo=1<<EOF\nhi\nEOF\n"
=> "declare -x foo=1 <<EOF\nhi\nEOF\n";
}
default_unchanged_cases! {
standalone_assignments_do_not_gain_trailing_spaces:
"x=1\n";
preserves_blank_lines_between_commands:
"set -u\n\nfoo\n";
}
default_format_cases! {
collapses_extra_blank_lines_between_items:
"set -u\n\n\n# ready\n\n\nfoo\n"
=> "set -u\n\n# ready\n\nfoo\n";
}
#[test]
fn preserves_blank_lines_after_functions() {
let source = "foo() {\n echo hi\n}\n\nbar\n";
let options = ShellFormatOptions::default().with_dialect(ShellDialect::Bash);
assert_formats_to(
source,
Some(Path::new("function_gap.bash")),
&options,
"foo() {\n\techo hi\n}\n\nbar\n",
);
}
default_unchanged_cases! {
preserves_blank_lines_after_leading_comments:
"#!/usr/bin/env bash\n\nset -u\n";
}
default_format_cases! {
trims_trailing_comment_whitespace:
"# note \nfoo # bar\t\n"
=> "# note\nfoo # bar\n";
}
#[test]
fn trailing_newline_preserves_operator_prefixed_comment_backslash() {
let source = "echo ok;#note\\";
assert_eq!(
format_to_string(source, None, &ShellFormatOptions::default()),
"echo ok #note\\\n"
);
}
bash_format_ast_cases! {
preserves_final_comment_backslash_when_adding_trailing_newline:
"aws logs filter-log-events \\\n \"$@\"\n #--max-items 1 \\\n #--end-time \"$(date '+%s')000\" \\\n"
=> "aws logs filter-log-events \\\n\t\"$@\"\n#--max-items 1 \\\n#--end-time \"$(date '+%s')000\" \\\n";
}
#[test]
fn parsed_assignment_value_render_syntax_is_trimmed() {
let parsed = parse_for_ast_format("x=1\n", None, &ShellFormatOptions::default());
let Command::Simple(command) = &parsed.file.body[0].command else {
panic!("expected a simple command");
};
let AssignmentValue::Scalar(value) = &command.assignments[0].value else {
panic!("expected a scalar assignment");
};
assert_eq!(value.render_syntax("x=1\n"), "1");
}
default_unchanged_cases! {
preserves_escaped_quotes_in_double_quoted_assignments:
"fzf_completion=\"source \\\"$fzf_base/shell/completion.${shell}\\\"\"\n";
preserves_prompt_escapes_in_double_quoted_assignments:
"PS1=\"\\u:\\W \\$ \"\n";
}
default_unchanged_ast_cases! {
preserves_escaped_dollar_literals_after_command_substitutions:
"RUNTIME_CLASSPATH=$(echo $ALL_JARS | xargs printf -- \"\\$this_dir/%s:\"):\\$this_dir\n";
preserves_escaped_dollar_literals_inside_quoted_command_substitutions:
"XDGPATH=$(echo \"foreach dir [split [::tcl::tm::path list]] {puts \\$dir}\" | tclsh | tail -n1)\n";
}
bash_unchanged_ast_cases! {
preserves_escaped_html_closing_tags_in_double_quoted_assignments:
"_link=\"<a href=\\\"${target//' '/%20}\\\">[[${label:-}]]</a>\"\n";
}
default_format_ast_cases! {
normalizes_backtick_escaped_dollar_literals_once:
"XDGPATH=`echo \"foreach dir [split [::tcl::tm::path list]] {puts \\\\$dir}\" | tclsh | tail -n1`\n"
=> "XDGPATH=$(echo \"foreach dir [split [::tcl::tm::path list]] {puts \\$dir}\" | tclsh | tail -n1)\n";
}
#[test]
fn preserves_escaped_dollar_command_substitutions_in_prompt_assignments() {
let source = r##"PS1="\$([[ -n \$(git branch 2> /dev/null) ]] && echo \" on ${icon_branch} \")${white?}$(scm_prompt_info)${normal?}\n${icon_end}"
"##;
assert_unchanged_with_ast(source, None, &ShellFormatOptions::default());
}
#[test]
fn preserves_multiline_prompt_assignments_with_backslash_continuations() {
let source = r##"PS1="$TITLEBAR\
$YELLOW\u$LIGHT_BLUE@$YELLOW\h\
$LIGHT_BLUE-$(__theme_clock)\
$WHITE\$ $NO_COLOUR "
"##;
assert_unchanged_with_ast(source, None, &ShellFormatOptions::default());
}
#[test]
fn preserves_multiline_prompt_assignments_with_leading_continuation_lines() {
assert_unchanged_with_ast(
TONKA_PROMPT_ASSIGNMENT,
None,
&ShellFormatOptions::default(),
);
}
#[test]
fn preserves_indented_multiline_prompt_assignments_with_leading_continuation_lines() {
let source = format!("prompt() {{\n {TONKA_PROMPT_ASSIGNMENT}}}\n");
assert_formats_to_with_ast(
&source,
None,
&ShellFormatOptions::default(),
format!("prompt() {{\n\t{TONKA_PROMPT_ASSIGNMENT}}}\n"),
);
}
default_format_ast_cases! {
preserves_multiline_double_quoted_argument_alignment:
"if true; then\n gcloud secrets list \\\n --filter=\"labels.kubernetes-cluster=$current_cluster \\\n AND NOT \\\n labels.foo ~ .\" |\n while read -r secret; do\n echo \"$secret\"\n done\nfi\n"
=> "if true; then\n\tgcloud secrets list \\\n\t\t--filter=\"labels.kubernetes-cluster=$current_cluster \\\n AND NOT \\\n labels.foo ~ .\" |\n\t\twhile read -r secret; do\n\t\t\techo \"$secret\"\n\t\tdone\nfi\n";
preserves_background_terminator_at_if_branch_boundary:
"if [ -z \"$SUBIT\" ]; then\n eval $CMD_START_STANDALONE >${JBOSS_CONSOLE} 2>&1 &\nelse\n $SUBIT \"$CMD_START_STANDALONE >${JBOSS_CONSOLE} 2>&1 &\"\nfi\n"
=> "if [ -z \"$SUBIT\" ]; then\n\teval $CMD_START_STANDALONE >${JBOSS_CONSOLE} 2>&1 &\nelse\n\t$SUBIT \"$CMD_START_STANDALONE >${JBOSS_CONSOLE} 2>&1 &\"\nfi\n";
preserves_comment_after_then_without_raw_body_fallback:
"if ! type -P wget &>/dev/null ||\n type -P apk; then # Alpine built-in wget is not enough\n \"$srcdir/../packages/install_packages.sh\" wget\nfi\n"
=> "if ! type -P wget &>/dev/null ||\n\ttype -P apk; then # Alpine built-in wget is not enough\n\t\"$srcdir/../packages/install_packages.sh\" wget\nfi\n";
ignores_branch_keywords_inside_leading_comments:
"f() {\n if [ -f .iterate ]; then\n #ls ./*/.git &>/dev/null; then # note\n hr\n fi\n}\n"
=> "f() {\n\tif [ -f .iterate ]; then\n\t\t#ls ./*/.git &>/dev/null; then # note\n\t\thr\n\tfi\n}\n";
keeps_inline_case_conditions_before_then:
"if case \"$@\" in *--usecwd*) true ;; *) false ;; esac then\n USE_CWD=1\nfi\n"
=> "if case \"$@\" in *--usecwd*) true ;; *) false ;; esac then\n\tUSE_CWD=1\nfi\n";
}
bash_format_ast_cases! {
splits_redirect_only_statement_after_background:
"if ok; then\n run --flag & 2>/dev/null\nfi\n"
=> "if ok; then\n\trun --flag &\n\t2>/dev/null\nfi\n";
splits_following_statement_after_background:
"if ok; then\n run --flag 2>&1 & echo $! >\"$PIDFILE\"\nfi\n"
=> "if ok; then\n\trun --flag 2>&1 &\n\techo $! >\"$PIDFILE\"\nfi\n";
preserves_eval_conditional_syntax_as_arguments:
"if eval ! [[ \"$env_var\" =~ ^[[:digit:]]+$ ]]; then\n echo ok\nfi\n"
=> "if eval ! [[ \"$env_var\" =~ ^[[:digit:]]+$ ]]; then\n\techo ok\nfi\n";
keeps_assignment_command_substitution_condition_suffix_comment_after_then:
"if ! out=\"$(\n stat -c %Y \"$path\" 2>/dev/null\n )\" # GNU\nthen\n :\nfi\n"
=> "if ! out=\"$(\n\tstat -c %Y \"$path\" 2>/dev/null\n)\"; then # GNU\n\t:\nfi\n";
collapses_negated_condition_continuation_before_pipeline:
"while read -r module; do\n if ! \\\n git grep \"needle\" |\n grep -v requirements.txt |\n grep -q .; then\n echo \"$module\"\n fi\ndone\n"
=> "while read -r module; do\n\tif ! git grep \"needle\" |\n\t\tgrep -v requirements.txt |\n\t\tgrep -q .; then\n\t\techo \"$module\"\n\tfi\ndone\n";
preserves_comment_between_list_operator_and_rhs:
"if [ -z \"$jar\" ] ||\n # incomplete download, resume it\n ! jar tf \"$jar\" &>/dev/null; then\n echo fetch\nfi\n"
=> "if [ -z \"$jar\" ] ||\n\t# incomplete download, resume it\n\t! jar tf \"$jar\" &>/dev/null; then\n\techo fetch\nfi\n";
preserves_comment_between_list_operator_and_brace_group:
"docker-compose exec -T jenkins-server install-plugins.sh ||\n # New: later switch to\n {\n docker-compose cp plugins.txt jenkins-server:/\n } || :\n"
=> "docker-compose exec -T jenkins-server install-plugins.sh ||\n\t# New: later switch to\n\t{\n\t\tdocker-compose cp plugins.txt jenkins-server:/\n\t} || :\n";
keeps_command_list_rhs_brace_group_body_comments_inside_group:
"if { true; } &&\n command &&\n {\n # inside group\n [[ -t 1 ]] ||\n true\n }\nthen\n :\nfi\n"
=> "if { true; } &&\n\tcommand &&\n\t{\n\t\t# inside group\n\t\t[[ -t 1 ]] ||\n\t\t\ttrue\n\t}; then\n\t:\nfi\n";
}
default_unchanged_ast_cases! {
preserves_grouped_if_conditions_before_then:
"if {\n\t[ -n \"${SUDO_USER}\" ] || [ -n \"${DOAS_USER}\" ]\n} && [ \"$(id -ru)\" -eq 0 ]; then\n\tprintf '%s\\n' denied\nfi\n";
normalizes_nested_grouped_if_condition_indentation:
"setup() {\n\tif {\n\t\t[ -d \"/etc/dpkg/dpkg.cfg.d/\" ] || [ -d \"/usr/share/libalpm/scripts\" ]\n\t} && [ \"${init}\" -eq 0 ]; then\n\t\tsetup_hooks\n\tfi\n}\n";
}
default_format_ast_cases! {
preserves_inline_brace_group_loop_bodies:
"while read -r line; do {\n echo \"$line\"\n}; done\n"
=> "while read -r line; do {\n\techo \"$line\"\n}; done\n";
}
bash_format_ast_cases! {
collapses_then_after_multiline_grouped_if_conditions:
"if {\n [[ \"$group\" -eq 2 ]] &&\n contains first\n } || {\n [[ \"$group\" -eq 3 ]] &&\n ! contains second\n }\nthen\n return 0\nfi\n"
=> "if {\n\t[[ \"$group\" -eq 2 ]] &&\n\t\tcontains first\n} || {\n\t[[ \"$group\" -eq 3 ]] &&\n\t\t! contains second\n}; then\n\treturn 0\nfi\n";
keeps_wrapped_inline_brace_group_conditions_attached:
"if ! { [[ -d \"${status_file%/*}\" ]] \\\n && [[ -r \"${status_file}\" ]]; }; then\n echo \"\"\nfi\n"
=> "if ! { [[ -d \"${status_file%/*}\" ]] &&\n\t[[ -r \"${status_file}\" ]]; }; then\n\techo \"\"\nfi\n";
}
default_format_ast_cases! {
preserves_legacy_inline_do_brace_group_without_semicolon_before_done:
"for item in $items; do {\n case \"$item\" in\n a)\n echo a\n ;;\n esac\n} done\n"
=> "for item in $items; do {\n\tcase \"$item\" in\n\ta)\n\t\techo a\n\t\t;;\n\tesac\n} done\n";
preserves_legacy_inline_do_brace_group_ending_in_binary_compound:
"for item in $items; do {\n ok && {\n case \"$item\" in\n a)\n echo a\n ;;\n esac\n }\n} done\n"
=> "for item in $items; do {\n\tok && {\n\t\tcase \"$item\" in\n\t\ta)\n\t\t\techo a\n\t\t\t;;\n\t\tesac\n\t}\n} done\n";
terminates_legacy_inline_do_brace_group_ending_in_if:
"while read -r line; do {\n if ok; then\n :\n fi\n} done <file\n"
=> "while read -r line; do {\n\tif ok; then\n\t\t:\n\tfi\n}; done <file\n";
preserves_legacy_inline_do_brace_group_ending_in_loop_case:
"for dev in $devs; do {\n scan \"$dev\" | while read -r line; do {\n case \"$line\" in\n a)\n echo a\n ;;\n esac\n } done\n} done\n"
=> "for dev in $devs; do {\n\tscan \"$dev\" | while read -r line; do {\n\t\tcase \"$line\" in\n\t\ta)\n\t\t\techo a\n\t\t\t;;\n\t\tesac\n\t} done\n} done\n";
preserves_explicit_breaks_inside_conditional_binaries:
"[[ $a -le 255 && $b -le 255 &&\n $c -le 255 && $d -le 255 ]]\n"
=> "[[ $a -le 255 && $b -le 255 &&\n\t$c -le 255 && $d -le 255 ]]\n";
preserves_backslash_breaks_inside_conditional_binaries:
"[[ $a -le 255 && $b -le 255 \\\n && $c -le 255 && $d -le 255 ]]\n"
=> "[[ $a -le 255 && $b -le 255 &&\n\t$c -le 255 && $d -le 255 ]]\n";
}
bash_format_ast_cases! {
collapses_backslash_breaks_inside_conditional_comparisons:
"rename() {\n if [[ -n \"${_remote_head_branch:-}\" ]] &&\n [[ \"${_remote_branch_name:-\"${_current_branch}\"}\" == \\\n \"${_remote_head_branch:-}\" ]]\n then\n _exit_1 printf \"Only orphan branches can be renamed.\\\\n\"\n fi\n}\n"
=> "rename() {\n\tif [[ -n \"${_remote_head_branch:-}\" ]] &&\n\t\t[[ \"${_remote_branch_name:-\"${_current_branch}\"}\" == \"${_remote_head_branch:-}\" ]]; then\n\t\t_exit_1 printf \"Only orphan branches can be renamed.\\\\n\"\n\tfi\n}\n";
}
default_unchanged_cases! {
preserves_nested_parameter_expansions_inside_quoted_strings:
"nvm_err \"N/A: version \\\"${PREFIXED_VERSION:-$PROVIDED_VERSION}\\\" is not yet installed.\"\n";
}
default_format_ast_cases! {
preserves_default_redirect_spacing_without_space_redirects:
"archi=$(uname -smo 2> /dev/null || uname -sm)\n"
=> "archi=$(uname -smo 2>/dev/null || uname -sm)\n";
normalizes_redirect_spacing_inside_raw_multiline_command_substitutions:
"host_sockets=\"$(find /run/host/run \\\n\t-xdev \\\n\t2> /dev/null || :)\"\n"
=> "host_sockets=\"$(find /run/host/run \\\n\t-xdev \\\n\t2>/dev/null || :)\"\n";
keeps_command_substitution_condition_continuation_at_block_indent:
"if true; then\n if [[ $a != \"$(cat x)\" ||\n $b == c ]]; then\n echo yes\n fi\nfi\n"
=> "if true; then\n\tif [[ $a != \"$(cat x)\" ||\n\t$b == c ]]; then\n\t\techo yes\n\tfi\nfi\n";
normalizes_redirect_spacing_inside_parameter_default_commands:
"[[ -t 1 && \"${CLICOLOR:=$(tput colors 2> /dev/null)}\" -ge 8 ]]\n"
=> "[[ -t 1 && \"${CLICOLOR:=$(tput colors 2>/dev/null)}\" -ge 8 ]]\n";
normalizes_pipeline_spacing_inside_parameter_default_commands:
"value=${value:-$(printf x|tr x y)}\n"
=> "value=${value:-$(printf x | tr x y)}\n";
}
bash_format_ast_cases! {
normalizes_output_redirect_spacing_inside_raw_command_substitutions:
"if $(! /sbin/pidof $PRGNAM > /dev/null 2>&1 ) ; then\n echo stale\nfi\n"
=> "if $(! /sbin/pidof $PRGNAM >/dev/null 2>&1); then\n\techo stale\nfi\n";
normalizes_leading_pipe_continuations_inside_raw_command_substitutions:
"value=\"$(declare -f list_all \\\n\t| sed 's/list_all/list_all_without_hub/')\"\n"
=> "value=\"$(declare -f list_all |\n\tsed 's/list_all/list_all_without_hub/')\"\n";
normalizes_trailing_pipe_continuations_inside_raw_command_substitutions:
"value=\"$(\n # note\n foo | \\\n bar | \\\n baz\n)\"\n"
=> "value=\"$(\n\t# note\n\tfoo |\n\tbar |\n\tbaz\n)\"\n";
carries_normalized_pipeline_indent_inside_raw_command_substitutions:
"f() {\n value=\"$(\n # note\n docker-compose \\\n logs service | \\\n grep token | \\\n awk '{print $1}' || :\n )\"\n}\n"
=> "f() {\n\tvalue=\"$(\n\t\t# note\n\t\tdocker-compose \\\n\t\t\tlogs service |\n\t\t\tgrep token |\n\t\t\tawk '{print $1}' || :\n\t)\"\n}\n";
normalizes_leading_pipe_continuations_inside_process_substitutions:
"while read -r line; do :; done < <(\n\tcat clean_files.txt \\\n\t\t| grep -v '^#'\n)\n"
=> "while read -r line; do :; done < <(\n\tcat clean_files.txt |\n\t\tgrep -v '^#'\n)\n";
normalizes_here_string_spacing_inside_command_substitutions:
"[[ $versions = \"$(sort -V <<< \"$versions\")\" ]]\n"
=> "[[ $versions = \"$(sort -V <<<\"$versions\")\" ]]\n";
normalizes_here_string_spacing_in_raw_comment_command_substitutions:
"value=\"$(\n\t# keep comment\n\tcat <<< \"$payload\"\n)\"\n"
=> "value=\"$(\n\t# keep comment\n\tcat <<<\"$payload\"\n)\"\n";
normalizes_here_string_spacing:
"sort -V <<< \"$versions\"\n"
=> "sort -V <<<\"$versions\"\n";
preserves_multiline_here_string_literal_payload_indent:
"case $kind in\n service)\n cat >$unit <<<\"\n[Unit]\nDescription=$name\n\n[Service]\nExecStart=$bin\n\"\n ;;\nesac\n"
=> "case $kind in\nservice)\n\tcat >$unit <<<\"\n[Unit]\nDescription=$name\n\n[Service]\nExecStart=$bin\n\"\n\t;;\nesac\n";
indents_multiline_here_string_command_substitution_targets:
"f() {\n IFS=' ' read -ra tags <<<\"$(\n get_tags \"$1\" \"$2\"\n )\"\n}\n"
=> "f() {\n\tIFS=' ' read -ra tags <<<\"$(\n\t\tget_tags \"$1\" \"$2\"\n\t)\"\n}\n";
}
default_unchanged_ast_cases! {
preserves_empty_command_substitutions:
"result=$()\n";
}
bash_format_ast_cases! {
formats_commented_inline_subshell_here_string_targets:
"f() {\n\tIFS=\" \" read -r -a COMPREPLY <<< \"$( (\n\t\twhile read -r -d ' ' i; do\n\t\t\t[[ -z \"$i\" ]] && continue\n\t\t\t# flatten array with spaces on either side,\n\t\t\t# otherwise we cannot grep on word boundaries of\n\t\t\t# first and last word\n\t\t\tCOMPREPLYSTR=\" ${COMPREPLY[*]} \"\n\t\t\t# remove word from list of completions\n\t\t\tIFS=\" \" read -r -a COMPREPLY <<< \"${COMPREPLYSTR/ ${i%% *} / }\"\n\t\tdone\n\t\tprintf '%s ' \"${COMPREPLY[@]}\"\n\t) <<< \"${COMP_WORDS[@]}\")\"\n}\n"
=> "f() {\n\tIFS=\" \" read -r -a COMPREPLY <<<\"$( (\n\t\twhile read -r -d ' ' i; do\n\t\t\t[[ -z \"$i\" ]] && continue\n\t\t\t# flatten array with spaces on either side,\n\t\t\t# otherwise we cannot grep on word boundaries of\n\t\t\t# first and last word\n\t\t\tCOMPREPLYSTR=\" ${COMPREPLY[*]} \"\n\t\t\t# remove word from list of completions\n\t\t\tIFS=\" \" read -r -a COMPREPLY <<<\"${COMPREPLYSTR/ ${i%% *} / }\"\n\t\tdone\n\t\tprintf '%s ' \"${COMPREPLY[@]}\"\n\t) <<<\"${COMP_WORDS[@]}\")\"\n}\n";
}
bash_unchanged_ast_cases! {
preserves_unmodeled_command_substitution_bodies:
"themes=$(grep \\{EXTRA_THEMES install.sh | cut -d= -f2 | cut -d} -f1)\n";
}
bash_format_ast_cases! {
normalizes_redirect_spacing_in_raw_command_substitution_fallbacks:
"find_dig=$(which dig 2> /dev/null | grep -v \"no [^ ]* in\")\ncontext_id=\"$(jq_debug_pipe_dump <<< \"$output\" | jq -r \".items[] | select(.name == \\\"$context_name\\\") | .id\")\"\nurl=\"$(jq -r \"limit(1; .[] | select(.title == \\\"$selected\\\" or .animal == \\\"$selected\\\") ) | .cover_src\" < \"$json\")\"\nCOMPREPLY=($(compgen -W \"$(awk -F ':' 'BEGIN {print_line = 0}; /^[^ ]/ {print_line = 0}' < ${MASTER_CONFIG})\" -- \"${cur}\"))\n"
=> "find_dig=$(which dig 2>/dev/null | grep -v \"no [^ ]* in\")\ncontext_id=\"$(jq_debug_pipe_dump <<<\"$output\" | jq -r \".items[] | select(.name == \\\"$context_name\\\") | .id\")\"\nurl=\"$(jq -r \"limit(1; .[] | select(.title == \\\"$selected\\\" or .animal == \\\"$selected\\\") ) | .cover_src\" <\"$json\")\"\nCOMPREPLY=($(compgen -W \"$(awk -F ':' 'BEGIN {print_line = 0}; /^[^ ]/ {print_line = 0}' <${MASTER_CONFIG})\" -- \"${cur}\"))\n";
trims_inline_command_substitution_padding:
"echo \"MD5SUM=\\\"$( md5sum file | cut -d' ' -f1 )\\\"\"\nlocal minute=\"${MINUTE:-$( date +%H )}\"\noutput=$( ls packages 2> /dev/null | grep pattern )\n"
=> "echo \"MD5SUM=\\\"$(md5sum file | cut -d' ' -f1)\\\"\"\nlocal minute=\"${MINUTE:-$(date +%H)}\"\noutput=$(ls packages 2>/dev/null | grep pattern)\n";
trims_nested_inline_command_substitution_padding:
"_pre=\"$( echo $( du -hs \"$directory/\" ) )\"\n"
=> "_pre=\"$(echo $(du -hs \"$directory/\"))\"\n";
keeps_subshell_command_substitution_from_becoming_arithmetic:
"id=$( (echo hi ; echo there) | checksum )\n"
=> "id=$( (\n\techo hi\n\techo there\n) | checksum)\n";
keeps_inline_nested_subshells_from_becoming_arithmetic_commands:
"run() {\n ( ( echo hi ) 2>&1 | ( cat ) ) 5>&1\n}\n"
=> "run() {\n\t( (echo hi) 2>&1 | (cat)) 5>&1\n}\n";
normalizes_inline_command_substitution_internal_spacing:
"nlq=\"$( _sanitizer run \"$nlq\" numeric )\"\nline=$( head -n 2 $file|tail -n 1 )\nfile2patch=\"$( echo \"$line\" | cut -d' ' -f2 |cut -f1 )\"\nmsg=\"Welcome Hari - your last access was $(last|head -n2|tail -n1|sed 's/[^ ]\\+ \\+[^ ]\\+ \\+[^ ]\\+ \\+//;s/ *$//')\"\n"
=> "nlq=\"$(_sanitizer run \"$nlq\" numeric)\"\nline=$(head -n 2 $file | tail -n 1)\nfile2patch=\"$(echo \"$line\" | cut -d' ' -f2 | cut -f1)\"\nmsg=\"Welcome Hari - your last access was $(last | head -n2 | tail -n1 | sed 's/[^ ]\\+ \\+[^ ]\\+ \\+[^ ]\\+ \\+//;s/ *$//')\"\n";
}
default_format_ast_cases! {
formats_multiline_command_substitutions:
"result=$(\necho foo\necho bar\n)\n"
=> "result=$(\n\techo foo\n\techo bar\n)\n";
formats_block_command_substitutions_with_trailing_comments:
"size=$(\nstat -f\"%z\" \"$tmpFile\" 2> /dev/null; # OS X `stat`\nstat -c\"%s\" \"$tmpFile\" 2> /dev/null # GNU `stat`\n)\n"
=> "size=$(\n\tstat -f\"%z\" \"$tmpFile\" 2>/dev/null # OS X `stat`\n\tstat -c\"%s\" \"$tmpFile\" 2>/dev/null # GNU `stat`\n)\n";
preserves_command_substitutions_with_closing_paren_on_own_line:
"output=\"$(foo |\n bar\n )\"\n"
=> "output=\"$(\n\tfoo |\n\t\tbar\n)\"\n";
formats_multiline_command_substitutions_with_compound_commands:
"result=$(\nif foo; then\necho hi\nelse\necho bye\nfi\n)\n"
=> "result=$(\n\tif foo; then\n\t\techo hi\n\telse\n\t\techo bye\n\tfi\n)\n";
preserves_inline_continued_command_substitution_assignments:
"start() {\n CHOICE=$(whiptail --title x --menu \\\n foo 14 58 2 \\\n yes \" \" no \" \" 3>&2 2>&1 1>&3)\n}\n"
=> "start() {\n\tCHOICE=$(whiptail --title x --menu \\\n\t\tfoo 14 58 2 \\\n\t\tyes \" \" no \" \" 3>&2 2>&1 1>&3)\n}\n";
keeps_nested_command_substitution_multiline_literals_unindented:
"f() {\n case $prev in\n -soundhw)\n _comp_compgen_split -- \"$(\"$1\" -soundhw help | _comp_awk '\n function islower(s) { return length(s) > 0 && s == tolower(s); }\n islower(substr($0, 1, 1)) {print $1}') all\"\n ;;\n esac\n}\n"
=> "f() {\n\tcase $prev in\n\t-soundhw)\n\t\t_comp_compgen_split -- \"$(\"$1\" -soundhw help | _comp_awk '\n function islower(s) { return length(s) > 0 && s == tolower(s); }\n islower(substr($0, 1, 1)) {print $1}') all\"\n\t\t;;\n\tesac\n}\n";
}
bash_format_ast_cases! {
trims_command_substitution_close_line_continuations:
"tag=\"$(\n grep '\"tag_name.*\"'\".*$version\" \"$json\" \\\n | head -1 \\\n | sed 's,.*\"\\(gm'\"$version\"'[^\\\"]*\\)\".*,\\1,'\\\n )\"\n"
=> "tag=\"$(\n\tgrep '\"tag_name.*\"'\".*$version\" \"$json\" |\n\t\thead -1 |\n\t\tsed 's,.*\"\\(gm'\"$version\"'[^\\\"]*\\)\".*,\\1,'\n)\"\n";
keeps_quoted_command_substitution_continuation_indent_stable:
"icons() {\n icon_files=\"${icon_files}¤$(find \\\n /usr/share/icons \\\n /usr/share/pixmaps \\\n /var/lib/flatpak/exports/share/icons -iname \"*${icon}*\" \\\n -printf \"%p¤\" 2> /dev/null || :)\"\n}\n"
=> "icons() {\n\ticon_files=\"${icon_files}¤$(find \\\n\t\t/usr/share/icons \\\n\t\t/usr/share/pixmaps \\\n\t\t/var/lib/flatpak/exports/share/icons -iname \"*${icon}*\" \\\n\t\t-printf \"%p¤\" 2>/dev/null || :)\"\n}\n";
indents_inline_continued_command_substitution_assignments:
"_npm_completion() {\n compadd -- $(COMP_CWORD=$((CURRENT-1)) \\\n COMP_LINE=$BUFFER \\\n COMP_POINT=0 \\\n npm completion -- \"${words[@]}\" \\\n 2>/dev/null)\n}\n"
=> "_npm_completion() {\n\tcompadd -- $(COMP_CWORD=$((CURRENT - 1)) \\\n\t\tCOMP_LINE=$BUFFER \\\n\t\tCOMP_POINT=0 \\\n\t\tnpm completion -- \"${words[@]}\" \\\n\t\t2>/dev/null)\n}\n";
command_substitution_assignment_continuations_do_not_double_context_indent:
"get_pr_url(){\n local existing_pr\n existing_pr=\"$(gh pr list -R \"$owner/$repo\" \\\n --json baseRefName,changedFiles \\\n -q \".[] |\n select(.baseRefName == \\\"$base\\\")\n \")\"\n}\n"
=> "get_pr_url() {\n\tlocal existing_pr\n\texisting_pr=\"$(gh pr list -R \"$owner/$repo\" \\\n\t\t--json baseRefName,changedFiles \\\n\t\t-q \".[] |\n select(.baseRefName == \\\"$base\\\")\n \")\"\n}\n";
formats_commented_if_command_substitutions_structurally:
"_SCOPED=\"$(\n # selected notebook flag\n if [[ \"$a\" != \"$b\" ]]\n then\n printf \"1\\\\n\"\n else\n printf \"0\\\\n\"\n fi\n)\"\n"
=> "_SCOPED=\"$(\n\t# selected notebook flag\n\tif [[ \"$a\" != \"$b\" ]]; then\n\t\tprintf \"1\\\\n\"\n\telse\n\t\tprintf \"0\\\\n\"\n\tfi\n)\"\n";
}
default_unchanged_ast_cases! {
keeps_single_statement_command_substitutions_with_multiline_literals_inline:
"_comp_compgen_split -- \"$(\"$1\" -soundhw help | _comp_awk '\n function islower(s) { return length(s) > 0 && s == tolower(s); }\n islower(substr($0, 1, 1)) {print $1}') all\"\n";
command_substitutions_with_comments_fall_back_to_raw_source:
"result=$(echo foo # keep comment\necho bar)\n";
}
default_format_ast_cases! {
command_substitutions_with_heredocs_use_block_layout:
"result=$(cat <<EOF\nhello\nEOF\n)\n"
=> "result=$(\n\tcat <<EOF\nhello\nEOF\n)\n";
}
bash_format_ast_cases! {
formats_multiline_command_substitutions_with_escaped_quotes_structurally:
"response=\"$(\n download --flag \\\n \"https://example.test?url=${target}\" |\n LC_ALL=C sed -E \"s/.*\\\"url\\\": \\\"([^\\\"]+)\\\".*/\\1/g\" || printf \"\"\n)\"\n"
=> "response=\"$(\n\tdownload --flag \\\n\t\t\"https://example.test?url=${target}\" |\n\t\tLC_ALL=C sed -E \"s/.*\\\"url\\\": \\\"([^\\\"]+)\\\".*/\\1/g\" || printf \"\"\n)\"\n";
formats_commented_brace_group_pipeline_command_substitutions_structurally:
"content=\"$(\n {\n cat \"$file\"\n } | {\n if [[ \"$tool\" =~ readab ]] &&\n command -v readable; then # readability-cli\n readable \\\n --base \"$url\" \\\n --quiet \\\n 2>/dev/null || cat\n else\n cat\n fi\n }\n)\"\n"
=> "content=\"$(\n\t{\n\t\tcat \"$file\"\n\t} | {\n\t\tif [[ \"$tool\" =~ readab ]] &&\n\t\t\tcommand -v readable; then # readability-cli\n\t\t\treadable \\\n\t\t\t\t--base \"$url\" \\\n\t\t\t\t--quiet \\\n\t\t\t\t2>/dev/null || cat\n\t\telse\n\t\t\tcat\n\t\tfi\n\t}\n)\"\n";
command_substitutions_with_comments_and_own_line_close_use_block_layout:
"result=\"$(grep -En pattern \"$script\" |\n grep -Ev -e skip \\\n # keep this filter documented\n )\"\n"
=> "result=\"$(\n\tgrep -En pattern \"$script\" |\n\t\tgrep -Ev -e skip\n\t# keep this filter documented\n)\"\n";
normalizes_raw_block_command_substitution_short_space_indent:
"version=$(\n # keep the sourced version local\n source ./version.sh\n echo \"$VERSION\"\n)\n"
=> "version=$(\n\t# keep the sourced version local\n\tsource ./version.sh\n\techo \"$VERSION\"\n)\n";
command_substitution_heredoc_arguments_keep_body_unindented:
"if ok; then\n upload --policy \"$(cat <<EOF\n{\n \"items\": [\n$(\n for item in \"${items[@]}\"; do\n printf '\"%s\",\\n' \"$item\"\n done |\n sed '$ s/,$//'\n)\n ]\n}\nEOF\n)\"\nfi\n"
=> "if ok; then\n\tupload --policy \"$(\n\t\tcat <<EOF\n{\n \"items\": [\n$(\n\t\t\t\tfor item in \"${items[@]}\"; do\n\t\t\t\t\tprintf '\"%s\",\\n' \"$item\"\n\t\t\t\tdone |\n\t\t\t\t\tsed '$ s/,$//'\n\t\t\t)\n ]\n}\nEOF\n\t)\"\nfi\n";
command_substitution_heredocs_strip_wrapper_indent:
"if true; then\n\tjson+=$(\n\t\tcat << EOF\n\t\t\t\t,\nEOF\n\t)\nfi\n"
=> "if true; then\n\tjson+=$(\n\t\tcat <<EOF\n\t\t\t\t,\nEOF\n\t)\nfi\n";
command_substitution_heredocs_normalize_top_level_operator_spacing:
"json=$(\n\tcat << EOF\n{\n\t\"ok\": true\n}\nEOF\n)\n"
=> "json=$(\n\tcat <<EOF\n{\n\t\"ok\": true\n}\nEOF\n)\n";
command_substitution_stripped_heredoc_closer_follows_command_indent:
"x=\"$(\n if ok; then\n cat <<-EOF\nbody\nEOF\n fi\n)\"\n"
=> "x=\"$(\n\tif ok; then\n\t\tcat <<-EOF\n\t\t\tbody\n\t\tEOF\n\tfi\n)\"\n";
quoted_command_substitution_with_escaped_replacements_formats_structurally:
"sed_script=\"$(\n for prefix in $prefixes; do\n echo \"s|${prefix}\\\\>|$prefix|g;\"\n done\n)\"\n"
=> "sed_script=\"$(\n\tfor prefix in $prefixes; do\n\t\techo \"s|${prefix}\\\\>|$prefix|g;\"\n\tdone\n)\"\n";
raw_block_command_substitution_strips_wrapper_indent_with_comments:
"sed_script=\"$(\n while read -r directory prefix; do\n if [ -z \"$directory\" ]; then\n continue\n fi\n # catch whole scripts\n echo \"s|${prefix}\\\\>|$directory/${prefix}|g;\"\n done <<< \"$mappings\"\n)\"\n"
=> "sed_script=\"$(\n\twhile read -r directory prefix; do\n\t\tif [ -z \"$directory\" ]; then\n\t\t\tcontinue\n\t\tfi\n\t\t# catch whole scripts\n\t\techo \"s|${prefix}\\\\>|$directory/${prefix}|g;\"\n\tdone <<<\"$mappings\"\n)\"\n";
assignment_command_substitution_heredoc_keeps_literal_tail_unindented:
"if ok; then\n response=\"$(\n nc <<EOF || :\nHTTP/1.1 200 OK\n\naccepted\nEOF\n )\"\nfi\n"
=> "if ok; then\n\tresponse=\"$(\n\t\tnc <<EOF || :\nHTTP/1.1 200 OK\n\naccepted\nEOF\n\t)\"\nfi\n";
command_position_command_substitution_heredoc_keeps_literal_tail_unindented:
"(\n$(\ncat << HERE\nHERE\n)\n)\n"
=> "(\n\t$(\n\t\tcat <<HERE\nHERE\n\t)\n)\n";
process_substitution_heredoc_in_assignment_keeps_literal_tail_unindented:
"if ok; then\n result=$(OPENSSL_CONF=<(cat <<EOF\nbody\nEOF\n) curl url)\nfi\n"
=> "if ok; then\n\tresult=$(OPENSSL_CONF=<(\n\t\tcat <<EOF\nbody\nEOF\n\t) curl url)\nfi\n";
raw_block_command_substitution_indents_backslash_continuations_before_comment:
"if ok; then\n output=\"$(\n NO_TOKEN_AUTH=1 \\\n USERNAME=\"$SPOTIFY_ID\" \\\n PASSWORD=\"$SPOTIFY_SECRET\" \\\n -d code=\"$code\" \\\n #-d code_verifier=\"$code_verifier\"\n )\"\nfi\n"
=> "if ok; then\n\toutput=\"$(\n\t\tNO_TOKEN_AUTH=1 \\\n\t\t\tUSERNAME=\"$SPOTIFY_ID\" \\\n\t\t\tPASSWORD=\"$SPOTIFY_SECRET\" \\\n\t\t\t-d code=\"$code\"\n\t\t#-d code_verifier=\"$code_verifier\"\n\t)\"\nfi\n";
rendered_heredoc_bodies_preserve_escaped_variables:
"cat <<EOF > script\n#!/bin/bash\nexec $(which dart) \"\\$@\"\nEOF\n"
=> "cat <<EOF >script\n#!/bin/bash\nexec $(which dart) \"\\$@\"\nEOF\n";
}
bash_unchanged_ast_cases! {
rendered_heredoc_bodies_preserve_escaped_backslashes:
"cat <<EOF\nline \\\\\nEOF\n";
}
bash_format_ast_cases! {
heredoc_command_substitution_continuations_follow_shell_indent:
"if ok; then\n cat <<EOF\nx $(date +%F |\n # comment\n sed 's/-/--/g') y\nEOF\nfi\n"
=> "if ok; then\n\tcat <<EOF\nx $(date +%F |\n\t\t# comment\n\t\tsed 's/-/--/g') y\nEOF\nfi\n";
}
default_unchanged_ast_cases! {
command_substitution_bounds_do_not_capture_following_comments:
"value=$(pwd)\n# after\nnext\n";
}
bash_format_ast_cases! {
command_substitution_bounds_do_not_capture_following_list_operators:
"value=$(cmd \\\n arg) || echo no\n"
=> "value=$(cmd \\\n\targ) || echo no\n";
}
default_unchanged_ast_cases! {
dirname_command_substitution_bounds_do_not_capture_following_comments:
"cd \"$(dirname \"${BASH_SOURCE[0]}\")\"\n# after\nnext\n";
nested_command_substitution_bounds_do_not_capture_following_comments:
"INFO=\"$(which \"${COMMAND}\") ($(type \"${COMMAND}\" | command awk '{print $4}'))\"\n# after\nnext\n";
preserves_conditional_command_substitutions_with_nested_quoted_arguments:
"[[ \"$(get_permission \"$1\")\" != \"$(id -u)\" ]]\n";
preserves_file_not_grpowned_command_substitution_shape:
"[[ \" $(id -G \"${USER}\") \" != *\" $(get_group \"$1\") \"* ]]\n";
preserves_long_suffix_trim_operators_in_words:
"package_url=\"${package_url%%#*}\"\necho \"${1%%.*}\"\n";
}
bash_unchanged_ast_cases! {
preserves_escaped_quote_words_with_nested_quoted_command_substitutions:
"echo \"\\\"$BUILDSCRIPT\\\" -a \\\"$TERMUX_ARCH\\\" $TERMUX_DEBUG_BUILD --format \\\"$TERMUX_FORMAT\\\" --library $(test \"${PKG_DIR%/*}\" = \"gpkg\" && echo \"glibc\" || echo \"bionic\") ${TERMUX_OUTPUT_DIR+-o $TERMUX_OUTPUT_DIR} $TERMUX_INSTALL_DEPS \\\"$PKG_DIR\\\"\"\n";
}
default_format_ast_cases! {
preserves_parameter_replacements_and_slice_offsets:
"if [ \"$package_url\" != \"${package_url/\\#}\" ]; then\n echo \"${arg:$index:1}\"\n local fetch_args=(\"$package_name\" \"${@:1:$package_type_nargs}\")\n local y=${charmap:$((RANDOM%${#charmap})):1}\n for arg in \"${@:$(( $package_type_nargs + 1 ))}\"; do\n echo \"$arg\"\n done\nfi\n"
=> "if [ \"$package_url\" != \"${package_url/\\#/}\" ]; then\n\techo \"${arg:$index:1}\"\n\tlocal fetch_args=(\"$package_name\" \"${@:1:$package_type_nargs}\")\n\tlocal y=${charmap:$((RANDOM % ${#charmap})):1}\n\tfor arg in \"${@:$(($package_type_nargs + 1))}\"; do\n\t\techo \"$arg\"\n\tdone\nfi\n";
preserves_replacement_patterns_that_need_raw_delimiters:
"title=\"${title//\\\"}\"\nlocal profile=\"${1// }\"\n"
=> "title=\"${title//\\\"/}\"\nlocal profile=\"${1// /}\"\n";
}
default_unchanged_ast_cases! {
preserves_quoted_replacements_with_escaped_delimiters:
"query=\"${query//\\\"/\\\\\\\"}\"\nurl_path=\"${url_path//https:\\\\/\\\\/api.openai.com\\/v1}\"\n";
preserves_single_quoted_parameter_replacement_literals:
"echo '${v/pat}'\n";
preserves_single_quoted_parameter_operand_command_literals:
"value=${value:-'$(cmd 2>/dev/null||true)'}\n";
}
default_format_ast_cases! {
inserts_empty_replacement_delimiter_after_escaped_quote_replacements:
"playlist=\"${playlist//\\\\\"/\\\\\\\\\"}\"\nplaylist=\"${playlist//'/\\\\'}\"\n"
=> "playlist=\"${playlist//\\\\\"/\\\\\\\\\"/}\"\nplaylist=\"${playlist//'/\\\\'/}\"\n";
indents_multiline_parameter_replacement_payloads:
"f() {\n value=${value//nl/\nX${padding}}\n}\n"
=> "f() {\n\tvalue=${value//nl/\n\tX${padding}}\n}\n";
preserves_negative_parameter_slice_offset_spacing:
"if [ \"${filename: -5}\" != .orig ]; then\n echo no\nfi\n"
=> "if [ \"${filename: -5}\" != .orig ]; then\n\techo no\nfi\n";
}
bash_unchanged_ast_cases! {
compacts_parameter_slice_arithmetic_operands:
"region=\"${zone::${#zone}-1}\"\nindex=\"${items:1+2:count-1}\"\n";
}
default_format_ast_cases! {
formats_major_minor_multiline_command_substitution_like_shfmt:
"major_minor() {\n echo \"${1%%.*}.$(\n x=\"${1#*.}\"\n echo \"${x%%.*}\"\n )\"\n}\n"
=> "major_minor() {\n\techo \"${1%%.*}.$(\n\t\tx=\"${1#*.}\"\n\t\techo \"${x%%.*}\"\n\t)\"\n}\n";
formats_nvm_args_command_substitution_without_losing_sed_body_layout:
"ARGS=$(\n nvm_echo \"$@\" | command sed \"\n s/--progress-bar /--progress=bar /\n s/-s /-q /\n \"\n)\n"
=> "ARGS=$(\n\tnvm_echo \"$@\" | command sed \"\n s/--progress-bar /--progress=bar /\n s/-s /-q /\n \"\n)\n";
}
bash_format_ast_cases! {
formats_quoted_block_command_substitution_conditions_like_shfmt:
"f() {\n declare -i -r test_jobs_effective=\"$(\n if [[ \"${TEST_JOBS:-detect}\" = \"detect\" ]] \\\n && command -v nproc &> /dev/null; then\n nproc\n fi\n )\"\n}\n"
=> "f() {\n\tdeclare -i -r test_jobs_effective=\"$(\n\t\tif [[ \"${TEST_JOBS:-detect}\" = \"detect\" ]] &&\n\t\t\tcommand -v nproc &>/dev/null; then\n\t\t\tnproc\n\t\tfi\n\t)\"\n}\n";
formats_quoted_continued_command_substitution_lists_like_shfmt:
"f() {\n branchName=\"$(git symbolic-ref --quiet --short HEAD 2> /dev/null \\\n || git rev-parse --short HEAD 2> /dev/null \\\n || echo '(unknown)')\"\n}\n"
=> "f() {\n\tbranchName=\"$(git symbolic-ref --quiet --short HEAD 2>/dev/null ||\n\t\tgit rev-parse --short HEAD 2>/dev/null ||\n\t\techo '(unknown)')\"\n}\n";
indents_assignment_command_substitution_leading_pipe_continuations:
"f() {\n certText=$(echo \"${tmp}\" \\\n | openssl x509 -text -certopt \"no_header, no_serial, \\\n no_signame\")\n}\n"
=> "f() {\n\tcertText=$(echo \"${tmp}\" |\n\t\topenssl x509 -text -certopt \"no_header, no_serial, \\\n\t\tno_signame\")\n}\n";
indents_inline_command_substitution_backslash_continuations:
"f() {\n providers=\"$(find . |\n sed -e 's/^a/b/' \\\n -e 's/^c/d/')\"\n}\n"
=> "f() {\n\tproviders=\"$(find . |\n\t\tsed -e 's/^a/b/' \\\n\t\t\t-e 's/^c/d/')\"\n}\n";
indents_literal_assignment_command_substitution_pipeline_continuations:
"f() {\n while ok; do\n protected_branches=\"$protected_branches\n $(jq_debug_pipe_dump <<< \"$output\" |\n jq -r '.[] | select(.protected == true)')\"\n done\n}\n"
=> "f() {\n\twhile ok; do\n\t\tprotected_branches=\"$protected_branches\n $(jq_debug_pipe_dump <<<\"$output\" |\n\t\t\tjq -r '.[] | select(.protected == true)')\"\n\tdone\n}\n";
indents_command_substitution_continuations_after_multiline_literals:
"f() {\n allowed=\"$(sed 's/#.*//;\n s/^[[:space:]]*//;\n /^[[:space:]]*$/d;' \\\n \"$file\")\"\n}\n"
=> "f() {\n\tallowed=\"$(sed 's/#.*//;\n s/^[[:space:]]*//;\n /^[[:space:]]*$/d;' \\\n\t\t\"$file\")\"\n}\n";
indents_assignment_command_substitution_pipelines_after_multiline_literals:
"if [ \"$version\" = latest ]; then\n version=\"$(gh api \"repos/$owner_repo/tags\" \\\n --jq '\n .[] |\n select(.name | test(\"^go[0-9]\")) |\n .name\n ' --paginate |\n head -n1 |\n sed 's/^go//' || :)\"\nfi\n"
=> "if [ \"$version\" = latest ]; then\n\tversion=\"$(gh api \"repos/$owner_repo/tags\" \\\n\t\t--jq '\n .[] |\n select(.name | test(\"^go[0-9]\")) |\n .name\n ' --paginate |\n\t\thead -n1 |\n\t\tsed 's/^go//' || :)\"\nfi\n";
block_command_substitution_pipelines_keep_nested_indent:
"backups=\"$(\n while read -r mountpoint; do\n ls -t \"$mountpoint\" |\n sed '\n s|\\.backup/*$||;\n '\n done <<< \"$mountpoints\" |\n sort -r\n)\"\n"
=> "backups=\"$(\n\twhile read -r mountpoint; do\n\t\tls -t \"$mountpoint\" |\n\t\t\tsed '\n s|\\.backup/*$||;\n '\n\tdone <<<\"$mountpoints\" |\n\t\tsort -r\n)\"\n";
}
bash_format_ast_cases! {
block_command_substitution_pipeline_stage_after_multiline_literal_keeps_stage_indent:
"versions=\"$(\n grep rpm <<< \"$downloads_page\" |\n sed '\n s/^.*basic[[:alpha:]]*-//;\n s/linuxx64//;\n ' |\n sort -Vur\n)\"\n"
=> "versions=\"$(\n\tgrep rpm <<<\"$downloads_page\" |\n\t\tsed '\n s/^.*basic[[:alpha:]]*-//;\n s/linuxx64//;\n ' |\n\t\tsort -Vur\n)\"\n";
block_command_substitution_pipeline_after_command_continuations_keeps_stage_indent:
"artist_id=\"$(\n SEARCH_TYPE=artist \\\n SEARCH_LIMIT=50 \\\n \"$srcdir/search.sh\" \"$artist\" |\n jq -r \"\n .items[] |\n .id\n \" |\n head -n1\n)\"\n"
=> "artist_id=\"$(\n\tSEARCH_TYPE=artist \\\n\t\tSEARCH_LIMIT=50 \\\n\t\t\"$srcdir/search.sh\" \"$artist\" |\n\t\tjq -r \"\n .items[] |\n .id\n \" |\n\t\thead -n1\n)\"\n";
inline_assignment_command_substitution_pipeline_after_multiline_literal_keeps_body_indent:
"f() {\n packages=\"$(sed 's/#.*//;\n s/[<>=].*//;\n /^[[:space:]]*$/d;' $package_files |\n sort |\n uniq -d\n )\"\n}\n"
=> "f() {\n\tpackages=\"$(\n\t\tsed 's/#.*//;\n s/[<>=].*//;\n /^[[:space:]]*$/d;' $package_files |\n\t\t\tsort |\n\t\t\tuniq -d\n\t)\"\n}\n";
preserves_quoted_command_substitution_multiline_literals:
"f() {\n _comp_compgen_split -- \"$(cmd | _comp_awk '\n function islower(s) { return length(s) > 0 && s == tolower(s); }\n islower(substr($0, 1, 1)) {print $1}')\"\n}\n"
=> "f() {\n\t_comp_compgen_split -- \"$(cmd | _comp_awk '\n function islower(s) { return length(s) > 0 && s == tolower(s); }\n islower(substr($0, 1, 1)) {print $1}')\"\n}\n";
indents_block_command_substitution_assignments_with_multiline_literals:
"f() {\n gw=\"$(\n netstat -rn |\n awk '\n /^default/ { print $2 }\n '\n )\"\n}\n"
=> "f() {\n\tgw=\"$(\n\t\tnetstat -rn |\n\t\t\tawk '\n /^default/ { print $2 }\n '\n\t)\"\n}\n";
keeps_multiline_arithmetic_if_condition_then_attached:
"if ! (( BASH_VERSINFO[0] > 4 ||\n BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 2 )); then\n exit 1\nfi\n"
=> "if ! ((\\\nBASH_VERSINFO[0] > 4 || \\\nBASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 2)); then\n\texit 1\nfi\n";
formats_binary_spacing_around_command_substitution_arithmetic_operands:
"printf \"%s\\n\" \"$(($(foo)-bar))\"\n"
=> "printf \"%s\\n\" \"$(($(foo) - bar))\"\n";
}
bash_unchanged_ast_cases! {
preserves_inline_if_elif_command_substitutions:
"color=\"$(if [ \"$status\" = ok ]; then echo GREEN; elif [ \"$status\" = bad ]; then echo RED; else echo WHITE; fi)\"\n";
}
default_format_cases! {
formats_arithmetic_expansions_from_ruby_build:
"echo $(( ver[0]*100 + ver[1] ))\n"
=> "echo $((ver[0] * 100 + ver[1]))\n";
trims_arithmetic_command_delimiter_padding:
"if (( EUID == 0 )); then\n abort root\nfi\n"
=> "if ((EUID == 0)); then\n\tabort root\nfi\n";
formats_shell_style_variables_inside_arithmetic_expansions_like_shfmt:
"index=$(($index+1))\n"
=> "index=$(($index + 1))\n";
}
default_format_ast_cases! {
formats_arithmetic_for_init_assignment_spacing:
"for ((i=1;i<limit;++i)); do\n echo \"$i\"\ndone\nfor ((j = 1; ; j++)); do\n echo \"$j\"\ndone\n"
=> "for ((i = 1; i < limit; ++i)); do\n\techo \"$i\"\ndone\nfor ((j = 1; ; j++)); do\n\techo \"$j\"\ndone\n";
formats_arithmetic_command_assignment_spacing:
"((count+=1))\n((total = count + 1))\n((y=x+1))\nif ((${value:=0} == 1)); then\n return 0\nfi\n"
=> "((count += 1))\n((total = count + 1))\n((y = x + 1))\nif ((${value:=0} == 1)); then\n\treturn 0\nfi\n";
trims_command_substitution_padding_inside_arithmetic_expansions:
"echo $(( $( echo \"$speed\" | cut -d'k' -f1 ) * 1024 ))\nborder=$(( $( _system uptime days ) * 3 )) # daily\n"
=> "echo $(($(echo \"$speed\" | cut -d'k' -f1) * 1024))\nborder=$(($(_system uptime days) * 3)) # daily\n";
trims_arithmetic_expansion_padding_inside_double_quotes:
"echo \"$(( $(_system date unixtime) - DIFF ))\"\necho \"lasts $(( $t2 - $t1 )) seconds ($(( ($t2 - $t1) / 60 )) minutes)\"\n"
=> "echo \"$(($(_system date unixtime) - DIFF))\"\necho \"lasts $(($t2 - $t1)) seconds ($((($t2 - $t1) / 60)) minutes)\"\n";
}
default_unchanged_ast_cases! {
preserves_shell_style_variables_inside_arithmetic_expansions:
"index=$(($index + 1))\n";
preserves_command_substitutions_inside_arithmetic_expansions:
"echo $(($(echo \"$speed\" | cut -d'k' -f1) * 1024))\nborder=$(($(_system uptime days) * 3)) # daily\n";
}
default_format_ast_cases! {
keeps_array_subscript_arithmetic_compact_like_shfmt:
"x=${arr[$REPLY-1]}\ny=${arr[$(shuf -i 0-${#arr[@]} -n1) - 1]}\necho $((arr[i+1]*2))\necho $((a-1))\n"
=> "x=${arr[$REPLY-1]}\ny=${arr[$(shuf -i 0-${#arr[@]} -n1)-1]}\necho $((arr[i+1] * 2))\necho $((a - 1))\n";
}
bash_format_ast_cases! {
keeps_array_subscript_modulo_compact_like_shfmt:
"color=${AVAILABLE_COLORS[$RANDOM % ${#AVAILABLE_COLORS[@]}]}\n"
=> "color=${AVAILABLE_COLORS[$RANDOM%${#AVAILABLE_COLORS[@]}]}\n";
formats_arithmetic_expansion_array_subscripts_like_shfmt:
"echo ${options[$((choice*2+1))]}\n"
=> "echo ${options[$((choice * 2 + 1))]}\n";
formats_parenthesized_array_subscripts_like_shfmt:
"echo ${arr[(($i+1))]}\necho ${arr[((i+1))]}\necho ${arr[(i+1)]}\necho ${arr[($i+1)]}\necho ${arr[$i+1]}\n"
=> "echo ${arr[(($i + 1))]}\necho ${arr[((i + 1))]}\necho ${arr[(i + 1)]}\necho ${arr[($i + 1)]}\necho ${arr[$i+1]}\n";
keeps_nested_parameter_operand_subscripts_compact_like_shfmt:
": \"${BASH_IT_BASHRC:=${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}}\"\n"
=> ": \"${BASH_IT_BASHRC:=${BASH_SOURCE[${#BASH_SOURCE[@]}-1]}}\"\n";
keeps_plain_array_subscripts_compact_like_shfmt:
"prev=\"${COMP_WORDS[COMP_CWORD - 1]}\"\n"
=> "prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n";
keeps_identifier_array_subscript_arithmetic_compact_like_shfmt:
"source \"${_files[_file - __array_offset]}\"\n"
=> "source \"${_files[_file-__array_offset]}\"\n";
}
default_format_cases! {
formats_braced_shell_style_variables_inside_arithmetic_expansions_like_shfmt:
"echo $(( ${ver[0]}*100 + ${ver[1]} ))\n"
=> "echo $((${ver[0]} * 100 + ${ver[1]}))\n";
formats_arithmetic_expansions_from_pyenv_python_build:
"for arg in \"${@:$(( $package_type_nargs + 1 ))}\"; do\n echo \"$arg\"\ndone\n"
=> "for arg in \"${@:$(($package_type_nargs + 1))}\"; do\n\techo \"$arg\"\ndone\n";
normalizes_backtick_command_substitutions_to_dollar_paren:
"local computed_checksum=`echo \"$($checksum_command < \"$filename\")\" | tr [A-Z] [a-z]`\n"
=> "local computed_checksum=$(echo \"$($checksum_command <\"$filename\")\" | tr [A-Z] [a-z])\n";
}
bash_format_ast_cases! {
normalizes_backticks_inside_preserved_escaped_quote_words:
"eval printf \"\\\"$name -> $`echo \"${env_var}_DEFAULT\"` => \\\"\"\n"
=> "eval printf \"\\\"$name -> $$(echo \"${env_var}_DEFAULT\") => \\\"\"\n";
}
#[test]
fn backtick_in_redirect_is_idempotent() {
let source = "declare -x foo=1<\\`OF\nhi\nEF\n";
assert_idempotent(source, None, &ShellFormatOptions::default());
}
default_format_cases! {
preserves_backslash_continued_simple_commands_from_fzf_install:
"create_file \"$bind_file\" \\\n 'function fish_user_key_bindings' \\\n ' fzf --fish | source' \\\n 'end'\n"
=> "create_file \"$bind_file\" \\\n\t'function fish_user_key_bindings' \\\n\t' fzf --fish | source' \\\n\t'end'\n";
}
default_format_ast_cases! {
normalizes_indented_word_continuations_like_shfmt:
"cp -a \\\n docs README LICENSE\\\n $PKG/usr/doc\n"
=> "cp -a \\\n\tdocs README LICENSE $PKG/usr/doc\n";
preserves_word_continuation_without_space_before_backslash:
"printf '%s\\n' \\\n 'ime' 'desc'\\\n 'help' ''\n"
=> "printf '%s\\n' \\\n\t'ime' 'desc' \\\n\t'help' ''\n";
}
bash_unchanged_ast_cases! {
preserves_escaped_trailing_space_arguments:
"ARCH=$(uname -a | cut -f12 -d\\ )\n";
}
default_format_cases! {
preserves_backslash_continued_simple_commands_from_homebrew_install:
"\"$1\" --enable-frozen-string-literal --disable=gems,did_you_mean,rubyopt -rrubygems -e \\\n \"abort if Gem::Version.new(RUBY_VERSION) < \\\n Gem::Version.new('${REQUIRED_RUBY_VERSION}')\" 2>/dev/null\n"
=> "\"$1\" --enable-frozen-string-literal --disable=gems,did_you_mean,rubyopt -rrubygems -e \\\n\t\"abort if Gem::Version.new(RUBY_VERSION) < \\\n Gem::Version.new('${REQUIRED_RUBY_VERSION}')\" 2>/dev/null\n";
preserves_leading_redirect_placement_in_nvm_err_helpers:
"nvm_err() {\n >&2 nvm_echo \"$@\"\n}\n\nnvm_err_with_colors() {\n >&2 nvm_echo_with_colors \"$@\"\n}\n"
=> "nvm_err() {\n\t>&2 nvm_echo \"$@\"\n}\n\nnvm_err_with_colors() {\n\t>&2 nvm_echo_with_colors \"$@\"\n}\n";
}
default_unchanged_cases! {
preserves_inline_negated_subshell_conditions:
"if ! (try_curl \"$url\" || try_wget \"$url\"); then\n\treturn 1\nfi\n";
}
default_format_cases! {
negated_subshell_conditions_do_not_capture_later_file_comments:
"download() {\n local url\n url=https://github.com/junegunn/fzf/releases/download/v$version/${1}\n set -o pipefail\n if ! (try_curl $url || try_wget $url); then\n set +o pipefail\n binary_error=\"Failed to download with curl and wget\"\n return\n fi\n set +o pipefail\n}\n\n# Try to download binary executable\narchi=$(uname -smo 2> /dev/null || uname -sm)\n"
=> "download() {\n\tlocal url\n\turl=https://github.com/junegunn/fzf/releases/download/v$version/${1}\n\tset -o pipefail\n\tif ! (try_curl $url || try_wget $url); then\n\t\tset +o pipefail\n\t\tbinary_error=\"Failed to download with curl and wget\"\n\t\treturn\n\tfi\n\tset +o pipefail\n}\n\n# Try to download binary executable\narchi=$(uname -smo 2>/dev/null || uname -sm)\n";
preserves_else_branch_comments_inside_the_branch:
"if foo; then\n bar\nelse\n # branch comment\n baz\nfi\n"
=> "if foo; then\n\tbar\nelse\n\t# branch comment\n\tbaz\nfi\n";
preserves_outdented_else_branch_leading_comments:
"if foo; then\n bar\nelse\n baz=\n# disabled\nfi\n"
=> "if foo; then\n\tbar\nelse\n\tbaz=\n# disabled\nfi\n";
}
default_unchanged_cases! {
preserves_outdented_dangling_comments_before_fi:
"if outer; then\n\tif inner; then\n\t\tok\n\telse\n\t\tfallback\n\t# disabled\n\t# exit\n\tfi\nfi\n";
}
default_format_cases! {
preserves_space_indented_dangling_comments_before_fi:
"add_keys() {\n if [ \"$file\" = - ]; then\n file=/dev/stdin\n # sed reports this already\n #elif ! [ -f \"$file\" ]; then\n # die \"missing: $file\"\n fi\n}\n"
=> "add_keys() {\n\tif [ \"$file\" = - ]; then\n\t\tfile=/dev/stdin\n\t# sed reports this already\n\t#elif ! [ -f \"$file\" ]; then\n\t# die \"missing: $file\"\n\tfi\n}\n";
normalizes_underindented_dangling_comments_inside_case_bodies:
"case $x in\na)\nif outer; then\n\tif inner; then\n\t\tok\n\telse\n\t\t:\n\t\t# disabled\n\tfi\nfi\n;;\nesac\n"
=> "case $x in\na)\n\tif outer; then\n\t\tif inner; then\n\t\t\tok\n\t\telse\n\t\t\t:\n\t\t\t# disabled\n\t\tfi\n\tfi\n\t;;\nesac\n";
}
default_format_cases! {
formats_multiline_compound_assignments_structurally:
"directories=(\n bin\n etc\n Frameworks\n)\n"
=> "directories=(\n\tbin\n\tetc\n\tFrameworks\n)\n";
}
default_format_ast_cases! {
compound_assignment_command_substitution_heredoc_keeps_body_unindented:
"values=(\"$(\n cat <<EOF\nHello World\nTime is $(date)\nEOF\n)\")\n"
=> "values=(\"$(\n\tcat <<EOF\nHello World\nTime is $(date)\nEOF\n)\")\n";
compound_assignment_heredoc_does_not_close_on_indented_delimiter_text:
"values=(\"$(\n cat <<EOF\n\tEOF\nstill body\nEOF\n)\")\n"
=> "values=(\"$(\n\tcat <<EOF\n\tEOF\nstill body\nEOF\n)\")\n";
compound_assignment_tab_stripped_heredoc_keeps_valid_tab_indentation:
"values=(\"$(\n cat <<-EOF\n\tbody\n\tEOF\n)\")\n"
=> "values=(\"$(\n\tcat <<-EOF\n\tbody\n\tEOF\n)\")\n";
compound_assignment_quoted_heredoc_keeps_body_unindented:
"values=(\"$(\n cat << 'EOF'\nbody\nEOF\n)\")\n"
=> "values=(\"$(\n\tcat << 'EOF'\nbody\nEOF\n)\")\n";
preserves_inline_multiline_compound_assignment_delimiters:
"options=(path frozen without\n ssl_verify_mode system_bindir user_agent)\n"
=> "options=(path frozen without\n\tssl_verify_mode system_bindir user_agent)\n";
aligns_compound_assignment_command_substitution_close_suffixes:
"f() {\n case \"$prev\" in\n -a)\n COMPREPLY=($(compgen -W \"$(\n salt-key -l un --no-color\n salt-key -l rej --no-color\n )\" -- \"${cur}\"))\n ;;\n esac\n}\n"
=> "f() {\n\tcase \"$prev\" in\n\t-a)\n\t\tCOMPREPLY=($(compgen -W \"$(\n\t\t\tsalt-key -l un --no-color\n\t\t\tsalt-key -l rej --no-color\n\t\t)\" -- \"${cur}\"))\n\t\t;;\n\tesac\n}\n";
}
default_idempotent_cases! {
compound_assignment_command_substitution_heredoc_is_idempotent:
"values=(\"$(\n\tcat <<EOF\nHello World\nTime is $(date)\nEOF\n)\")\n",
"compound-heredoc.bash";
}
bash_format_ast_cases! {
removes_multiline_compound_assignment_line_continuations:
"if ok; then\n params+=(-Done=true -Dtwo=false \\\n -Dthree=false \\\n -Dfour=true)\nfi\n"
=> "if ok; then\n\tparams+=(-Done=true -Dtwo=false\n\t\t-Dthree=false\n\t\t-Dfour=true)\nfi\n";
preserves_escaped_multiline_double_quoted_compound_items:
"show() {\n items+=(\"\\\n $(printf \" ---------\")\n Text.\n\n $(\n for x in a b\n do\n echo \"$x\"\n done\n)\")\n}\n"
=> "show() {\n\titems+=(\"\\\n $(printf \" ---------\")\n Text.\n\n $(\n\t\tfor x in a b; do\n\t\t\techo \"$x\"\n\t\tdone\n\t)\")\n}\n";
strips_residual_indent_from_continued_array_rows:
"cmd=(\n grep -s \\\n -e \"^<${url}>\" \\\n -e \"^##\"\n)\n"
=> "cmd=(\n\tgrep -s\n\t-e \"^<${url}>\"\n\t-e \"^##\"\n)\n";
preserves_array_command_substitution_argument_continuations:
"x=( $(find . -not \\( -path ./x -prune \\) -not -name lib \\\n -not -name other | sort) )\n"
=> "x=($(find . -not \\( -path ./x -prune \\) -not -name lib \\\n\t-not -name other | sort))\n";
removes_decl_array_assignment_line_continuations:
"local cmd=(dialog --title \"Select\" --default-item \"$default\" \\\n --menu \"Choose\" 18 50 9)\n"
=> "local cmd=(dialog --title \"Select\" --default-item \"$default\"\n\t--menu \"Choose\" 18 50 9)\n";
normalizes_multiline_compound_assignment_row_spacing:
"options=(\n 1 \"1080p\" \"Set 1080p\"\n 2 \"720p\" \"Set 720p\"\n)\n"
=> "options=(\n\t1 \"1080p\" \"Set 1080p\"\n\t2 \"720p\" \"Set 720p\"\n)\n";
ignores_comments_for_multiline_compound_assignment_body_indent:
"versions=(1.16.0\n# Match the server package.\n 21.1.16)\n"
=> "versions=(1.16.0\n\t# Match the server package.\n\t21.1.16)\n";
preserves_blank_lines_inside_multiline_compound_assignments:
"args=(\n one\n\n # group\n two\n\n three\n)\n"
=> "args=(\n\tone\n\n\t# group\n\ttwo\n\n\tthree\n)\n";
normalizes_keyed_compound_assignment_row_indent:
"declare -A map=(\n [up]=one\n [down]=two\n\n [left]=three\n)\n"
=> "declare -A map=(\n\t[up]=one\n\t[down]=two\n\n\t[left]=three\n)\n";
normalizes_multiline_command_substitution_array_elements:
"items=(\nfirst\n $(\n for item in $items; do\n echo \"$item\"\n done\n )\n\n)\n"
=> "items=(\n\tfirst\n\t$(\n\t\tfor item in $items; do\n\t\t\techo \"$item\"\n\t\tdone\n\t)\n\n)\n";
keeps_single_quoted_command_substitution_literal_spacing_in_arrays:
"items=(\n '$( template )'\n)\n"
=> "items=(\n\t'$( template )'\n)\n";
normalizes_array_command_substitution_elements_like_shfmt:
"options=( \n config_file \"$(\n [[ \"$config\" == *.cfg ]] && echo ok\n )\"\n enabled \"$( [[ -n \"$flag\" ]] && echo true || echo false)\"\n)\n"
=> "options=(\n\tconfig_file \"$(\n\t\t[[ \"$config\" == *.cfg ]] && echo ok\n\t)\"\n\tenabled \"$([[ -n \"$flag\" ]] && echo true || echo false)\"\n)\n";
indents_array_append_command_substitution_elements_like_shfmt:
"f() {\n if ok; then\n opts+=(\"$(\n get x\n )\")\n fi\n}\n"
=> "f() {\n\tif ok; then\n\t\topts+=(\"$(\n\t\t\tget x\n\t\t)\")\n\tfi\n}\n";
preserves_compound_assignment_command_substitution_body_indent:
"f() {\n _files=($(\n while [[ \"$PWD\" != \"/\" ]]; do\n _file=\"$PWD/.env\"\n if [[ -e \"${_file}\" ]]; then\n echo \"${_file}\"\n fi\n builtin cd .. || true\n done\n ))\n}\n"
=> "f() {\n\t_files=($(\n\t\twhile [[ \"$PWD\" != \"/\" ]]; do\n\t\t\t_file=\"$PWD/.env\"\n\t\t\tif [[ -e \"${_file}\" ]]; then\n\t\t\t\techo \"${_file}\"\n\t\t\tfi\n\t\t\tbuiltin cd .. || true\n\t\tdone\n\t))\n}\n";
preserves_compound_assignment_command_substitution_command_continuations:
"f() {\n _remote_branches=($(\n git -C \"$path\" ls-remote --heads \"$url\" 2>/dev/null \\\n | LC_ALL=C sed \"s/.*\\///g\"\n ))\n _diff=($(\n printf \"%s\\n\" \\\n \"${_index_list[@]:-}\" \\\n \"${_file_list[@]:-}\" \\\n | sort \\\n | uniq -u\n ))\n}\n"
=> "f() {\n\t_remote_branches=($(\n\t\tgit -C \"$path\" ls-remote --heads \"$url\" 2>/dev/null |\n\t\t\tLC_ALL=C sed \"s/.*\\///g\"\n\t))\n\t_diff=($(\n\t\tprintf \"%s\\n\" \\\n\t\t\t\"${_index_list[@]:-}\" \\\n\t\t\t\"${_file_list[@]:-}\" |\n\t\t\tsort |\n\t\t\tuniq -u\n\t))\n}\n";
normalizes_array_command_substitution_leading_list_operators:
"f() {\n x=(\\\n $(printf \"%s\\n\" \"${xs[@]}\" \\\n | sort \\\n | cut -d: -f1 \\\n || true))\n}\n"
=> "f() {\n\tx=(\n\t\t$(printf \"%s\\n\" \"${xs[@]}\" |\n\t\t\tsort |\n\t\t\tcut -d: -f1 ||\n\t\t\ttrue))\n}\n";
strips_redirect_residual_indent_in_compound_assignment_command_substitutions:
"f() {\n _remote_branches=($(\n git -C \"$path\" ls-remote \\\n --heads \"$url\" \\\n 2>/dev/null \\\n | LC_ALL=C sed \"s/.*\\///g\" || :\n ))\n}\n"
=> "f() {\n\t_remote_branches=($(\n\t\tgit -C \"$path\" ls-remote \\\n\t\t\t--heads \"$url\" \\\n\t\t\t2>/dev/null |\n\t\t\tLC_ALL=C sed \"s/.*\\///g\" || :\n\t))\n}\n";
expands_multistatement_command_substitutions_in_array_values:
"f() {\n local -A ver=(\n [libx11]=\"$(. \"${TERMUX_SCRIPTDIR}/packages/libx11/build.sh\"; echo \"${TERMUX_PKG_VERSION}\")\"\n )\n}\n"
=> "f() {\n\tlocal -A ver=(\n\t\t[libx11]=\"$(\n\t\t\t. \"${TERMUX_SCRIPTDIR}/packages/libx11/build.sh\"\n\t\t\techo \"${TERMUX_PKG_VERSION}\"\n\t\t)\"\n\t)\n}\n";
expands_multistatement_assignment_command_substitutions:
"x=$(cd /tmp ; ls | wc -l )\n"
=> "x=$(\n\tcd /tmp\n\tls | wc -l\n)\n";
indents_quoted_block_command_substitution_loop_close:
"f() {\n eval \"$(\n for key in a b; do\n awk -F= \"/$key/\" <<< \"$profile_data\"\n done\n )\"\n}\n"
=> "f() {\n\teval \"$(\n\t\tfor key in a b; do\n\t\t\tawk -F= \"/$key/\" <<<\"$profile_data\"\n\t\tdone\n\t)\"\n}\n";
normalizes_raw_block_command_substitution_comment_indent:
"f() {\n value=\"$(\n docker-compose -f file.yml \\\n exec -T service cat secret </dev/null\n # keep this note with the command\n )\"\n}\n"
=> "f() {\n\tvalue=\"$(\n\t\tdocker-compose -f file.yml \\\n\t\t\texec -T service cat secret </dev/null\n\t\t# keep this note with the command\n\t)\"\n}\n";
normalizes_raw_block_command_substitution_shell_indent:
"f() {\n value=\"$(\n aws service call \\\n --query 'Items[]{\n \"Name\": Name\n }' \\\n --output json |\n jq -r \"\n .[]\n \"\n )\"\n}\n"
=> "f() {\n\tvalue=\"$(\n\t\taws service call \\\n\t\t\t--query 'Items[]{\n \"Name\": Name\n }' \\\n\t\t\t--output json |\n\t\t\tjq -r \"\n .[]\n \"\n\t)\"\n}\n";
indents_raw_block_command_substitution_pipeline_after_compound_close:
"regions=\"$(\n # choose enabled regions by default\n if [ -n \"${ALL_REGIONS:-}\" ]; then\n list_regions --all\n else\n list_regions\n fi |\n jq -r '.Regions[] | .Name'\n)\"\n"
=> "regions=\"$(\n\t# choose enabled regions by default\n\tif [ -n \"${ALL_REGIONS:-}\" ]; then\n\t\tlist_regions --all\n\telse\n\t\tlist_regions\n\tfi |\n\t\tjq -r '.Regions[] | .Name'\n)\"\n";
}
default_unchanged_cases! {
preserves_case_pattern_escapes:
"case \"$archi\" in\nDarwin\\ arm64*) download foo ;;\nesac\n";
}
default_format_ast_cases! {
preserves_comments_in_empty_case_items:
"case \"$x\" in\n1)\n# keep\n;;\nesac\n"
=> "case \"$x\" in\n1)\n\t# keep\n\t;;\nesac\n";
preserves_comments_after_final_case_terminator:
"case $key in\nfoo)\n echo foo\n ;;\n\n #if TestValue --function equals --value \"$value\" --search \"1\"; then\n # echo \"Found $value\"\n #else\n # echo \"Not found\"\n #fi\nesac\n"
=> "case $key in\nfoo)\n\techo foo\n\t;;\n\n\t#if TestValue --function equals --value \"$value\" --search \"1\"; then\n\t# echo \"Found $value\"\n\t#else\n\t# echo \"Not found\"\n\t#fi\nesac\n";
}
bash_format_ast_cases! {
case_item_comments_do_not_leak_to_previous_arm:
"case \"$arg\" in\n--squash-msg)\n SQUASH_MSG=1\n ;;\n*)\n # set the argument back\n set -- \"$@\" \"$arg\"\n ;;\nesac\n"
=> "case \"$arg\" in\n--squash-msg)\n\tSQUASH_MSG=1\n\t;;\n*)\n\t# set the argument back\n\tset -- \"$@\" \"$arg\"\n\t;;\nesac\n";
preserves_comments_before_case_patterns:
"case \"$1\" in\n# Fetch config\n--xsel | -b)\n INIT_CONFIG_VAL=$(xsel -b)\n ;;\n# Additional env vars\n-e | --env)\n CONTAINER_ENV+=(\"$2\")\n ;;\nesac\n"
=> "case \"$1\" in\n# Fetch config\n--xsel | -b)\n\tINIT_CONFIG_VAL=$(xsel -b)\n\t;;\n# Additional env vars\n-e | --env)\n\tCONTAINER_ENV+=(\"$2\")\n\t;;\nesac\n";
preserves_body_indented_comments_before_case_patterns:
"f() {\n case \"$prev\" in\n -G)\n echo grains\n ;;\n # FIXME\n -R)\n echo range\n ;;\n esac\n}\n"
=> "f() {\n\tcase \"$prev\" in\n\t-G)\n\t\techo grains\n\t\t;;\n\t\t# FIXME\n\t-R)\n\t\techo range\n\t\t;;\n\tesac\n}\n";
preserves_body_indented_comments_before_overindented_case_patterns:
"if [ -z \"$ARCH\" ]; then\n case \"$( uname -m )\" in\n arm*) ARCH=arm\n NO_ASM=1 ;;\n # comment\n *) ARCH=$( uname -m ) ;;\n esac\nfi\n"
=> "if [ -z \"$ARCH\" ]; then\n\tcase \"$(uname -m)\" in\n\tarm*)\n\t\tARCH=arm\n\t\tNO_ASM=1\n\t\t;;\n\t\t# comment\n\t*) ARCH=$(uname -m) ;;\n\tesac\nfi\n";
indents_disabled_case_arm_comments_before_next_pattern:
"f() {\n case \"$mode\" in\n client)\n echo client\n ;;\n#\t\thybrid)\n#\t\t\techo hybrid\n#\t\t;;\n *)\n echo default\n ;;\n esac\n}\n"
=> "f() {\n\tcase \"$mode\" in\n\tclient)\n\t\techo client\n\t\t;;\n\t\t#\t\thybrid)\n\t\t#\t\t\techo hybrid\n\t\t#\t\t;;\n\t*)\n\t\techo default\n\t\t;;\n\tesac\n}\n";
keeps_space_disabled_case_pattern_comments_at_pattern_indent:
"if [ -z \"$ARCH\" ]; then\n case \"$( uname -m )\" in\n# i?86) ARCH=i586 ;;\n arm*) ARCH=arm ;;\n esac\nfi\n"
=> "if [ -z \"$ARCH\" ]; then\n\tcase \"$(uname -m)\" in\n\t# i?86) ARCH=i586 ;;\n\tarm*) ARCH=arm ;;\n\tesac\nfi\n";
indents_space_disabled_case_pattern_comments_between_arms:
"if [ -z \"$ARCH\" ]; then\n case \"$( uname -m )\" in\n i?86) ARCH=i586 ;;\n# arm*) ARCH=arm ;;\n *) ARCH=$( uname -m ) ;;\n esac\nfi\n"
=> "if [ -z \"$ARCH\" ]; then\n\tcase \"$(uname -m)\" in\n\ti?86) ARCH=i586 ;;\n\t\t# arm*) ARCH=arm ;;\n\t*) ARCH=$(uname -m) ;;\n\tesac\nfi\n";
indents_aligned_disabled_case_arm_comments_before_next_pattern:
"case \"$ext\" in\n #.envrc) cd \"$dirname\" && direnv allow .\n .envrc) shellcheck \"$basename\"\n ;;\nesac\n"
=> "case \"$ext\" in\n#.envrc) cd \"$dirname\" && direnv allow .\n.envrc)\n\tshellcheck \"$basename\"\n\t;;\nesac\n";
keeps_disabled_case_comments_with_explanatory_prefix_comments:
"f() {\n case \"$ext\" in\n # this command does not fail when missing\n #.vimrc) if ! vim -c \"source $basename\" -c \"q\"; then\n .vimrc) echo ok\n ;;\n esac\n}\n"
=> "f() {\n\tcase \"$ext\" in\n\t# this command does not fail when missing\n\t#.vimrc) if ! vim -c \"source $basename\" -c \"q\"; then\n\t.vimrc)\n\t\techo ok\n\t\t;;\n\tesac\n}\n";
keeps_disabled_case_arm_body_comments_at_pattern_indent_without_hash_tabs:
"case \"$GUI\" in\n # disabled for now\n #QT) UI=Qt4\n #sed -i x\n #;;\n QT5) UI=Qt5 ;;\nesac\n"
=> "case \"$GUI\" in\n# disabled for now\n#QT) UI=Qt4\n#sed -i x\n#;;\nQT5) UI=Qt5 ;;\nesac\n";
keeps_aligned_disabled_case_patterns_at_pattern_indent:
"case ${FUNCTION} in\n \"equals\")\n CMP1=$(echo ${SEARCH} | tr '[:upper:]' '[:lower:]')\n if [ \"${CMP1}\" = \"${CMP1}\" ]; then RETVAL=0; else RETVAL=1; fi\n ;;\n #\"not-equal\") COLOR=$WHITE ;;\n #\"lt\" | \"less-than\") COLOR=$YELLOW ;;\n *) echo \"INVALID OPTION USED\"; exit 1 ;;\nesac\n"
=> "case ${FUNCTION} in\n\"equals\")\n\tCMP1=$(echo ${SEARCH} | tr '[:upper:]' '[:lower:]')\n\tif [ \"${CMP1}\" = \"${CMP1}\" ]; then RETVAL=0; else RETVAL=1; fi\n\t;;\n#\"not-equal\") COLOR=$WHITE ;;\n#\"lt\" | \"less-than\") COLOR=$YELLOW ;;\n*)\n\techo \"INVALID OPTION USED\"\n\texit 1\n\t;;\nesac\n";
preserves_prefix_comments_before_empty_case_items:
"case \"$LUA\" in\n # LUA=no: accept and do nothing\n no) ;;\n # Anything else is a fail\n *) echo fail ;;\nesac\n"
=> "case \"$LUA\" in\n# LUA=no: accept and do nothing\nno) ;;\n# Anything else is a fail\n*) echo fail ;;\nesac\n";
keeps_case_header_comments_before_inline_first_pattern:
"# For 15.0\n# otherwise cater to current\n#\ncase $(cmake --version | head -1) in 3.2*.*)\n echo old\n ;;\nesac\n"
=> "# For 15.0\n# otherwise cater to current\n#\ncase $(cmake --version | head -1) in 3.2*.*)\n\techo old\n\t;;\nesac\n";
}
default_format_ast_cases! {
preserves_blank_line_after_case_pattern:
"case $x in\na)\n\n echo a\n ;;\nesac\n"
=> "case $x in\na)\n\n\techo a\n\t;;\nesac\n";
preserves_blank_line_after_case_pattern_with_prefix_comments:
"case $x in\na)\n echo a\n ;;\n# disabled *)\n# note\n*)\n\n echo default\n ;;\nesac\n"
=> "case $x in\na)\n\techo a\n\t;;\n# disabled *)\n# note\n*)\n\n\techo default\n\t;;\nesac\n";
does_not_treat_comment_internal_blank_as_case_pattern_gap:
"case $x in\n*)\n # first\n\n # second\n echo a\n ;;\nesac\n"
=> "case $x in\n*)\n\t# first\n\n\t# second\n\techo a\n\t;;\nesac\n";
preserves_blank_line_before_esac:
"case $x in\na)\n echo a\n ;;\n\nesac\n"
=> "case $x in\na)\n\techo a\n\t;;\n\nesac\n";
preserves_blank_line_before_esac_after_missing_terminator:
"case $x in\n*) echo \"$x\"\n\nesac\n"
=> "case $x in\n*) echo \"$x\" ;;\n\nesac\n";
preserves_blank_line_before_case_item_terminator:
"case $x in\na)\n echo a\n\n ;;\nesac\n"
=> "case $x in\na)\n\techo a\n\n\t;;\nesac\n";
does_not_treat_comment_internal_blank_as_case_terminator_gap:
"case $x in\na)\n echo a\n\n # note\n ;;\nesac\n"
=> "case $x in\na)\n\techo a\n\n\t# note\n\t;;\nesac\n";
preserves_case_pattern_suffix_comments:
"case $x in\n*) # default branch\nbreak ;;\nesac\n"
=> "case $x in\n*) # default branch\n\tbreak ;;\nesac\n";
}
bash_format_ast_cases! {
adds_blank_line_after_multiline_case_patterns:
"case \"$1\" in\n --disable \\\n | --disable-http \\\n | --disable-https \\\n )\n apache_args+=(\"$1\")\n ;;\nesac\n"
=> "case \"$1\" in\n--disable | \\\n\t--disable-http | \\\n\t--disable-https)\n\n\tapache_args+=(\"$1\")\n\t;;\nesac\n";
trims_final_case_pattern_continuation_before_close_paren:
"case \"$1\" in\n *.xsl|\\\n *.[ch]\\\n ) pygmentize -f 256 \"$1\"\n ;;\nesac\n"
=> "case \"$1\" in\n*.xsl | \\\n\t*.[ch])\n\tpygmentize -f 256 \"$1\"\n\t;;\nesac\n";
keeps_attached_multiline_case_patterns_compact:
"case \"$1\" in\n --nginx-additional-configuration \\\n | --nginx-external-configuration)\n nginx_args+=(\"$1\")\n ;;\nesac\n"
=> "case \"$1\" in\n--nginx-additional-configuration | \\\n\t--nginx-external-configuration)\n\tnginx_args+=(\"$1\")\n\t;;\nesac\n";
}
default_unchanged_ast_cases! {
preserves_blank_line_between_case_items:
"case $x in\na) echo a ;;\n\nb) echo b ;;\nesac\n";
preserves_blank_line_after_case_in:
"case $x in\n\na) echo a ;;\nesac\n";
preserves_blank_line_after_case_in_comments:
"case $x in\n\n# next\na) echo a ;;\nesac\n";
preserves_blank_line_before_case_item_comments:
"case $x in\na) echo a ;;\n\n# next\nb) echo b ;;\nesac\n";
}
#[test]
fn aligns_case_pattern_suffix_comments_with_body_comments() {
let source = "case \"$NODENUMBER\" in\n\t100)\t# j4\n\t\t$IPT -I FORWARD -i $LANDEV -o $WIFIDEV\t# 2nd rule = up\n\t\t$IPT -I FORWARD -i $WIFIDEV -o $LANDEV\t# 1st rule = down\n\t;;\nesac\n";
let options = ShellFormatOptions::default();
let expected = format!(
"case \"$NODENUMBER\" in\n100){}# j4\n\t$IPT -I FORWARD -i $LANDEV -o $WIFIDEV # 2nd rule = up\n\t$IPT -I FORWARD -i $WIFIDEV -o $LANDEV # 1st rule = down\n\t;;\nesac\n",
" ".repeat(36)
);
assert_formats_to_with_ast(source, None, &options, expected);
}
default_unchanged_ast_cases! {
preserves_case_terminator_suffix_comments:
"case $x in\n*) return 0 ;; # not needed\nesac\n";
}
default_format_ast_cases! {
preserves_case_terminator_suffix_comment_alignment:
"case ${PAGE} in\n \"Folio\") W=612; H=936;; # 8.5 x 13 in.\n \"Quarto\") W=612, H=780;; # 8.5 x 10.8 in.\nesac\n"
=> "case ${PAGE} in\n\"Folio\")\n\tW=612\n\tH=936\n\t;; # 8.5 x 13 in.\n\"Quarto\") W=612, H=780 ;; # 8.5 x 10.8 in.\nesac\n";
}
#[test]
fn aligns_case_terminator_comments_by_contiguous_runs() {
let source = "case \"$match\" in\n\tolsr1) \t\techo \"u32 match $udp match ip dport 698 0xffff\" ;;\t# UDP dport 698\n\tolsr2) \t\techo \"u32 match $udp match ip dport 269 0xffff\" ;;\t# UDP dport 269\n\ttcp_with_ack) \techo \"u32 match $tcp match u8 0x10 0xff at nexthdr+13\" ;;\n\ttcp_with_ack2)\techo \"u32 match $tcp match u8 0x05 0x0f at 0 match u16 0x0000 0xffc0 at 2 match u8 0x10 0xff at 33\" ;; # wondershaper\n\tvoip1)\t\t_netfilter tc_match_voip_codec '00' ;;\t# PCMU\n\tvoip2)\t\t_netfilter tc_match_voip_codec '04' ;;\t# G723\nesac\n";
let options = ShellFormatOptions::default();
let tcp_with_ack2 = "tcp_with_ack2) echo \"u32 match $tcp match u8 0x05 0x0f at 0 match u16 0x0000 0xffc0 at 2 match u8 0x10 0xff at 33\" ;;";
let voip1 = "voip1) _netfilter tc_match_voip_codec '00' ;;";
let voip2 = "voip2) _netfilter tc_match_voip_codec '04' ;;";
let target_column = tcp_with_ack2.len() + 1;
assert_formats_to_with_ast(
source,
None,
&options,
format!(
"case \"$match\" in\nolsr1) echo \"u32 match $udp match ip dport 698 0xffff\" ;; # UDP dport 698\nolsr2) echo \"u32 match $udp match ip dport 269 0xffff\" ;; # UDP dport 269\ntcp_with_ack) echo \"u32 match $tcp match u8 0x10 0xff at nexthdr+13\" ;;\ntcp_with_ack2) echo \"u32 match $tcp match u8 0x05 0x0f at 0 match u16 0x0000 0xffc0 at 2 match u8 0x10 0xff at 33\" ;; # wondershaper\n{voip1}{}# PCMU\n{voip2}{}# G723\nesac\n",
" ".repeat(target_column - voip1.len()),
" ".repeat(target_column - voip2.len())
),
);
}
default_format_ast_cases! {
aligns_case_terminator_comments_after_pattern_pipe_spacing:
"case \"$mac\" in\n 19|'6470028b2260') PORT=7534 ;; # first\n 16|'6470028b1ba2') PORT= ;; # second\n 8|'f4ec38c9c32c') PORT=7783 ;; # third\nesac\n"
=> "case \"$mac\" in\n19 | '6470028b2260') PORT=7534 ;; # first\n16 | '6470028b1ba2') PORT= ;; # second\n8 | 'f4ec38c9c32c') PORT=7783 ;; # third\nesac\n";
aligns_case_terminator_comments_when_single_digit_patterns_have_source_padding:
"case \"$mac\" in\n\t19|'6470028b2260') PORT=7534 ;; # first\n\t16|'6470028b1ba2') PORT= ;; # second\n\t 8|'f4ec38c9c32c') PORT=7783 ;; # third\n\t 6|'b2487abecab8') PORT=7528 ;; # fourth\nesac\n"
=> "case \"$mac\" in\n19 | '6470028b2260') PORT=7534 ;; # first\n16 | '6470028b1ba2') PORT= ;; # second\n8 | 'f4ec38c9c32c') PORT=7783 ;; # third\n6 | 'b2487abecab8') PORT=7528 ;; # fourth\nesac\n";
preserves_case_suffix_comments_before_commented_compound_body:
"case $x in\n*) # default branch\n# explain\nif test -n \"$x\"; then\n echo \"$x\"\nfi\n;;\nesac\n"
=> "case $x in\n*) # default branch\n\t# explain\n\tif test -n \"$x\"; then\n\t\techo \"$x\"\n\tfi\n\t;;\nesac\n";
preserves_comment_after_case_in_keyword:
"case \"$( cut -d';' -f5 \"$FILE\" | md5sum )\" in # hash over costs\n\"$forced_hash\"*)\n _log ok\n;;\nesac\n"
=> "case \"$(cut -d';' -f5 \"$FILE\" | md5sum)\" in # hash over costs\n\"$forced_hash\"*)\n\t_log ok\n\t;;\nesac\n";
preserves_case_in_comment_containing_in_words:
"case $NETWORK in\t\t# new nodes start at $I, with registering until old nodes are in database\nffweimar) I=500 ;;\nesac\n"
=> "case $NETWORK in # new nodes start at $I, with registering until old nodes are in database\nffweimar) I=500 ;;\nesac\n";
}
default_unchanged_ast_cases! {
case_terminator_suffix_scan_handles_utf8_prefixes:
"# 不支持\ncase $x in\n*) echo ok ;;\nesac\n";
}
default_format_cases! {
preserves_brace_group_wrapper_comments:
"# c\n{ # note\na=1\n}\n"
=> "# c\n{ # note\n\ta=1\n}\n";
}
bash_format_ast_cases! {
preserves_nested_brace_group_after_case_body_open_comment:
"case \"$x\" in\na)\n grep x f && { # note\n {\n echo\n } >>\"$file\"\n } # note\n ;;\nesac\n"
=> "case \"$x\" in\na)\n\tgrep x f && { # note\n\t\t{\n\t\t\techo\n\t\t} >>\"$file\"\n\t}\n\t;;\nesac\n";
}
default_format_ast_cases! {
does_not_duplicate_leading_comments_inside_brace_groups:
"f() {\n # before group\n {\n # inside group\n echo ok\n }\n}\n"
=> "f() {\n\t# before group\n\t{\n\t\t# inside group\n\t\techo ok\n\t}\n}\n";
preserves_leading_comments_before_brace_group_pipelines:
"f() {\n # before group\n {\n echo \"$@\"\n } |\n cat\n}\n"
=> "f() {\n\t# before group\n\t{\n\t\techo \"$@\"\n\t} |\n\t\tcat\n}\n";
keeps_pipeline_rhs_brace_group_body_comments_inside_group:
"f() {\n {\n echo left\n } |\n {\n # inside group\n echo right\n }\n}\n"
=> "f() {\n\t{\n\t\techo left\n\t} |\n\t\t{\n\t\t\t# inside group\n\t\t\techo right\n\t\t}\n}\n";
keeps_same_line_pipeline_rhs_brace_group_attached:
"f() {\n {\n echo body\n } | {\n # Header\n cat\n } | {\n # Footer\n cat\n }\n}\n"
=> "f() {\n\t{\n\t\techo body\n\t} | {\n\t\t# Header\n\t\tcat\n\t} | {\n\t\t# Footer\n\t\tcat\n\t}\n}\n";
keeps_nested_same_line_pipeline_rhs_brace_group_attached:
"f() {\n {\n {\n echo body\n } || {\n echo fallback\n }\n } | {\n # Header\n cat\n } | {\n # Footer\n cat\n }\n}\n"
=> "f() {\n\t{\n\t\t{\n\t\t\techo body\n\t\t} || {\n\t\t\techo fallback\n\t\t}\n\t} | {\n\t\t# Header\n\t\tcat\n\t} | {\n\t\t# Footer\n\t\tcat\n\t}\n}\n";
}
default_format_cases! {
standalone_brace_groups_do_not_consume_later_file_comments:
"[ -n \"$x\" ] && {\nset -x\n}\n# later\nnext\n"
=> "[ -n \"$x\" ] && {\n\tset -x\n}\n# later\nnext\n";
}
default_unchanged_cases! {
preserves_single_line_function_bodies:
"tty_escape() { printf \"\\\\033[%sm\" \"$1\"; }\n";
preserves_single_line_nested_function_bodies:
"setup() { shellspec_type_name() { eval echo type_name ${1+'\"$@\"'}; }; }\n";
preserves_single_line_subshells:
"(cd \"$fzf_base\"/bin && rm -f fzf && ln -sf \"$which_fzf\" fzf)\n";
}
default_format_ast_cases! {
preserves_multiline_subshell_open_and_close_placement:
"if ok; then\n (mkdir -p -- \"$cachedir\" &&\n echo \"$cache_id_line\"$'\\n'\"$output\" >\"$cachefile\") 2>/dev/null\nfi\n"
=> "if ok; then\n\t(mkdir -p -- \"$cachedir\" &&\n\t\techo \"$cache_id_line\"$'\\n'\"$output\" >\"$cachefile\") 2>/dev/null\nfi\n";
preserves_multiline_subshell_around_loop:
"f() {\n (while sudo -v; do\n sleep 50\n done) &\n}\n"
=> "f() {\n\t(while sudo -v; do\n\t\tsleep 50\n\tdone) &\n}\n";
preserves_single_line_subshell_with_background_body:
"if ready; then\n ($REGEN_CMD &)\nfi\n"
=> "if ready; then\n\t($REGEN_CMD &)\nfi\n";
}
bash_format_ast_cases! {
expands_subshells_with_line_continuation_headers:
"(cd samples/ && \\\n find . -name \"build.sh\" -exec chmod 0755 {} \\;\n)\n"
=> "(\n\tcd samples/ &&\n\t\tfind . -name \"build.sh\" -exec chmod 0755 {} \\;\n)\n";
preserves_compound_redirect_continuations:
"(\n echo '#!/usr/bin/wish -f'\n cat completion.tcl\n sed '1,5d' $PRGNAM\n) \\\n >$PKG/usr/bin/$PRGNAM\n"
=> "(\n\techo '#!/usr/bin/wish -f'\n\tcat completion.tcl\n\tsed '1,5d' $PRGNAM\n) \\\n\t>$PKG/usr/bin/$PRGNAM\n";
}
default_format_cases! {
preserves_single_line_subshells_inside_case_bodies:
"build_package_pyston() {\n build_package_copy\n mkdir -p \"${PREFIX_PATH}/bin\" \"${PREFIX_PATH}/lib\"\n local bin\n shopt -s nullglob\n for bin in \"bin/\"*; do\n if [ -f \"${bin}\" ] && [ -x \"${bin}\" ] && [ ! -L \"${bin}\" ]; then\n case \"${bin##*/}\" in\n \"pyston\"* )\n ( cd \"${PREFIX_PATH}/bin\" && ln -fs \"${bin##*/}\" \"python\" )\n ;;\n esac\n fi\n done\n shopt -u nullglob\n}\n"
=> "build_package_pyston() {\n\tbuild_package_copy\n\tmkdir -p \"${PREFIX_PATH}/bin\" \"${PREFIX_PATH}/lib\"\n\tlocal bin\n\tshopt -s nullglob\n\tfor bin in \"bin/\"*; do\n\t\tif [ -f \"${bin}\" ] && [ -x \"${bin}\" ] && [ ! -L \"${bin}\" ]; then\n\t\t\tcase \"${bin##*/}\" in\n\t\t\t\"pyston\"*)\n\t\t\t\t(cd \"${PREFIX_PATH}/bin\" && ln -fs \"${bin##*/}\" \"python\")\n\t\t\t\t;;\n\t\t\tesac\n\t\tfi\n\tdone\n\tshopt -u nullglob\n}\n";
preserves_inline_group_redirect_suffixes:
"build_package_activepython() {\n local package_name=\"$1\"\n { bash \"install.sh\" --install-dir \"${PREFIX_PATH}\"\n } >&4 2>&1\n}\n"
=> "build_package_activepython() {\n\tlocal package_name=\"$1\"\n\t{\n\t\tbash \"install.sh\" --install-dir \"${PREFIX_PATH}\"\n\t} >&4 2>&1\n}\n";
trailing_comments_on_function_closing_braces_do_not_poison_following_layout:
"foo() {\necho hi\n} # trailing\nbar\n"
=> "foo() {\n\techo hi\n} # trailing\nbar\n";
}
default_unchanged_ast_cases! {
preserves_escaped_command_names:
"\\grep -q foo file\n";
preserves_ansi_c_quoted_assignment_values:
"x=$'\\n'\n";
preserves_concatenated_ansi_c_quoted_assignment_values:
"local excluded=$'\\ndefault\\n'${prefix//:/foo}\n";
preserves_concatenated_ansi_c_quoted_arguments:
"echo \"$cache_id_line\"$'\\n'\"$output\" >\"$cachefile\"\n";
preserves_concatenated_ansi_c_and_escaped_double_quoted_arguments:
"echo $'\\n'\"TERMUX_APP_PACKAGE: \\\"$TERMUX_APP_PACKAGE\\\"\"\n";
preserves_ansi_c_quoted_condition_patterns:
"[[ \"$c\" == $'\\r' || \"$c\" == $'\\n' ]]\n";
preserves_multiline_function_bodies:
"version_gt() {\n\t[[ \"${1%.*}\" -gt \"${2%.*}\" ]]\n}\n";
}
bash_unchanged_ast_cases! {
heredoc_function_close_suffix_keeps_closing_brace:
"foo() {\ncat <<EOF\nbody\nEOF\n} # trailing\nbar() { :; }\n";
preserves_alias_expanded_simple_commands_verbatim:
"shopt -s expand_aliases\nalias die='EXIT=$? LINE=$LINENO error_exit'\ndie \"A problem occured.\"\n";
preserves_alias_expanded_commands_before_case_terminators:
"shopt -s expand_aliases\nalias die='EXIT=$? LINE=$LINENO error_exit'\ncase $CLASS in\n*) false || die \"Invalid storage class.\" ;;\nesac\n";
}
default_format_ast_cases! {
preserves_leading_comments_inside_function_bodies:
"function f() {\n # parse all defined shortcuts ${BASH_IT_DIRS_BKS}\n if [[ -s x ]]; then\n echo yes\n fi\n}\n"
=> "function f() {\n\t# parse all defined shortcuts ${BASH_IT_DIRS_BKS}\n\tif [[ -s x ]]; then\n\t\techo yes\n\tfi\n}\n";
}
default_unchanged_ast_cases! {
preserves_process_substitution_redirect_spacing:
"cat < <(which -a foo)\n";
}
bash_format_ast_cases! {
preserves_process_substitution_redirect_spacing_inside_command_substitutions:
"output=\"$(\n exec > >(tee /dev/fd/2) 2>&1\n)\"\nlatest=\"$(grep x < <(jq -r '.body' <<< \"$response\"))\"\n"
=> "output=\"$(\n\texec > >(tee /dev/fd/2) 2>&1\n)\"\nlatest=\"$(grep x < <(jq -r '.body' <<<\"$response\"))\"\n";
}
default_format_ast_cases! {
formats_redirect_spacing_inside_process_substitution:
"read -ra candidates < <(complete words 2> /dev/null)\n"
=> "read -ra candidates < <(complete words 2>/dev/null)\n";
}
default_unchanged_ast_cases! {
preserves_process_substitution_attached_after_equals:
"setfacl --restore=<(grep -E -v '^# (owner|group):' \"$tmp_file\")\n";
preserves_inline_multiline_process_substitution_continuations:
"while read -r line; do\n\techo \"$line\"\ndone < <(comm -23 <(printf \"%s\\n\" \"${left[@]}\" | sort) \\\n\t<(printf \"%s\\n\" \"${right[@]}\" | sort))\n";
}
default_format_ast_cases! {
normalizes_inline_process_substitution_source_indent:
"unsetall() {\n while read -r env_var; do\n unset \"$env_var\"\n done < <( env |\n grep -i \"$match\" |\n sed 's/=.*//' )\n}\n"
=> "unsetall() {\n\twhile read -r env_var; do\n\t\tunset \"$env_var\"\n\tdone < <(env |\n\t\tgrep -i \"$match\" |\n\t\tsed 's/=.*//')\n}\n";
}
bash_unchanged_ast_cases! {
aligns_multiline_single_quoted_process_substitution_words:
"_sqlmap() {\n\tif [[ \"$cur\" == * ]]; then\n\t\twhile IFS='' read -r line; do COMPREPLY+=(\"$line\"); done < <(\n\t\t\tcompgen -W '-h --help \\\n\t\t\t--data --cookie \\\n\t\t\t--wizard' -- \"$cur\"\n\t\t)\n\tfi\n}\n";
}
default_format_ast_cases! {
formats_process_substitution_with_own_line_close_as_block:
"while read x; do\n :\ndone < <(cmd | \\\n awk 'BEGIN {x=0} /Sink/ {\n x=$1\n }'\n )\n"
=> "while read x; do\n\t:\ndone < <(\n\tcmd |\n\t\tawk 'BEGIN {x=0} /Sink/ {\n x=$1\n }'\n)\n";
}
bash_format_ast_cases! {
formats_process_substitution_heredocs_as_blocks:
"curl -d @<(cat <<EOF\nbody\nEOF\n)\n"
=> "curl -d @<(\n\tcat <<EOF\nbody\nEOF\n)\n";
}
default_unchanged_ast_cases! {
preserves_block_process_substitution_source_indentation:
"while read -r line; do\n\techo \"$line\"\ndone < <(\n\tprintf \"%s\\n\" \"${items[@]}\"\n)\n";
}
default_format_ast_cases! {
normalizes_block_process_substitution_source_indent:
"while read -r line; do\n echo \"$line\"\ndone < <(\n produce_items\n)\n"
=> "while read -r line; do\n\techo \"$line\"\ndone < <(\n\tproduce_items\n)\n";
}
bash_format_ast_cases! {
expands_multistatement_process_substitutions_as_blocks:
"while read game; do\n echo \"$game\"\ndone < <(_get_opts; echo -e \"a\\nb\")\n"
=> "while read game; do\n\techo \"$game\"\ndone < <(\n\t_get_opts\n\techo -e \"a\\nb\"\n)\n";
normalizes_process_substitution_block_indent_from_partial_source_indent:
"if ok; then\n cat < <(produce; consume)\nfi\n"
=> "if ok; then\n\tcat < <(\n\t\tproduce\n\t\tconsume\n\t)\nfi\n";
keeps_inline_process_substitution_brace_group_attached:
"while read -r line; do\n menu+=(\"$line\")\ndone < <( { echo \"$a\"; echo \"$b\"; } | sort -u )\n"
=> "while read -r line; do\n\tmenu+=(\"$line\")\ndone < <({\n\techo \"$a\"\n\techo \"$b\"\n} | sort -u)\n";
does_not_duplicate_process_substitution_comments_before_pipeline_rhs:
"while read -r item; do\n echo \"$item\"\ndone < <(\n # note\n produce_items\n) |\nconsume_items\n"
=> "while read -r item; do\n\techo \"$item\"\ndone < <(\n\t# note\n\tproduce_items\n) |\n\tconsume_items\n";
indents_process_substitution_pipeline_comments:
"cat < <(\n produce_items |\n # keep this filter documented\n filter_items |\n sort_items\n)\n"
=> "cat < <(\n\tproduce_items |\n\t\t# keep this filter documented\n\t\tfilter_items |\n\t\tsort_items\n)\n";
aligns_raw_pipeline_comments_after_continuation_stages:
"cat < <(\n produce_items |\n filter_items |\n # keep this filter documented\n normalize_items |\n sort_items\n)\n"
=> "cat < <(\n\tproduce_items |\n\t\tfilter_items |\n\t\t# keep this filter documented\n\t\tnormalize_items |\n\t\tsort_items\n)\n";
compacts_raw_process_substitution_semicolon_terminators:
"cat < <(\n produce_items |\n # keep this filter documented\n { filter_items || : ; } |\n sort_items\n)\n"
=> "cat < <(\n\tproduce_items |\n\t\t# keep this filter documented\n\t\t{ filter_items || :; } |\n\t\tsort_items\n)\n";
}
bash_format_ast_cases! {
keeps_nested_process_substitution_close_indent_in_raw_command_substitutions:
"urls=\"$(\n while read -r filename; do\n # keep comment with raw block path\n { grep -E \"$url_regex\" \"$filename\" || : ; } |\n if [ -n \"${URL_LINKS_IGNORED:-}\" ]; then\n grep -Eivf <(\n tr '[:space:]' '\\n' <<< \"$URL_LINKS_IGNORED\" |\n sed 's/^[[:space:]]*//;\n s/[[:space:]]*$//;\n /^[[:space:]]*$/d'\n )\n else\n cat\n fi\n done |\n sort -uf\n)\"\n"
=> "urls=\"$(\n\twhile read -r filename; do\n\t\t# keep comment with raw block path\n\t\t{ grep -E \"$url_regex\" \"$filename\" || :; } |\n\t\t\tif [ -n \"${URL_LINKS_IGNORED:-}\" ]; then\n\t\t\t\tgrep -Eivf <(\n\t\t\t\t\ttr '[:space:]' '\\n' <<<\"$URL_LINKS_IGNORED\" |\n\t\t\t\t\t\tsed 's/^[[:space:]]*//;\n s/[[:space:]]*$//;\n /^[[:space:]]*$/d'\n\t\t\t\t)\n\t\t\telse\n\t\t\t\tcat\n\t\t\tfi\n\tdone |\n\t\tsort -uf\n)\"\n";
indents_continued_process_substitution_comments_once:
"cmd \\\n<(\n produce |\n sort #|\n # keep the sorted stream documented\n # before the process substitution closes\n) \\\n<(\n # describe target stream\n consume\n) |\nsed 's/x/y/'\n"
=> "cmd \\\n\t<(\n\t\tproduce |\n\t\t\tsort #|\n\t\t# keep the sorted stream documented\n\t\t# before the process substitution closes\n\t) \\\n\t<(\n\t\t# describe target stream\n\t\tconsume\n\t) |\n\tsed 's/x/y/'\n";
preserves_raw_block_multiline_literal_payloads:
"value=\"$(\n produce_items |\n sed '\n s/a/b ;\n ' |\n consume_items\n)\"\n"
=> "value=\"$(\n\tproduce_items |\n\t\tsed '\n s/a/b ;\n ' |\n\t\tconsume_items\n)\"\n";
shifts_raw_pipeline_compound_bodies_with_continuation:
"value=\"$(\n produce_items |\n # keep this filter documented\n while read -r item; do\n consume_item \"$item\"\n done || :\n)\"\n"
=> "value=\"$(\n\tproduce_items |\n\t\t# keep this filter documented\n\t\twhile read -r item; do\n\t\t\tconsume_item \"$item\"\n\t\tdone || :\n)\"\n";
}
default_unchanged_ast_cases! {
preserves_fd_duplication_redirect_targets:
"cmd 2>&$fd\ncmd 1>&/dev/null\ncmd >&file\n";
preserves_append_both_redirect_spelling:
"cmd &>>/dev/null\ncmd &>>log <input\n";
}
bash_unchanged_ast_cases! {
preserves_adjacent_numeric_fd_heredoc_redirects:
"exec \"${SHELL:-sh}\" -i 3<<EOF 4<&0 <&3\n set +e\nEOF\n";
preserves_fd_close_redirect_targets:
"cmd 2>&-\nexec <&-\nexec {ACCEPT_FD}>&-\n";
preserves_multi_digit_fd_duplication_redirect_prefixes:
"exec 99>&1\nexec 99>&-\nread 42<&0\n";
}
default_unchanged_ast_cases! {
preserves_explicit_stdout_fd_on_dup_redirects:
"cat 1>&2 <<EOF\nhi\nEOF\n";
preserves_simple_command_redirect_positions:
"echo >&2 \"bad news\"\n";
}
bash_format_ast_cases! {
separates_numeric_word_before_output_both_redirects:
"echo \"Usage\" 1&>2\n"
=> "echo \"Usage\" 1 &>2\n";
moves_interspersed_simple_command_redirects_after_arguments:
"curl -sSf >\"$jar\" \"$url\"\ncmd a >out b 2>err c\ncmd >out a 2>err b\n"
=> "curl -sSf \"$url\" >\"$jar\"\ncmd a b c >out 2>err\ncmd >out a b 2>err\n";
preserves_time_command_trailing_comment_after_redirect:
"time nice ffmpeg -i \"$filepath\" \"$mp4_filepath\" < /dev/null # note\n"
=> "time nice ffmpeg -i \"$filepath\" \"$mp4_filepath\" </dev/null # note\n";
}
default_unchanged_ast_cases! {
preserves_regex_operands_in_conditionals:
"[[ \"$x\" =~ \"git version \"([^ ]*).* ]]\n";
}
bash_format_ast_cases! {
preserves_regex_alternation_operands_in_conditionals:
"if [[ $line =~ \\<(target|extension-point)[[:space:]].*name=[\\\"\\']([^\\\"\\']+) ]]; then\n echo \"${BASH_REMATCH[2]}\"\nfi\n"
=> "if [[ $line =~ \\<(target|extension-point)[[:space:]].*name=[\\\"\\']([^\\\"\\']+) ]]; then\n\techo \"${BASH_REMATCH[2]}\"\nfi\n";
preserves_escaped_spaces_in_conditional_regex_operands:
"if [[ \"$line\" =~ ^=\\ ]]; then\n echo ok\nfi\nif [[ ! \"$line\" =~ ^=\\ ]] && [[ \"$n\" -gt 20 ]]; then\n break\nfi\n"
=> "if [[ \"$line\" =~ ^=\\ ]]; then\n\techo ok\nfi\nif [[ ! \"$line\" =~ ^=\\ ]] && [[ \"$n\" -gt 20 ]]; then\n\tbreak\nfi\n";
}
#[test]
fn preserves_explicit_line_break_after_list_operator() {
let source = "command -v curl >/dev/null &&\n if [[ $1 =~ tar.gz$ ]]; then\n curl -fL $1 | tar $tar_opts\n else\n echo nope\n fi\n";
let options = ShellFormatOptions::default().with_dialect(ShellDialect::Bash);
assert_formats_to(
source,
Some(Path::new("list_operator_if.bash")),
&options,
"command -v curl >/dev/null &&\n\tif [[ $1 =~ tar.gz$ ]]; then\n\t\tcurl -fL $1 | tar $tar_opts\n\telse\n\t\techo nope\n\tfi\n",
);
}
bash_format_ast_cases! {
preserves_list_break_after_multiline_condition:
"f() {\n [[ ! -f \"$cert_file\" ||\n \"$cert_file\" -ot /one ||\n \"$cert_file\" -ot /two\n ]] || (( ${force:-0} > 0 ))\n}\n"
=> "f() {\n\t[[ ! -f \"$cert_file\" ||\n\t\t\"$cert_file\" -ot /one ||\n\t\t\"$cert_file\" -ot /two ]] ||\n\t\t((${force:-0} > 0))\n}\n";
keeps_inline_list_rhs_after_wrapped_conditional:
"f() {\n [[ \"${show:-}\" != true ||\n -z \"$(which todo.sh)\" ]] && return\n}\n"
=> "f() {\n\t[[ \"${show:-}\" != true ||\n\t\t-z \"$(which todo.sh)\" ]] && return\n}\n";
preserves_explicit_line_break_when_list_operator_starts_continued_line:
"_command_exists goenv \\\n || [[ -x \"$GOENV_ROOT/bin/goenv\" ]] \\\n || return 0\n"
=> "_command_exists goenv ||\n\t[[ -x \"$GOENV_ROOT/bin/goenv\" ]] ||\n\treturn 0\n";
}
bash_unchanged_ast_cases! {
keeps_inline_list_operator_before_multiline_brace_group:
r#"function S() {
about 'save a bookmark'
param '1: bookmark name'
example '$ S mybkmrk'
group 'dirs'
[[ $# -eq 1 ]] || {
echo "${FUNCNAME[0]} function requires 1 argument"
return 1
}
echo "$1"=\""${PWD}"\" >>"${BASH_IT_DIRS_BKS?}"
}
function R() {
about 'remove a bookmark'
param '1: bookmark name'
example '$ R mybkmrk'
group 'dirs'
[[ $# -eq 1 ]] || {
echo "${FUNCNAME[0]} function requires 1 argument"
return 1
}
}
"#;
}
bash_format_ast_cases! {
keeps_multiline_condition_list_operator_before_brace_group:
"while [[ -n \"$x\" ]] &&\n ! {\n [[ -d \"$x\" ]] &&\n [[ -f \"$x\" ]]\n } && {\n {\n [[ \"$x\" =~ ^/ ]] &&\n [[ \"$x\" != / ]]\n } || {\n [[ \"$x\" != /tmp ]]\n }\n }; do\n x=\"${x%/*}\"\ndone\n"
=> "while [[ -n \"$x\" ]] &&\n\t! {\n\t\t[[ -d \"$x\" ]] &&\n\t\t\t[[ -f \"$x\" ]]\n\t} && {\n\t{\n\t\t[[ \"$x\" =~ ^/ ]] &&\n\t\t\t[[ \"$x\" != / ]]\n\t} || {\n\t\t[[ \"$x\" != /tmp ]]\n\t}\n}; do\n\tx=\"${x%/*}\"\ndone\n";
}
format_cases_with_options! {
formats_completion_function_subscripts_and_case_indent_like_shfmt:
ShellFormatOptions::default().with_dialect(ShellDialect::Bash),
r#"_saltkey() {
local cur prev opts prev pprev
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD - 1]}"
if [ "${COMP_CWORD}" -gt 2 ]; then
pprev="${COMP_WORDS[COMP_CWORD - 2]}"
fi
case "${prev}" in
-a | --accept)
COMPREPLY=($(compgen -W "$(
salt-key -l un --no-color
salt-key -l rej --no-color
)" -- "${cur}"))
return 0
;;
esac
return 0
}
"#
=> r#"_saltkey() {
local cur prev opts prev pprev
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [ "${COMP_CWORD}" -gt 2 ]; then
pprev="${COMP_WORDS[COMP_CWORD-2]}"
fi
case "${prev}" in
-a | --accept)
COMPREPLY=($(compgen -W "$(
salt-key -l un --no-color
salt-key -l rej --no-color
)" -- "${cur}"))
return 0
;;
esac
return 0
}
"#;
}
bash_format_ast_cases! {
formats_case_command_substitution_array_assignment_like_shfmt:
r#"_genkernel() {
declare args rhs
args=( $(case $args in
('<0-5>') compgen -W "$(echo {1..5})" -- "$rhs" ;;
('<outfile>'|'<file>') compgen -A file -o plusdirs -- "$rhs" ;;
(*) compgen -o bashdefault -- "$rhs" ;; # punt
esac) )
}
"#
=> r#"_genkernel() {
declare args rhs
args=($(case $args in
'<0-5>') compgen -W "$(echo {1..5})" -- "$rhs" ;;
'<outfile>' | '<file>') compgen -A file -o plusdirs -- "$rhs" ;;
*) compgen -o bashdefault -- "$rhs" ;; # punt
esac))
}
"#;
}
default_format_ast_cases! {
preserves_explicit_multiline_pipeline_by_default:
"kubectl get secrets |\n grep -v '^NAME[[:space:]]' |\n awk '{print $1}'\n"
=> "kubectl get secrets |\n\tgrep -v '^NAME[[:space:]]' |\n\tawk '{print $1}'\n";
keeps_pipeline_blank_before_disabled_comment_block:
"produce_json |\n\n#if disabled; then\n# old_filter\n#fi\n\njq -r '.items[]'\n"
=> "produce_json |\n\n\t#if disabled; then\n\t# old_filter\n\t#fi\n\tjq -r '.items[]'\n";
preserves_comments_between_pipeline_and_compound_command:
"while read -r value; do\n echo \"$value\"\ndone |\n# keep alternate implementation note\nif type -P helper >/dev/null; then\n helper\nelse\n cat\nfi\n"
=> "while read -r value; do\n\techo \"$value\"\ndone |\n\t# keep alternate implementation note\n\tif type -P helper >/dev/null; then\n\t\thelper\n\telse\n\t\tcat\n\tfi\n";
preserves_explicit_multiline_pipeline_when_operator_starts_continued_line:
"find $PKG -print0 | xargs -0 file | grep ELF \\\n | cut -f 1 -d : | xargs strip --strip-unneeded 2> /dev/null || true\n"
=> "find $PKG -print0 | xargs -0 file | grep ELF |\n\tcut -f 1 -d : | xargs strip --strip-unneeded 2>/dev/null || true\n";
preserves_continued_redirect_targets:
"sed s/x/y/ in > \\\n out\n"
=> "sed s/x/y/ in > \\\n\tout\n";
preserves_for_in_first_word_continuation:
"for net_mount in \\\n ${HOST_MOUNTS_RO} ${HOST_MOUNTS} \\\n '/dev' '/proc'; do\n echo \"$net_mount\"\ndone\n"
=> "for net_mount in \\\n\t${HOST_MOUNTS_RO} ${HOST_MOUNTS} \\\n\t'/dev' '/proc'; do\n\techo \"$net_mount\"\ndone\n";
preserves_comment_after_loop_do_without_raw_body_fallback:
"for J in \"${I}\"/*; do # iterate over folders in a safe way\n FIND=$(echo \"${J}\")\n if [ -f \"${J}\" ]; then\n echo \"${FIND}\"\n fi\ndone\n"
=> "for J in \"${I}\"/*; do # iterate over folders in a safe way\n\tFIND=$(echo \"${J}\")\n\tif [ -f \"${J}\" ]; then\n\t\techo \"${FIND}\"\n\tfi\ndone\n";
preserves_inline_do_if_body_layout:
"for ITEM in ${LIST}; do if DirectoryExists ${ITEM}; then FOUND=1; break; fi; done\n"
=> "for ITEM in ${LIST}; do if DirectoryExists ${ITEM}; then\n\tFOUND=1\n\tbreak\nfi; done\n";
preserves_brace_group_attached_after_pipeline_operator:
"link=$(cat \"${postdetailslog}\" | {\n nc -w 3 termbin.com 9999\n echo $? > /tmp/nc_exit_status\n} | tr -d '\\n\\0')\n"
=> "link=$(cat \"${postdetailslog}\" | {\n\tnc -w 3 termbin.com 9999\n\techo $? >/tmp/nc_exit_status\n} | tr -d '\\n\\0')\n";
}
bash_format_ast_cases! {
keeps_pipeline_continuation_at_list_rhs_indent:
"if true; then\n ffmpeg \\\n && convert GIF:- \\\n | gifsicle > out || return 2\nfi\n"
=> "if true; then\n\tffmpeg &&\n\t\tconvert GIF:- |\n\t\tgifsicle >out || return 2\nfi\n";
expands_inline_command_substitution_pipeline_brace_groups:
"f() {\n title=\"$(curl -sS --fail \"$url\" | { head -n1 | sed 's/^#*//'; cat >/dev/null; } )\"\n}\n"
=> "f() {\n\ttitle=\"$(curl -sS --fail \"$url\" | {\n\t\thead -n1 | sed 's/^#*//'\n\t\tcat >/dev/null\n\t})\"\n}\n";
}
default_format_cases! {
preserves_comments_between_pipeline_commands:
"dat() {\n find . -type f |\n # keep this filter\n grep -v patch |\n sort\n}\n"
=> "dat() {\n\tfind . -type f |\n\t\t# keep this filter\n\t\tgrep -v patch |\n\t\tsort\n}\n";
}
default_unchanged_ast_cases! {
preserves_for_targets_inside_inline_command_substitutions:
"pass=\"$(for i in $(eval \"echo {1..$length}\"); do pickfrom /usr/share/dict/words; done)\"\n";
preserves_inline_then_if_body_layout:
"if [ -n \"${TMPFILE}\" ]; then if [ -f ${TMPFILE} ]; then rm -f ${TMPFILE}; fi; fi\n";
}
bash_format_ast_cases! {
indents_raw_command_substitution_brace_group_bodies:
"items=\"$(\n {\n # primary items\n produce_items |\n sort_items\n }\n)\"\n"
=> "items=\"$(\n\t{\n\t\t# primary items\n\t\tproduce_items |\n\t\t\tsort_items\n\t}\n)\"\n";
indents_raw_command_substitution_pipeline_continuations:
"url=\"$(\n git remote -v |\n awk '{print $2}' |\n head -n 1\n)\"\n"
=> "url=\"$(\n\tgit remote -v |\n\t\tawk '{print $2}' |\n\t\thead -n 1\n)\"\n";
indents_raw_command_substitution_pipeline_before_multiline_literal_command:
"if ok; then\n url=\"$(\n git remote -v |\n awk '{print $2}' |\n perl -pe \"\n s/foo/bar/\n \"\n )\"\nfi\n"
=> "if ok; then\n\turl=\"$(\n\t\tgit remote -v |\n\t\t\tawk '{print $2}' |\n\t\t\tperl -pe \"\n s/foo/bar/\n \"\n\t)\"\nfi\n";
expands_nested_inline_command_substitutions_inside_raw_blocks:
"value=\"$(\n {\n sort |\n uniq -d\n } |\n grep -vi $(IFS=$'\\n'; for line in $ignored_lines_regex; do [[ \"$line\" =~ ^[[:space:]]*$ ]] && continue; printf \"%s\" \" -e '$line'\"; done)\n)\"\n"
=> "value=\"$(\n\t{\n\t\tsort |\n\t\t\tuniq -d\n\t} |\n\t\tgrep -vi $(\n\t\t\tIFS=$'\\n'\n\t\t\tfor line in $ignored_lines_regex; do\n\t\t\t\t[[ \"$line\" =~ ^[[:space:]]*$ ]] && continue\n\t\t\t\tprintf \"%s\" \" -e '$line'\"\n\t\t\tdone\n\t\t)\n)\"\n";
normalizes_nested_command_substitution_argument_indent:
"f() {\n result=\"$(\n add --content \"$(\n printf \"%b\\\\n\" \"$body\" \\\n | tr -d $'\\r'\n )\" --skip\n )\"\n}\n"
=> "f() {\n\tresult=\"$(\n\t\tadd --content \"$(\n\t\t\tprintf \"%b\\\\n\" \"$body\" |\n\t\t\t\ttr -d $'\\r'\n\t\t)\" --skip\n\t)\"\n}\n";
indents_raw_command_substitution_compound_pipeline_bodies:
"urls=\"$(\n find files |\n if [ -n \"$filter\" ]; then\n grep \"$filter\" || :\n else\n cat\n fi |\n while read -r file; do\n [ -f \"$file\" ] || continue\n grep \"$file\" |\n if [ -n \"$ignored\" ]; then\n grep -v \"$ignored\"\n else\n cat\n fi\n done |\n sort -u\n)\"\n"
=> "urls=\"$(\n\tfind files |\n\t\tif [ -n \"$filter\" ]; then\n\t\t\tgrep \"$filter\" || :\n\t\telse\n\t\t\tcat\n\t\tfi |\n\t\twhile read -r file; do\n\t\t\t[ -f \"$file\" ] || continue\n\t\t\tgrep \"$file\" |\n\t\t\t\tif [ -n \"$ignored\" ]; then\n\t\t\t\t\tgrep -v \"$ignored\"\n\t\t\t\telse\n\t\t\t\t\tcat\n\t\t\t\tfi\n\t\tdone |\n\t\tsort -u\n)\"\n";
indents_inline_raw_command_substitution_compound_pipeline_bodies:
"playlist_id=\"$(producer |\n if [ \"$x\" ]; then\n # keep exact match\n while read -r id name; do\n if [[ \"$name\" = \"$playlist_name\" ]]; then\n echo \"$id\"\n break\n fi\n done\n else\n grep -Fi \"$playlist_name\" |\n awk '{print $1}'\n fi || :\n)\"\n"
=> "playlist_id=\"$(\n\tproducer |\n\t\tif [ \"$x\" ]; then\n\t\t\t# keep exact match\n\t\t\twhile read -r id name; do\n\t\t\t\tif [[ \"$name\" = \"$playlist_name\" ]]; then\n\t\t\t\t\techo \"$id\"\n\t\t\t\t\tbreak\n\t\t\t\tfi\n\t\t\tdone\n\t\telse\n\t\t\tgrep -Fi \"$playlist_name\" |\n\t\t\t\tawk '{print $1}'\n\t\tfi || :\n)\"\n";
preserves_loop_body_brace_group_background_before_done:
"for workflow_name in $workflows; do\n {\n output=\"$(printf '%s\\n' \"$workflow_name\")\"\n echo \"$output\"\n } &\ndone |\nsort\n"
=> "for workflow_name in $workflows; do\n\t{\n\t\toutput=\"$(printf '%s\\n' \"$workflow_name\")\"\n\t\techo \"$output\"\n\t} &\ndone |\n\tsort\n";
indents_block_command_substitution_loop_body_comments:
"tests=\"$(\n for filename in $filelist; do\n # expensive filter\n echo \"check $filename\"\n done\n)\"\n"
=> "tests=\"$(\n\tfor filename in $filelist; do\n\t\t# expensive filter\n\t\techo \"check $filename\"\n\tdone\n)\"\n";
indents_block_command_substitution_pipeline_comments_inside_assignments:
"snapshots=\"$(\n tmutil listlocalsnapshots \"$path\" |\n tail -n +2 |\n # update snapshots can't be deleted so just take the date timestamped ones:\n #\n # 2026-02-14-041148\n command ggrep -oP '\\d{4}-\\d\\d-\\d\\d-\\d+'\n)\"\n"
=> "snapshots=\"$(\n\ttmutil listlocalsnapshots \"$path\" |\n\t\ttail -n +2 |\n\t\t# update snapshots can't be deleted so just take the date timestamped ones:\n\t\t#\n\t\t# 2026-02-14-041148\n\t\tcommand ggrep -oP '\\d{4}-\\d\\d-\\d\\d-\\d+'\n)\"\n";
normalizes_raw_block_command_substitution_inline_comment_padding:
"resources=\"$(\n kubectl api-resources |\n tail -n +2 || : # ignore incomplete API discovery\n)\"\n"
=> "resources=\"$(\n\tkubectl api-resources |\n\t\ttail -n +2 || : # ignore incomplete API discovery\n)\"\n";
normalizes_raw_command_substitution_leading_list_operators:
"matches=\"$(git grep -Ei \\\n -e a \\\n | grep -Fv x \\\n || :\n # note\n)\"\n"
=> "matches=\"$(\n\tgit grep -Ei \\\n\t\t-e a |\n\t\tgrep -Fv x ||\n\t\t:\n\t# note\n)\"\n";
preserves_repeated_inline_substitution_continuation_indent:
"matches=\"$(git grep -Ei \\\n -e first \\\n -e second \\\n -e third \\\n | grep -Fv skip \\\n || :\n # note\n)\"\n"
=> "matches=\"$(\n\tgit grep -Ei \\\n\t\t-e first \\\n\t\t-e second \\\n\t\t-e third |\n\t\tgrep -Fv skip ||\n\t\t:\n\t# note\n)\"\n";
}
default_unchanged_ast_cases! {
preserves_comment_indentation_inside_inline_command_substitutions:
"if ok; then\n\tfor item in $(printenv |\n\t\t# keep env names\n\t\tgrep '^APP_'); do\n\t\techo \"$item\"\n\tdone\nfi\n";
indents_block_command_substitution_comments_inside_assignments:
"if ok; then\n\titems=$(\n\t\t# keep generated names\n\t\tfind . -type f |\n\t\t\tsort\n\t)\nfi\n";
}
default_format_ast_cases! {
preserves_command_substitution_assignment_continuation_alignment:
"LIBS=\"$(pkg-config --libs openssl)\" \\\nCFLAGS=\"$SLKCFLAGS -Wl,-s -I$(pwd)/lib\" \\\n./configure \\\n--prefix=/usr\n"
=> "LIBS=\"$(pkg-config --libs openssl)\" \\\nCFLAGS=\"$SLKCFLAGS -Wl,-s -I$(pwd)/lib\" \\\n\t./configure \\\n\t--prefix=/usr\n";
keeps_leading_command_substitution_assignment_continuations_flush_left:
"A=$(pwd) \\\nB=1 \\\nC=2 \\\ncmd\n"
=> "A=$(pwd) \\\nB=1 \\\nC=2 \\\n\tcmd\n";
indents_assignment_continuations_after_nonleading_command_substitution:
"A=1 \\\nB=$(pwd) \\\nC=2 \\\ncmd\n"
=> "A=1 \\\n\tB=$(pwd) \\\n\tC=2 \\\n\tcmd\n";
preserves_multiline_decl_compound_assignment_lines:
"case $prev in\n--warnings)\n local cats=(cross gnu obsolete override portability syntax\n unsupported)\n return\n ;;\nesac\n"
=> "case $prev in\n--warnings)\n\tlocal cats=(cross gnu obsolete override portability syntax\n\t\tunsupported)\n\treturn\n\t;;\nesac\n";
preserves_expanded_decl_compound_assignment_delimiters:
"f() {\n local commands=(\n build\n version\n )\n}\n"
=> "f() {\n\tlocal commands=(\n\t\tbuild\n\t\tversion\n\t)\n}\n";
aligns_runs_of_trailing_comments:
"short=1 # first\nmuch_longer=2 # second\n"
=> "short=1 # first\nmuch_longer=2 # second\n";
aligns_trailing_comments_after_empty_array_assignments:
"x=() # first\nyyy=() # second\n"
=> "x=() # first\nyyy=() # second\n";
aligns_trailing_comments_after_normalized_arithmetic_assignments:
"border=$(( $(_system uptime days) * 3 )) # normally\nborder=$(( border + basecount )) # later\n"
=> "border=$(($(_system uptime days) * 3)) # normally\nborder=$((border + basecount)) # later\n";
aligns_trailing_comments_after_normalized_command_substitutions:
"SPACER1=\"$(_sanitizer run \"$MAX1 $LOCAL\" add_length_diff_with_spaces)\" # one\nSPACER2=\"$(_sanitizer run \"$MAX2 $REMOTE\" add_length_diff_with_spaces)\" # two\n"
=> "SPACER1=\"$(_sanitizer run \"$MAX1 $LOCAL\" add_length_diff_with_spaces)\" # one\nSPACER2=\"$(_sanitizer run \"$MAX2 $REMOTE\" add_length_diff_with_spaces)\" # two\n";
aligns_trailing_comments_after_parameter_replacements:
"while read line; do\n line=${line%%#*} # Remove comments\n line=${line//:/ } # Change colon delimiter to space\n line=${line//,/ } # Change comma delimiter to space\ndone\n"
=> "while read line; do\n\tline=${line%%#*} # Remove comments\n\tline=${line//:/ } # Change colon delimiter to space\n\tline=${line//,/ } # Change comma delimiter to space\ndone\n";
}
bash_format_ast_cases! {
aligns_trailing_comments_after_normalized_array_assignments:
"args=( \"${args[@]/%/ }\" )\t\t\t# add space to all\nargs=( \"${args[@]/%$slash /$slash}\" )\t# remove space from dirs\n"
=> "args=(\"${args[@]/%/ }\") # add space to all\nargs=(\"${args[@]/%$slash /$slash}\") # remove space from dirs\n";
aligns_trailing_comments_after_empty_parameter_replacements:
"MAINVER=\"${VERSION//_*}\" # e.g. 1.8.0_9 => 1.8.0\nDEBVER=\"${VERSION//*_}\" # e.g. 1.8.0_9 => 9\n"
=> "MAINVER=\"${VERSION//_*/}\" # e.g. 1.8.0_9 => 1.8.0\nDEBVER=\"${VERSION//*_/}\" # e.g. 1.8.0_9 => 9\n";
}
#[test]
fn aligns_trailing_comments_after_parameter_replacements_inside_functions() {
let source = "read_conf() {\n while read line; do\n line=${line%%#*} # Remove comments\n line=${line//:/ } # Change colon delimiter to space\n line=${line//,/ } # Change comma delimiter to space\n done\n}\n";
let options = ShellFormatOptions::default().with_dialect(ShellDialect::Bash);
assert_formats_to_with_ast(
source,
Some(Path::new("ldconfig.in-r3")),
&options,
"read_conf() {\n\twhile read line; do\n\t\tline=${line%%#*} # Remove comments\n\t\tline=${line//:/ } # Change colon delimiter to space\n\t\tline=${line//,/ } # Change comma delimiter to space\n\tdone\n}\n",
);
}
default_format_ast_cases! {
aligns_trailing_comments_after_adjacent_redirect_commands:
"if ok; then\n rm -f /tmp/OLSR/meshrdf_neighs* 2>/dev/null # enforce rewrite some lines later\n echo >>$SCHEDULER \"_wifi speed check $gateway\" # will only test once\nfi\n"
=> "if ok; then\n\trm -f /tmp/OLSR/meshrdf_neighs* 2>/dev/null # enforce rewrite some lines later\n\techo >>$SCHEDULER \"_wifi speed check $gateway\" # will only test once\nfi\n";
aligns_trailing_comments_after_normalized_redirect_spacing:
"netint=$(${ipcommand} -o addr | grep \"${ip}\" | awk '{print $2}') # e.g eth0\nnetlink=$(${ethtoolcommand} \"${netint}\" 2> /dev/null | grep Speed | awk '{print $2}') # e.g 1000Mb/s\n"
=> "netint=$(${ipcommand} -o addr | grep \"${ip}\" | awk '{print $2}') # e.g eth0\nnetlink=$(${ethtoolcommand} \"${netint}\" 2>/dev/null | grep Speed | awk '{print $2}') # e.g 1000Mb/s\n";
preserves_if_condition_on_own_line:
"case $mode in\nprompt)\n if\n [[ -n ${ZSH_VERSION:-} ]]\n then\n echo zsh\n fi\n ;;\nesac\n"
=> "case $mode in\nprompt)\n\tif\n\t\t[[ -n ${ZSH_VERSION:-} ]]\n\tthen\n\t\techo zsh\n\tfi\n\t;;\nesac\n";
}
bash_format_ast_cases! {
aligns_trailing_comments_after_removed_semicolons:
"BUILD_TNC=${BUILD_TNC:-true} ; # build tnc XML validator module\nBUILD_TDOMHTML=${BUILD_TDOMHTML:-true} ; # build tdomhtml html generation module\n"
=> "BUILD_TNC=${BUILD_TNC:-true} # build tnc XML validator module\nBUILD_TDOMHTML=${BUILD_TDOMHTML:-true} # build tdomhtml html generation module\n";
aligns_trailing_comments_after_bare_redirect_spacing:
"cp -ar SlackBuild $PKG/opt/$PRGNAM/ # Copy the SlackBuild script\ncat $PRGNAM.sh > $PKG/opt/$PRGNAM/$PRGNAM.sh # Copy the launcher script\n"
=> "cp -ar SlackBuild $PKG/opt/$PRGNAM/ # Copy the SlackBuild script\ncat $PRGNAM.sh >$PKG/opt/$PRGNAM/$PRGNAM.sh # Copy the launcher script\n";
splits_multistatement_if_conditions_like_shfmt:
"f() {\n if curl -X PUT -k \"${@:2}\"\n \"$url\" \\\n -H x \\\n -d y; then\n echo ok\n fi\n}\n"
=> "f() {\n\tif\n\t\tcurl -X PUT -k \"${@:2}\"\n\t\t\"$url\" \\\n\t\t\t-H x \\\n\t\t\t-d y\n\tthen\n\t\techo ok\n\tfi\n}\n";
collapses_then_on_next_line_after_simple_if_conditions_like_shfmt:
"f() {\n if [ -z \"${EDITOR:-}\" ]\n then\n EDITOR=vi\n elif grep -q \"$cur\" <<<'-g'\n then\n COMPREPLY+=(\"-g\")\n fi\n if ! ContainsString \"lock\" \"$value\"\n then\n FOUND=1\n fi\n}\n"
=> "f() {\n\tif [ -z \"${EDITOR:-}\" ]; then\n\t\tEDITOR=vi\n\telif grep -q \"$cur\" <<<'-g'; then\n\t\tCOMPREPLY+=(\"-g\")\n\tfi\n\tif ! ContainsString \"lock\" \"$value\"; then\n\t\tFOUND=1\n\tfi\n}\n";
}
bash_format_ast_cases! {
keeps_multiline_literal_if_conditions_inline_like_shfmt:
"case \"$ext\" in\n.vimrc) if vim -c \"\n if !filereadable('$basename') |\n cquit 1\n endif\n \" -c \"q\"; then\n echo ok\nfi\n;;\nesac\n"
=> "case \"$ext\" in\n.vimrc)\n\tif vim -c \"\n if !filereadable('$basename') |\n cquit 1\n endif\n \" -c \"q\"; then\n\t\techo ok\n\tfi\n\t;;\nesac\n";
splits_multistatement_elif_conditions_like_shfmt:
"f() {\n if type -p perl >/dev/null; then\n perl -pe decode\n elif type -p python3 >/dev/null &&\n log \"using python\"\n python3 -c 'import html' >/dev/null; then\n python3 -c decode\n elif type -p xmlstarlet >/dev/null; then\n xmlstarlet unesc\n fi\n}\n"
=> "f() {\n\tif type -p perl >/dev/null; then\n\t\tperl -pe decode\n\telif\n\t\ttype -p python3 >/dev/null &&\n\t\t\tlog \"using python\"\n\t\tpython3 -c 'import html' >/dev/null\n\tthen\n\t\tpython3 -c decode\n\telif type -p xmlstarlet >/dev/null; then\n\t\txmlstarlet unesc\n\tfi\n}\n";
splits_multistatement_loop_conditions_like_shfmt:
"while read mac; read name; do\n printf '%s\\n' \"$mac:$name\"\ndone\nuntil poll; sleep 1; do\n :\ndone\n"
=> "while\n\tread mac\n\tread name\ndo\n\tprintf '%s\\n' \"$mac:$name\"\ndone\nuntil\n\tpoll\n\tsleep 1\ndo\n\t:\ndone\n";
}
bash_unchanged_ast_cases! {
preserves_if_chain_condition_on_own_line:
"f() {\n\tif\n\t\t[[ -z \"${remote:-}\" ]]\n\tthen\n\t\techo missing\n\telif\n\t\tfile_exists_at_url \"$remote\"\n\tthen\n\t\techo remote\n\telse\n\t\techo none\n\tfi\n}\n";
}
default_format_ast_cases! {
aligns_trailing_comments_across_tab_indented_if_body:
"check_restart() {\n\tif [ $percent -gt 300 -a $OPENWRT_REV -gt 0 ]; then\t# seems busy\n\t\treturn 1\t\t# sometimes high\n\tfi\n}\n"
=> "check_restart() {\n\tif [ $percent -gt 300 -a $OPENWRT_REV -gt 0 ]; then # seems busy\n\t\treturn 1 # sometimes high\n\tfi\n}\n";
}
bash_format_ast_cases! {
aligns_comments_across_space_indented_multiline_headers:
"search() {\n if ok\n then\n ag \\\n --filename \\\n --hidden \\\n --ignore \".git\" \\\n --ignore-case \\\n --noheading \\\n \"${_search_args[@]}\" \\\n \"${_query}\" \\\n \"${_search_paths[@]}\" \\\n || return 0 # Don't fail out within a single scope.\n elif _search_with \"ack\" \"${_search_utility:-}\"\n then # ack is available.\n :\n fi\n}\n"
=> "search() {\n\tif ok; then\n\t\tag \\\n\t\t\t--filename \\\n\t\t\t--hidden \\\n\t\t\t--ignore \".git\" \\\n\t\t\t--ignore-case \\\n\t\t\t--noheading \\\n\t\t\t\"${_search_args[@]}\" \\\n\t\t\t\"${_query}\" \\\n\t\t\t\"${_search_paths[@]}\" ||\n\t\t\treturn 0 # Don't fail out within a single scope.\n\telif _search_with \"ack\" \"${_search_utility:-}\"; then # ack is available.\n\t\t:\n\tfi\n}\n";
aligns_list_rhs_comments_with_following_branch_header_comments:
"search() {\n for __target_path in \"${_target_paths[@]:-}\"\n do\n {\n if _search_with \"ag\" \"${_search_utility:-}\"\n then\n ag \\\n --filename \\\n --hidden \\\n --ignore \".git\" \\\n --ignore-case \\\n --noheading \\\n \"${_search_args[@]}\" \\\n \"${_query}\" \\\n \"${_search_paths[@]}\" \\\n || return 0 # Don't fail out within a single scope.\n elif _search_with \"ack\" \"${_search_utility:-}\"\n then # ack is available.\n :\n fi\n }\n done\n}\n"
=> "search() {\n\tfor __target_path in \"${_target_paths[@]:-}\"; do\n\t\t{\n\t\t\tif _search_with \"ag\" \"${_search_utility:-}\"; then\n\t\t\t\tag \\\n\t\t\t\t\t--filename \\\n\t\t\t\t\t--hidden \\\n\t\t\t\t\t--ignore \".git\" \\\n\t\t\t\t\t--ignore-case \\\n\t\t\t\t\t--noheading \\\n\t\t\t\t\t\"${_search_args[@]}\" \\\n\t\t\t\t\t\"${_query}\" \\\n\t\t\t\t\t\"${_search_paths[@]}\" ||\n\t\t\t\t\treturn 0 # Don't fail out within a single scope.\n\t\t\telif _search_with \"ack\" \"${_search_utility:-}\"; then # ack is available.\n\t\t\t\t:\n\t\t\tfi\n\t\t}\n\tdone\n}\n";
aligns_else_comments_with_space_indented_nested_then_comments:
"show() {\n if outer\n then\n if ok\n then\n rm -f \"${_rendered_temp_file_path:?}\"\n else # default\n if ((_print_output))\n then # `show --print [--no-color]`\n if ((_COLOR_ENABLED))\n then # `show --print`\n _highlight_syntax_if_available \"${_target_path}\"\n else # `show --print --no-color`\n cat \"${_target_path}\"\n fi\n fi\n fi\n fi\n}\n"
=> "show() {\n\tif outer; then\n\t\tif ok; then\n\t\t\trm -f \"${_rendered_temp_file_path:?}\"\n\t\telse # default\n\t\t\tif ((_print_output)); then # `show --print [--no-color]`\n\t\t\t\tif ((_COLOR_ENABLED)); then # `show --print`\n\t\t\t\t\t_highlight_syntax_if_available \"${_target_path}\"\n\t\t\t\telse # `show --print --no-color`\n\t\t\t\t\tcat \"${_target_path}\"\n\t\t\t\tfi\n\t\t\tfi\n\t\tfi\n\tfi\n}\n";
}
#[test]
fn aligns_trailing_comments_with_following_outdented_branch() {
let source = "cell_ram() {\n\tcase \"$ram_size\" in\n\t\t12*|13*)\n\t\t\tif [ ${#ram_size} -eq 5 ]; then\t\t\t# size\n\t\t\t\tif [ -z \"$zram_memusage\" ]; then\n\t\t\t\t\tbgcolor=\"$color_alarm\"\t\t# disabled\n\t\t\t\telif [ \"$zram_memusage\" -lt 320000 ]; then\t# pppoe\n\t\t\t\t\tbgcolor=\"$color_lightgreen\"\n\t\t\t\tfi\n\t\t\tfi\n\t\t;;\n\tesac\n}\n";
let options = ShellFormatOptions::default();
let alarm_prefix = "\t\t\t\tbgcolor=\"$color_alarm\"";
let elif_prefix = "\t\t\telif [ \"$zram_memusage\" -lt 320000 ]; then ";
let alarm_padding = " ".repeat(elif_prefix.len() - alarm_prefix.len());
assert_formats_to_with_ast(
source,
None,
&options,
format!(
"cell_ram() {{\n\tcase \"$ram_size\" in\n\t12* | 13*)\n\t\tif [ ${{#ram_size}} -eq 5 ]; then # size\n\t\t\tif [ -z \"$zram_memusage\" ]; then\n{alarm_prefix}{alarm_padding}# disabled\n\t\t\telif [ \"$zram_memusage\" -lt 320000 ]; then # pppoe\n\t\t\t\tbgcolor=\"$color_lightgreen\"\n\t\t\tfi\n\t\tfi\n\t\t;;\n\tesac\n}}\n"
),
);
}
#[test]
fn preserves_simple_elif_condition_on_own_line_after_heredoc_branch() {
let source = "#!/bin/sh\n\nif [ \"$1\" = --query ]; then\n\n cat <<EOF\nquery\nEOF\n\nelif\n [ \"$1\" = --listmonitors ]\nthen\n\n cat <<EOF\nmonitors\nEOF\nfi\n";
let options = ShellFormatOptions::default().with_dialect(ShellDialect::Posix);
assert_formats_to_with_ast(
source,
None,
&options,
"#!/bin/sh\n\nif [ \"$1\" = --query ]; then\n\n\tcat <<EOF\nquery\nEOF\n\nelif\n\t[ \"$1\" = --listmonitors ]\nthen\n\n\tcat <<EOF\nmonitors\nEOF\nfi\n",
);
}
default_format_ast_cases! {
aligns_inline_if_close_comments_after_reindent:
"scan() {\n if IsRunning \"sentineld\"; then SENTINELONE_SCANNER_RUNNING=1; fi # macOS\n if IsRunning \"s1-agent\"; then SENTINELONE_SCANNER_RUNNING=1; fi # Linux\n if IsRunning \"SentinelAgent\"; then SENTINELONE_SCANNER_RUNNING=1; fi # Windows\n}\n"
=> "scan() {\n\tif IsRunning \"sentineld\"; then SENTINELONE_SCANNER_RUNNING=1; fi # macOS\n\tif IsRunning \"s1-agent\"; then SENTINELONE_SCANNER_RUNNING=1; fi # Linux\n\tif IsRunning \"SentinelAgent\"; then SENTINELONE_SCANNER_RUNNING=1; fi # Windows\n}\n";
preserves_blank_line_after_if_then:
"if true; then\n\n echo yes\nfi\n"
=> "if true; then\n\n\techo yes\nfi\n";
preserves_blank_line_before_if_fi:
"if true; then\n if other; then\n echo yes\n else\n echo no\n fi\n\nfi\n"
=> "if true; then\n\tif other; then\n\t\techo yes\n\telse\n\t\techo no\n\tfi\n\nfi\n";
preserves_blank_line_before_simple_fi:
"if true; then\n echo yes\n\nfi\n"
=> "if true; then\n\techo yes\n\nfi\n";
does_not_treat_comment_internal_blank_as_fi_gap:
"if true; then\n echo yes\n\n # disabled\nfi\n"
=> "if true; then\n\techo yes\n\n\t# disabled\nfi\n";
preserves_blank_line_before_if_branches:
"if true; then\n echo yes\n\nelif false; then\n echo no\n\nelse\n echo maybe\nfi\n"
=> "if true; then\n\techo yes\n\nelif false; then\n\techo no\n\nelse\n\techo maybe\nfi\n";
preserves_blank_line_before_commented_if_branch:
"if true; then\n echo yes\n\n# try the fallback\nelif false; then\n echo no\nfi\n"
=> "if true; then\n\techo yes\n\n# try the fallback\nelif false; then\n\techo no\nfi\n";
preserves_blank_line_before_fi_after_elif_branch:
"if true; then\n echo yes\nelif false; then\n echo no\n\nfi\n"
=> "if true; then\n\techo yes\nelif false; then\n\techo no\n\nfi\n";
does_not_preserve_branch_blanks_from_inline_keywords:
"# setup\n\nif true; then yes; else\n no\nfi\n"
=> "# setup\n\nif true; then yes; else\n\tno\nfi\n";
preserves_blank_line_after_while_do:
"while read -r dep; do\n\n ver=${dep#*=}\ndone\n"
=> "while read -r dep; do\n\n\tver=${dep#*=}\ndone\n";
preserves_blank_line_before_done:
"while true; do\n echo yes\n\ndone\n"
=> "while true; do\n\techo yes\n\ndone\n";
preserves_blank_line_after_brace_group_open:
"if true; then\n [ -n \"$x\" ] && {\n\n echo yes\n }\nfi\n"
=> "if true; then\n\t[ -n \"$x\" ] && {\n\n\t\techo yes\n\t}\nfi\n";
preserves_blank_line_after_brace_group_open_suffix_comment:
"if true; then\n [ -n \"$x\" ] || { # note\n\n echo yes\n }\nfi\n"
=> "if true; then\n\t[ -n \"$x\" ] || { # note\n\n\t\techo yes\n\t}\nfi\n";
}
bash_format_ast_cases! {
preserves_blank_line_after_if_then_suffix_comment:
"if [[ -s ./bin/rails ]]; then # binstub\n\n ruby ./bin/rails\nfi\n"
=> "if [[ -s ./bin/rails ]]; then # binstub\n\n\truby ./bin/rails\nfi\n";
does_not_insert_blank_after_then_suffix_comment_before_body_comment:
"if [[ \"${#_test_line}\" -gt \"${_COLUMNS}\" ]]\nthen # wrap to next line\n # Use the existing value.\n echo yes\nfi\n"
=> "if [[ \"${#_test_line}\" -gt \"${_COLUMNS}\" ]]; then # wrap to next line\n\t# Use the existing value.\n\techo yes\nfi\n";
indents_dangling_comments_before_done_like_shfmt:
"while true; do\n echo ok\n# buffered input\ndone\n"
=> "while true; do\n\techo ok\n\t# buffered input\ndone\n";
}
bash_unchanged_ast_cases! {
preserves_while_condition_on_own_line:
"f() {\n\twhile\n\t\t[[ ! -r \"$target\" && \"$target\" != \"\" ]]\n\tdo\n\t\tchmod ugo+rX \"$target\"\n\t\ttarget=\"${target%/*}\"\n\tdone\n}\n";
}
#[test]
fn aligns_brace_group_open_suffix_comments_with_body_comments() {
let source = "if true; then\n\t[ $LASTSEEN -gt 350000 ] && {\t\t# 97 hours\n\t\tLASTSEEN=\"$(( $LOCALUNIXTIME - $( stat -c \"%Y\" \"$FILE\" ) ))\"\t\t# Y = last modification time\n\t}\nfi\n";
let options = ShellFormatOptions::default();
let open_line = "[ $LASTSEEN -gt 350000 ] && {";
let assignment_line = "LASTSEEN=\"$(($LOCALUNIXTIME - $(stat -c \"%Y\" \"$FILE\")))\"";
let target_column = assignment_line.len() + 2;
assert_formats_to_with_ast(
source,
None,
&options,
format!(
"if true; then\n\t{open_line}{}# 97 hours\n\t\t{assignment_line} # Y = last modification time\n\t}}\nfi\n",
" ".repeat(target_column - open_line.len())
),
);
}
default_format_ast_cases! {
does_not_insert_blank_before_body_leading_brace_pipeline:
"if ok; then\n {\n echo yes\n } | cat\nelse\n {\n echo no\n } | cat\nfi\n"
=> "if ok; then\n\t{\n\t\techo yes\n\t} | cat\nelse\n\t{\n\t\techo no\n\t} | cat\nfi\n";
preserves_blank_line_before_inline_do_brace_close:
"while read -r line; do {\n echo \"$line\"\n\n} done <file\n"
=> "while read -r line; do {\n\techo \"$line\"\n\n}; done <file\n";
preserves_blank_line_before_inline_do_brace_close_after_nested_group:
"while read -r line; do {\n [ -n \"$line\" ] && {\n echo \"$line\"\n }\n\n} done <file\n"
=> "while read -r line; do {\n\t[ -n \"$line\" ] && {\n\t\techo \"$line\"\n\t}\n\n}; done <file\n";
formats_multiline_arithmetic_commands_with_continuations:
"if true; then\n ((\n I++,\n IDX = 16\n + R * 5\n + G * 6\n ))\nelse\n echo no\nfi\n"
=> "if true; then\n\t((\\\n\tI++, \\\n\tIDX = 16 + \\\n\tR * 5 + \\\n\tG * 6))\n\nelse\n\techo no\nfi\n";
}
bash_format_ast_cases! {
formats_multiline_arithmetic_expansions_with_continuations:
"_auto_limit_amount=\"$((
${_available_lines:-1} -
${_header_and_footer_line_count:-0} +
${_auto_limit_adjustment:-0}
))\"\n"
=> "_auto_limit_amount=\"$((\\\n\t${_available_lines:-1} - \\\n\t${_header_and_footer_line_count:-0} + \\\n\t${_auto_limit_adjustment:-0}))\"\n";
}
default_format_ast_cases! {
preserves_continued_semicolon_terminators:
"ln -s foo bar \\\n ;\nrm bar\n"
=> "ln -s foo bar \\\n\t;\nrm bar\n";
preserves_multiline_single_quoted_argument_payload_indentation:
"cat \"$@\" |\n python -c '\nfrom __future__ import print_function\nprint(\"ok\")\n'\n"
=> "cat \"$@\" |\n\tpython -c '\nfrom __future__ import print_function\nprint(\"ok\")\n'\n";
preserves_multiline_assignment_payload_indentation:
"if true; then\n section+=\"\n$line\"\nfi\n"
=> "if true; then\n\tsection+=\"\n$line\"\nfi\n";
removes_unquoted_assignment_continuation_backslashes:
"packages=$one\\\n$two\\\n$three\n"
=> "packages=$one$two$three\n";
preserves_multiline_assignment_continuation_payload_indentation:
"if true; then\n INCLUDE_TESTS=\"boot_services kernel \\\n filesystems usb \\\n hardening\"\nfi\n"
=> "if true; then\n\tINCLUDE_TESTS=\"boot_services kernel \\\n filesystems usb \\\n hardening\"\nfi\n";
normalizes_multiline_command_substitution_arguments:
"_comp_compgen_split -- \"$(\"$1\" -watchdog help 2>&1 |\n _comp_awk '{print $1}')\"\n"
=> "_comp_compgen_split -- \"$(\"$1\" -watchdog help 2>&1 |\n\t_comp_awk '{print $1}')\"\n";
indents_inline_command_substitution_pipeline_words:
"f() {\n for fl in \"$HOME/.ssh/config\" \\\n $(grep \"^\\s*Include\" \"$HOME/.ssh/config\" |\n awk '{for (i=2; i<=NF; i++) print $i}' |\n sed -Ee \"s|^([^/~])|$HOME/.ssh/\\1|\"); do\n echo \"$fl\"\n done\n}\n"
=> "f() {\n\tfor fl in \"$HOME/.ssh/config\" \\\n\t\t$(grep \"^\\s*Include\" \"$HOME/.ssh/config\" |\n\t\t\tawk '{for (i=2; i<=NF; i++) print $i}' |\n\t\t\tsed -Ee \"s|^([^/~])|$HOME/.ssh/\\1|\"); do\n\t\techo \"$fl\"\n\tdone\n}\n";
}
default_unchanged_ast_cases! {
preserves_command_substitution_padding_inside_single_quotes:
"echo >>$TOOLS 'x=$( uptime_in_seconds )'\n";
preserves_multiline_quoted_assignment_payloads_with_nested_expansions:
"result_command=\"${result_command}\n\t--label \\\"manager=distrobox\\\"\n\t--env \\\"SHELL=$(basename \"${SHELL:-\"/bin/bash\"}\")\\\"\n\t--env \\\"HOME=${container_user_home}\\\"\"\n";
preserves_inline_multiline_command_substitution_source_indentation:
"result_command=\"${result_command}\n\t\t$(printenv | grep '=' |\n\t\tgrep -Ev '^_' |\n\t\tsed 's/x/y/')\"\n";
}
default_format_ast_cases! {
normalizes_redirect_spacing_in_inline_multiline_command_substitutions:
"binary_files=\"$(grep -rl \"# distrobox_binary\" \"${HOME}/.local/bin\" 2> /dev/null | sed 's/./\\\\&/g' |\n\txargs -I{} grep -le \"# name: ${container_name}$\" \"{}\" | sed 's/./\\\\&/g' |\n\txargs -I{} printf \"%s¤\" \"{}\" 2> /dev/null || :)\"\n"
=> "binary_files=\"$(grep -rl \"# distrobox_binary\" \"${HOME}/.local/bin\" 2>/dev/null | sed 's/./\\\\&/g' |\n\txargs -I{} grep -le \"# name: ${container_name}$\" \"{}\" | sed 's/./\\\\&/g' |\n\txargs -I{} printf \"%s¤\" \"{}\" 2>/dev/null || :)\"\n";
trims_source_indent_from_block_command_substitutions:
"f() {\n\tdesktop_files=$(\n\t\t# keep this with the nested command\n\t\tfind \"$dir\" -type f 2> /dev/null | sed 's/./\\\\&/g' |\n\t\t\txargs printf '%s\\n'\n\t)\n}\n"
=> "f() {\n\tdesktop_files=$(\n\t\t# keep this with the nested command\n\t\tfind \"$dir\" -type f 2>/dev/null | sed 's/./\\\\&/g' |\n\t\t\txargs printf '%s\\n'\n\t)\n}\n";
}
bash_format_ast_cases! {
preserves_multiline_compound_assignment_literal_shape:
"case $mode in\ndocs)\n CMD=(zsh -ilsc\n 'sudo chown /src &&\n make -C /src doc')\n ;;\nesac\n"
=> "case $mode in\ndocs)\n\tCMD=(zsh -ilsc\n\t\t'sudo chown /src &&\n make -C /src doc')\n\t;;\nesac\n";
preserves_decl_multiline_compound_assignment_literal_shape:
"f() {\n local options=(\n 1 \"Short\"\n \"First line\n\nliteral continuation\"\n )\n}\n"
=> "f() {\n\tlocal options=(\n\t\t1 \"Short\"\n\t\t\"First line\n\nliteral continuation\"\n\t)\n}\n";
}
format_cases_with_options! {
binary_next_line_pipeline_keeps_heredoc_body_unindented:
ShellFormatOptions::default().with_binary_next_line(true),
"cat foo | \\\ncat<<EOF\nhello\nEOF\n",
=> "cat foo \\\n\t| cat <<EOF\nhello\nEOF\n";
}
default_format_ast_cases! {
tab_stripping_heredocs_indent_body_with_context:
"case $mode in\nnew)\n cat >$file <<-EOF\nbody\nEOF\n ;;\nesac\n"
=> "case $mode in\nnew)\n\tcat >$file <<-EOF\n\t\tbody\n\tEOF\n\t;;\nesac\n";
}
unchanged_cases_with_options! {
binary_next_line_does_not_force_single_line_pipelines_to_wrap:
ShellFormatOptions::default().with_binary_next_line(true),
"cat foo | cat bar\n";
}
#[test]
fn space_indented_binary_next_line_command_substitution_keeps_continuations() {
let options = ShellFormatOptions::default()
.with_indent_style(IndentStyle::Space)
.with_indent_width(4)
.with_binary_next_line(true);
let source = r#"#!/bin/bash
ls -al \
| grep "$USER" \
| grep -i "jul"
foobar=$(ls -al \
| grep "$USER" \
| grep -i "jul")
echo "$foobar"
"#;
assert_unchanged_with_ast(source, Some(Path::new("sample.sh")), &options);
assert!(!format_to_string(source, Some(Path::new("sample.sh")), &options).contains('\t'));
}
format_cases_with_options! {
space_indented_command_substitution_normalizes_with_spaces:
ShellFormatOptions::default()
.with_indent_style(IndentStyle::Space)
.with_indent_width(4),
"foobar=$(ls -al \\\n | grep \"$USER\" \\\n | grep -i \"jul\")\n",
=> "foobar=$(ls -al |\n grep \"$USER\" |\n grep -i \"jul\")\n";
}
format_cases_with_options! {
honors_function_next_line_option:
ShellFormatOptions::default().with_function_next_line(true),
"foo(){\necho hi\n}\n"
=> "foo()\n{\n\techo hi\n}\n";
function_next_line_expands_inline_brace_body:
ShellFormatOptions::default().with_function_next_line(true),
"foo() { echo hi; }\n"
=> "foo()\n{\n\techo hi\n}\n";
function_next_line_expands_inline_subshell_body:
ShellFormatOptions::default().with_function_next_line(true),
"foo() (echo hi)\n"
=> "foo()\n(\n\techo hi\n)\n";
minify_drops_comments_but_preserves_shebang:
ShellFormatOptions::default().with_minify(true),
"#!/bin/bash\necho hi # note\n",
=> "#!/bin/bash\necho hi\n";
}
default_format_cases! {
formats_case_items_with_expected_indentation:
"case $x in\na) echo a;;\nb) echo b;;\nesac\n"
=> "case $x in\na) echo a ;;\nb) echo b ;;\nesac\n";
}
default_format_ast_cases! {
keeps_inline_case_inside_if_command_lists:
"if case \"${icon_name}\" in \"/\"*) true ;; *) false ;; esac &&\n [ -e \"${icon_name}\" ]; then\n echo yes\nfi\n"
=> "if case \"${icon_name}\" in \"/\"*) true ;; *) false ;; esac &&\n\t[ -e \"${icon_name}\" ]; then\n\techo yes\nfi\n";
keeps_standalone_inline_case_commands:
"for src in $source; do\n case \"$src\" in */*) continue ;; esac\n echo \"$src\"\ndone\n"
=> "for src in $source; do\n\tcase \"$src\" in */*) continue ;; esac\n\techo \"$src\"\ndone\n";
keeps_inline_case_commands_with_missing_terminators:
"shellspec_is_number() {\n case ${1:-} in ( '' | *[!0-9]* ) return 1; esac\n return 0\n}\n"
=> "shellspec_is_number() {\n\tcase ${1:-} in '' | *[!0-9]*) return 1 ;; esac\n\treturn 0\n}\n";
keeps_inline_case_arms_inside_command_substitutions:
"value=\"$(\n while read -r key; do\n case \"$key\" in\n A) echo A ;;\n B) echo B ;;\n esac\n done\n)\"\n\n# later comment\nnext() { :; }\n"
=> "value=\"$(\n\twhile read -r key; do\n\t\tcase \"$key\" in\n\t\tA) echo A ;;\n\t\tB) echo B ;;\n\t\tesac\n\tdone\n)\"\n\n# later comment\nnext() { :; }\n";
keeps_case_items_multiline_when_terminator_was_multiline:
"case \"$x\" in\n-h|--help) usage\n ;;\nesac\n"
=> "case \"$x\" in\n-h | --help)\n\tusage\n\t;;\nesac\n";
keeps_inline_case_item_when_terminator_is_missing:
"case \"$x\" in\n*) usage\nesac\n"
=> "case \"$x\" in\n*) usage ;;\nesac\n";
keeps_case_header_items_inline_when_later_body_wraps:
"case \"$mode\" in a) ;; b) ;; c)\n echo c\nesac\n"
=> "case \"$mode\" in a) ;; b) ;; c)\n\techo c\n\t;;\nesac\n";
keeps_missing_terminator_case_item_body_on_pattern_line:
"case \"$x\" in\n*) value= && for item in $items; do {\n echo \"$item\"\n} done\nesac\n"
=> "case \"$x\" in\n*) value= && for item in $items; do {\n\techo \"$item\"\n}; done ;;\nesac\n";
}
bash_format_ast_cases! {
keeps_inline_case_arms_with_if_bodies:
"case \"$name\" in\nFastfile) if [[ \"$path\" =~ /fastlane/Fastfile ]]; then\n ruby -c \"$name\"\nfi ;;\nesac\n"
=> "case \"$name\" in\nFastfile) if [[ \"$path\" =~ /fastlane/Fastfile ]]; then\n\truby -c \"$name\"\nfi ;;\nesac\n";
keeps_inline_case_arms_with_if_else_bodies:
"case \"$RETROARCH\" in\n*) if [ -x /usr/share/games/retroarch ]; then\n build_ra=yes\nelse\n build_ra=no\nfi ;;\nesac\n"
=> "case \"$RETROARCH\" in\n*) if [ -x /usr/share/games/retroarch ]; then\n\tbuild_ra=yes\nelse\n\tbuild_ra=no\nfi ;;\nesac\n";
splits_nested_case_body_when_outer_terminator_was_on_next_line:
"case $x in\na) case $y in\nb) echo b ;; esac # note\n;;\nesac\n"
=> "case $x in\na)\n\tcase $y in\n\tb) echo b ;; esac # note\n\t;;\nesac\n";
keeps_empty_case_terminators_after_pattern_suffix_comments_parseable:
"case \"$line\" in\n\"status-filtered \"*) # ignore other status-filtered lines\n ;;\n\"#\"*) # allow for comments\n ;;\nesac\n"
=> "case \"$line\" in\n\"status-filtered \"*) # ignore other status-filtered lines\n\t;;\n\"#\"*) # allow for comments\n\t;;\nesac\n";
}
default_unchanged_ast_cases! {
keeps_inline_case_commands_with_multiple_patterns:
"case ${1:-} in '' | *[!0-9]*) return 1 ;; esac\n";
}
format_cases_with_options! {
switch_case_indent_indents_patterns_and_bodies:
ShellFormatOptions::default().with_switch_case_indent(true),
"case $x in\na) echo a;;\nesac\n"
=> "case $x in\n\ta)\n\t\techo a\n\t\t;;\nesac\n";
space_redirects_insert_spaces_between_operator_and_target:
ShellFormatOptions::default().with_space_redirects(true),
"echo hi>out\n"
=> "echo hi > out\n";
}
unchanged_cases_with_options! {
keep_padding_preserves_safe_verbatim_regions:
ShellFormatOptions::default().with_keep_padding(true),
"a=1 b=2\n";
}
#[test]
fn keep_padding_still_formats_unpadded_syntax() {
let options = ShellFormatOptions::default().with_keep_padding(true);
assert_formats_to(
"#!/bin/bash\n echo hi\n",
None,
&options,
"#!/bin/bash\necho hi\n",
);
assert!(!source_is_formatted("#!/bin/bash\n echo hi\n", None, &options).unwrap());
}
#[test]
fn normalizes_extra_crlf_trailing_newlines() {
let source = "echo hi\r\n\r\n";
let options = ShellFormatOptions::default();
assert_formats_to(source, Some(Path::new("test.sh")), &options, "echo hi\r\n");
assert!(!source_is_formatted(source, Some(Path::new("test.sh")), &options).unwrap());
}
format_cases_with_options! {
never_split_prefers_compact_layouts:
ShellFormatOptions::default().with_never_split(true),
"if true; then\necho hi\nfi\n",
=> "if true; then echo hi; fi\n";
}
#[test]
fn auto_dialect_honors_env_shebang() {
let error = format_source(
"#!/usr/bin/env sh\n[[ foo == bar ]]\n",
None,
&ShellFormatOptions::default(),
)
.unwrap_err();
assert!(matches!(
error,
FormatError::Parse { message, .. } if message.contains("[[ ]] conditionals")
));
let split_error = format_source(
"#!/usr/bin/env -S sh -e\n[[ foo == bar ]]\n",
None,
&ShellFormatOptions::default(),
)
.unwrap_err();
assert!(matches!(
split_error,
FormatError::Parse { message, .. } if message.contains("[[ ]] conditionals")
));
}
#[test]
fn auto_dialect_honors_zsh_paths_and_shebangs() {
assert_unchanged(
"print ${(m)foo}\n",
Some(Path::new("script.zsh")),
&ShellFormatOptions::default(),
);
assert_unchanged_default("#!/usr/bin/env zsh\nprint ${(m)foo}\n");
assert_unchanged_default("#!/usr/bin/env -S zsh -f\nprint ${(m)foo}\n");
}
#[test]
fn zsh_only_forms_round_trip_without_corruption() {
let source = "\
print ${(M)${(k)parameters[@]}:#__gitcomp_builtin_*}
print ${(m)foo#${needle}} ${(S)foo/$pattern/$replacement} ${(m)foo:$offset:${length}} ${(m)foo:^other}
print (#i)*.jpg (#b)(*) *.log(#qN) **/*(#q.om[1,3])
repeat 3 print hi
for version ($versions); do print $version; done
for key value in a b c d; { print -r -- \"$key:$value\"; }
for 1 2 3; do print -r -- \"$1|$2|$3\"; done
if [[ -n $foo ]] { print foo; } else { print bar; }
{ print body; } always { print cleanup; }
print quiet &|
print hidden &!
";
let options = ShellFormatOptions::default()
.with_dialect(ShellDialect::Zsh)
.with_simplify(true);
assert_unchanged_with_ast(source, Some(Path::new("script.zsh")), &options);
}
unchanged_cases_with_options! {
mksh_dialect_formats_select_commands:
ShellFormatOptions::default().with_dialect(ShellDialect::Mksh),
"select name in foo; do echo \"$name\"; done\n";
}
#[test]
fn posix_dialect_propagates_parse_errors() {
let options = ShellFormatOptions::default().with_dialect(ShellDialect::Posix);
let error =
format_source("[[ foo == bar ]]\n", Some(Path::new("test.sh")), &options).unwrap_err();
assert!(matches!(
error,
FormatError::Parse { message, .. } if message.contains("[[ ]] conditionals")
));
}
#[test]
fn preserves_quoted_associative_subscript_syntax_when_formatting() {
let options = ShellFormatOptions::default();
for source in [
"printf '%s\\n' ${assoc[\"key\"]}\n",
"printf '%s\\n' ${assoc['k']}\n",
"[[ -v assoc[\"k\"] ]]\n",
"declare -A assoc=([\"key\"]=v ['alt']+=w)\n",
] {
assert_unchanged_with_ast(source, None, &options);
}
}
default_unchanged_ast_cases! {
preserves_prefix_match_selector_kind_when_formatting:
"printf '%s\\n' \"${!prefix@}\" \"${!prefix*}\"\n";
}
#[test]
fn format_file_ast_matches_format_source_for_formatter_fixtures() {
let fixture_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/oracle-fixtures");
for (fixture, filename, options) in stable_formatter_fixture_cases() {
let source = fs::read_to_string(fixture_root.join(fixture)).unwrap();
assert_source_and_ast_paths_match(&source, Some(Path::new(filename)), &options);
}
let source = fs::read_to_string(fixture_root.join("binary_next_line.sh")).unwrap();
let options = ShellFormatOptions::default().with_binary_next_line(true);
assert_source_and_ast_paths_match(&source, Some(Path::new("binary_next_line.sh")), &options);
assert_source_and_ast_paths_match(
"if true; then\n# note\necho hi\nfi\n",
Some(Path::new("if_body_comment.sh")),
&ShellFormatOptions::default(),
);
assert_source_and_ast_paths_match(
"cat <<EOF # note\nhi\nEOF\n",
Some(Path::new("heredoc_trailing_comment.sh")),
&ShellFormatOptions::default(),
);
assert_source_and_ast_paths_match(
"declare -x foo=1<<EOF\nhi\nEOF\n",
Some(Path::new("decl_heredoc.sh")),
&ShellFormatOptions::default(),
);
}
#[test]
fn stable_formatter_fixtures_are_idempotent() {
let fixture_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/oracle-fixtures");
for (fixture, filename, options) in stable_formatter_fixture_cases() {
let source = fs::read_to_string(fixture_root.join(fixture)).unwrap();
assert_idempotent(&source, Some(Path::new(filename)), &options);
}
for (source, filename, options) in [
(
"if true; then\n\techo hi\nfi\n",
"if_body.sh",
ShellFormatOptions::default(),
),
(
"foo() {\n\techo hi\n}\n",
"func.sh",
ShellFormatOptions::default(),
),
(
"echo hi > out\n",
"redirect.sh",
ShellFormatOptions::default().with_space_redirects(true),
),
] {
assert_idempotent(source, Some(Path::new(filename)), &options);
}
}
default_idempotent_cases! {
preserves_group_spacing_idempotently_for_nested_subshells:
"foo(foo()) \n",
"nested_subshell_spacing.sh";
preserves_blank_lines_after_multiline_subshells_idempotently:
"(\n\techo hi\n)\n\nfoo() {\n\techo bye\n}\n",
"multiline_subshell_gap.sh";
preserves_blank_lines_after_multiline_brace_groups_idempotently:
"{\n\techo hi\n}\n\nfoo() {\n\techo bye\n}\n",
"multiline_brace_gap.sh";
preserves_spacing_after_nested_multiline_subshells_before_simple_commands:
"(\n\t(\n\t\tfalse\n\t)\n)\ngrep \"name delta\"\n",
"nested_multiline_subshell_then_stmt.sh";
preserves_spacing_after_nested_multiline_subshells_before_other_groups:
"(\n\t(\n\t\tfalse\n\t)\n)\n(\n\twhile true; do\n\t\t:\n\tdone\n)\n",
"nested_multiline_subshell_then_group.bash";
preserves_spacing_after_subshells_that_end_with_function_definitions:
"foo() {\n\t(\n\t\tbar() {\n\t\t\techo hi\n\t\t}\n\t)\n\n\tprintf x\n}\n",
"subshell_function_tail_gap.bash";
preserves_shebang_spacing_before_nested_multiline_groups_idempotently:
"#!/usr/bin/env bash\n\n(\n\t(\n\t\techo hi\n\t)\n)\n",
"shebang_nested_groups.bash";
preserves_legacy_bracket_arithmetic_idempotently:
"#!/bin/sh\n\ni=$[$i+1]\n",
"legacy_bracket_arithmetic.sh";
}
#[test]
fn invalid_shebang_like_line_does_not_move_off_the_first_line() {
let source = "#!/bin/bash<echo hi # note\n";
let formatted = match format_source(
source,
Some(Path::new("fuzz.sh")),
&ShellFormatOptions::default(),
)
.unwrap()
{
FormattedSource::Unchanged => source.to_string(),
FormattedSource::Formatted(formatted) => formatted,
};
assert_eq!(formatted, source);
}
#[test]
fn preserves_conditional_words_that_look_like_unary_operators() {
assert_default_idempotent("[[ -n]]\n", "fuzz.bash");
assert_default_idempotent("[[ ! -n]]\n", "fuzz.bash");
}
#[test]
fn keeps_unterminated_heredoc_closers_on_their_own_lines() {
let source = "x foo=1<<EOF=1<<EOF\nhi";
let formatted = format_to_string(
source,
Some(Path::new("fuzz.sh")),
&ShellFormatOptions::default(),
);
assert_eq!(formatted, "x foo=1 <<EOF=1 <<EOF\nhi\nEOF=1\nEOF\n");
assert_default_idempotent(source, "fuzz.sh");
}
#[test]
fn preserves_body_lines_when_synthesizing_missing_heredoc_closers() {
let source = "d8<<EGF\nhi\nEOF";
let formatted = format_to_string(
source,
Some(Path::new("fuzz.sh")),
&ShellFormatOptions::default(),
);
assert_eq!(formatted, "d8 <<EGF\nhi\nEOF\nEGF\n");
assert_default_idempotent(source, "fuzz.sh");
}
#[test]
fn keeps_trailing_backslash_pipeline_reproducer_parseable() {
let source = "cat foo | \\\\t foo | \\";
let path = Some(Path::new("fuzz.sh"));
let options = ShellFormatOptions::default();
let once = format_to_string(source, path, &options);
let twice = format_source(&once, path, &options).unwrap_or_else(|err| {
panic!("second format pass failed: {err}\nformatted source:\n{once:?}")
});
let twice = match twice {
FormattedSource::Unchanged => once.clone(),
FormattedSource::Formatted(formatted) => formatted,
};
assert_eq!(once, twice);
}
#[test]
fn does_not_introduce_unused_assignment_for_backslash_heredoc_reproducer() {
let source = "cat foo E\r\nnnnnnnnnn1jn \\\ncat=,EOlo\nEOF\n";
let path = Path::new("fuzz.sh");
let formatted = format_to_string(source, Some(path), &ShellFormatOptions::default());
let original_c001 = diagnostic_count(&lint_source_posix_strict(source, path), "C001");
let formatted_c001 = diagnostic_count(&lint_source_posix_strict(&formatted, path), "C001");
assert!(
formatted_c001 <= original_c001,
"formatter introduced extra C001 diagnostics: original={original_c001}, formatted={formatted_c001}\nformatted source:\n{formatted:?}"
);
}