summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorEric Lin <anselor@gmail.com>2018-05-16 10:39:49 -0400
committerEric Lin <anselor@gmail.com>2018-05-16 10:39:49 -0400
commit371284d20370a8e85dd8527d9bbcc6267b335cde (patch)
tree002ac709eca15522a0b1abaa235aae04975b08e6 /tests
parentab8194e92b9c3728d8f86cb9c81de180b6884eee (diff)
parenta9b712108e5af49937b0af3aa51db2ebe5c159e4 (diff)
downloadcmd2-git-371284d20370a8e85dd8527d9bbcc6267b335cde.tar.gz
Merge branch 'master' into pyscript
Diffstat (limited to 'tests')
-rw-r--r--tests/test_argparse.py13
-rw-r--r--tests/test_bashcompletion.py232
-rw-r--r--tests/test_cmd2.py38
-rw-r--r--tests/test_completion.py133
-rw-r--r--tests/test_parsing.py177
-rw-r--r--tests/test_submenu.py181
-rw-r--r--tests/test_transcript.py175
-rw-r--r--tests/transcripts/from_cmdloop.txt1
8 files changed, 450 insertions, 500 deletions
diff --git a/tests/test_argparse.py b/tests/test_argparse.py
index 94a7b5ed..f1a2b357 100644
--- a/tests/test_argparse.py
+++ b/tests/test_argparse.py
@@ -119,6 +119,11 @@ def argparse_app():
return app
+def test_invalid_syntax(argparse_app, capsys):
+ run_cmd(argparse_app, 'speak "')
+ out, err = capsys.readouterr()
+ assert err == "ERROR: Invalid syntax: No closing quotation\n"
+
def test_argparse_basic_command(argparse_app):
out = run_cmd(argparse_app, 'say hello')
assert out == ['hello']
@@ -135,6 +140,14 @@ def test_argparse_with_list_and_empty_doc(argparse_app):
out = run_cmd(argparse_app, 'speak -s hello world!')
assert out == ['HELLO WORLD!']
+def test_argparse_comment_stripping(argparse_app):
+ out = run_cmd(argparse_app, 'speak it was /* not */ delicious! # Yuck!')
+ assert out == ['it was delicious!']
+
+def test_argparser_correct_args_with_quotes_and_midline_options(argparse_app):
+ out = run_cmd(argparse_app, "speak 'This is a' -s test of the emergency broadcast system!")
+ assert out == ['THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!']
+
def test_argparse_quoted_arguments_multiple(argparse_app):
out = run_cmd(argparse_app, 'say "hello there" "rick & morty"')
assert out == ['hello there rick & morty']
diff --git a/tests/test_bashcompletion.py b/tests/test_bashcompletion.py
new file mode 100644
index 00000000..ceae2aa9
--- /dev/null
+++ b/tests/test_bashcompletion.py
@@ -0,0 +1,232 @@
+# coding=utf-8
+"""
+Unit/functional testing for argparse completer in cmd2
+
+Copyright 2018 Eric Lin <anselor@gmail.com>
+Released under MIT license, see LICENSE file
+"""
+import os
+import pytest
+import sys
+from typing import List
+
+from cmd2.argparse_completer import ACArgumentParser, AutoCompleter
+
+
+try:
+ from cmd2.argcomplete_bridge import CompletionFinder
+ skip_reason1 = False
+ skip_reason = ''
+except ImportError:
+ # Don't test if argcomplete isn't present (likely on Windows)
+ skip_reason1 = True
+ skip_reason = "argcomplete isn't installed\n"
+
+skip_reason2 = "TRAVIS" in os.environ and os.environ["TRAVIS"] == "true"
+if skip_reason2:
+ skip_reason += 'These tests cannot run on TRAVIS\n'
+
+skip_reason3 = sys.platform.startswith('win')
+if skip_reason3:
+ skip_reason = 'argcomplete doesn\'t support Windows'
+
+skip = skip_reason1 or skip_reason2 or skip_reason3
+
+skip_mac = sys.platform.startswith('dar')
+
+
+actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew',
+ 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac',
+ 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman',
+ 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee']
+
+
+def query_actors() -> List[str]:
+ """Simulating a function that queries and returns a completion values"""
+ return actors
+
+
+@pytest.fixture
+def parser1():
+ """creates a argparse object to test completion against"""
+ ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17']
+
+ def _do_media_movies(self, args) -> None:
+ if not args.command:
+ self.do_help('media movies')
+ else:
+ print('media movies ' + str(args.__dict__))
+
+ def _do_media_shows(self, args) -> None:
+ if not args.command:
+ self.do_help('media shows')
+
+ if not args.command:
+ self.do_help('media shows')
+ else:
+ print('media shows ' + str(args.__dict__))
+
+ media_parser = ACArgumentParser(prog='media')
+
+ media_types_subparsers = media_parser.add_subparsers(title='Media Types', dest='type')
+
+ movies_parser = media_types_subparsers.add_parser('movies')
+ movies_parser.set_defaults(func=_do_media_movies)
+
+ movies_commands_subparsers = movies_parser.add_subparsers(title='Commands', dest='command')
+
+ movies_list_parser = movies_commands_subparsers.add_parser('list')
+
+ movies_list_parser.add_argument('-t', '--title', help='Title Filter')
+ movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+',
+ choices=ratings_types)
+ movies_list_parser.add_argument('-d', '--director', help='Director Filter')
+ movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append')
+
+ movies_add_parser = movies_commands_subparsers.add_parser('add')
+ movies_add_parser.add_argument('title', help='Movie Title')
+ movies_add_parser.add_argument('rating', help='Movie Rating', choices=ratings_types)
+ movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), required=True)
+ movies_add_parser.add_argument('actor', help='Actors', nargs='*')
+
+ movies_commands_subparsers.add_parser('delete')
+
+ shows_parser = media_types_subparsers.add_parser('shows')
+ shows_parser.set_defaults(func=_do_media_shows)
+
+ shows_commands_subparsers = shows_parser.add_subparsers(title='Commands', dest='command')
+
+ shows_commands_subparsers.add_parser('list')
+
+ return media_parser
+
+
+# noinspection PyShadowingNames
+@pytest.mark.skipif(skip, reason=skip_reason)
+def test_bash_nocomplete(parser1):
+ completer = CompletionFinder()
+ result = completer(parser1, AutoCompleter(parser1))
+ assert result is None
+
+
+# save the real os.fdopen
+os_fdopen = os.fdopen
+
+
+def my_fdopen(fd, mode, *args):
+ """mock fdopen that redirects 8 and 9 from argcomplete to stdin/stdout for testing"""
+ if fd > 7:
+ return os_fdopen(fd - 7, mode, *args)
+ return os_fdopen(fd, mode)
+
+
+# noinspection PyShadowingNames
+@pytest.mark.skipif(skip, reason=skip_reason)
+def test_invalid_ifs(parser1, mock):
+ completer = CompletionFinder()
+
+ mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
+ '_ARGCOMPLETE_IFS': '\013\013'})
+
+ mock.patch.object(os, 'fdopen', my_fdopen)
+
+ with pytest.raises(SystemExit):
+ completer(parser1, AutoCompleter(parser1), exit_method=sys.exit)
+
+
+# noinspection PyShadowingNames
+@pytest.mark.skipif(skip or skip_mac, reason=skip_reason)
+@pytest.mark.parametrize('comp_line, exp_out, exp_err', [
+ ('media ', 'movies\013shows', ''),
+ ('media mo', 'movies', ''),
+ ('media movies add ', '\013\013 ', '''
+Hint:
+ TITLE Movie Title'''),
+ ('media movies list -a "J', '"John Boyega"\013"Jake Lloyd"', ''),
+ ('media movies list ', '', '')
+])
+def test_commands(parser1, capfd, mock, comp_line, exp_out, exp_err):
+ completer = CompletionFinder()
+
+ mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
+ '_ARGCOMPLETE_IFS': '\013',
+ 'COMP_TYPE': '63',
+ 'COMP_LINE': comp_line,
+ 'COMP_POINT': str(len(comp_line))})
+
+ mock.patch.object(os, 'fdopen', my_fdopen)
+
+ with pytest.raises(SystemExit):
+ choices = {'actor': query_actors, # function
+ }
+ autocompleter = AutoCompleter(parser1, arg_choices=choices)
+ completer(parser1, autocompleter, exit_method=sys.exit)
+
+ out, err = capfd.readouterr()
+ assert out == exp_out
+ assert err == exp_err
+
+
+def fdopen_fail_8(fd, mode, *args):
+ """mock fdopen that forces failure if fd == 8"""
+ if fd == 8:
+ raise IOError()
+ return my_fdopen(fd, mode, *args)
+
+
+# noinspection PyShadowingNames
+@pytest.mark.skipif(skip, reason=skip_reason)
+def test_fail_alt_stdout(parser1, mock):
+ completer = CompletionFinder()
+
+ comp_line = 'media movies list '
+ mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
+ '_ARGCOMPLETE_IFS': '\013',
+ 'COMP_TYPE': '63',
+ 'COMP_LINE': comp_line,
+ 'COMP_POINT': str(len(comp_line))})
+ mock.patch.object(os, 'fdopen', fdopen_fail_8)
+
+ try:
+ choices = {'actor': query_actors, # function
+ }
+ autocompleter = AutoCompleter(parser1, arg_choices=choices)
+ completer(parser1, autocompleter, exit_method=sys.exit)
+ except SystemExit as err:
+ assert err.code == 1
+
+
+def fdopen_fail_9(fd, mode, *args):
+ """mock fdopen that forces failure if fd == 9"""
+ if fd == 9:
+ raise IOError()
+ return my_fdopen(fd, mode, *args)
+
+
+# noinspection PyShadowingNames
+@pytest.mark.skipif(skip or skip_mac, reason=skip_reason)
+def test_fail_alt_stderr(parser1, capfd, mock):
+ completer = CompletionFinder()
+
+ comp_line = 'media movies add '
+ exp_out = '\013\013 '
+ exp_err = '''
+Hint:
+ TITLE Movie Title'''
+
+ mock.patch.dict(os.environ, {'_ARGCOMPLETE': '1',
+ '_ARGCOMPLETE_IFS': '\013',
+ 'COMP_TYPE': '63',
+ 'COMP_LINE': comp_line,
+ 'COMP_POINT': str(len(comp_line))})
+ mock.patch.object(os, 'fdopen', fdopen_fail_9)
+
+ with pytest.raises(SystemExit):
+ choices = {'actor': query_actors, # function
+ }
+ autocompleter = AutoCompleter(parser1, arg_choices=choices)
+ completer(parser1, autocompleter, exit_method=sys.exit)
+
+ out, err = capfd.readouterr()
+ assert out == exp_out
+ assert err == exp_err
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index b6416005..6e4a5a3e 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1430,7 +1430,7 @@ def test_clipboard_failure(capsys):
# Make sure we got the error output
out, err = capsys.readouterr()
assert out == ''
- assert 'Cannot redirect to paste buffer; install ``xclip`` and re-run to enable' in err
+ assert "Cannot redirect to paste buffer; install 'pyperclip' and re-run to enable" in err
class CmdResultApp(cmd2.Cmd):
@@ -1688,12 +1688,6 @@ def test_alias_lookup_invalid_alias(base_app, capsys):
out, err = capsys.readouterr()
assert "not found" in err
-def test_alias_with_invalid_name(base_app, capsys):
- run_cmd(base_app, 'alias @ help')
- out, err = capsys.readouterr()
- assert "can only contain the following characters" in err
-
-
def test_unalias(base_app):
# Create an alias
run_cmd(base_app, 'alias fake pyscript')
@@ -1711,6 +1705,18 @@ def test_unalias_non_existing(base_app, capsys):
out, err = capsys.readouterr()
assert "does not exist" in err
+@pytest.mark.parametrize('alias_name', [
+ '">"',
+ '"no>pe"',
+ '"no spaces"',
+ '"nopipe|"',
+ '"noterm;"',
+ 'noembedded"quotes',
+])
+def test_create_invalid_alias(base_app, alias_name, capsys):
+ run_cmd(base_app, 'alias {} help'.format(alias_name))
+ out, err = capsys.readouterr()
+ assert "can not contain" in err
def test_ppaged(base_app):
msg = 'testing...'
@@ -1718,3 +1724,21 @@ def test_ppaged(base_app):
base_app.ppaged(msg)
out = base_app.stdout.buffer
assert out == msg + end
+
+# we override cmd.parseline() so we always get consistent
+# command parsing by parent methods we don't override
+# don't need to test all the parsing logic here, because
+# parseline just calls StatementParser.parse_command_only()
+def test_parseline_empty(base_app):
+ statement = ''
+ command, args, line = base_app.parseline(statement)
+ assert not command
+ assert not args
+ assert not line
+
+def test_parseline(base_app):
+ statement = " command with 'partially completed quotes "
+ command, args, line = base_app.parseline(statement)
+ assert command == 'command'
+ assert args == "with 'partially completed quotes"
+ assert line == statement.strip()
diff --git a/tests/test_completion.py b/tests/test_completion.py
index a027d780..c7650dbb 100644
--- a/tests/test_completion.py
+++ b/tests/test_completion.py
@@ -887,7 +887,7 @@ def test_cmd2_subcmd_with_unknown_completion_nomatch(scu_app):
assert first_match is None
-def test_cmd2_help_subcommand_completion_single(scu_app):
+def test_cmd2_help_subcommand_completion_single_scu(scu_app):
text = 'base'
line = 'help {}'.format(text)
endidx = len(line)
@@ -895,7 +895,7 @@ def test_cmd2_help_subcommand_completion_single(scu_app):
assert scu_app.complete_help(text, line, begidx, endidx) == ['base']
-def test_cmd2_help_subcommand_completion_multiple(scu_app):
+def test_cmd2_help_subcommand_completion_multiple_scu(scu_app):
text = ''
line = 'help base {}'.format(text)
endidx = len(line)
@@ -905,7 +905,7 @@ def test_cmd2_help_subcommand_completion_multiple(scu_app):
assert matches == ['bar', 'foo', 'sport']
-def test_cmd2_help_subcommand_completion_nomatch(scu_app):
+def test_cmd2_help_subcommand_completion_nomatch_scu(scu_app):
text = 'z'
line = 'help base {}'.format(text)
endidx = len(line)
@@ -913,7 +913,7 @@ def test_cmd2_help_subcommand_completion_nomatch(scu_app):
assert scu_app.complete_help(text, line, begidx, endidx) == []
-def test_subcommand_tab_completion(scu_app):
+def test_subcommand_tab_completion_scu(scu_app):
# This makes sure the correct completer for the sport subcommand is called
text = 'Foot'
line = 'base sport {}'.format(text)
@@ -926,7 +926,7 @@ def test_subcommand_tab_completion(scu_app):
assert first_match is not None and scu_app.completion_matches == ['Football ']
-def test_subcommand_tab_completion_with_no_completer(scu_app):
+def test_subcommand_tab_completion_with_no_completer_scu(scu_app):
# This tests what happens when a subcommand has no completer
# In this case, the foo subcommand has no completer defined
text = 'Foot'
@@ -938,7 +938,7 @@ def test_subcommand_tab_completion_with_no_completer(scu_app):
assert first_match is None
-def test_subcommand_tab_completion_space_in_text(scu_app):
+def test_subcommand_tab_completion_space_in_text_scu(scu_app):
text = 'B'
line = 'base sport "Space {}'.format(text)
endidx = len(line)
@@ -949,124 +949,3 @@ def test_subcommand_tab_completion_space_in_text(scu_app):
assert first_match is not None and \
scu_app.completion_matches == ['Ball" '] and \
scu_app.display_matches == ['Space Ball']
-
-####################################################
-
-
-class SecondLevel(cmd2.Cmd):
- """To be used as a second level command class. """
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = '2ndLevel '
-
- def do_foo(self, line):
- self.poutput("You called a command in SecondLevel with '%s'. " % line)
-
- def help_foo(self):
- self.poutput("This is a second level menu. Options are qwe, asd, zxc")
-
- def complete_foo(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
-
-second_level_cmd = SecondLevel()
-
-
-@cmd2.AddSubmenu(second_level_cmd,
- command='second',
- require_predefined_shares=False)
-class SubmenuApp(cmd2.Cmd):
- """To be used as the main / top level command class that will contain other submenus."""
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = 'TopLevel '
-
-
-@pytest.fixture
-def sb_app():
- app = SubmenuApp()
- return app
-
-
-def test_cmd2_submenu_completion_single_end(sb_app):
- text = 'f'
- line = 'second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
-
- # It is at end of line, so extra space is present
- assert first_match is not None and sb_app.completion_matches == ['foo ']
-
-
-def test_cmd2_submenu_completion_multiple(sb_app):
- text = 'e'
- line = 'second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- expected = ['edit', 'eof', 'eos']
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
-
- assert first_match is not None and sb_app.completion_matches == expected
-
-
-def test_cmd2_submenu_completion_nomatch(sb_app):
- text = 'z'
- line = 'second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
- assert first_match is None
-
-
-def test_cmd2_submenu_completion_after_submenu_match(sb_app):
- text = 'a'
- line = 'second foo {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
- assert first_match is not None and sb_app.completion_matches == ['asd ']
-
-
-def test_cmd2_submenu_completion_after_submenu_nomatch(sb_app):
- text = 'b'
- line = 'second foo {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- first_match = complete_tester(text, line, begidx, endidx, sb_app)
- assert first_match is None
-
-
-def test_cmd2_help_submenu_completion_multiple(sb_app):
- text = 'p'
- line = 'help second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- matches = sorted(sb_app.complete_help(text, line, begidx, endidx))
- assert matches == ['py', 'pyscript']
-
-
-def test_cmd2_help_submenu_completion_nomatch(sb_app):
- text = 'fake'
- line = 'help second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
- assert sb_app.complete_help(text, line, begidx, endidx) == []
-
-
-def test_cmd2_help_submenu_completion_subcommands(sb_app):
- text = 'p'
- line = 'help second {}'.format(text)
- endidx = len(line)
- begidx = endidx - len(text)
-
- matches = sorted(sb_app.complete_help(text, line, begidx, endidx))
- assert matches == ['py', 'pyscript']
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 7940bbd8..41966c71 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -16,7 +16,7 @@ from cmd2 import utils
def parser():
parser = StatementParser(
allow_redirection=True,
- terminators=[';'],
+ terminators=[';', '&'],
multiline_commands=['multiline'],
aliases={'helpalias': 'help',
'42': 'theanswer',
@@ -38,12 +38,22 @@ def test_parse_empty_string(parser):
('command /* with some comment */ arg', ['command', 'arg']),
('command arg1 arg2 # comment at the end', ['command', 'arg1', 'arg2']),
('42 arg1 arg2', ['theanswer', 'arg1', 'arg2']),
- ('l', ['shell', 'ls', '-al'])
+ ('l', ['shell', 'ls', '-al']),
+ ('termbare ; > /tmp/output', ['termbare', ';', '>', '/tmp/output']),
+ ('termbare; > /tmp/output', ['termbare', ';', '>', '/tmp/output']),
+ ('termbare & > /tmp/output', ['termbare', '&', '>', '/tmp/output']),
+ ('termbare& > /tmp/output', ['termbare', '&', '>', '/tmp/output']),
+ ('help|less', ['help', '|', 'less']),
+ ('l|less', ['shell', 'ls', '-al', '|', 'less']),
])
def test_tokenize(parser, line, tokens):
tokens_to_test = parser.tokenize(line)
assert tokens_to_test == tokens
+def test_tokenize_unclosed_quotes(parser):
+ with pytest.raises(ValueError):
+ _ = parser.tokenize('command with "unclosed quotes')
+
@pytest.mark.parametrize('tokens,command,args', [
([], None, None),
(['command'], 'command', None),
@@ -59,35 +69,45 @@ def test_command_and_args(parser, tokens, command, args):
'"one word"',
"'one word'",
])
-def test_single_word(parser, line):
+def test_parse_single_word(parser, line):
statement = parser.parse(line)
assert statement.command == line
assert not statement.args
assert statement.argv == [utils.strip_quotes(line)]
-def test_word_plus_terminator(parser):
- line = 'termbare;'
+@pytest.mark.parametrize('line,terminator', [
+ ('termbare;', ';'),
+ ('termbare ;', ';'),
+ ('termbare&', '&'),
+ ('termbare &', '&'),
+])
+def test_parse_word_plus_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
- assert statement.terminator == ';'
+ assert statement.terminator == terminator
assert statement.argv == ['termbare']
-def test_suffix_after_terminator(parser):
- line = 'termbare; suffx'
+@pytest.mark.parametrize('line,terminator', [
+ ('termbare; suffx', ';'),
+ ('termbare ;suffx', ';'),
+ ('termbare& suffx', '&'),
+ ('termbare &suffx', '&'),
+])
+def test_parse_suffix_after_terminator(parser, line, terminator):
statement = parser.parse(line)
assert statement.command == 'termbare'
- assert statement.terminator == ';'
+ assert statement.terminator == terminator
assert statement.suffix == 'suffx'
assert statement.argv == ['termbare']
-def test_command_with_args(parser):
+def test_parse_command_with_args(parser):
line = 'command with args'
statement = parser.parse(line)
assert statement.command == 'command'
assert statement.args == 'with args'
assert statement.argv == ['command', 'with', 'args']
-def test_command_with_quoted_args(parser):
+def test_parse_command_with_quoted_args(parser):
line = 'command with "quoted args" and "some not"'
statement = parser.parse(line)
assert statement.command == 'command'
@@ -103,20 +123,20 @@ def test_parse_command_with_args_terminator_and_suffix(parser):
assert statement.suffix == 'and suffix'
assert statement.argv == ['command', 'with', 'args', 'and', 'terminator']
-def test_hashcomment(parser):
+def test_parse_hashcomment(parser):
statement = parser.parse('hi # this is all a comment')
assert statement.command == 'hi'
assert not statement.args
assert statement.argv == ['hi']
-def test_c_comment(parser):
+def test_parse_c_comment(parser):
statement = parser.parse('hi /* this is | all a comment */')
assert statement.command == 'hi'
assert not statement.args
assert not statement.pipe_to
assert statement.argv == ['hi']
-def test_c_comment_empty(parser):
+def test_parse_c_comment_empty(parser):
statement = parser.parse('/* this is | all a comment */')
assert not statement.command
assert not statement.args
@@ -130,14 +150,18 @@ def test_parse_what_if_quoted_strings_seem_to_start_comments(parser):
assert not statement.pipe_to
assert statement.argv == ['what', 'if', 'quoted strings /* seem to ', 'start', 'comments?']
-def test_simple_piped(parser):
- statement = parser.parse('simple | piped')
+@pytest.mark.parametrize('line',[
+ 'simple | piped',
+ 'simple|piped',
+])
+def test_parse_simple_pipe(parser, line):
+ statement = parser.parse(line)
assert statement.command == 'simple'
assert not statement.args
assert statement.argv == ['simple']
- assert statement.pipe_to == 'piped'
+ assert statement.pipe_to == ['piped']
-def test_double_pipe_is_not_a_pipe(parser):
+def test_parse_double_pipe_is_not_a_pipe(parser):
line = 'double-pipe || is not a pipe'
statement = parser.parse(line)
assert statement.command == 'double-pipe'
@@ -145,17 +169,30 @@ def test_double_pipe_is_not_a_pipe(parser):
assert statement.argv == ['double-pipe', '||', 'is', 'not', 'a', 'pipe']
assert not statement.pipe_to
-def test_complex_pipe(parser):
- line = 'command with args, terminator;sufx | piped'
+def test_parse_complex_pipe(parser):
+ line = 'command with args, terminator&sufx | piped'
statement = parser.parse(line)
assert statement.command == 'command'
assert statement.args == "with args, terminator"
assert statement.argv == ['command', 'with', 'args,', 'terminator']
- assert statement.terminator == ';'
+ assert statement.terminator == '&'
assert statement.suffix == 'sufx'
- assert statement.pipe_to == 'piped'
+ assert statement.pipe_to == ['piped']
+
+@pytest.mark.parametrize('line,output', [
+ ('help > out.txt', '>'),
+ ('help>out.txt', '>'),
+ ('help >> out.txt', '>>'),
+ ('help>>out.txt', '>>'),
+])
+def test_parse_redirect(parser,line, output):
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.output == output
+ assert statement.output_to == 'out.txt'
-def test_output_redirect(parser):
+def test_parse_redirect_with_args(parser):
line = 'output into > afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
@@ -164,7 +201,7 @@ def test_output_redirect(parser):
assert statement.output == '>'
assert statement.output_to == 'afile.txt'
-def test_output_redirect_with_dash_in_path(parser):
+def test_parse_redirect_with_dash_in_path(parser):
line = 'output into > python-cmd2/afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
@@ -173,7 +210,7 @@ def test_output_redirect_with_dash_in_path(parser):
assert statement.output == '>'
assert statement.output_to == 'python-cmd2/afile.txt'
-def test_output_redirect_append(parser):
+def test_parse_redirect_append(parser):
line = 'output appended to >> /tmp/afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
@@ -182,7 +219,7 @@ def test_output_redirect_append(parser):
assert statement.output == '>>'
assert statement.output_to == '/tmp/afile.txt'
-def test_pipe_and_redirect(parser):
+def test_parse_pipe_and_redirect(parser):
line = 'output into;sufx | pipethrume plz > afile.txt'
statement = parser.parse(line)
assert statement.command == 'output'
@@ -190,9 +227,9 @@ def test_pipe_and_redirect(parser):
assert statement.argv == ['output', 'into']
assert statement.terminator == ';'
assert statement.suffix == 'sufx'
- assert statement.pipe_to == 'pipethrume plz'
- assert statement.output == '>'
- assert statement.output_to == 'afile.txt'
+ assert statement.pipe_to == ['pipethrume', 'plz', '>', 'afile.txt']
+ assert not statement.output
+ assert not statement.output_to
def test_parse_output_to_paste_buffer(parser):
line = 'output to paste buffer >> '
@@ -202,9 +239,10 @@ def test_parse_output_to_paste_buffer(parser):
assert statement.argv == ['output', 'to', 'paste', 'buffer']
assert statement.output == '>>'
-def test_has_redirect_inside_terminator(parser):
- """The terminator designates the end of the commmand/arguments portion. If a redirector
- occurs before a terminator, then it will be treated as part of the arguments and not as a redirector."""
+def test_parse_redirect_inside_terminator(parser):
+ """The terminator designates the end of the commmand/arguments portion.
+ If a redirector occurs before a terminator, then it will be treated as
+ part of the arguments and not as a redirector."""
line = 'has > inside;'
statement = parser.parse(line)
assert statement.command == 'has'
@@ -221,13 +259,16 @@ def test_parse_unfinished_multiliine_command(parser):
assert statement.argv == ['multiline', 'has', '>', 'inside', 'an', 'unfinished', 'command']
assert not statement.terminator
-def test_parse_multiline_command_ignores_redirectors_within_it(parser):
- line = 'multiline has > inside;'
+@pytest.mark.parametrize('line,terminator',[
+ ('multiline has > inside;', ';'),
+ ('multiline has > inside &', '&'),
+])
+def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, terminator):
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
assert statement.args == 'has > inside'
assert statement.argv == ['multiline', 'has', '>', 'inside']
- assert statement.terminator == ';'
+ assert statement.terminator == terminator
def test_parse_multiline_with_incomplete_comment(parser):
"""A terminator within a comment will be ignored and won't terminate a multiline command.
@@ -290,6 +331,10 @@ def test_parse_redirect_to_unicode_filename(parser):
assert statement.output == '>'
assert statement.output_to == 'café'
+def test_parse_unclosed_quotes(parser):
+ with pytest.raises(ValueError):
+ _ = parser.tokenize("command with 'unclosed quotes")
+
def test_empty_statement_raises_exception():
app = cmd2.Cmd()
with pytest.raises(cmd2.EmptyStatement):
@@ -307,12 +352,12 @@ def test_empty_statement_raises_exception():
('!ls -al /tmp', 'shell', 'ls -al /tmp'),
('l', 'shell', 'ls -al')
])
-def test_alias_and_shortcut_expansion(parser, line, command, args):
+def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
statement = parser.parse(line)
assert statement.command == command
assert statement.args == args
-def test_alias_on_multiline_command(parser):
+def test_parse_alias_on_multiline_command(parser):
line = 'anothermultiline has > inside an unfinished command'
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
@@ -320,12 +365,41 @@ def test_alias_on_multiline_command(parser):
assert statement.args == 'has > inside an unfinished command'
assert not statement.terminator
+@pytest.mark.parametrize('line,output', [
+ ('helpalias > out.txt', '>'),
+ ('helpalias>out.txt', '>'),
+ ('helpalias >> out.txt', '>>'),
+ ('helpalias>>out.txt', '>>'),
+])
+def test_parse_alias_redirection(parser, line, output):
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.output == output
+ assert statement.output_to == 'out.txt'
+
+@pytest.mark.parametrize('line', [
+ 'helpalias | less',
+ 'helpalias|less',
+])
+def test_parse_alias_pipe(parser, line):
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.pipe_to == ['less']
+
+def test_parse_alias_terminator_no_whitespace(parser):
+ line = 'helpalias;'
+ statement = parser.parse(line)
+ assert statement.command == 'help'
+ assert not statement.args
+ assert statement.terminator == ';'
+
def test_parse_command_only_command_and_args(parser):
line = 'help history'
statement = parser.parse_command_only(line)
assert statement.command == 'help'
assert statement.args == 'history'
- assert statement.argv == ['help', 'history']
assert statement.command_and_args == line
def test_parse_command_only_emptyline(parser):
@@ -345,7 +419,6 @@ def test_parse_command_only_strips_line(parser):
statement = parser.parse_command_only(line)
assert statement.command == 'help'
assert statement.args == 'history'
- assert statement.argv == ['help', 'history']
assert statement.command_and_args == line.strip()
def test_parse_command_only_expands_alias(parser):
@@ -353,14 +426,12 @@ def test_parse_command_only_expands_alias(parser):
statement = parser.parse_command_only(line)
assert statement.command == 'pyscript'
assert statement.args == 'foobar.py'
- assert statement.argv == ['pyscript', 'foobar.py']
def test_parse_command_only_expands_shortcuts(parser):
line = '!cat foobar.txt'
statement = parser.parse_command_only(line)
assert statement.command == 'shell'
assert statement.args == 'cat foobar.txt'
- assert statement.argv == ['shell', 'cat', 'foobar.txt']
assert statement.command_and_args == 'shell cat foobar.txt'
def test_parse_command_only_quoted_args(parser):
@@ -369,3 +440,27 @@ def test_parse_command_only_quoted_args(parser):
assert statement.command == 'shell'
assert statement.args == 'ls -al "/tmp/directory with spaces/doit.sh"'
assert statement.command_and_args == line.replace('l', 'shell ls -al')
+
+@pytest.mark.parametrize('line', [
+ 'helpalias > out.txt',
+ 'helpalias>out.txt',
+ 'helpalias >> out.txt',
+ 'helpalias>>out.txt',
+ 'help|less',
+ 'helpalias;',
+])
+def test_parse_command_only_specialchars(parser, line):
+ statement = parser.parse_command_only(line)
+ assert statement.command == 'help'
+
+@pytest.mark.parametrize('line', [
+ ';',
+ '>',
+ "'",
+ '"',
+ '|',
+])
+def test_parse_command_only_none(parser, line):
+ statement = parser.parse_command_only(line)
+ assert statement.command == None
+ assert statement.args == None
diff --git a/tests/test_submenu.py b/tests/test_submenu.py
deleted file mode 100644
index db334daa..00000000
--- a/tests/test_submenu.py
+++ /dev/null
@@ -1,181 +0,0 @@
-# coding=utf-8
-"""
-Cmd2 testing for argument parsing
-"""
-import pytest
-
-from cmd2 import cmd2
-from .conftest import run_cmd, StdOut, normalize
-
-
-class SecondLevelB(cmd2.Cmd):
- """To be used as a second level command class. """
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = '2ndLevel B '
-
- def do_get_top_level_attr(self, line):
- self.poutput(str(self.top_level_attr))
-
- def do_set_top_level_attr(self, line):
- self.top_level_attr = 987654321
-
-
-class SecondLevel(cmd2.Cmd):
- """To be used as a second level command class. """
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = '2ndLevel '
- self.top_level_attr = None
-
- def do_say(self, line):
- self.poutput("You called a command in SecondLevel with '%s'. " % line)
-
- def help_say(self):
- self.poutput("This is a second level menu. Options are qwe, asd, zxc")
-
- def complete_say(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
- def do_get_top_level_attr(self, line):
- self.poutput(str(self.top_level_attr))
-
- def do_get_prompt(self, line):
- self.poutput(self.prompt)
-
-
-second_level_cmd = SecondLevel()
-second_level_b_cmd = SecondLevelB()
-
-
-@cmd2.AddSubmenu(SecondLevelB(),
- command='should_work_with_default_kwargs')
-@cmd2.AddSubmenu(second_level_b_cmd,
- command='secondb',
- shared_attributes=dict(top_level_attr='top_level_attr'),
- require_predefined_shares=False,
- preserve_shares=True
- )
-@cmd2.AddSubmenu(second_level_cmd,
- command='second',
- aliases=('second_alias',),
- shared_attributes=dict(top_level_attr='top_level_attr'))
-class SubmenuApp(cmd2.Cmd):
- """To be used as the main / top level command class that will contain other submenus."""
-
- def __init__(self, *args, **kwargs):
- cmd2.Cmd.__init__(self, *args, **kwargs)
- self.prompt = 'TopLevel '
- self.top_level_attr = 123456789
-
- def do_say(self, line):
- self.poutput("You called a command in TopLevel with '%s'. " % line)
-
- def help_say(self):
- self.poutput("This is a top level submenu. Options are qwe, asd, zxc")
-
- def complete_say(self, text, line, begidx, endidx):
- return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)]
-
-
-@pytest.fixture
-def submenu_app():
- app = SubmenuApp()
- app.stdout = StdOut()
- second_level_cmd.stdout = StdOut()
- second_level_b_cmd.stdout = StdOut()
- return app
-
-
-@pytest.fixture
-def secondlevel_app():
- app = SecondLevel()
- app.stdout = StdOut()
- return app
-
-
-@pytest.fixture
-def secondlevel_app_b():
- app = SecondLevelB()
- app.stdout = StdOut()
- return app
-
-
-def run_submenu_cmd(app, second_level_app, cmd):
- """ Clear StdOut buffers, run the command, extract the buffer contents."""
- app.stdout.clear()
- second_level_app.stdout.clear()
- app.onecmd_plus_hooks(cmd)
- out1 = app.stdout.buffer
- out2 = second_level_app.stdout.buffer
- app.stdout.clear()
- second_level_app.stdout.clear()
- return normalize(out1), normalize(out2)
-
-
-def test_submenu_say_from_top_level(submenu_app):
- line = 'testing'
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'say ' + line)
- assert len(out1) == 1
- assert len(out2) == 0
- assert out1[0] == "You called a command in TopLevel with {!r}.".format(line)
-
-
-def test_submenu_second_say_from_top_level(submenu_app):
- line = 'testing'
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second say ' + line)
-
- # No output expected from the top level
- assert out1 == []
-
- # Output expected from the second level
- assert len(out2) == 1
- assert out2[0] == "You called a command in SecondLevel with {!r}.".format(line)
-
-
-def test_submenu_say_from_second_level(secondlevel_app):
- line = 'testing'
- out = run_cmd(secondlevel_app, 'say ' + line)
- assert out == ["You called a command in SecondLevel with '%s'." % line]
-
-
-def test_submenu_help_second_say_from_top_level(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say')
- # No output expected from the top level
- assert out1 == []
-
- # Output expected from the second level
- assert out2 == ["This is a second level menu. Options are qwe, asd, zxc"]
-
-
-def test_submenu_help_say_from_second_level(secondlevel_app):
- out = run_cmd(secondlevel_app, 'help say')
- assert out == ["This is a second level menu. Options are qwe, asd, zxc"]
-
-
-def test_submenu_help_second(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second')
- out3 = run_cmd(second_level_cmd, 'help')
- assert out2 == out3
-
-
-def test_submenu_from_top_help_second_say(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say')
- out3 = run_cmd(second_level_cmd, 'help say')
- assert out2 == out3
-
-
-def test_submenu_shared_attribute(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second get_top_level_attr')
- assert out2 == [str(submenu_app.top_level_attr)]
-
-
-def test_submenu_shared_attribute_preserve(submenu_app):
- out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr')
- assert out2 == [str(submenu_app.top_level_attr)]
- out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb set_top_level_attr')
- assert submenu_app.top_level_attr == 987654321
- out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr')
- assert out2 == [str(987654321)]
diff --git a/tests/test_transcript.py b/tests/test_transcript.py
index c0fb49c1..70658161 100644
--- a/tests/test_transcript.py
+++ b/tests/test_transcript.py
@@ -10,12 +10,13 @@ import os
import sys
import re
import random
+import tempfile
from unittest import mock
import pytest
from cmd2 import cmd2
-from .conftest import run_cmd, StdOut, normalize
+from .conftest import run_cmd, StdOut
class CmdLineApp(cmd2.Cmd):
@@ -26,7 +27,6 @@ class CmdLineApp(cmd2.Cmd):
def __init__(self, *args, **kwargs):
self.multiline_commands = ['orate']
self.maxrepeats = 3
- self.redirector = '->'
# Add stuff to settable and/or shortcuts before calling base class initializer
self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed'
@@ -63,7 +63,7 @@ class CmdLineApp(cmd2.Cmd):
def do_mumble(self, opts, arg):
"""Mumbles what you tell me to."""
repetitions = opts.repeat or 1
- arg = arg.split()
+ #arg = arg.split()
for i in range(min(repetitions, self.maxrepeats)):
output = []
if random.random() < .33:
@@ -77,136 +77,6 @@ class CmdLineApp(cmd2.Cmd):
self.poutput(' '.join(output))
-class DemoApp(cmd2.Cmd):
- hello_parser = argparse.ArgumentParser()
- hello_parser.add_argument('-n', '--name', help="your name")
- @cmd2.with_argparser_and_unknown_args(hello_parser)
- def do_hello(self, opts, arg):
- """Says hello."""
- if opts.name:
- self.stdout.write('Hello {}\n'.format(opts.name))
- else:
- self.stdout.write('Hello Nobody\n')
-
-
-@pytest.fixture
-def _cmdline_app():
- c = CmdLineApp()
- c.stdout = StdOut()
- return c
-
-
-@pytest.fixture
-def _demo_app():
- c = DemoApp()
- c.stdout = StdOut()
- return c
-
-
-def _get_transcript_blocks(transcript):
- cmd = None
- expected = ''
- for line in transcript.splitlines():
- if line.startswith('(Cmd) '):
- if cmd is not None:
- yield cmd, normalize(expected)
-
- cmd = line[6:]
- expected = ''
- else:
- expected += line + '\n'
- yield cmd, normalize(expected)
-
-
-def test_base_with_transcript(_cmdline_app):
- app = _cmdline_app
- transcript = """
-(Cmd) help
-
-Documented commands (type help <topic>):
-========================================
-alias help load orate pyscript say shell speak
-edit history mumble py quit set shortcuts unalias
-
-(Cmd) help say
-usage: speak [-h] [-p] [-s] [-r REPEAT]
-
-Repeats what you tell me to.
-
-optional arguments:
- -h, --help show this help message and exit
- -p, --piglatin atinLay
- -s, --shout N00B EMULATION MODE
- -r REPEAT, --repeat REPEAT
- output [n] times
-
-(Cmd) say goodnight, Gracie
-goodnight, Gracie
-(Cmd) say -ps --repeat=5 goodnight, Gracie
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-(Cmd) set maxrepeats 5
-maxrepeats - was: 3
-now: 5
-(Cmd) say -ps --repeat=5 goodnight, Gracie
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-(Cmd) history
--------------------------[1]
-help
--------------------------[2]
-help say
--------------------------[3]
-say goodnight, Gracie
--------------------------[4]
-say -ps --repeat=5 goodnight, Gracie
--------------------------[5]
-set maxrepeats 5
--------------------------[6]
-say -ps --repeat=5 goodnight, Gracie
-(Cmd) history -r 4
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-OODNIGHT, GRACIEGAY
-(Cmd) set prompt "---> "
-prompt - was: (Cmd)
-now: --->
-"""
-
- for cmd, expected in _get_transcript_blocks(transcript):
- out = run_cmd(app, cmd)
- assert out == expected
-
-
-class TestMyAppCase(cmd2.Cmd2TestCase):
- CmdApp = CmdLineApp
- CmdApp.testfiles = ['tests/transcript.txt']
-
-
-def test_comment_stripping(_cmdline_app):
- out = run_cmd(_cmdline_app, 'speak it was /* not */ delicious! # Yuck!')
- expected = normalize("""it was delicious!""")
- assert out == expected
-
-
-def test_argparser_correct_args_with_quotes_and_midline_options(_cmdline_app):
- out = run_cmd(_cmdline_app, "speak 'This is a' -s test of the emergency broadcast system!")
- expected = normalize("""THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!""")
- assert out == expected
-
-
-def test_argparser_options_with_spaces_in_quotes(_demo_app):
- out = run_cmd(_demo_app, "hello foo -n 'Bugs Bunny' bar baz")
- expected = normalize("""Hello Bugs Bunny""")
- assert out == expected
-
-
def test_commands_at_invocation():
testargs = ["prog", "say hello", "say Gracie", "quit"]
expected = "This is an intro banner ...\nhello\nGracie\n"
@@ -217,19 +87,12 @@ def test_commands_at_invocation():
out = app.stdout.buffer
assert out == expected
-def test_invalid_syntax(_cmdline_app, capsys):
- run_cmd(_cmdline_app, 'speak "')
- out, err = capsys.readouterr()
- expected = normalize("""ERROR: Invalid syntax: No closing quotation""")
- assert normalize(str(err)) == expected
-
-
-@pytest.mark.parametrize('filename, feedback_to_output', [
+@pytest.mark.parametrize('filename,feedback_to_output', [
('bol_eol.txt', False),
('characterclass.txt', False),
('dotstar.txt', False),
('extension_notation.txt', False),
- # ('from_cmdloop.txt', True),
+ ('from_cmdloop.txt', True),
('multiline_no_regex.txt', False),
('multiline_regex.txt', False),
('regex_set.txt', False),
@@ -237,7 +100,7 @@ def test_invalid_syntax(_cmdline_app, capsys):
('slashes_escaped.txt', False),
('slashslash.txt', False),
('spaces.txt', False),
- # ('word_boundaries.txt', False),
+ ('word_boundaries.txt', False),
])
def test_transcript(request, capsys, filename, feedback_to_output):
# Create a cmd2.Cmd() instance and make sure basic settings are
@@ -263,6 +126,32 @@ def test_transcript(request, capsys, filename, feedback_to_output):
assert err.startswith(expected_start)
assert err.endswith(expected_end)
+def test_history_transcript(request, capsys):
+ app = CmdLineApp()
+ app.stdout = StdOut()
+ run_cmd(app, 'orate this is\na /multiline/\ncommand;\n')
+ run_cmd(app, 'speak /tmp/file.txt is not a regex')
+
+ expected = r"""(Cmd) orate this is
+> a /multiline/
+> command;
+this is a \/multiline\/ command
+(Cmd) speak /tmp/file.txt is not a regex
+\/tmp\/file.txt is not a regex
+"""
+
+ # make a tmp file
+ fd, history_fname = tempfile.mkstemp(prefix='', suffix='.txt')
+ os.close(fd)
+
+ # tell the history command to create a transcript
+ run_cmd(app, 'history -t "{}"'.format(history_fname))
+
+ # read in the transcript created by the history command
+ with open(history_fname) as f:
+ transcript = f.read()
+
+ assert transcript == expected
@pytest.mark.parametrize('expected, transformed', [
# strings with zero or one slash or with escaped slashes means no regular
diff --git a/tests/transcripts/from_cmdloop.txt b/tests/transcripts/from_cmdloop.txt
index 13b61b00..5f22d756 100644
--- a/tests/transcripts/from_cmdloop.txt
+++ b/tests/transcripts/from_cmdloop.txt
@@ -19,7 +19,6 @@ optional arguments:/ */
-s, --shout N00B EMULATION MODE/ */
-r REPEAT, --repeat REPEAT/ */
output [n] times
-
(Cmd) say goodnight, Gracie
goodnight, Gracie
(Cmd) say -ps --repeat=5 goodnight, Gracie