Skip to content

Commit e0043e0

Browse files
ellen364Andy C
and
Andy C
authored
[osh] Implement shopt -s nocasematch (#1748)
All tests in spec/nocasematch-match/test.sh now pass (3 failures previously allowed). [translation] Use #included macros directly. We should move away from this style with mycpp/yaks "modules" --------- Co-authored-by: Andy C <andy@oilshell.org>
1 parent c50f341 commit e0043e0

9 files changed

+43
-20
lines changed

cpp/libc.cc

+5-5
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ BigStr* realpath(BigStr* path) {
3636
return result;
3737
}
3838

39-
int fnmatch(BigStr* pat, BigStr* str) {
39+
int fnmatch(BigStr* pat, BigStr* str, int flags) {
4040
// TODO: We should detect this at ./configure time, and then maybe flag these
4141
// at parse time, not runtime
4242
#ifdef FNM_EXTMATCH
43-
int flags = FNM_EXTMATCH;
43+
int flags_todo = FNM_EXTMATCH;
4444
#else
45-
int flags = 0;
45+
int flags_todo = 0;
4646
#endif
4747

48-
int result = ::fnmatch(pat->data_, str->data_, flags);
48+
int result = ::fnmatch(pat->data_, str->data_, flags_todo);
4949
switch (result) {
5050
case 0:
5151
return 1;
@@ -106,7 +106,7 @@ List<BigStr*>* glob(BigStr* pat) {
106106

107107
// Raises RuntimeError if the pattern is invalid. TODO: Use a different
108108
// exception?
109-
List<BigStr*>* regex_match(BigStr* pattern, BigStr* str) {
109+
List<BigStr*>* regex_match(BigStr* pattern, BigStr* str, int flags) {
110110
List<BigStr*>* results = NewList<BigStr*>();
111111

112112
regex_t pat;

cpp/libc.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ BigStr* realpath(BigStr* path);
2020

2121
BigStr* gethostname();
2222

23-
int fnmatch(BigStr* pat, BigStr* str);
23+
int fnmatch(BigStr* pat, BigStr* str, int flags = 0);
2424

2525
List<BigStr*>* glob(BigStr* pat);
2626

2727
Tuple2<int, int>* regex_first_group_match(BigStr* pattern, BigStr* str,
2828
int pos);
2929

30-
List<BigStr*>* regex_match(BigStr* pattern, BigStr* str);
30+
List<BigStr*>* regex_match(BigStr* pattern, BigStr* str, int flags = 0);
3131

3232
int wcswidth(BigStr* str);
3333
int get_terminal_width();

cpp/preamble.h

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#include <errno.h>
44
#include <fcntl.h> // e.g. F_DUPFD used directly
5+
#include <fnmatch.h> // FNM_CASEFOLD in osh/sh_expr_eval.py
6+
#include <regex.h> // REG_ICASE in osh/sh_expr_eval.py
57
#include <sys/wait.h> // e.g. WIFSIGNALED() called directly
68

79
#include "_gen/core/optview.h"

frontend/option_def.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,6 @@ def DoneWithImplementedOptions(self):
232232
'mailwarn',
233233
'no_empty_cmd_completion',
234234
'nocaseglob',
235-
'nocasematch',
236235
'progcomp_alias',
237236
'promptvars',
238237
'restricted_shell',
@@ -278,6 +277,7 @@ def _Init(opt_def):
278277
# shopt options that aren't in any groups.
279278
opt_def.Add('failglob')
280279
opt_def.Add('extglob')
280+
opt_def.Add('nocasematch')
281281

282282
# Compatibility
283283
opt_def.Add(

osh/cmd_eval.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@
8989

9090
import posix_ as posix
9191
import libc # for fnmatch
92+
# Import this name directly because the C++ translation uses macros literally.
93+
from libc import FNM_CASEFOLD
9294

9395
from typing import List, Dict, Tuple, Optional, Any, cast, TYPE_CHECKING
9496

@@ -1436,6 +1438,7 @@ def _DoCase(self, node):
14361438
# type: (command.Case) -> int
14371439

14381440
to_match = self._EvalCaseArg(node.to_match, node.case_kw)
1441+
fnmatch_flags = FNM_CASEFOLD if self.exec_opts.nocasematch() else 0
14391442
self._MaybeRunDebugTrap()
14401443

14411444
status = 0 # If there are no arms, it should be zero?
@@ -1454,7 +1457,7 @@ def _DoCase(self, node):
14541457
word_val = self.word_ev.EvalWordToString(
14551458
pat_word, word_eval.QUOTE_FNMATCH)
14561459

1457-
if libc.fnmatch(word_val.s, to_match_str.s):
1460+
if libc.fnmatch(word_val.s, to_match_str.s, fnmatch_flags):
14581461
status = self._ExecuteList(case_arm.action)
14591462
matched = True # TODO: Parse ;;& and for fallthrough and such?
14601463
break

osh/sh_expr_eval.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@
5757
from osh import word_eval
5858

5959
import libc # for fnmatch
60+
# Import these names directly because the C++ translation uses macros literally.
61+
from libc import FNM_CASEFOLD, REG_ICASE
6062

6163
from typing import Tuple, Optional, cast, TYPE_CHECKING
6264
if TYPE_CHECKING:
@@ -1044,14 +1046,15 @@ def EvalB(self, node):
10441046
raise AssertionError(op_id) # should never happen
10451047

10461048
if arg_type == bool_arg_type_e.Str:
1049+
fnmatch_flags = FNM_CASEFOLD if self.exec_opts.nocasematch() else 0
10471050

10481051
if op_id in (Id.BoolBinary_GlobEqual,
10491052
Id.BoolBinary_GlobDEqual):
10501053
#log('Matching %s against pattern %s', s1, s2)
1051-
return libc.fnmatch(s2, s1)
1054+
return libc.fnmatch(s2, s1, fnmatch_flags)
10521055

10531056
if op_id == Id.BoolBinary_GlobNEqual:
1054-
return not libc.fnmatch(s2, s1)
1057+
return not libc.fnmatch(s2, s1, fnmatch_flags)
10551058

10561059
if op_id in (Id.BoolBinary_Equal, Id.BoolBinary_DEqual):
10571060
return s1 == s2
@@ -1062,8 +1065,10 @@ def EvalB(self, node):
10621065
if op_id == Id.BoolBinary_EqualTilde:
10631066
# TODO: This should go to --debug-file
10641067
#log('Matching %r against regex %r', s1, s2)
1068+
regex_flags = REG_ICASE if self.exec_opts.nocasematch() else 0
1069+
10651070
try:
1066-
matches = libc.regex_match(s2, s1)
1071+
matches = libc.regex_match(s2, s1, regex_flags)
10671072
except RuntimeError as e:
10681073
# Status 2 indicates a regex parse error. This is fatal in OSH but
10691074
# not in bash, which treats [[ like a command with an exit code.

pyext/libc.c

+15-5
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ static PyObject *
5555
func_fnmatch(PyObject *self, PyObject *args) {
5656
const char *pattern;
5757
const char *str;
58+
int flags = 0;
5859

59-
if (!PyArg_ParseTuple(args, "ss", &pattern, &str)) {
60+
if (!PyArg_ParseTuple(args, "ss|i", &pattern, &str, &flags)) {
6061
return NULL;
6162
}
6263

63-
int flags = 0;
6464
// NOTE: Testing for __GLIBC__ is the version detection anti-pattern. We
6565
// should really use feature detection in our configure script. But I plan
6666
// to get rid of the dependency on FNM_EXTMATCH because it doesn't work on
@@ -185,12 +185,15 @@ static PyObject *
185185
func_regex_match(PyObject *self, PyObject *args) {
186186
const char* pattern;
187187
const char* str;
188-
if (!PyArg_ParseTuple(args, "ss", &pattern, &str)) {
188+
int flags = 0;
189+
190+
if (!PyArg_ParseTuple(args, "ss|i", &pattern, &str, &flags)) {
189191
return NULL;
190192
}
191193

194+
flags |= REG_EXTENDED;
192195
regex_t pat;
193-
int status = regcomp(&pat, pattern, REG_EXTENDED);
196+
int status = regcomp(&pat, pattern, flags);
194197
if (status != 0) {
195198
char error_string[80];
196199
regerror(status, &pat, error_string, 80);
@@ -408,7 +411,14 @@ static PyMethodDef methods[] = {
408411
#endif
409412

410413
void initlibc(void) {
411-
Py_InitModule("libc", methods);
414+
PyObject *module;
415+
416+
module = Py_InitModule("libc", methods);
417+
if (module != NULL) {
418+
PyModule_AddIntConstant(module, "FNM_CASEFOLD", FNM_CASEFOLD);
419+
PyModule_AddIntConstant(module, "REG_ICASE", REG_ICASE);
420+
}
421+
412422
errno_error = PyErr_NewException("libc.error",
413423
PyExc_IOError, NULL);
414424
}

pyext/libc.pyi

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from typing import List, Optional, Tuple
22

3+
FNM_CASEFOLD: int
4+
REG_ICASE: int
5+
36
def gethostname() -> str: ...
47
def glob(pat: str) -> List[str]: ...
5-
def fnmatch(pat: str, s: str) -> bool: ...
8+
def fnmatch(pat: str, s: str, flags: int = 0) -> bool: ...
69
def regex_first_group_match(regex: str, s: str, pos: int) -> Optional[Tuple[int, int]]: ...
7-
def regex_match(regex: str, s: str) -> Optional[List[str]]: ...
10+
def regex_match(regex: str, s: str, flags: int = 0) -> Optional[List[str]]: ...
811
def wcswidth(s: str) -> int: ...
912
def get_terminal_width() -> int: ...
1013
def print_time(real: float, user: float, sys: float) -> None: ...

spec/nocasematch-match.test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## compare_shells: bash
2-
## oils_failures_allowed: 3
2+
## oils_failures_allowed: 0
33

44
# Tests nocasematch matching
55

0 commit comments

Comments
 (0)