Skip to content

Commit

Permalink
[osh] Implement shopt -s nocasematch (#1748)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
ellen364 and Andy C authored Oct 28, 2023
1 parent c50f341 commit e0043e0
Show file tree
Hide file tree
Showing 9 changed files with 43 additions and 20 deletions.
10 changes: 5 additions & 5 deletions cpp/libc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ BigStr* realpath(BigStr* path) {
return result;
}

int fnmatch(BigStr* pat, BigStr* str) {
int fnmatch(BigStr* pat, BigStr* str, int flags) {
// TODO: We should detect this at ./configure time, and then maybe flag these
// at parse time, not runtime
#ifdef FNM_EXTMATCH
int flags = FNM_EXTMATCH;
int flags_todo = FNM_EXTMATCH;
#else
int flags = 0;
int flags_todo = 0;
#endif

int result = ::fnmatch(pat->data_, str->data_, flags);
int result = ::fnmatch(pat->data_, str->data_, flags_todo);
switch (result) {
case 0:
return 1;
Expand Down Expand Up @@ -106,7 +106,7 @@ List<BigStr*>* glob(BigStr* pat) {

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

regex_t pat;
Expand Down
4 changes: 2 additions & 2 deletions cpp/libc.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ BigStr* realpath(BigStr* path);

BigStr* gethostname();

int fnmatch(BigStr* pat, BigStr* str);
int fnmatch(BigStr* pat, BigStr* str, int flags = 0);

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

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

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

int wcswidth(BigStr* str);
int get_terminal_width();
Expand Down
2 changes: 2 additions & 0 deletions cpp/preamble.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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

#include "_gen/core/optview.h"
Expand Down
2 changes: 1 addition & 1 deletion frontend/option_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ def DoneWithImplementedOptions(self):
'mailwarn',
'no_empty_cmd_completion',
'nocaseglob',
'nocasematch',
'progcomp_alias',
'promptvars',
'restricted_shell',
Expand Down Expand Up @@ -278,6 +277,7 @@ def _Init(opt_def):
# shopt options that aren't in any groups.
opt_def.Add('failglob')
opt_def.Add('extglob')
opt_def.Add('nocasematch')

# Compatibility
opt_def.Add(
Expand Down
5 changes: 4 additions & 1 deletion osh/cmd_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@

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

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

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

to_match = self._EvalCaseArg(node.to_match, node.case_kw)
fnmatch_flags = FNM_CASEFOLD if self.exec_opts.nocasematch() else 0
self._MaybeRunDebugTrap()

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

if libc.fnmatch(word_val.s, to_match_str.s):
if libc.fnmatch(word_val.s, to_match_str.s, fnmatch_flags):
status = self._ExecuteList(case_arm.action)
matched = True # TODO: Parse ;;& and for fallthrough and such?
break
Expand Down
11 changes: 8 additions & 3 deletions osh/sh_expr_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
from osh import word_eval

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

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

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

if op_id in (Id.BoolBinary_GlobEqual,
Id.BoolBinary_GlobDEqual):
#log('Matching %s against pattern %s', s1, s2)
return libc.fnmatch(s2, s1)
return libc.fnmatch(s2, s1, fnmatch_flags)

if op_id == Id.BoolBinary_GlobNEqual:
return not libc.fnmatch(s2, s1)
return not libc.fnmatch(s2, s1, fnmatch_flags)

if op_id in (Id.BoolBinary_Equal, Id.BoolBinary_DEqual):
return s1 == s2
Expand All @@ -1062,8 +1065,10 @@ def EvalB(self, node):
if op_id == Id.BoolBinary_EqualTilde:
# TODO: This should go to --debug-file
#log('Matching %r against regex %r', s1, s2)
regex_flags = REG_ICASE if self.exec_opts.nocasematch() else 0

try:
matches = libc.regex_match(s2, s1)
matches = libc.regex_match(s2, s1, regex_flags)
except RuntimeError as e:
# Status 2 indicates a regex parse error. This is fatal in OSH but
# not in bash, which treats [[ like a command with an exit code.
Expand Down
20 changes: 15 additions & 5 deletions pyext/libc.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ static PyObject *
func_fnmatch(PyObject *self, PyObject *args) {
const char *pattern;
const char *str;
int flags = 0;

if (!PyArg_ParseTuple(args, "ss", &pattern, &str)) {
if (!PyArg_ParseTuple(args, "ss|i", &pattern, &str, &flags)) {
return NULL;
}

int flags = 0;
// NOTE: Testing for __GLIBC__ is the version detection anti-pattern. We
// should really use feature detection in our configure script. But I plan
// to get rid of the dependency on FNM_EXTMATCH because it doesn't work on
Expand Down Expand Up @@ -185,12 +185,15 @@ static PyObject *
func_regex_match(PyObject *self, PyObject *args) {
const char* pattern;
const char* str;
if (!PyArg_ParseTuple(args, "ss", &pattern, &str)) {
int flags = 0;

if (!PyArg_ParseTuple(args, "ss|i", &pattern, &str, &flags)) {
return NULL;
}

flags |= REG_EXTENDED;
regex_t pat;
int status = regcomp(&pat, pattern, REG_EXTENDED);
int status = regcomp(&pat, pattern, flags);
if (status != 0) {
char error_string[80];
regerror(status, &pat, error_string, 80);
Expand Down Expand Up @@ -408,7 +411,14 @@ static PyMethodDef methods[] = {
#endif

void initlibc(void) {
Py_InitModule("libc", methods);
PyObject *module;

module = Py_InitModule("libc", methods);
if (module != NULL) {
PyModule_AddIntConstant(module, "FNM_CASEFOLD", FNM_CASEFOLD);
PyModule_AddIntConstant(module, "REG_ICASE", REG_ICASE);
}

errno_error = PyErr_NewException("libc.error",
PyExc_IOError, NULL);
}
7 changes: 5 additions & 2 deletions pyext/libc.pyi
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from typing import List, Optional, Tuple

FNM_CASEFOLD: int
REG_ICASE: int

def gethostname() -> str: ...
def glob(pat: str) -> List[str]: ...
def fnmatch(pat: str, s: str) -> bool: ...
def fnmatch(pat: str, s: str, flags: int = 0) -> bool: ...
def regex_first_group_match(regex: str, s: str, pos: int) -> Optional[Tuple[int, int]]: ...
def regex_match(regex: str, s: str) -> Optional[List[str]]: ...
def regex_match(regex: str, s: str, flags: int = 0) -> Optional[List[str]]: ...
def wcswidth(s: str) -> int: ...
def get_terminal_width() -> int: ...
def print_time(real: float, user: float, sys: float) -> None: ...
Expand Down
2 changes: 1 addition & 1 deletion spec/nocasematch-match.test.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## compare_shells: bash
## oils_failures_allowed: 3
## oils_failures_allowed: 0

# Tests nocasematch matching

Expand Down

0 comments on commit e0043e0

Please sign in to comment.