<?php
namespace Tlf;
class Cli {
use Cli\Msgs;
/**
* Set true to run main instead of showing an error when the first arg is not a valid command.
*/
public bool $main_on_command_not_found = false;
/**
* array<int index, string command_name> Array of commands to exclude from the help menu
* @usage `$cli->exclude_from_help[] = 'main';`
*/
public array $exclude_from_help = [];
/**
* The current working directory
*/
public $pwd;
/** map of command name to callable
*/
public $command_map = [];
/**
* command_name=>help_msg
*/
public $help_map = [];
/**
* the array of commands (for environments that have sub-commands)
*/
// public $commands;
/**
* The command being executed
*/
public $command = 'main';
/**
* The args to pass to your command's callable (key=>value array)
*/
public $args = [];
/**
* The name of the command used to execute this (not path)
*/
public $name;
/**
* Directory to write log files to.
*/
public string $log_dir;
/**
*
*/
public $argv;
public function __construct($pwd = null, $argv = null){
$this->pwd = $pwd ?? $_SERVER['PWD'];
$this->argv = $argv ?? $_SERVER['argv'];
$this->load_command('help', [$this, 'help_menu'], 'show this help menu');
$this->help_map['main'] = 'When the script is called with no arguments';
$this->name = basename($this->argv[0]);
}
/**
* Log $message to disk with a date & timestamp. You must set `$cli->log_dir` to a valid directory.
*
* @param $message a message to log. New lines are padded, and it is prefixed with a date & time.
* @param $log_file the name of a file to log to inside `$cli->log_dir`. The file does not need to exist prior to logging.
*
* @return void does not return anything
*/
public function log(string $message, string $log_file = 'tlf-main'){
if (!isset($this->log_dir)){
echo "\nERROR: Cannot log. log_dir not set.\n";
return;
}
$fh = fopen($this->log_dir.'/'.$log_file,'a');
if ($fh===false){
echo "\nERROR: Cannot log to file $log_file. fopen() failed.\n";
return;
}
$date = new \DateTime();
$date_string = $date->format('Y-m-d H:i:s T');
$parts = explode("\n", trim($message));
$parts = array_map('trim', $parts);
$padded_message = implode("\n ", $parts);
$write_message = "[$date_string]: $padded_message\n";
fwrite($fh, $write_message);
fclose($fh);
}
/**
* load standard inputs from cli
* @param $stdin (optional) array of arguments like `['filename', 'subcommand', '-option', 'option_value', '--flag', 'etc']`
* @default $stdin to $_SERVER['argv']
*/
public function load_stdin($stdin=null){
$stdin = $stdin ?? $this->argv;
$this->script = basename($stdin[0]);
if (!isset($stdin[1])||substr($stdin[1],0,1)=='-'){
$this->command = $this->command;
$slice = 1;
} else {
$this->command = $stdin[1];
$slice = 2;
}
$args = $this->parse_args(array_slice($stdin,$slice), $this->args);
$this->args = array_merge($this->args, $args);
}
public function parse_args($stdin_args, $args=[]){
$last_key = null;
foreach ($stdin_args as $arg){
if ($arg[0]=='-'){
if ($arg[1]=='-'){
$last_key= substr($arg,2);
$args[$last_key] = true;
continue;
}
$last_key = substr($arg,1);
if (!isset($args[$last_key])){
$args[$last_key] = null;
}
} else if ($last_key==null){
$args['--'][] = $arg;
} else {
if ($arg==='true')$arg = true;
else if ($arg === 'false')$arg = false;
if (is_array($args[$last_key])){
$args[$last_key][] = $arg;
} else {
$args[$last_key] = $arg;
}
$last_key = null;
}
}
return $args;
}
/**
* @param $command the command
* @param $callable Ex: `function(\Tlf\Cli $cli, array $args){}`
* @param $help_msg short description for help menu
*/
public function load_command(string $command, $callable, string $help_msg=''){
$this->command_map[$command] = $callable;
if ($help_msg!='')$this->help_map[$command] = $help_msg;
}
/**
* Load a json settings file, if it exists. Fails silently if it does not exist.
*
* @param string $file
* @return array of data from the file, or false on failure
*/
public function load_json_file(string $file){
if (!file_exists($file))return false;
$content = file_get_contents($file);
$settings = json_decode($content, true);
$this->load_inputs($settings);
return $settings;
}
public function load_inputs($args){
$this->args = array_merge($this->args, $args);
}
/**
*
* Execute a command by name
*
* @param $command the name of the command to execute
* @param $args args to execute
*/
public function call_command(string $command, array $args=[]){
if (!isset($this->command_map[$command])){
if ($this->main_on_command_not_found){
if (!isset($args['--']))$args['--'] = [];
array_unshift($args['--'], $command);
$command = 'main';
} else {
echo 'Command "'.$command.'" does not exist.'."\n";
echo "Try \"$command help \".";
return;
}
}
$call = $this->command_map[$command];
$ret = $call($this, $args);
echo "\n";
return $ret;
}
/**
* Run the cli based on loaded inputs
*/
public function execute(){
return $this->call_command($this->command, $this->args);
}
public function help_menu($cli, $args){
foreach ($this->command_map as $command => $c){
// if ($command=='main')continue;
if (array_search($command, $this->exclude_from_help) === false){
echo "\n $command - ". ($this->help_map[$command]??'');
}
}
echo "\n";
}
/**
* Prompt user for y/no answer. Lowercase 'y' is yes. All other answers are no.
*
* @param $msg string message to display (newline added before, question mark after)
* @param $success_func (optional) a callback that receives ...$args if user answers 'y'.
* @param ...$args, args to pass to the success function
*
* @return true if user answers 'y', false otherwsie.
*/
public function ask(string $msg, $success_func=null,...$args){
$answer = readline("\n".$msg."(y/n)? ");
if (strtolower($answer)!='y')return false;
if ($success_func==null)return true;
$success_func(...$args);
return true;
}
/**
* Prompt user for string input
*
* @param $msg string message to display (newline added before, colon after)
*
* @return user's input, trimmed.
*/
public function prompt(string $msg){
$answer = readline("\n".$msg.": ");
return trim($answer);
}
/**
* Print an array as a table, just like mysql cli does. all values will be printed. column width will be fixed to the maximum length
*
* @param $rows array<string key, mixed value> keys will be printed as headers.
*/
public function print_table(array $rows){
$stats = [];
foreach ($rows as $rownum=>$row){
foreach ($row as $key=>$value){
$cur_len = $stats[$key] ?? 0;
if (($new_len = strlen($value)) > $cur_len)$stats[$key] = $new_len;
if (($keylen = strlen($key)) > $new_len) $stats[$key] = $keylen;
}
}
$total_len = 0;
$lines = ["","",""];
$last_line = '';
foreach ($stats as $key=>$len){
$pluspart = "+". str_repeat('-',$len + 2);
$lines[0] .= $pluspart;
$lines[1] .= "| $key ";
$lines[2] .= $pluspart;
$last_line .= $pluspart;
if (($diff = ($len - strlen($key) ) ) >0)$lines[1] .= str_repeat(' ',$diff);
}
$lines[0] .= '+';
$lines[1] .= '|';
$lines[2] .= '+';
foreach ($lines as $line){
echo "\n".$line;
}
foreach ($rows as $row){
echo "\n";
foreach ($row as $key=>$value){
$value = str_replace("\n", "\\n", $value);
echo "| ".$value .' ';
$maxlen = $stats[$key] ?? 0;
$thislen = strlen($value);
if (($diff = ($maxlen - $thislen)) > 0) echo str_repeat(" ", $diff);
}
echo "|";
}
echo "\n{$last_line}+";
echo "\n";
// EXAMPLE ... except we're not right-justifying nulls & numbers
//+----+---------------+-------------+--------------+---------------------+---------+--------------------+
//| id | title | description | slug | created_at | status | related_article_id |
//+----+---------------+-------------+--------------+---------------------+---------+--------------------+
//| 1 | One | Desc 1 | one | 2023-11-27 15:31:01 | public | NULL |
//| 2 | Two | Desc 2 | two | 2023-11-27 15:31:01 | public | NULL |
//| 3 | Three | Desc 3 | three | 2023-11-27 15:31:01 | public | 1 |
//| 4 | Four, Private | Desc 4 | four-private | 2023-11-27 15:31:01 | private | 1 |
//+----+---------------+-------------+--------------+---------------------+---------+--------------------+
}
}