* @copyright 1997-Present dealnews.com, Inc. * @license http://opensource.org/licenses/bsd-license.php BSD */ namespace DealNews\Console; /** * Manages command line arguments, pid files and verbosity levels */ class Console { /** * Verbosity levels */ const VERBOSITY_QUIET = 1; const VERBOSITY_NORMAL = 2; const VERBOSITY_VERBOSE = 3; const VERBOSITY_INFO = 4; const VERBOSITY_DEBUG = 16; /** * Settings for the optional argument for command line options */ const OPTIONAL = 256; const REQUIRED = 512; const ONE_REQUIRED = 1024; /** * Return values for the checkPid function */ const PID_OK = 8192; const PID_OTHER_RUNNING = 16384; const PID_OTHER_NOT_RUNNING = 32767; const PID_OTHER_UNKNOWN = 65534; /** * Stores the verbosity for this console application * * @var int */ protected static $verbosity = self::VERBOSITY_NORMAL; /** * The unique PID file for this command * * @var string */ protected $pid_file = ""; /** * The last pid which ran this command * * @var integer */ protected $last_pid = 0; /** * The timestamp the last time the same command was run * * @var mixed */ protected $last_pid_start_time = null; /** * Holds various config options * * @var array */ protected $config = array( "wrap" => 78, "copyright" => array(), "help" => array( "header" => "", "footer" => "" ) ); /** * Holds the getopt() options return value * @var array */ protected $opts = array(); /** * Holds the defined options * * There are some built in options. * * -h for help * -v for verbosity * -q for quiet with overrides verbosity * * @var array */ protected $options = array( "h" => array( "optional" => self::OPTIONAL, "description" => "Shows this help" ), "v" => array( "optional" => self::OPTIONAL, "description" => "Be verbose. Additional v will increase verbosity. e.g. -vvv", "param_optional" => true ), "q" => array( "optional" => self::OPTIONAL, "description" => "Be quiet. Will override -v" ) ); /** * Creates the new Console object * * @param string $msg The message to be displayed that explains * what the programs does * @param array $options An array of command line options. * @return mixed * */ public function __construct(array $config = array(), array $options = array()) { if (!empty($config)) { foreach ($config as $key => $value) { if (isset($this->config[$key])) { $this->config[$key] = $value; } } } if (!empty($options)) { $this->options = array_merge($this->options, $options); } } /** * Starts the process of getting command line arguments and preparing * them for use * * @return void */ public function run() { $this->options = $this->normalizeOptions($this->options); $this->opts = $this->getOpts($this->options); if (!empty($this->opts["h"])) { $this->showHelp(); } foreach ($this->options as $option => $config) { if ($config["optional"] == self::REQUIRED) { if (!isset($this->opts[$option])) { $this->showHelp("Missing required option `$option`"); } } } $missing_options = array(); foreach ($this->options as $option => $config) { if ($config["optional"] == self::ONE_REQUIRED) { if (!isset($this->opts[$option])) { $missing_options[] = $option; } else { // we have at least one required option, break $missing_options = array(); break; } } } if (!empty($missing_options)) { $this->showHelp("Missing one of the required options `".implode("`, `", $missing_options)."`"); } if (isset($this->opts["q"])) { self::$verbosity = self::VERBOSITY_QUIET; } elseif (isset($this->opts["v"])) { self::$verbosity = self::VERBOSITY_VERBOSE; if (!is_bool($this->opts["v"])) { if (is_array($this->opts["v"])) { $level = count($this->opts["v"]); } else { $level = strlen($this->opts["v"]) + 1; } switch ($level) { case 1: self::$verbosity = self::VERBOSITY_NORMAL; break; case 2: self::$verbosity = self::VERBOSITY_VERBOSE; break; case 3: self::$verbosity = self::VERBOSITY_INFO; break; default: self::$verbosity = self::VERBOSITY_DEBUG; break; } } } $this->opts["v"] = self::$verbosity; } /** * Can be used by other Console objects to determine if a verbosity * for the running application has been set without having access * to the instance that was created. * * @return int */ public static function verbosity() { return self::$verbosity; } /** * Magic getter for various properties * * @param string $name Property to retrieve * * @return mixed */ public function __get($name) { $value = null; switch ($name) { case "pid_file": case "last_pid": case "last_pid_start_time": $value = $this->$name; break; case "verbosity": $value = self::$verbosity; break; default: // Allow access to the options via the getter too if (array_key_exists($name, $this->options)) { $value = $this->getOpt($name); } else { $trace = debug_backtrace(); trigger_error("Undefined property $name in ".$trace[0]['file']." on line ".$trace[0]['line'], E_USER_ERROR); } } return $value; } /** * Get a single options value * * @param string $name Option name to get the value of * * @return mixed */ public function getOpt($name) { $value = null; if (array_key_exists($name, $this->opts)) { $value = $this->opts[$name]; } elseif (!array_key_exists($name, $this->options)) { if (self::$verbosity != self::VERBOSITY_QUIET) { trigger_error("Option $name is not a valid option", E_USER_WARNING); } } return $value; } /** * Clean up the options and validate they are sane * * @param array $options Array of command line options * * @return array */ public function normalizeOptions($options) { $normalized_options = array(); foreach ($options as $option => $config) { if (!is_array($config)) { trigger_error("Invalid option $option", E_USER_ERROR); } if (!isset($config["optional"])) { $config["optional"] = self::OPTIONAL; } if (empty($config["description"])) { trigger_error("Missing description for option $option", E_USER_ERROR); } if (!empty($confi["param"])) { $config["param"] = trim($config["param"]); } $trim_option = trim($option); if ($trim_option != $option) { unset($options[$option]); $option = $trim_option; } $normalized_options[$option] = $config; } ksort($normalized_options); return $normalized_options; } /** * Writes to standard out conditionally based on the current verbosity * level * * @param string $buffer Data to write * @param int $level Verbosity level * * @return void */ public function write($buffer, $level = self::VERBOSITY_NORMAL) { if (self::$verbosity != self::VERBOSITY_QUIET && $level <= self::$verbosity) { fputs(STDOUT, $buffer."\n"); } } /** * Calls getopt * * @param array $options Command line options array * * @return array */ public function getOpts(array $options) { list($short_opts, $long_opts) = $this->buildGetopts($options); $opts = getopt(implode("", $short_opts), $long_opts); // getopt() sets options that don't take a parameter to false // this can create some confusing issues. So, set any truly // false values to true instead foreach ($opts as $key => $value) { if ($value === false) { $opts[$key] = true; } } return $opts; } /** * Build the getopt input values. Returns an array where the first element * is the short options and the second is the long options * * @param array $options Command line options array * * @return array */ public function buildGetopts($options) { $short_opts = array(); $long_opts = array(); foreach ($options as $o=>$i) { $opt = $o; if (!empty($i["param_optional"])) { $opt.="::"; } elseif (!empty($i["param"])) { $opt.=":"; } if (strlen($o) > 1) { $long_opts[] = $opt; } else { $short_opts[] = $opt; } } return array( $short_opts, $long_opts ); } /** * Builds the help output * * @param array $config A configuration array * @param array $options Command line options array * * @return string */ public function buildHelp(array $config, array $options) { $max_name_len = 0; $max_param_len = 0; $required_params = array(); $optional_params = array(); $optional_required_params = array(); foreach ($options as $o=>$i) { // determine the maximum length of options and parameters $max_name_len = max($max_name_len, strlen($o)); if (isset($i["param"])) { $max_param_len = max($max_param_len, strlen($i["param"])); } if ($o == "h") { continue; } // build an example usage string if (strlen($o) > 1) { $usage = "--$o"; } else { $usage = "-$o"; } if (!empty($i["param"])) { $usage.= " ".$i["param"]; } if ($i["optional"] == self::OPTIONAL) { $optional_params[] = $usage; } elseif ($i["optional"] == self::ONE_REQUIRED) { $optional_required_params[] = $usage; } else { $required_params[] = $usage; } } $usage_example = " -h"; if (!empty($required_params)) { $usage_example.= " | ".implode(" | ", $required_params); } if (!empty($optional_required_params)) { $usage_example.= " [".implode(" | ", $optional_required_params)."]"; } if (!empty($optional_params)) { $usage_example.= " [".implode("] [", $optional_params)."]"; } $help = ""; if (!empty($config["help"]["header"])) { $help.= wordwrap($config["help"]["header"], $config["wrap"])."\n"; } $help.= "USAGE:\n"; $help.= " ".basename($_SERVER["PHP_SELF"])." $usage_example\n\n"; $help.= "OPTIONS:\n"; $name_pad = $max_name_len + 2; $param_pad = $max_param_len + 2; foreach ($options as $o=>$i) { if (strlen($o) > 1) { $opt = " --"; } else { $opt = " -"; } $opt.= str_pad($o, $name_pad); if ($max_param_len>0) { if (!empty($i["param"])) { $param = $i["param"]; } else { $param = ""; } $opt.= str_pad($param, $param_pad); } if (!empty($i["description"])) { $opt.= wordwrap($i["description"], $config["wrap"] - (strlen($opt)), "\n".str_repeat(" ", strlen($opt))); } $help .= "$opt\n"; } if (!empty($config["copyright"])) { if (empty($config["copyright"]["owner"])) { trigger_error("Copyright owner is required", E_USER_ERROR); } $help.= "\n"; $help.= "Copyright {$config["copyright"]["owner"]} "; if (!empty($config["copyright"]["year"])) { $help.= " ".$config["copyright"]["year"]; } $help.= "\n"; } if (!empty($config["help"]["footer"])) { $help.= "\n"; $help.= $config["help"]["footer"]."\n"; } $help.= "\n"; return $help; } /** * Shows the help and an optional message. Exits with provided exit status * * @param type $message A message to show above the help * @param int $exit Exit status code * @return void * */ public function showHelp($message = "", $exit = 0) { $bt = debug_backtrace(); if (!empty($message)) { fputs(STDERR, wordwrap($message)."\n\n"); } $help = $this->buildHelp($this->config, $this->options); echo $help; exit((int)$exit); } /** * Check for existing pid file * * This function will create a pid file for the current process. * If a pid file exists, it will check to see if the process is running * and return of the PID_* constants. * * This function will exit if an existing pid file is found. * * @param boolean $use_arguments If true, command line options will be * used to create a unique pid file name * @param string $unique_id Optional unique identifier used to * create the pid file name * @return mixed */ public function checkPid($use_arguments = true, $unique_id = null) { if ($use_arguments) { $opts = $this->opts; // remove the built paramaters that control verbosity // They don't likely effect the job the script is doing if (isset($opts["v"])) { unset($opts["v"]); } if (isset($opts["q"])) { unset($opts["q"]); } } else { $opts = array(); } $this->pid_file = $this->generatePidFilename($opts, $unique_id); if (!file_exists($this->pid_file)) { $status = self::PID_OK; // write the pid file $fp = fopen($this->pid_file, "w"); if ($fp) { fputs($fp, getmypid()."|".time()); fclose($fp); if (self::$verbosity == self::VERBOSITY_DEBUG) { echo "Creating PID file $this->pid_file\n"; } } else { if (self::$verbosity != self::VERBOSITY_QUIET) { trigger_error("Failed to create PID file $this->pid_file", E_USER_WARNING); } } register_shutdown_function(array($this, "clearPid")); } else { if (self::$verbosity == self::VERBOSITY_DEBUG) { echo "Found existing PID file $this->pid_file\n"; } list($pid, $started) = explode("|", file_get_contents($this->pid_file)); if ($pid == getmypid()) { $status = self::PID_OK; } else { $this->last_pid = $pid; $this->last_pid_start_time = $started; if (file_exists("/proc")) { if (file_exists("/proc/$pid/status")) { if (self::$verbosity == self::VERBOSITY_DEBUG) { echo "Command still running with PID $this->last_pid\n"; } $status = self::PID_OTHER_RUNNING; } else { if (self::$verbosity == self::VERBOSITY_DEBUG) { echo "Command no longer running with PID $this->last_pid\n"; } $status = self::PID_OTHER_NOT_RUNNING; } } else { if (self::$verbosity == self::VERBOSITY_DEBUG) { echo "Unknown status of last PID $this->last_pid\n"; } $status = self::PID_OTHER_UNKNOWN; } } } return $status; } /** * Clear the pid file for the current process * * @return bool */ public function clearPid($pid_file = null) { if ($pid_file === null) { $pid_file = $this->pid_file; } $sucess = false; if (empty($pid_file)) { if (self::$verbosity != self::VERBOSITY_QUIET) { trigger_error("No PID file to clear.", E_USER_WARNING); } } else { if (file_exists($pid_file)) { $success = @unlink($pid_file); if (!$success) { if (!is_writable($pid_file)) { if (self::$verbosity != self::VERBOSITY_QUIET) { trigger_error("Invalid permisssion to clear PID file $pid_file.", E_USER_WARNING); } } } else { if (self::$verbosity == self::VERBOSITY_DEBUG) { echo "Removed PID file $pid_file\n"; } } } else { if (self::$verbosity != self::VERBOSITY_QUIET) { trigger_error("PID file $pid_file not found.", E_USER_NOTICE); } } } return $success; } /** * Generates an array of PID filenames. * * @param boolean $opts An array of options like those returned * from getopt that can be used to generate * a unique PID file name. * @param string $unique_id Optional unique identifier used to * create the pid file name * @return string */ public function generatePidFilename(array $opts = array(), $unique_id = null) { $pid_file = sys_get_temp_dir(); $pid_file.= DIRECTORY_SEPARATOR; $pid_file.= preg_replace("/[^A-Za-z0-9_-]+/", "_", basename($_SERVER["PHP_SELF"])); if (!empty($opts)) { $pid_file.= "_".sha1(serialize($opts)); } if (!empty($unique_id)) { $pid_file.= "_".$unique_id; } $pid_file.= ".pid"; return $pid_file; } }