I've been thinking for a while now that POSIX made a mistake when it permitted ';;' before the closing 'esac'. If ';;' were prohibited there, then the parser could be sure that the next word after every ';;' would be a pattern, even if it looks like 'esac'. But as things stand, there's an ambiguity which has traditionally been resolved by assuming an unquoted 'esac' occurring in the 'expect-a-pattern' state is actually a case-statement terminator. (And don't get me started on the stupidity of intentionally mismatched parentheses.)
But that's all water under the bridge. Prior to version 5.2 of Bash, even inserting '(' before esac wasn't enough to hide it: $ bash-5.1.12p1-release -c 'a () { case $1 in (esac) echo esac ; esac } ; type a' bash-5.1.12p1-release: -c: line 1: syntax error near unexpected token `esac' bash-5.1.12p1-release: -c: line 1: `a () { case $1 in (esac) echo esac ; esac } ; type a' $ bash-5.2.0p1-alpha -c 'a () { case $1 in (esac) echo esac ; esac } ; type a' a is a function a () { case $1 in esac) echo esac ;; esac } A better approach might be simply to quote 'esac' as a pattern words in the output of declare or type. Herewith a patch that fixes both annoyances: $ build/bash $ ./bash $ a() { case $1 in "") echo None ;; (esac) echo Esac ; esac } $ shopt -p balanced_case_parens shopt -u balanced_case_parens $ type a a is a function a () { case $1 in "") echo None ;; \esac) echo Esac esac } $ shopt -s balanced_case_parens $ type a a is a function a () { case $1 in ("") echo None ;; (esac) echo Esac esac } $ git d devel..@ diff --git a/builtins/shopt.def b/builtins/shopt.def index b3e1cfe5..0a385a58 100644 --- a/builtins/shopt.def +++ b/builtins/shopt.def @@ -75,6 +75,7 @@ $END #define OPTFMT "%-15s\t%s\n" extern int allow_null_glob_expansion, fail_glob_expansion, glob_dot_filenames; +extern int balanced_case_parens; extern int cdable_vars, mail_warning, source_uses_path; extern int no_exit_on_failed_exec, print_shift_error; extern int check_hashed_filenames, promptvars; @@ -182,6 +183,7 @@ static struct { { "array_expand_once", &expand_once_flag, set_array_expand }, { "assoc_expand_once", &expand_once_flag, set_array_expand }, #endif + { "balanced_case_parens", &balanced_case_parens, (shopt_set_func_t *)NULL }, { "cdable_vars", &cdable_vars, (shopt_set_func_t *)NULL }, { "cdspell", &cdspelling, (shopt_set_func_t *)NULL }, { "checkhash", &check_hashed_filenames, (shopt_set_func_t *)NULL }, diff --git a/print_cmd.c b/print_cmd.c index 330223d3..892443ab 100644 --- a/print_cmd.c +++ b/print_cmd.c @@ -52,6 +52,8 @@ extern int printf (const char *, ...); /* Yuck. Double yuck. */ static int indentation; static int indentation_amount = 4; +int balanced_case_parens = 0; + typedef void PFUNC (const char *, ...); static void cprintf (const char *, ...) __attribute__((__format__ (printf, 1, 2))); @@ -771,6 +773,11 @@ print_case_clauses (PATTERN_LIST *clauses) if (printing_comsub == 0 || first == 0) newline (""); first = 0; + if (balanced_case_parens) + cprintf("("); + else if (!strcmp(clauses->patterns->word->word, "esac")) + cprintf("\\"); + command_print_word_list (clauses->patterns, " | "); cprintf (")\n"); indentation += indentation_amount; @@ -781,7 +788,7 @@ print_case_clauses (PATTERN_LIST *clauses) newline (";&"); else if (clauses->flags & CASEPAT_TESTNEXT) newline (";;&"); - else + else if (clauses->next) /* be unambiguous: omit last ';;' */ newline (";;"); clauses = clauses->next; } -Martin