Skip to content
3 changes: 2 additions & 1 deletion src/fabric_cli/commands/config/fab_config_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import json
import os
import sys
from argparse import Namespace
from typing import Any

Expand Down Expand Up @@ -115,4 +116,4 @@ def _handle_fab_config_mode(previous_mode: str, current_mode: str) -> None:

if previous_mode == fab_constant.FAB_MODE_INTERACTIVE:
utils_ui.print("Exiting interactive mode. Goodbye!")
os._exit(0)
sys.exit(fab_constant.EXIT_CODE_SUCCESS)
2 changes: 1 addition & 1 deletion src/fabric_cli/commands/fs/cp/fab_fs_cp_folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def copy_folder(
if not args.force and not utils_ui.prompt_confirm(
f"Folder '{from_folder.name}' contains items that do not support copying: {unsupported_items_names}. Do you still want to proceed?"
):
return 0
return fab_constant.EXIT_CODE_SUCCESS

supported_items: list[Item] = []
folders: list[Folder] = []
Expand Down
5 changes: 3 additions & 2 deletions src/fabric_cli/commands/fs/mv/fab_fs_mv_folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from fabric_cli.client import fab_api_folders as folders_api
from fabric_cli.commands.fs.cp import fab_fs_cp_folder as cp_folder
from fabric_cli.core import fab_constant
from fabric_cli.core.hiearchy.fab_folder import Folder
from fabric_cli.core.hiearchy.fab_hiearchy import Workspace
from fabric_cli.utils import fab_mem_store as utils_mem_store
Expand Down Expand Up @@ -56,9 +57,9 @@ def move_folder(
)
elif from_folder.parent != to_context:
_change_folder_parent(from_folder, to_context)
return 1
return fab_constant.EXIT_CODE_ERROR

return 0
return fab_constant.EXIT_CODE_SUCCESS


def move_folders(
Expand Down
11 changes: 9 additions & 2 deletions src/fabric_cli/core/fab_constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,18 @@
ERROR_SPN_AUTH_MISSING = "ServicePrincipalAuthMissing"
ERROR_JOB_FAILED = "JobFailed"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please scan the cli commands and make sure they are complaint to POSIX

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scanned all CLI commands for POSIX compliance. Fixed 3 violations in commit 40f2a03:

  1. fab_describe_parser.py: Replaced hardcoded sys.exit(2) with EXIT_CODE_CANCELLED_OR_MISUSE_BUILTINS
  2. fab_parser_setup.py: Replaced hardcoded sys.exit(2) with EXIT_CODE_CANCELLED_OR_MISUSE_BUILTINS
  3. fab_config_set.py: Replaced os._exit(0) (private function) with sys.exit(EXIT_CODE_SUCCESS)

All CLI commands now properly use POSIX-compliant exit code constants. Verified: exit codes (0, 1, 2, 126, 127, 128+n), error handling, signal handling, and output streams all compliant.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot i meant also the fs command file system POSIX complaint

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scanned all filesystem (fs) commands for POSIX compliance. Fixed 2 violations in commit b8ba571:

  1. fab_fs_mv_folder.py: Replaced hardcoded return 0/1 with EXIT_CODE_SUCCESS/EXIT_CODE_ERROR
  2. fab_fs_cp_folder.py: Replaced hardcoded return 0 with EXIT_CODE_SUCCESS

All fs commands now use POSIX-compliant exit code constants. Helper functions returning item IDs (not exit codes) are correct as-is. All fs command parsers already use proper POSIX option syntax.

# Exit codes
# Exit codes (POSIX compliant)
# 0 - Success
# 1 - General errors
# 2 - Misuse of shell builtins
# 126 - Command cannot execute
# 127 - Command not found
# 128+n - Fatal error signal "n"
EXIT_CODE_SUCCESS = 0
EXIT_CODE_ERROR = 1
EXIT_CODE_CANCELLED_OR_MISUSE_BUILTINS = 2
EXIT_CODE_AUTHORIZATION_REQUIRED = 4
EXIT_CODE_CANNOT_EXECUTE = 126 # Used for authorization/permission errors
EXIT_CODE_COMMAND_NOT_FOUND = 127

# Contextual commands
OS_COMMANDS = {
Expand Down
6 changes: 3 additions & 3 deletions src/fabric_cli/core/fab_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import fabric_cli.core.fab_logger as fab_logger
from fabric_cli.core.fab_constant import (
ERROR_UNAUTHORIZED,
EXIT_CODE_AUTHORIZATION_REQUIRED,
EXIT_CODE_CANNOT_EXECUTE,
EXIT_CODE_ERROR,
)
from fabric_cli.core.fab_exceptions import FabricCLIError
Expand Down Expand Up @@ -42,9 +42,9 @@ def wrapper(*args, **kwargs):
args[0].command_path,
output_format_type=args[0].output_format,
)
# If the error is an unauthorized error, return 4
# If the error is an unauthorized error, return 126 (POSIX: command cannot execute)
if e.status_code == ERROR_UNAUTHORIZED:
return EXIT_CODE_AUTHORIZATION_REQUIRED
return EXIT_CODE_CANNOT_EXECUTE
# Return a generic error code
return EXIT_CODE_ERROR

Expand Down
10 changes: 7 additions & 3 deletions src/fabric_cli/core/fab_parser_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ def format_help(self):
help_message = help_message.replace("positional arguments:", "Arg(s):")
help_message = help_message.replace("options:", "Flags:")

# Remove help flag from output (it's implicit)
# Note: We now use standard -h/--help instead of -help
help_message = re.sub(
r"\s*-h, --help\s*(Show help for command|show this help message and exit)?",
"",
Expand All @@ -76,6 +78,8 @@ def format_help(self):
help_message = help_message.replace("[-h] ", "")
help_message = help_message.replace("[-help] ", "")
help_message = help_message.replace("[-help]", "")
help_message = help_message.replace("[--help] ", "")
help_message = help_message.replace("[--help]", "")

if "Flags:" in help_message:
flags_section = help_message.split("Flags:")[1].strip()
Expand Down Expand Up @@ -162,7 +166,7 @@ def error(self, message):
fab_logger.log_warning(message)

if self.fab_mode == fab_constant.FAB_MODE_COMMANDLINE:
sys.exit(2)
sys.exit(fab_constant.EXIT_CODE_CANCELLED_OR_MISUSE_BUILTINS)


# Global parser instances
Expand All @@ -181,8 +185,8 @@ def create_parser_and_subparsers():
help="Run commands in non-interactive mode",
)

# -version and --version
parser.add_argument("-v", "--version", action="store_true")
# -v/-V and --version (POSIX compliant: both short and long forms)
parser.add_argument("-v", "-V", "--version", action="store_true")

subparsers = parser.add_subparsers(dest="command", required=False)

Expand Down
49 changes: 49 additions & 0 deletions src/fabric_cli/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

import signal
import sys

import argcomplete
Expand All @@ -16,7 +17,55 @@
from fabric_cli.utils.fab_commands import COMMANDS


# POSIX-compliant signal handler
def _signal_handler(signum, frame):
"""
Handle POSIX signals gracefully.
Args:
signum: Signal number
frame: Current stack frame
"""
signal_names = {
signal.SIGINT: "SIGINT",
signal.SIGTERM: "SIGTERM",
signal.SIGHUP: "SIGHUP",
signal.SIGQUIT: "SIGQUIT",
}

signal_name = signal_names.get(signum, f"Signal {signum}")

# Print to stderr as per POSIX
sys.stderr.write(f"\n{signal_name} received, exiting gracefully...\n")
sys.stderr.flush()

# Exit with 128 + signal number (POSIX convention)
sys.exit(128 + signum)


def _setup_signal_handlers():
"""
Setup POSIX-compliant signal handlers.
Handles SIGINT, SIGTERM, SIGHUP, and SIGQUIT.
"""
# Handle SIGINT (Ctrl+C)
signal.signal(signal.SIGINT, _signal_handler)

# Handle SIGTERM (termination request)
signal.signal(signal.SIGTERM, _signal_handler)

# Handle SIGQUIT (Ctrl+\)
signal.signal(signal.SIGQUIT, _signal_handler)

# Handle SIGHUP (terminal disconnect) - only on Unix-like systems
if hasattr(signal, 'SIGHUP'):
signal.signal(signal.SIGHUP, _signal_handler)


def main():
# Setup POSIX-compliant signal handlers
_setup_signal_handlers()

parser, subparsers = get_global_parser_and_subparsers()

argcomplete.autocomplete(parser, default_completer=None)
Expand Down
2 changes: 1 addition & 1 deletion src/fabric_cli/parsers/fab_describe_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def _show_commands_supported(args: Namespace) -> None:
f"Usage: {usage_format}\n\n" f"Available elements:\n {element_list}\n"
)
utils_ui.print(custom_message)
sys.exit(2)
sys.exit(fab_constant.EXIT_CODE_CANCELLED_OR_MISUSE_BUILTINS)


def _print_supported_commands_by_element(element_or_path: str) -> None:
Expand Down
6 changes: 4 additions & 2 deletions src/fabric_cli/parsers/fab_global_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ def add_global_flags(parser) -> None:

Args:
parser: The argparse parser to add flags to.

Note: argparse automatically adds -h/--help, so we don't need to add it manually.
"""
# Add help flag
parser.add_argument("-help", action="help")
# Note: -h/--help is automatically added by argparse by default
# We don't need to explicitly add it

# Add format flag to override output format
parser.add_argument(
Expand Down
Loading