lib/tty/exit.rb
# frozen_string_literal: true
require_relative "exit/code"
require_relative "exit/registry"
require_relative "exit/version"
module TTY
# Terminal exit codes for humans and machines
module Exit
extend self
Error = Class.new(StandardError)
# @api private
def self.included(base)
base.instance_eval do
def register_exit(*args)
Registry.register_exit(*args)
end
end
end
NAME_TO_EXIT_CODE = {
ok: Code::SUCCESS,
success: Code::SUCCESS,
error: Code::ERROR,
shell_misuse: Code::SHELL_MISUSE,
usage_error: Code::USAGE_ERROR,
data_err: Code::DATA_ERROR,
data_error: Code::DATA_ERROR,
no_input: Code::NO_INPUT,
no_user: Code::NO_USER,
no_host: Code::NO_HOST,
service_unavailable: Code::SERVICE_UNAVAILABLE,
software_error: Code::SOFTWARE_ERROR,
system_error: Code::SYSTEM_ERROR,
system_file_missing: Code::SYSTEM_FILE_MISSING,
cant_create: Code::CANT_CREATE,
io_error: Code::IO_ERROR,
io_err: Code::IO_ERROR,
temp_fail: Code::TEMP_FAIL,
protocol: Code::PROTOCOL,
no_perm: Code::NO_PERM,
no_permission: Code::NO_PERM,
config_error: Code::CONFIG_ERROR,
configuration_error: Code::CONFIG_ERROR,
cannot_execute: Code::CANNOT_EXECUTE,
not_found: Code::COMMAND_NOT_FOUND,
command_not_found: Code::COMMAND_NOT_FOUND,
invalid_argument: Code::INVALID_ARGUMENT,
hangup: Code::HANGUP,
interrupt: Code::INTERRUPT,
quit: Code::QUIT,
illegal_instruction: Code::ILLEGAL_INSTRUCTION,
trace_trap: Code::TRACE_TRAP,
abort: Code::ABORT,
kill: Code::KILL,
bus_error: Code::BUS_ERROR,
memory_error: Code::MEMORY_ERROR,
segmentation_fault: Code::MEMORY_ERROR,
pipe: Code::PIPE,
alarm: Code::ALARM,
user1: Code::USER1,
user2: Code::USER2
}.freeze
private_constant :NAME_TO_EXIT_CODE
CODE_TO_EXIT_MESSAGE = {
Code::SUCCESS => "Successful termination",
Code::ERROR => "An error occurred",
Code::SHELL_MISUSE => "Misuse of shell builtins",
Code::USAGE_ERROR => "Command line usage error",
Code::DATA_ERROR => "Data format error",
Code::NO_INPUT => "Cannot open input",
Code::NO_USER => "User name unknown",
Code::NO_HOST => "Host name unknown",
Code::SERVICE_UNAVAILABLE => "Service unavailable",
Code::SOFTWARE_ERROR => "Internal software error",
Code::SYSTEM_ERROR => "System error (e.g. can't fork)",
Code::SYSTEM_FILE_MISSING => "Critical OS file missing",
Code::CANT_CREATE => "Can't create user output file",
Code::IO_ERROR => "Input/output error",
Code::TEMP_FAIL => "Temp failure, user is invited to retry",
Code::PROTOCOL => "Remote error in protocol",
Code::NO_PERM => "Permission denied",
Code::CONFIG_ERROR => "Configuration error",
Code::CANNOT_EXECUTE => "Command invoked cannot execute",
Code::COMMAND_NOT_FOUND => "Command not found",
Code::INVALID_ARGUMENT => "Invalid argument",
Code::HANGUP => "Hangup detected on controlling terminal or death of controlling process.",
Code::INTERRUPT => "Interrupted by Control-C",
Code::QUIT => "Quit program",
Code::ILLEGAL_INSTRUCTION => "Illegal instruction",
Code::TRACE_TRAP => "Trace/breakpoint trap",
Code::ABORT => "Abort program",
Code::KILL => "Kill program",
Code::BUS_ERROR => "Access to an undefined portion of a memory object",
Code::MEMORY_ERROR => "An invalid virtual memory reference or segmentation fault",
Code::PIPE => "Write on a pipe with no one to read it",
Code::ALARM => "Alarm clock",
Code::USER1 => "User-defined signal 1",
Code::USER2 => "User-defined signal 2"
}.freeze
private_constant :CODE_TO_EXIT_MESSAGE
# Check if an exit code is valid, that it's within the 0-255 (inclusive)
#
# @param [Integer] code
# the code to check
#
# @return [Boolean]
#
# @api public
def exit_valid?(code)
code >= 0 && code <= 255
end
# Check if an exit code is already defined by Unix system
#
# @param [Integer] code
# the code to check
#
# @return [Boolean]
#
# @api public
def exit_reserved?(code)
(code >= Code::SUCCESS && code <= Code::SHELL_MISUSE) ||
(code >= Code::USAGE_ERROR && code <= Code::CONFIG_ERROR) ||
(code >= Code::CANNOT_EXECUTE && code <= Code::USER2)
end
# Check if the exit status was successful.
#
# @param [Integer] code
#
# @api public
def exit_success?(code)
code == Code::SUCCESS
end
# A user friendly explanation of the exit code
#
# @example
# TTY::Exit.exit_message(:usage_error)
# # => "Command line usage error"
#
# @param [String,Integer] name_or_code
#
# @api public
def exit_message(name_or_code = :ok)
(Registry.exits[name_or_code] || {})[:message] ||
CODE_TO_EXIT_MESSAGE[exit_code(name_or_code)] || ""
end
# Provide a list of reserved status messages
#
# @api public
def exit_messages
CODE_TO_EXIT_MESSAGE
end
# Provide exit code for a name or status
#
# @example
# TTY::Exit.exit_code(:usage_error)
# # => 64
#
# @param [String,Integer] name_or_code
#
# @return [Integer]
# the exit code
#
# @api public
def exit_code(name_or_code = :ok)
case name_or_code
when String, Symbol
(Registry.exits[name_or_code.to_sym] || {})[:code] ||
NAME_TO_EXIT_CODE.fetch(name_or_code.to_sym) do
raise Error, "Name '#{name_or_code}' isn't recognized."
end
when Numeric
if exit_valid?(name_or_code.to_i)
name_or_code.to_i
else
raise Error, "Provided code outside of the range (0 - 255)"
end
else
raise Error, "Provide a name or a number as an exit code"
end
end
# Provide a list of reserved codes
#
# @api public
def exit_codes
NAME_TO_EXIT_CODE
end
# Exit this process with a given status code
#
# @param [String,Integer] name_or_code
# The name for an exit code or code itself
# @param [String] message
# The message to print to io stream
# @param [IO] io
# The io to print message to
#
# @return [nil]
#
# @api public
def exit_with(name_or_code = :ok, message = nil, io: $stderr)
if message == :default
message = "ERROR(#{exit_code(name_or_code)}): #{exit_message(name_or_code)}"
end
io.print(message) if message
::Kernel.exit(exit_code(name_or_code))
end
end # Exit
end # TTY