Skip to content

Commit

Permalink
;&, ;|, and ;;& in case command items (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
magicant authored Nov 20, 2024
2 parents b67c96f + 166e05f commit d306b74
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 45 deletions.
5 changes: 5 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
directory names for the first operand even before the user enters a
slash.
- Improved POSIX.1-2024 support:
- Case command items now can be terminated by `;&` instead of `;;'
to force the shell to execute the next item.
- The non-standard terminators `;|` and `;;&` are also supported
to resume pattern matching with the next item unless in the
POSIXly-correct mode.
- After the `bg` built-in resumed a job, the `!` special parameter
expands to the process ID of the job.
- An interactive shell no longer exits on an error in the `exec`
Expand Down
4 changes: 4 additions & 0 deletions NEWS.ja
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
- [行編集] . 組込みコマンドの最初の引数の補完で、スラッシュを入力する
前からディレクトリ名を補完候補として出すようにした
- POSIX.1-2024 のサポートを強化:
- `case` コマンドの分岐を `;;` の代わりに `;&` で区切ることで次の
分岐も実行させることができるようになった
- 非標準の拡張として `;|` もしくは `;;&` で区切ることで次の分岐
からパターンマッチングを再開させることもできる
- `bg` 組込みでジョブを再開した後は `!` 特殊パラメータはジョブの
プロセス ID に展開されるようになった
- POSIX 準拠モードであっても、対話シェルが `exec` 組込みで失敗した
Expand Down
2 changes: 1 addition & 1 deletion doc/ja/posix.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ POSIX 準拠モードを有効にすると、yash は POSIX の規定にでき
- グローバル{zwsp}link:syntax.html#aliases[エイリアス]の置換を行いません。
- link:syntax.html#compound[複合コマンド]の{zwsp}link:syntax.html#grouping[グルーピング]や link:syntax.html#if[if 文]の内容が空の場合エラーになります。
- link:syntax.html#for[For ループ]で展開した単語は link:_set.html#so-forlocal[for-local オプション]に関係なくグローバル変数として代入します。変数名はポータブルな (すなわち ASCII の範囲内の) 文字しか使えません。
- link:syntax.html#case[Case 文]の最初のパターンを +esac+ にすることはできません
- link:syntax.html#case[Case 文]の caseitem を +;|+ または +;;&+ で区切ることはできません
- 予約語 +!+ の直後に空白を置かずに +(+ を置くことはできません。
- link:syntax.html#double-bracket[二重ブラケットコマンド]は使えません。
- 予約語 +function+ を用いる形式の{zwsp}link:syntax.html#funcdef[関数定義]構文は使えません。関数名はポータブルな (すなわち ASCII の範囲内の) 文字しか使えません。
Expand Down
26 changes: 21 additions & 5 deletions doc/ja/syntax.txt
Original file line number Diff line number Diff line change
Expand Up @@ -219,18 +219,34 @@ Case 文の構文::
+case {{単語}} in {{caseitem}}... esac+

Caseitem の構文::
+({{パターン}}) {{コマンド}}...;;+
+({{パターン}}) {{コマンド}}... ;;+
+
+({{パターン}}) {{コマンド}}... ;&+
+
+({{パターン}}) {{コマンド}}... ;|+
+
+({{パターン}}) {{コマンド}}... ;;&+

+case+ と +in+ の間の単語はちょうど一トークンでなければなりません。この単語トークンは予約語としては認識されませんが、+&+ などの記号を含めるには適切な<<quotes,クォート>>が必要です。+in+ と +esac+ の間には任意の個数の caseitem を置きます (0 個でもよい)。Caseitem の最初の +(+ は (最初の{{パターン}}が +esac+ でない限り) 省略できます。最後の{{コマンド}}が改行で終端されている場合は +esac+ の直前の +;;+ は省略できます。Caseitem の +)+ と +;;+ との間に{{コマンド}}が一つもなくても構いません
+case+ と +in+ の間の単語はちょうど一トークンでなければなりません。この単語トークンは予約語としては認識されませんが、+&+ や +|+ などの記号を含めるには適切な<<quotes,クォート>>が必要です。+in+ と +esac+ の間には任意の個数の caseitem を置きます (0 個でもよい)。

Caseitem の{{パターン}}にはトークンを指定します。各トークンを +|+ で区切ることで複数のトークンをパターンとして指定することもできます。
Caseitem の最初の +(+ は (最初の{{パターン}}が +esac+ でない限り) 省略できます。
Caseitem の{{パターン}}にはトークンを指定します。各トークンの間を +|+ で区切ることで複数のトークンをパターンとして指定することもできます。

Caseitem には任意の個数の{{コマンド}}を含めることができます (0 個でもよい)。
Case 文の最後の caseitem が +;;+ で終端されている場合は、その +;;+ は省略できます。

Case 文の実行では、まず{{単語}}が{zwsp}link:expand.html[四種展開]されます。その後、各 caseitem に対して順に以下の動作を行います。

. {{パターン}}トークンを{{単語}}と同様に展開し、展開したパターンが展開した単語にマッチするかどうか調べます (link:pattern.html[パターンマッチング記法]参照)。{{パターン}}として指定されたトークンが複数ある場合はそれら各トークンに対してマッチするかどうか調べます (どれかのパターントークンがマッチしたらそれ以降のパターントークンは展開されません。Yash はトークンが書かれている順番にマッチするかどうかを調べますが、他のシェルもこの順序で調べるとは限りません)。
. マッチした場合は、直後の{{コマンド}}を実行し、それでこの case 文の実行は終了です。マッチしなかった場合は、次の caseitem の処理に移ります。
. マッチした場合は、直後の{{コマンド}}を実行します。その後の動作は caseitem の最後のトークンにより変わります。
* Caseitem が +;;+ で終わる (または +;;+ が省略されている) 場合、case 文の実行はそこで終了し、残りの caseitem は無視されます。
* Caseitem が +;&+ で終わる場合、次の caseitem の処理に進みます。ただし{{パターン}}の展開やマッチングは行わず、{{コマンド}}が直ちに実行されます。
* Caseitem が +;|+ または +;;&+ で終わる場合、後続の caseitem の処理を継続します。
. マッチしなかった場合は、次の caseitem の処理に移ります。

Case 文全体の終了ステータスは、実行した最後の{{コマンド}}の終了ステータスです。コマンドが実行されなかった場合 (caseitem が一つもないか、どのパターンもマッチしなかったか、最後にマッチしたパターンの後の{{コマンド}}が空の場合) は、終了ステータスは 0 です。

Case 文全体の終了ステータスは、実行した{{コマンド}}の終了ステータスです。{{コマンド}}が実行されなかった場合 (どのパターンもマッチしなかったか、caseitem が一つもないか、マッチしたパターンの後にコマンドがない場合) は、終了ステータスは 0 です
+;|+ と +;;&+ は link:posix.html[POSIX 準拠モード]では使用できない拡張機能です

[[double-bracket]]
=== 二重ブラケットコマンド
Expand Down
3 changes: 2 additions & 1 deletion doc/posix.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ When the POSIXly-correct mode is enabled:
- The link:syntax.html#for[for loop] iteration variable is created as global,
regardless of the link:_set.html#so-forlocal[for-local] shell option.
The variable must have a portable (ASCII-only) name.
- The first pattern in a link:syntax.html#case[case command] cannot be +esac+.
- The +;|+ and +;;&+ terminators cannot be used in a
link:syntax.html#case[case command].
- The +!+ keyword cannot be followed by +(+ without any whitespaces
in-between.
- The link:syntax.html#double-bracket[double-bracket command] cannot be used.
Expand Down
44 changes: 32 additions & 12 deletions doc/syntax.txt
Original file line number Diff line number Diff line change
Expand Up @@ -366,20 +366,29 @@ Case command syntax::
+case {{word}} in {{caseitem}}... esac+

Case item syntax::
+({{patterns}}) {{command}}...;;+
+({{patterns}}) {{command}}... ;;+
+
+({{patterns}}) {{command}}... ;&+
+
+({{patterns}}) {{command}}... ;|+
+
+({{patterns}}) {{command}}... ;;&+

The {{word}} between the +case+ and +in+ tokens must be exactly one word. The
{{word}} is not treated as a keyword, but you need to <<quotes,quote>>
separator characters (such as +&+ and +|+) to include them as part of the
{{word}}. Between the +in+ and +esac+ tokens you can put any number of case
items (may be none). You can omit the first +(+ token of a case item unless
the first pattern is +esac+. If the last {{command}} is terminated by a
linebreak, you can omit the last +;;+ token before the +esac+ token. The
{{command}}s in a case item may be empty.
items (or none).

You can omit the first +(+ token of a case item unless the first pattern is
+esac+.
The {{patterns}} in a case item are one or more tokens each separated by a +|+
token.

The {{command}}s in a case item may be empty.
If the last case item of a case command is terminated by +;;+, you can omit
the +;;+ token.

The execution of a case command starts with subjecting the {{word}} to
link:expand.html[the four expansions]. Next, the following steps are taken for
each case item (in the order of appearance):
Expand All @@ -391,13 +400,24 @@ each case item (in the order of appearance):
expanded. Yash expands and tests the patterns in the order of appearance,
but it may not be the case for other shells.)
. If one of the {{patterns}} was found to match the {{word}} in the previous
step, the {{command}}s in this case item are executed and the execution of
the whole case item ends. Otherwise, proceed to the next case item.

The exit status of a case command is that of the {{command}}s executed. The
exit status is zero if no
{{command}}s were executed, that is, there were no case items, no matching
pattern was found, or no commands were associated with the matching pattern.
step, the {{command}}s in this case item are executed.
* If the case item is terminated by +;;+ (or the +;;+ token is omitted), any
remaining case items are ignored and the execution of the whole case
command ends.
* If the case item is terminated by +;&+, continue to the next case item.
However, the {{patterns}} of the next item are not expanded nor tested,
and the {{command}}s are unconditionally executed.
* If the case item is terminated by +;|+ or +;;&+, continue to the next case
item, looking for a next match.
. If none of the {{patterns}} match the word, proceed to the next case item.

The exit status of a case command is that of the last {{command}}s executed.
The exit status is zero if commands were not executed, that is, there were no
case items, no matching pattern was found, or the {{command}}s were empty in
the last executed case item.

The +;|+ and +;;&+ terminators are a non-standard extension that cannot be
used in the link:posix.html[POSIXly-correct mode].

[[double-bracket]]
=== Double-bracket command
Expand Down
37 changes: 25 additions & 12 deletions exec.c
Original file line number Diff line number Diff line change
Expand Up @@ -1395,28 +1395,41 @@ void exec_case(const command_T *c, bool finally_exit)
if (word == NULL)
goto fail;

bool match = false, emptycmd = true;

for (const caseitem_T *ci = c->c_casitems; ci != NULL; ci = ci->next) {
for (void **pats = ci->ci_patterns; *pats != NULL; pats++) {
for (void **pats = ci->ci_patterns; !match && *pats != NULL; pats++) {
wchar_t *pattern =
expand_single(*pats, TT_SINGLE, Q_WORD, ES_QUOTED);
if (pattern == NULL)
goto fail;

bool match = match_pattern(word, pattern);
match = match_pattern(word, pattern);
free(pattern);
if (match) {
if (ci->ci_commands != NULL) {
exec_and_or_lists(ci->ci_commands, finally_exit);
goto done;
} else {
goto success;
}
}
}
if (!match)
continue;

exec_and_or_lists(
ci->ci_commands,
finally_exit && (ci->next == NULL || ci->ci_cont == CC_BREAK));
emptycmd = (ci->ci_commands == NULL);

switch (ci->ci_cont) {
case CC_BREAK:
goto done;
case CC_FALLTHRU:
match = true;
break;
case CC_CONTINUE:
match = false;
break;
}
}
success:
laststatus = Exit_SUCCESS;

done:
if (emptycmd)
laststatus = Exit_SUCCESS;
free(word);
if (finally_exit)
exit_shell();
Expand Down
63 changes: 53 additions & 10 deletions parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ typedef enum tokentype_T {
/* operators */
TT_NEWLINE,
TT_AMP, TT_AMPAMP, TT_LPAREN, TT_RPAREN, TT_SEMICOLON, TT_DOUBLE_SEMICOLON,
TT_SEMICOLONAMP, TT_SEMICOLONPIPE, TT_DOUBLE_SEMICOLON_AMP,
TT_PIPE, TT_PIPEPIPE, TT_LESS, TT_LESSLESS, TT_LESSAMP, TT_LESSLESSDASH,
TT_LESSLESSLESS, TT_LESSGREATER, TT_LESSLPAREN, TT_GREATER,
TT_GREATERGREATER, TT_GREATERGREATERPIPE, TT_GREATERPIPE, TT_GREATERAMP,
Expand Down Expand Up @@ -526,6 +527,9 @@ bool is_closing_tokentype(tokentype_T tt)
case TT_DO:
case TT_DONE:
case TT_DOUBLE_SEMICOLON:
case TT_DOUBLE_SEMICOLON_AMP:
case TT_SEMICOLONAMP:
case TT_SEMICOLONPIPE:
case TT_ESAC:
return true;
default:
Expand Down Expand Up @@ -881,6 +885,9 @@ const char *get_errmsg_unexpected_tokentype(tokentype_T tokentype)
case TT_RBRACE:
return Ngt("encountered `%ls' without a matching `{'");
case TT_DOUBLE_SEMICOLON:
case TT_DOUBLE_SEMICOLON_AMP:
case TT_SEMICOLONAMP:
case TT_SEMICOLONPIPE:
return Ngt("`%ls' is used outside `case'");
case TT_BANG:
return Ngt("`%ls' cannot be used as a command name");
Expand Down Expand Up @@ -1050,11 +1057,19 @@ void next_token(parsestate_T *ps)
case L')': ps->tokentype = TT_RPAREN; index++; break;
case L';':
maybe_line_continuations(ps, ++index);
if (ps->src.contents[index] == L';') {
ps->tokentype = TT_DOUBLE_SEMICOLON;
index++;
} else {
ps->tokentype = TT_SEMICOLON;
switch (ps->src.contents[index]) {
default: ps->tokentype = TT_SEMICOLON; break;
case L'&': ps->tokentype = TT_SEMICOLONAMP; index++; break;
case L'|': ps->tokentype = TT_SEMICOLONPIPE; index++; break;
case L';':
maybe_line_continuations(ps, ++index);
if (ps->src.contents[index] == L'&') {
ps->tokentype = TT_DOUBLE_SEMICOLON_AMP;
index++;
} else {
ps->tokentype = TT_DOUBLE_SEMICOLON;
}
break;
}
break;
case L'&':
Expand Down Expand Up @@ -2673,11 +2688,27 @@ caseitem_T *parse_case_list(parsestate_T *ps)
ci->ci_patterns = parse_case_patterns(ps);
ci->ci_commands = parse_compound_list(ps);
/* `ci_commands' may be NULL unlike for and while commands */
if (ps->tokentype == TT_DOUBLE_SEMICOLON)
next_token(ps);
else
break;
switch (ps->tokentype) {
case TT_DOUBLE_SEMICOLON:
ci->ci_cont = CC_BREAK;
break;
case TT_SEMICOLONAMP:
ci->ci_cont = CC_FALLTHRU;
break;
case TT_SEMICOLONPIPE:
case TT_DOUBLE_SEMICOLON_AMP:
ci->ci_cont = CC_CONTINUE;
if (posixly_correct)
serror(ps, Ngt("The ;| or ;;& operator is not supported "
"in the POSIXly-correct mode"));
break;
default:
ci->ci_cont = CC_BREAK;
goto done;
}
next_token(ps);
} while (!ps->error);
done:
return first;
}

Expand Down Expand Up @@ -3389,6 +3420,8 @@ static void print_caseitems(
struct print *restrict pr, const caseitem_T *restrict caseitems,
unsigned indent)
__attribute__((nonnull(1)));
static const wchar_t *case_item_terminator(casecont_T cc)
__attribute__((const));
#if YASH_ENABLE_DOUBLE_BRACKET
static void print_double_bracket(
struct print *restrict pr, const command_T *restrict c, unsigned indent)
Expand Down Expand Up @@ -3722,13 +3755,23 @@ void print_caseitems(struct print *restrict pr, const caseitem_T *restrict ci,
}

print_indent(pr, indent + 1);
wb_cat(&pr->buffer, L";;");
wb_cat(&pr->buffer, case_item_terminator(ci->ci_cont));
print_space_or_newline(pr);

ci = ci->next;
}
}

const wchar_t *case_item_terminator(casecont_T cc)
{
switch (cc) {
case CC_BREAK: return L";;";
case CC_FALLTHRU: return L";&";
case CC_CONTINUE: return L";|";
}
assert(false);
}

#if YASH_ENABLE_DOUBLE_BRACKET

void print_double_bracket(
Expand Down
10 changes: 9 additions & 1 deletion parser.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* Yash: yet another shell */
/* parser.h: syntax parser */
/* (C) 2007-2018 magicant */
/* (C) 2007-2024 magicant */

/* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -131,11 +131,19 @@ typedef struct ifcommand_T {
} ifcommand_T;
/* For an "else" clause, `next' and `ic_condition' are NULL. */

/* type of an case item terminator symbol */
typedef enum {
CC_BREAK, // ;;
CC_FALLTHRU, // ;&
CC_CONTINUE, // ;| aka ;;&
} casecont_T;

/* patterns and commands of a case command */
typedef struct caseitem_T {
struct caseitem_T *next;
void **ci_patterns; /* patterns to do matching */
struct and_or_T *ci_commands; /* commands executed if match succeeds */
casecont_T ci_cont; /* terminator symbol type */
} caseitem_T;
/* `ci_patterns' is a NULL-terminated array of pointers to `wordunit_T' that are
* cast to `void *'. */
Expand Down
21 changes: 21 additions & 0 deletions tests/case-p.tst
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,27 @@ case $(echo 2; exit 2) in
esac
__IN__

test_oE -e 42 'executing item after ;&'
case 1 in
0) echo not reached 0;;
1) echo matched 1;&
2) echo matched 2; (exit 42);&
esac
__IN__
matched 1
matched 2
__OUT__

test_oE 'exit status after empty ;& in case command'
(exit 1)
case i in
i) ;&
j) echo $?
esac
__IN__
1
__OUT__

test_oE 'patterns can be preceded by ('
case a in
(a) echo matched 1;;
Expand Down
Loading

0 comments on commit d306b74

Please sign in to comment.