37
37
import re
38
38
import sys
39
39
import threading
40
+ from collections import namedtuple
40
41
from typing import Any , Callable , Dict , List , Mapping , Optional , Tuple , Type , Union , IO
41
42
42
43
import colorama
@@ -279,6 +280,10 @@ class EmptyStatement(Exception):
279
280
pass
280
281
281
282
283
+ # Contains data about a disabled command which is used to restore its original functions when the command is enabled
284
+ DisabledCommand = namedtuple ('DisabledCommand' , ['command_function' , 'help_function' ])
285
+
286
+
282
287
class Cmd (cmd .Cmd ):
283
288
"""An easy but powerful framework for writing line-oriented command interpreters.
284
289
@@ -521,6 +526,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
521
526
# being printed by a command.
522
527
self .terminal_lock = threading .RLock ()
523
528
529
+ # Commands that have been disabled from use. This is to support commands that are only available
530
+ # during specific states of the application. This dictionary's keys are the command names and its
531
+ # values are DisabledCommand objects.
532
+ self .disabled_commands = dict ()
533
+
524
534
# ----- Methods related to presenting output to the user -----
525
535
526
536
@property
@@ -587,7 +597,7 @@ def perror(self, err: Union[str, Exception], traceback_war: bool = True, err_col
587
597
:param err_color: (optional) color escape to output error with
588
598
:param war_color: (optional) color escape to output warning with
589
599
"""
590
- if self .debug :
600
+ if self .debug and sys . exc_info () != ( None , None , None ) :
591
601
import traceback
592
602
traceback .print_exc ()
593
603
@@ -1562,14 +1572,19 @@ def get_all_commands(self) -> List[str]:
1562
1572
if name .startswith (COMMAND_FUNC_PREFIX ) and callable (getattr (self , name ))]
1563
1573
1564
1574
def get_visible_commands (self ) -> List [str ]:
1565
- """Returns a list of commands that have not been hidden."""
1575
+ """Returns a list of commands that have not been hidden or disabled ."""
1566
1576
commands = self .get_all_commands ()
1567
1577
1568
1578
# Remove the hidden commands
1569
1579
for name in self .hidden_commands :
1570
1580
if name in commands :
1571
1581
commands .remove (name )
1572
1582
1583
+ # Remove the disabled commands
1584
+ for name in self .disabled_commands :
1585
+ if name in commands :
1586
+ commands .remove (name )
1587
+
1573
1588
return commands
1574
1589
1575
1590
def get_alias_names (self ) -> List [str ]:
@@ -1953,7 +1968,7 @@ def cmd_func_name(self, command: str) -> str:
1953
1968
def onecmd (self , statement : Union [Statement , str ]) -> bool :
1954
1969
""" This executes the actual do_* method for a command.
1955
1970
1956
- If the command provided doesn't exist, then it executes _default () instead.
1971
+ If the command provided doesn't exist, then it executes default () instead.
1957
1972
1958
1973
:param statement: intended to be a Statement instance parsed command from the input stream, alternative
1959
1974
acceptance of a str is present only for backward compatibility with cmd
@@ -1969,8 +1984,9 @@ def onecmd(self, statement: Union[Statement, str]) -> bool:
1969
1984
else :
1970
1985
func = self .cmd_func (statement .command )
1971
1986
if func :
1972
- # Since we have a valid command store it in the history
1973
- if statement .command not in self .exclude_from_history :
1987
+ # Check to see if this command should be stored in history
1988
+ if statement .command not in self .exclude_from_history \
1989
+ and statement .command not in self .disabled_commands :
1974
1990
self .history .append (statement )
1975
1991
1976
1992
stop = func (statement )
@@ -3186,13 +3202,15 @@ def do_history(self, args: argparse.Namespace) -> None:
3186
3202
3187
3203
# -v must be used alone with no other options
3188
3204
if args .verbose :
3189
- if args .clear or args .edit or args .output_file or args .run or args .transcript or args .expanded or args .script :
3205
+ if args .clear or args .edit or args .output_file or args .run or args .transcript \
3206
+ or args .expanded or args .script :
3190
3207
self .poutput ("-v can not be used with any other options" )
3191
3208
self .poutput (self .history_parser .format_usage ())
3192
3209
return
3193
3210
3194
3211
# -s and -x can only be used if none of these options are present: [-c -r -e -o -t]
3195
- if (args .script or args .expanded ) and (args .clear or args .edit or args .output_file or args .run or args .transcript ):
3212
+ if (args .script or args .expanded ) \
3213
+ and (args .clear or args .edit or args .output_file or args .run or args .transcript ):
3196
3214
self .poutput ("-s and -x can not be used with -c, -r, -e, -o, or -t" )
3197
3215
self .poutput (self .history_parser .format_usage ())
3198
3216
return
@@ -3605,6 +3623,95 @@ def set_window_title(self, title: str) -> None: # pragma: no cover
3605
3623
else :
3606
3624
raise RuntimeError ("another thread holds terminal_lock" )
3607
3625
3626
+ def enable_command (self , command : str ) -> None :
3627
+ """
3628
+ Enable a command by restoring its functions
3629
+ :param command: the command being enabled
3630
+ """
3631
+ # If the commands is already enabled, then return
3632
+ if command not in self .disabled_commands :
3633
+ return
3634
+
3635
+ help_func_name = HELP_FUNC_PREFIX + command
3636
+
3637
+ # Restore the command and help functions to their original values
3638
+ dc = self .disabled_commands [command ]
3639
+ setattr (self , self .cmd_func_name (command ), dc .command_function )
3640
+
3641
+ if dc .help_function is None :
3642
+ delattr (self , help_func_name )
3643
+ else :
3644
+ setattr (self , help_func_name , dc .help_function )
3645
+
3646
+ # Remove the disabled command entry
3647
+ del self .disabled_commands [command ]
3648
+
3649
+ def enable_category (self , category : str ) -> None :
3650
+ """
3651
+ Enable an entire category of commands
3652
+ :param category: the category to enable
3653
+ """
3654
+ for cmd_name in list (self .disabled_commands ):
3655
+ dc = self .disabled_commands [cmd_name ]
3656
+ cmd_category = getattr (dc .command_function , HELP_CATEGORY , None )
3657
+ if cmd_category is not None and cmd_category == category :
3658
+ self .enable_command (cmd_name )
3659
+
3660
+ def disable_command (self , command : str , message_to_print : str ) -> None :
3661
+ """
3662
+ Disable a command and overwrite its functions
3663
+ :param command: the command being disabled
3664
+ :param message_to_print: what to print when this command is run or help is called on it while disabled
3665
+ """
3666
+ import functools
3667
+
3668
+ # If the commands is already disabled, then return
3669
+ if command in self .disabled_commands :
3670
+ return
3671
+
3672
+ # Make sure this is an actual command
3673
+ command_function = self .cmd_func (command )
3674
+ if command_function is None :
3675
+ raise AttributeError ("{} does not refer to a command" .format (command ))
3676
+
3677
+ help_func_name = HELP_FUNC_PREFIX + command
3678
+
3679
+ # Add the disabled command record
3680
+ self .disabled_commands [command ] = DisabledCommand (command_function = command_function ,
3681
+ help_function = getattr (self , help_func_name , None ))
3682
+
3683
+ # Overwrite the command and help functions to print the message
3684
+ new_func = functools .partial (self ._report_disabled_command_usage , message_to_print = message_to_print )
3685
+ setattr (self , self .cmd_func_name (command ), new_func )
3686
+ setattr (self , help_func_name , new_func )
3687
+
3688
+ def disable_category (self , category : str , message_to_print : str ) -> None :
3689
+ """
3690
+ Disable an entire category of commands
3691
+ :param category: the category to disable
3692
+ :param message_to_print: what to print when anything in this category is run or help is called on it
3693
+ while disabled
3694
+ """
3695
+ all_commands = self .get_all_commands ()
3696
+
3697
+ for cmd_name in all_commands :
3698
+ func = self .cmd_func (cmd_name )
3699
+ cmd_category = getattr (func , HELP_CATEGORY , None )
3700
+
3701
+ # If this command is in the category, then disable it
3702
+ if cmd_category is not None and cmd_category == category :
3703
+ self .disable_command (cmd_name , message_to_print )
3704
+
3705
+ # noinspection PyUnusedLocal
3706
+ def _report_disabled_command_usage (self , * args , message_to_print : str , ** kwargs ) -> None :
3707
+ """
3708
+ Report when a disabled command has been run or had help called on it
3709
+ :param args: not used
3710
+ :param message_to_print: the message reporting that the command is disabled
3711
+ :param kwargs: not used
3712
+ """
3713
+ self .poutput (message_to_print )
3714
+
3608
3715
def cmdloop (self , intro : Optional [str ] = None ) -> None :
3609
3716
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.
3610
3717
0 commit comments