-
Notifications
You must be signed in to change notification settings - Fork 27
feat: Switching to interactive when running "fab" #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| kind: added | ||
| body: Support fab command to start interactive (repl) mode | ||
| time: 2026-01-01T12:57:18.777151041Z |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,9 +22,12 @@ class InteractiveCLI: | |
| def __init__(self, parser=None, subparsers=None): | ||
| """Initialize the interactive CLI.""" | ||
| if parser is None or subparsers is None: | ||
| from fabric_cli.core.fab_parser_setup import get_global_parser_and_subparsers | ||
| from fabric_cli.core.fab_parser_setup import ( | ||
| get_global_parser_and_subparsers, | ||
| ) | ||
|
|
||
| parser, subparsers = get_global_parser_and_subparsers() | ||
|
|
||
| self.parser = parser | ||
| self.parser.set_mode(fab_constant.FAB_MODE_INTERACTIVE) | ||
| self.subparsers = subparsers | ||
|
|
@@ -47,22 +50,27 @@ def handle_command(self, command): | |
| """Process the user command.""" | ||
| fab_logger.print_log_file_path() | ||
|
|
||
| command_parts = shlex.split(command.strip()) # Split the command into parts | ||
| command_parts = shlex.split(command.strip()) | ||
|
|
||
| # Handle special commands first | ||
| if command in fab_constant.INTERACTIVE_QUIT_COMMANDS: | ||
| utils_ui.print(fab_constant.INTERACTIVE_EXIT_MESSAGE) | ||
| return True # Exit | ||
| return True | ||
| elif command in fab_constant.INTERACTIVE_HELP_COMMANDS: | ||
| utils_ui.display_help( | ||
| fab_commands.COMMANDS, "Usage: <command> <subcommand> [flags]" | ||
| ) | ||
| return False # Do not exit | ||
| return False | ||
| elif command in fab_constant.INTERACTIVE_VERSION_COMMANDS: | ||
| utils_ui.print_version() | ||
| return False # Do not exit | ||
| return False | ||
| elif command.strip() == "fab": | ||
| utils_ui.print( | ||
| "In interactive mode, commands don't require the fab prefix. Use --help to view the list of supported commands." | ||
| ) | ||
| return False | ||
| elif not command.strip(): | ||
| return False | ||
|
|
||
| # Interactive mode | ||
| self.parser.set_mode(fab_constant.FAB_MODE_INTERACTIVE) | ||
|
|
||
| # Now check for subcommands | ||
|
|
@@ -88,10 +96,9 @@ def handle_command(self, command): | |
| f"No function associated with the command: {command.strip()}" | ||
| ) | ||
| except SystemExit: | ||
| # Catch SystemExit raised by ArgumentParser and prevent exiting | ||
| return | ||
| else: | ||
| self.parser.error(f"invalid choice: '{command.strip()}'") | ||
| self.parser.error(f"invalid choice: '{command.strip()}'. Type 'help' for available commands.") | ||
|
|
||
| return False | ||
|
|
||
|
|
@@ -102,31 +109,46 @@ def start_interactive(self): | |
| return | ||
|
|
||
| self._is_running = True | ||
|
|
||
| try: | ||
| utils_ui.print("\nWelcome to the Fabric CLI β‘") | ||
| utils_ui.print("Type 'help' for help. \n") | ||
|
|
||
| while True: | ||
| context = Context().context | ||
| pwd_context = f"/{context.path.strip('/')}" | ||
|
|
||
| prompt_text = HTML( | ||
| f"<prompt>fab</prompt><detail>:</detail><context>{html.escape(pwd_context)}</context><detail>$</detail> " | ||
| ) | ||
|
|
||
| user_input = self.session.prompt( | ||
| prompt_text, | ||
| style=self.custom_style, | ||
| cursor=CursorShape.BLINKING_BEAM, | ||
| enable_history_search=True, | ||
| ) | ||
| should_exit = self.handle_command(user_input) | ||
| if should_exit: # Check if the command was to exit | ||
| break | ||
| try: | ||
| context = Context().context | ||
| pwd_context = f"/{context.path.strip('/')}" | ||
|
|
||
| prompt_text = HTML( | ||
| f"<prompt>fab</prompt><detail>:</detail><context>{html.escape(pwd_context)}</context><detail>$</detail> " | ||
| ) | ||
|
|
||
| user_input = self.session.prompt( | ||
| prompt_text, | ||
| style=self.custom_style, | ||
| cursor=CursorShape.BLINKING_BEAM, | ||
| enable_history_search=True, | ||
| ) | ||
| should_exit = self.handle_command(user_input) | ||
| if should_exit: # Check if the command was to exit | ||
| break | ||
|
|
||
| except KeyboardInterrupt: | ||
| # Handle Ctrl+C gracefully during command input | ||
| utils_ui.print("\nUse 'quit' or 'exit' to leave interactive mode.") | ||
| continue | ||
| except Exception as e: | ||
| # Handle unexpected errors during prompt processing | ||
| utils_ui.print(f"Error in interactive session: {str(e)}") | ||
| utils_ui.print("Session will continue. Type 'quit' to exit safely.") | ||
| continue | ||
|
Comment on lines
+136
to
+144
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you explain why we need these changes?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. today when user hit Ctrl+C we exit the interactive mode. regarding the second exception, these lines handle exceptions in the interactive session loop, they catch errors that occur during: Context retrieval, Prompt rendering, Session management issues
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What will happen if user types ctrl+c? It should interrupt the current running command. There are other Python REPL keyboard shortcuts that should terminate the interactive mode - CTRL+D for example, will it work?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if user types ctrl+c the command will be interrupt but will not terminate the interactive mode - to exit interactive mode user should use quit/exit Do you think it is wrong? |
||
|
|
||
| except (EOFError, KeyboardInterrupt): | ||
| utils_ui.print(f"\n{fab_constant.INTERACTIVE_EXIT_MESSAGE}") | ||
| except Exception as e: | ||
| # Handle critical errors that would terminate the session | ||
| utils_ui.print(f"\nCritical error in interactive mode: {str(e)}") | ||
| utils_ui.print(fab_constant.INTERACTIVE_EXIT_MESSAGE) | ||
| finally: | ||
| self._is_running = False | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,11 +9,11 @@ | |
| from fabric_cli.core import fab_constant, fab_logger, fab_state_config | ||
| from fabric_cli.core.fab_commands import Command | ||
| from fabric_cli.core.fab_exceptions import FabricCLIError | ||
| from fabric_cli.core.fab_interactive import start_interactive_mode | ||
| from fabric_cli.core.fab_parser_setup import get_global_parser_and_subparsers | ||
| from fabric_cli.parsers import fab_auth_parser as auth_parser | ||
| from fabric_cli.utils import fab_ui | ||
| from fabric_cli.utils.fab_commands import COMMANDS | ||
| from fabric_cli.core.fab_interactive import start_interactive_mode | ||
| from fabric_cli.core.fab_parser_setup import get_global_parser_and_subparsers | ||
|
|
||
|
|
||
| def main(): | ||
|
|
@@ -25,6 +25,7 @@ def main(): | |
|
|
||
| try: | ||
| fab_state_config.init_defaults() | ||
|
|
||
| if args.command == "auth" and args.auth_command == None: | ||
| auth_parser.show_help(args) | ||
| return | ||
|
|
@@ -35,8 +36,8 @@ def main(): | |
| fab_state_config.get_config(fab_constant.FAB_MODE) | ||
| == fab_constant.FAB_MODE_INTERACTIVE | ||
| ): | ||
| # Use shared interactive mode startup | ||
| start_interactive_mode() | ||
| return | ||
|
|
||
| if args.command == "auth" and args.auth_command == "logout": | ||
| login.logout(args) | ||
|
|
@@ -47,6 +48,7 @@ def main(): | |
| return | ||
|
|
||
| last_exit_code = fab_constant.EXIT_CODE_SUCCESS | ||
|
|
||
| if args.command: | ||
| if args.command not in ["auth"]: | ||
| fab_logger.print_log_file_path() | ||
|
|
@@ -56,21 +58,21 @@ def main(): | |
| commands_execs = 0 | ||
| for index, command in enumerate(args.command): | ||
| command_parts = command.strip().split() | ||
| subparser = subparsers.choices[command_parts[0]] | ||
| subparser_args = subparser.parse_args(command_parts[1:]) | ||
| subparser_args.command = command_parts[0] | ||
| last_exit_code = _execute_command( | ||
| subparser_args, subparsers, parser | ||
| ) | ||
| commands_execs += 1 | ||
| if index != len(args.command) - 1: | ||
| fab_ui.print_grey("------------------------------") | ||
| if command_parts: # Ensure we have valid command parts | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this required? Isn't "if args.command" above sufficient?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if I understand the flow correctly, here is the case where we have concat commands (in command line mode) so args.command means we gave command provided, command_parts is after we split the commands and here, we check if indeed we have more than one command provided. |
||
| subparser = subparsers.choices[command_parts[0]] | ||
| subparser_args = subparser.parse_args(command_parts[1:]) | ||
| subparser_args.command = command_parts[0] | ||
| last_exit_code = _execute_command( | ||
| subparser_args, subparsers, parser | ||
| ) | ||
| commands_execs += 1 | ||
| if index != len(args.command) - 1: | ||
| fab_ui.print_grey("------------------------------") | ||
| if commands_execs > 1: | ||
| fab_ui.print("\n") | ||
| fab_ui.print_output_format( | ||
| args, message=f"{len(args.command)} commands executed." | ||
| ) | ||
|
|
||
| else: | ||
| last_exit_code = _execute_command(args, subparsers, parser) | ||
|
|
||
|
|
@@ -82,26 +84,55 @@ def main(): | |
| elif args.version: | ||
| fab_ui.print_version() | ||
| else: | ||
| # Display help if "fab" | ||
| fab_ui.display_help(COMMANDS) | ||
| # AUTO-REPL: When no command is provided, automatically enter interactive mode | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you consider using a dedicated command? for example, in azure-cli the command is "az interactive". Not sure if this was already discussed and agreed.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the idea was to implement the logic similar to how it is done in node/python etc... for example to start REPL in python you just type python + enter |
||
| _start_auto_repl() | ||
|
|
||
| except KeyboardInterrupt: | ||
| fab_ui.print_output_error( | ||
| FabricCLIError( | ||
| "Operation cancelled", | ||
| fab_constant.ERROR_OPERATION_CANCELLED, | ||
| ), | ||
| output_format_type=args.output_format, | ||
| ) | ||
| sys.exit(fab_constant.EXIT_CODE_CANCELLED_OR_MISUSE_BUILTINS) | ||
| _handle_keyboard_interrupt(args) | ||
| except Exception as err: | ||
| _handle_unexpected_error(err, args) | ||
|
|
||
|
|
||
| def _start_auto_repl(): | ||
| """Start Auto-REPL with proper initialization and error handling.""" | ||
| try: | ||
| start_interactive_mode() | ||
| except Exception as err: | ||
| fab_ui.print_output_error( | ||
| FabricCLIError(err.args[0], fab_constant.ERROR_UNEXPECTED_ERROR), | ||
| output_format_type=args.output_format, | ||
| FabricCLIError( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we move it inside start_interactive_mode? just the printing and re-throw. I guess we would like to print it in all places where we call start_interactive_mode
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually it might be redundant since we have try catch inside start_interactive_mode (in start_interactive). will check it |
||
| f"Failed to start interactive mode: {str(err)}", | ||
| fab_constant.ERROR_UNEXPECTED_ERROR | ||
| ) | ||
| ) | ||
| sys.exit(fab_constant.EXIT_CODE_ERROR) | ||
|
|
||
|
|
||
| def _handle_keyboard_interrupt(args): | ||
| """Handle KeyboardInterrupt with proper error formatting.""" | ||
| fab_ui.print_output_error( | ||
| FabricCLIError( | ||
| "Operation cancelled", | ||
| fab_constant.ERROR_OPERATION_CANCELLED, | ||
| ), | ||
| output_format_type=args.output_format, | ||
| ) | ||
| sys.exit(fab_constant.EXIT_CODE_CANCELLED_OR_MISUSE_BUILTINS) | ||
|
|
||
|
|
||
| def _handle_unexpected_error(err, args): | ||
| """Handle unexpected errors with proper error formatting.""" | ||
| try: | ||
| error_message = str(err.args[0]) if err.args else str(err) | ||
| except: | ||
| error_message = "An unexpected error occurred" | ||
|
|
||
| fab_ui.print_output_error( | ||
| FabricCLIError(error_message, fab_constant.ERROR_UNEXPECTED_ERROR), | ||
| output_format_type=args.output_format, | ||
| ) | ||
| sys.exit(fab_constant.EXIT_CODE_ERROR) | ||
|
|
||
|
|
||
| def _execute_command(args, subparsers, parser): | ||
| if args.command in subparsers.choices: | ||
| subparser_args = args | ||
|
|
@@ -120,3 +151,4 @@ def _execute_command(args, subparsers, parser): | |
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.