Skip to content

Commit 59a5923

Browse files
authored
Merge pull request #295 from python-cmd2/case_sensitivity
Removed support for case-insensitive command parsing
2 parents 40b5225 + 254cc08 commit 59a5923

File tree

10 files changed

+29
-108
lines changed

10 files changed

+29
-108
lines changed

.pytest_cache/v/cache/lastfailed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
* See [tab_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_completion.py)
1919
* Attributes Removed (**can cause breaking changes**)
2020
* ``abbrev`` - Removed support for abbreviated commands
21-
* Good tab completion makes this unnecessary
21+
* Good tab completion makes this unnecessary and its presence could cause harmful unintended actions
22+
* ``case_insensitive`` - Removed support for case-insensitive command parsing
23+
* Its presence wasn't very helpful and could cause harmful unintended actions
2224

2325
## 0.8.0 (February 1, 2018)
2426
* Bug Fixes

README.md

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Main Features
2626
- Redirect command output to file with `>`, `>>`; input from file with `<`
2727
- Bare `>`, `>>` with no filename send output to paste buffer (clipboard)
2828
- `py` enters interactive Python console (opt-in `ipy` for IPython console)
29-
- Multi-line and case-insensitive commands
29+
- Multi-line commands
3030
- Special-character command shortcuts (beyond cmd's `@` and `!`)
3131
- Settable environment parameters
3232
- Parsing commands with arguments using `argparse`, including support for sub-commands
@@ -94,10 +94,6 @@ Instructions for implementing each feature follow.
9494
The program will keep expecting input until a line ends with any of the characters
9595
in `Cmd.terminators` . The default terminators are `;` and `/n` (empty newline).
9696

97-
- Case-insensitive commands
98-
99-
All commands are case-insensitive, unless ``Cmd.caseInsensitive`` is set to ``False``.
100-
10197
- Special-character shortcut commands (beyond cmd's "@" and "!")
10298

10399
To create a single-character shortcut for a command, update `Cmd.shortcuts`.

cmd2.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
To use, simply import cmd2.Cmd instead of cmd.Cmd; use precisely as though you
66
were using the standard library's cmd, while enjoying the extra features.
77
8-
Searchable command history (commands: "history", "list", "run")
8+
Searchable command history (commands: "history")
99
Load commands from file, save to file, edit commands in file
1010
Multi-line commands
11-
Case-insensitive commands
1211
Special-character shortcut commands (beyond cmd's "@" and "!")
1312
Settable environment parameters
14-
Optional _onchange_{paramname} called when environment parameter changes
15-
Parsing commands with `optparse` options (flags)
13+
Parsing commands with `argparse` argument parsers (flags)
1614
Redirection to file with >, >>; input from file with <
1715
Easy transcript-based testing of applications (see examples/example.py)
1816
Bash-style ``select`` available
@@ -991,7 +989,6 @@ class Cmd(cmd.Cmd):
991989
"""
992990
# Attributes used to configure the ParserManager (all are not dynamically settable at runtime)
993991
blankLinesAllowed = False
994-
case_insensitive = True # Commands recognized regardless of case
995992
commentGrammars = pyparsing.Or([pyparsing.pythonStyleComment, pyparsing.cStyleComment])
996993
commentInProgress = pyparsing.Literal('/*') + pyparsing.SkipTo(pyparsing.stringEnd ^ '*/')
997994
legalChars = u'!#$%.:?@_-' + pyparsing.alphanums + pyparsing.alphas8bit
@@ -1086,7 +1083,6 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_histor
10861083
multilineCommands=self.multilineCommands,
10871084
legalChars=self.legalChars, commentGrammars=self.commentGrammars,
10881085
commentInProgress=self.commentInProgress,
1089-
case_insensitive=self.case_insensitive,
10901086
blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser,
10911087
preparse=self.preparse, postparse=self.postparse, shortcuts=self.shortcuts)
10921088
self._transcript_files = transcript_files
@@ -1216,12 +1212,8 @@ def colorize(self, val, color):
12161212
# noinspection PyMethodOverriding
12171213
def completenames(self, text, line, begidx, endidx):
12181214
"""Override of cmd method which completes command names both for command completion and help."""
1219-
command = text
1220-
if self.case_insensitive:
1221-
command = text.lower()
1222-
12231215
# Call super class method. Need to do it this way for Python 2 and 3 compatibility
1224-
cmd_completion = cmd.Cmd.completenames(self, command)
1216+
cmd_completion = cmd.Cmd.completenames(self, text)
12251217

12261218
# If we are completing the initial command name and get exactly 1 result and are at end of line, add a space
12271219
if begidx == 0 and len(cmd_completion) == 1 and endidx == len(line):
@@ -1954,15 +1946,14 @@ def cmdenvironment(self):
19541946
:return: str - summary report of read-only settings which the user cannot modify at runtime
19551947
"""
19561948
read_only_settings = """
1957-
Commands are case-sensitive: {}
19581949
Commands may be terminated with: {}
19591950
Arguments at invocation allowed: {}
19601951
Output redirection and pipes allowed: {}
19611952
Parsing of @options commands:
19621953
Shell lexer mode for command argument splitting: {}
19631954
Strip Quotes after splitting arguments: {}
19641955
Argument type: {}
1965-
""".format(not self.case_insensitive, str(self.terminators), self.allow_cli_args, self.allow_redirection,
1956+
""".format(str(self.terminators), self.allow_cli_args, self.allow_redirection,
19661957
"POSIX" if POSIX_SHLEX else "non-POSIX",
19671958
"True" if STRIP_QUOTES_FOR_NON_POSIX and not POSIX_SHLEX else "False",
19681959
"List of argument strings" if USE_ARG_LIST else "string of space-separated arguments")
@@ -2575,7 +2566,7 @@ class ParserManager:
25752566
Class which encapsulates all of the pyparsing parser functionality for cmd2 in a single location.
25762567
"""
25772568
def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars, commentInProgress,
2578-
case_insensitive, blankLinesAllowed, prefixParser, preparse, postparse, shortcuts):
2569+
blankLinesAllowed, prefixParser, preparse, postparse, shortcuts):
25792570
"""Creates and uses parsers for user input according to app's parameters."""
25802571

25812572
self.commentGrammars = commentGrammars
@@ -2586,13 +2577,12 @@ def __init__(self, redirector, terminators, multilineCommands, legalChars, comme
25862577
self.main_parser = self._build_main_parser(redirector=redirector, terminators=terminators,
25872578
multilineCommands=multilineCommands, legalChars=legalChars,
25882579
commentInProgress=commentInProgress,
2589-
case_insensitive=case_insensitive,
25902580
blankLinesAllowed=blankLinesAllowed, prefixParser=prefixParser)
25912581
self.input_source_parser = self._build_input_source_parser(legalChars=legalChars,
25922582
commentInProgress=commentInProgress)
25932583

2594-
def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars,
2595-
commentInProgress, case_insensitive, blankLinesAllowed, prefixParser):
2584+
def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars, commentInProgress,
2585+
blankLinesAllowed, prefixParser):
25962586
"""Builds a PyParsing parser for interpreting user commands."""
25972587

25982588
# Build several parsing components that are eventually compiled into overall parser
@@ -2604,7 +2594,7 @@ def _build_main_parser(self, redirector, terminators, multilineCommands, legalCh
26042594
[(hasattr(t, 'parseString') and t) or pyparsing.Literal(t) for t in terminators])('terminator')
26052595
string_end = pyparsing.stringEnd ^ '\nEOF'
26062596
multilineCommand = pyparsing.Or(
2607-
[pyparsing.Keyword(c, caseless=case_insensitive) for c in multilineCommands])('multilineCommand')
2597+
[pyparsing.Keyword(c, caseless=False) for c in multilineCommands])('multilineCommand')
26082598
oneline_command = (~multilineCommand + pyparsing.Word(legalChars))('command')
26092599
pipe = pyparsing.Keyword('|', identChars='|')
26102600
do_not_parse = self.commentGrammars | commentInProgress | pyparsing.quotedString
@@ -2614,12 +2604,9 @@ def _build_main_parser(self, redirector, terminators, multilineCommands, legalCh
26142604
pyparsing.Optional(output_destination_parser +
26152605
pyparsing.SkipTo(string_end,
26162606
ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('outputTo'))
2617-
if case_insensitive:
2618-
multilineCommand.setParseAction(lambda x: x[0].lower())
2619-
oneline_command.setParseAction(lambda x: x[0].lower())
2620-
else:
2621-
multilineCommand.setParseAction(lambda x: x[0])
2622-
oneline_command.setParseAction(lambda x: x[0])
2607+
2608+
multilineCommand.setParseAction(lambda x: x[0])
2609+
oneline_command.setParseAction(lambda x: x[0])
26232610

26242611
if blankLinesAllowed:
26252612
blankLineTerminationParser = pyparsing.NoMatch
@@ -2687,7 +2674,7 @@ def parsed(self, raw):
26872674
s = self.input_source_parser.transformString(s.lstrip())
26882675
s = self.commentGrammars.transformString(s)
26892676
for (shortcut, expansion) in self.shortcuts:
2690-
if s.lower().startswith(shortcut):
2677+
if s.startswith(shortcut):
26912678
s = s.replace(shortcut, expansion + ' ', 1)
26922679
break
26932680
try:

docs/settingchanges.rst

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,6 @@ A parameter can also be changed at runtime by the user *if*
88
its name is included in the dictionary ``app.settable``.
99
(To define your own user-settable parameters, see :ref:`parameters`)
1010

11-
Case-insensitivity
12-
==================
13-
14-
By default, all ``cmd2`` command names are case-insensitive;
15-
``sing the blues`` and ``SiNg the blues`` are equivalent. To change this,
16-
set ``App.case_insensitive`` to False.
17-
18-
Whether or not you set ``case_insensitive``, *please do not* define
19-
command method names with any uppercase letters. ``cmd2`` expects all command methods
20-
to be lowercase.
2111

2212
Shortcuts (command aliases)
2313
===========================

examples/case_sensitive.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
- Redirect command output to file with `>`, `>>`; input from file with `<`
2727
- Bare `>`, `>>` with no filename send output to paste buffer (clipboard)
2828
- `py` enters interactive Python console (opt-in `ipy` for IPython console)
29-
- Multi-line and case-insensitive commands
29+
- Multi-line commands
3030
- Special-character command shortcuts (beyond cmd's `@` and `!`)
3131
- Settable environment parameters
3232
- Parsing commands with arguments using `argparse`, including support for sub-commands

tests/test_cmd2.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ def test_base_show_readonly(base_app):
8888
base_app.editor = 'vim'
8989
out = run_cmd(base_app, 'set -a')
9090
expected = normalize(SHOW_TXT + '\nRead only settings:' + """
91-
Commands are case-sensitive: {}
9291
Commands may be terminated with: {}
9392
Arguments at invocation allowed: {}
9493
Output redirection and pipes allowed: {}
@@ -97,7 +96,7 @@ def test_base_show_readonly(base_app):
9796
Strip Quotes after splitting arguments: {}
9897
Argument type: {}
9998
100-
""".format(not base_app.case_insensitive, base_app.terminators, base_app.allow_cli_args, base_app.allow_redirection,
99+
""".format(base_app.terminators, base_app.allow_cli_args, base_app.allow_redirection,
101100
"POSIX" if cmd2.POSIX_SHLEX else "non-POSIX",
102101
"True" if cmd2.STRIP_QUOTES_FOR_NON_POSIX and not cmd2.POSIX_SHLEX else "False",
103102
"List of argument strings" if cmd2.USE_ARG_LIST else "string of space-separated arguments"))

tests/test_completion.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ def cmd2_app():
2626

2727
@pytest.fixture
2828
def cs_app():
29-
cmd2.Cmd.case_insensitive = False
3029
c = cmd2.Cmd()
3130
return c
3231

@@ -136,21 +135,13 @@ def get_endidx():
136135

137136
assert first_match is None
138137

139-
def test_cmd2_command_completion_is_case_insensitive_by_default(cmd2_app):
138+
def test_cmd2_command_completion_is_case_sensitive(cmd2_app):
140139
text = 'HE'
141140
line = 'HE'
142141
endidx = len(line)
143142
begidx = endidx - len(text)
144143
# It is at end of line, so extra space is present
145-
assert cmd2_app.completenames(text, line, begidx, endidx) == ['help ']
146-
147-
def test_cmd2_case_sensitive_command_completion(cs_app):
148-
text = 'HE'
149-
line = 'HE'
150-
endidx = len(line)
151-
begidx = endidx - len(text)
152-
# It is at end of line, so extra space is present
153-
assert cs_app.completenames(text, line, begidx, endidx) == []
144+
assert cmd2_app.completenames(text, line, begidx, endidx) == []
154145

155146
def test_cmd2_command_completion_single_mid(cmd2_app):
156147
text = 'he'

tests/test_parsing.py

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,41 +20,26 @@ def hist():
2020
h = cmd2.History([HistoryItem('first'), HistoryItem('second'), HistoryItem('third'), HistoryItem('fourth')])
2121
return h
2222

23-
# Case-insensitive parser
23+
# Case-sensitive parser
2424
@pytest.fixture
2525
def parser():
2626
c = cmd2.Cmd()
2727
c.multilineCommands = ['multiline']
28-
c.case_insensitive = True
29-
c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands,
30-
legalChars=c.legalChars, commentGrammars=c.commentGrammars,
31-
commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive,
28+
c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators,
29+
multilineCommands=c.multilineCommands, legalChars=c.legalChars,
30+
commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress,
3231
blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser,
3332
preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts)
3433
return c.parser_manager.main_parser
3534

36-
# Case-insensitive ParserManager
37-
@pytest.fixture
38-
def ci_pm():
39-
c = cmd2.Cmd()
40-
c.multilineCommands = ['multiline']
41-
c.case_insensitive = True
42-
c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands,
43-
legalChars=c.legalChars, commentGrammars=c.commentGrammars,
44-
commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive,
45-
blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser,
46-
preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts)
47-
return c.parser_manager
48-
4935
# Case-sensitive ParserManager
5036
@pytest.fixture
5137
def cs_pm():
5238
c = cmd2.Cmd()
5339
c.multilineCommands = ['multiline']
54-
c.case_insensitive = False
55-
c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands,
56-
legalChars=c.legalChars, commentGrammars=c.commentGrammars,
57-
commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive,
40+
c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators,
41+
multilineCommands=c.multilineCommands, legalChars=c.legalChars,
42+
commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress,
5843
blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser,
5944
preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts)
6045
return c.parser_manager
@@ -167,7 +152,7 @@ def test_parse_suffix_after_terminator(parser):
167152
assert results.suffix == 'suffx'
168153

169154
def test_parse_command_with_args(parser):
170-
line = 'COMmand with args'
155+
line = 'command with args'
171156
results = parser.parseString(line)
172157
assert results.command == 'command'
173158
assert results.args == 'with args'
@@ -219,11 +204,6 @@ def test_parse_output_redirect_with_dash_in_path(parser):
219204
assert results.outputTo == 'python-cmd2/afile.txt'
220205

221206

222-
def test_case_insensitive_parsed_single_word(ci_pm):
223-
line = 'HeLp'
224-
statement = ci_pm.parsed(line)
225-
assert statement.parsed.command == line.lower()
226-
227207
def test_case_sensitive_parsed_single_word(cs_pm):
228208
line = 'HeLp'
229209
statement = cs_pm.parsed(line)

0 commit comments

Comments
 (0)