diff --git a/.github/workflows/php81.yaml b/.github/workflows/php81.yaml index fead114..77e68ff 100644 --- a/.github/workflows/php81.yaml +++ b/.github/workflows/php81.yaml @@ -8,14 +8,14 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.1' code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.1' coverage-file: 'php-8.1-coverage.xml' diff --git a/.github/workflows/php82.yaml b/.github/workflows/php82.yaml index 978033a..9e431d3 100644 --- a/.github/workflows/php82.yaml +++ b/.github/workflows/php82.yaml @@ -8,7 +8,7 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.2' phpunit-config: "tests/phpunit10.xml" @@ -16,7 +16,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.2' coverage-file: 'php-8.2-coverage.xml' diff --git a/.github/workflows/php83.yaml b/.github/workflows/php83.yaml index 56bbbd1..17c1972 100644 --- a/.github/workflows/php83.yaml +++ b/.github/workflows/php83.yaml @@ -12,7 +12,7 @@ jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.3' phpunit-config: 'tests/phpunit10.xml' @@ -21,7 +21,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.3' coverage-file: 'php-8.3-coverage.xml' diff --git a/.github/workflows/php84.yaml b/.github/workflows/php84.yaml index 1ce3097..adcc94b 100644 --- a/.github/workflows/php84.yaml +++ b/.github/workflows/php84.yaml @@ -8,7 +8,7 @@ on: jobs: test: name: Run Tests - uses: WebFiori/workflows/.github/workflows/test-php.yaml@main + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 with: php-version: '8.4' phpunit-config: "tests/phpunit10.xml" @@ -16,7 +16,7 @@ jobs: code-coverage: name: Coverage needs: test - uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 with: php-version: '8.4' coverage-file: 'php-8.4-coverage.xml' diff --git a/.github/workflows/php85.yaml b/.github/workflows/php85.yaml new file mode 100644 index 0000000..b6743fd --- /dev/null +++ b/.github/workflows/php85.yaml @@ -0,0 +1,27 @@ +name: Build PHP 8.5 + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] +jobs: + test: + name: Run Tests + uses: WebFiori/workflows/.github/workflows/test-php.yaml@v1.2.1 + with: + php-version: '8.5' + phpunit-config: "tests/phpunit10.xml" + + code-coverage: + name: Coverage + needs: test + uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1 + with: + php-version: '8.5' + coverage-file: 'php-8.5-coverage.xml' + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + + diff --git a/README.md b/README.md index 35ffc2d..7b879ab 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Class library that can help in writing command line based applications with minimum dependencies using PHP.

- - + + @@ -56,6 +56,7 @@ Class library that can help in writing command line based applications with mini | | | | | | +| | ## Features * **Easy Command Creation**: Simple class-based approach to building CLI commands diff --git a/WebFiori/Cli/Command.php b/WebFiori/Cli/Command.php index 276f767..faafdd0 100644 --- a/WebFiori/Cli/Command.php +++ b/WebFiori/Cli/Command.php @@ -579,6 +579,56 @@ public function getInput(string $prompt, ?string $default = null, ?InputValidato return null; } /** + * Reads user input with characters masked by a specified character. + * + * This method is similar to getInput() but masks the input characters as the user types, + * making it suitable for sensitive information like passwords, tokens, or secrets. + * The actual input value is captured but only mask characters are displayed in the terminal. + * + * @param string $prompt The prompt message to display to the user. Must be non-empty. + * + * @param string $mask The character to display instead of the actual input characters. + * Default is '*'. Can be any single character or string. + * + * @param string|null $default An optional default value to use if the user provides + * empty input. If provided, it will be shown in the prompt. + * + * @param InputValidator|null $validator An optional validator to validate the input. + * If validation fails, the user will be prompted again. + * + * @return string|null Returns the actual input value (not masked) if valid input is provided, + * or null if the prompt is empty. + * + * @since 1.1.0 + */ + public function getMaskedInput(string $prompt, string $mask = '*', ?string $default = null, ?InputValidator $validator = null): ?string { + $trimmed = trim($prompt); + + if (strlen($trimmed) > 0) { + do { + $this->prints($trimmed, [ + 'color' => 'gray', + 'bold' => true + ]); + + if ($default !== null) { + $this->prints(" Enter = '".$default."'", [ + 'color' => 'light-blue' + ]); + } + $this->println(); + $input = trim($this->readMaskedLine($mask)); + + $check = $this->getInputHelper($input, $validator, $default); + + if ($check['valid']) { + return $check['value']; + } + } while (true); + } + + return null; + } /** * Returns the stream at which the command is sing to read inputs. * * @return null|InputStream If the stream is set, it will be returned as @@ -964,7 +1014,63 @@ public function readInteger(string $prompt, ?int $default = null) : int { public function readln() : string { return $this->getInputStream()->readLine(); } - + /** + * Reads a line from input stream with character masking. + * + * This method reads input character by character and displays mask characters + * instead of the actual input. It handles backspace for character deletion + * and ignores special keys like ESC and arrow keys. + * + * @param string $mask The character to display instead of actual input characters. + * + * @return string The actual input string (unmasked). + * + * @since 1.1.0 + */ + private function readMaskedLine(string $mask = '*'): string { + $input = ''; + + // For testing with ArrayInputStream, read the whole line at once + if ($this->getInputStream() instanceof \WebFiori\Cli\Streams\ArrayInputStream) { + $input = $this->getInputStream()->readLine(); + // Simulate masking output for testing + $this->prints(str_repeat($mask, strlen($input))); + $this->println(); + return $input; + } + + // Set terminal to raw mode with echo disabled for real-time character reading + $sttyMode = null; + if (function_exists('shell_exec') && PHP_OS_FAMILY !== 'Windows') { + $sttyMode = shell_exec('stty -g 2>/dev/null'); + shell_exec('stty -echo -icanon 2>/dev/null'); + } + + try { + // For real terminal input, read character by character + while (true) { + $char = KeysMap::readAndTranslate($this->getInputStream()); + + if ($char === 'LF' || $char === 'CR' || $char === '') { + break; + } elseif ($char === 'BACKSPACE' && strlen($input) > 0) { + $input = substr($input, 0, -1); + $this->prints("\x08 \x08"); // Backspace, space, backspace + } elseif ($char !== 'BACKSPACE' && $char !== 'ESC' && $char !== 'DOWN' && $char !== 'UP' && $char !== 'LEFT' && $char !== 'RIGHT') { + $input .= $char === 'SPACE' ? ' ' : $char; + $this->prints($mask); + } + } + } finally { + // Restore terminal settings + if ($sttyMode !== null) { + shell_exec('stty ' . $sttyMode . ' 2>/dev/null'); + } + } + + $this->println(); + return $input; + } /** * Reads a string that represents class namespace. * diff --git a/WebFiori/Cli/Commands/MakeCommand.php b/WebFiori/Cli/Commands/MakeCommand.php new file mode 100644 index 0000000..832f360 --- /dev/null +++ b/WebFiori/Cli/Commands/MakeCommand.php @@ -0,0 +1,221 @@ +templateManager = new TemplateManager(); + + parent::__construct('make:command', [ + '--name' => [ + ArgumentOption::DESCRIPTION => 'The name of the command (e.g., "user:create")', + ArgumentOption::OPTIONAL => false + ], + '--class' => [ + ArgumentOption::DESCRIPTION => 'The class name (e.g., "CreateUserCommand")', + ArgumentOption::OPTIONAL => true + ], + '--path' => [ + ArgumentOption::DESCRIPTION => 'Output directory path', + ArgumentOption::OPTIONAL => true, + ArgumentOption::DEFAULT => 'commands' + ], + '--namespace' => [ + ArgumentOption::DESCRIPTION => 'PHP namespace for the command class', + ArgumentOption::OPTIONAL => true + ], + '--interactive' => [ + ArgumentOption::DESCRIPTION => 'Generate command with interactive prompts', + ArgumentOption::OPTIONAL => true + ], + '--args' => [ + ArgumentOption::DESCRIPTION => 'Add command arguments (comma-separated)', + ArgumentOption::OPTIONAL => true + ], + '--template' => [ + ArgumentOption::DESCRIPTION => 'Template type to use', + ArgumentOption::OPTIONAL => true, + ArgumentOption::VALUES => $this->templateManager->getAvailableTemplates(), + ArgumentOption::DEFAULT => 'basic' + ] + ], 'Generate a new CLI command class with scaffolding'); + } + + public function exec(): int { + $this->println('🚀 WebFiori CLI Command Generator'); + $this->println('================================='); + $this->println(); + + // Get command details + $commandName = $this->getArgValue('--name'); + $className = $this->getArgValue('--class') ?? $this->generateClassName($commandName); + $outputPath = $this->getArgValue('--path') ?? 'commands'; + $namespace = $this->getArgValue('--namespace'); + $template = $this->getArgValue('--template') ?? 'basic'; + $interactive = $this->isArgProvided('--interactive'); + $args = $this->getArgValue('--args'); + + // Interactive mode for missing details + if (!$namespace) { + $namespace = $this->getInput('Enter namespace (optional): ') ?: null; + } + + // Validate inputs + if (!$this->validateInputs($commandName, $className)) { + return 1; + } + + // Generate command + try { + $filePath = $this->generateCommand([ + 'name' => $commandName, + 'class' => $className, + 'path' => $outputPath, + 'namespace' => $namespace, + 'template' => $template, + 'interactive' => $interactive, + 'args' => $args ? explode(',', $args) : [] + ]); + + $this->success("✅ Command generated successfully!"); + $this->info("📁 File: $filePath"); + $this->info("🏷️ Class: $className"); + $this->info("⚡ Command: $commandName"); + + $this->println(); + $this->println("Next steps:"); + $this->println("1. Register the command in your application"); + $this->println("2. Implement the exec() method logic"); + $this->println("3. Add any additional arguments or validation"); + + return 0; + } catch (\Exception $e) { + $this->error("❌ Failed to generate command: " . $e->getMessage()); + return 1; + } + } + + /** + * Generate class name from command name. + */ + private function generateClassName(string $commandName): string { + // Convert command-name or namespace:command to ClassName + $parts = preg_split('/[:\-_]/', $commandName); + $className = ''; + + foreach ($parts as $part) { + $className .= ucfirst(strtolower($part)); + } + + return $className . 'Command'; + } + + /** + * Validate command inputs. + */ + private function validateInputs(string $commandName, string $className): bool { + // Validate command name + if (!preg_match('/^[a-z][a-z0-9\-:_]*$/', $commandName)) { + $this->error('Command name must start with a letter and contain only lowercase letters, numbers, hyphens, colons, and underscores.'); + return false; + } + + // Validate class name + if (!preg_match('/^[A-Z][a-zA-Z0-9]*$/', $className)) { + $this->error('Class name must be a valid PHP class name (PascalCase).'); + return false; + } + + return true; + } + + /** + * Generate the command file. + */ + private function generateCommand(array $config): string { + $content = $this->templateManager->processTemplate($config['template'], [ + 'namespace' => $config['namespace'] ? "namespace {$config['namespace']};\n\n" : '', + 'use_statements' => $this->generateUseStatements($config), + 'class_name' => $config['class'], + 'command_name' => $config['name'], + 'command_description' => "Description for {$config['name']} command", + 'arguments' => $this->generateArguments($config['args']) + ]); + + // Ensure output directory exists + $outputDir = $config['path']; + if (!is_dir($outputDir)) { + mkdir($outputDir, 0755, true); + } + + // Generate file path + $fileName = $config['class'] . '.php'; + $filePath = rtrim($outputDir, '/') . '/' . $fileName; + + // Check if file exists + if (file_exists($filePath)) { + $overwrite = $this->confirm("File $filePath already exists. Overwrite?"); + if (!$overwrite) { + throw new \Exception("File already exists and overwrite was declined."); + } + } + + // Write file + file_put_contents($filePath, $content); + + return $filePath; + } + + /** + * Generate use statements. + */ + private function generateUseStatements(array $config): string { + $uses = [ + 'use WebFiori\Cli\Command;', + 'use WebFiori\Cli\ArgumentOption;' + ]; + + if ($config['interactive'] || $config['template'] === 'interactive') { + $uses[] = 'use WebFiori\Cli\InputValidator;'; + } + + return implode("\n", $uses); + } + + /** + * Generate command arguments array. + */ + private function generateArguments(array $args): string { + if (empty($args)) { + return '[]'; + } + + $argStrings = []; + foreach ($args as $arg) { + $arg = trim($arg); + $argName = '--' . strtolower(str_replace(' ', '-', $arg)); + $argStrings[] = " '$argName' => [\n" . + " ArgumentOption::DESCRIPTION => 'Description for $arg',\n" . + " ArgumentOption::OPTIONAL => true\n" . + " ]"; + } + + return "[\n" . implode(",\n", $argStrings) . "\n ]"; + } +} diff --git a/WebFiori/Cli/Table/TableData.php b/WebFiori/Cli/Table/TableData.php index 631355f..c6d14b0 100644 --- a/WebFiori/Cli/Table/TableData.php +++ b/WebFiori/Cli/Table/TableData.php @@ -95,7 +95,7 @@ public static function fromCsv(string $csv, bool $hasHeaders = true, string $del continue; } - $row = str_getcsv($line, $delimiter); + $row = str_getcsv($line, $delimiter, '"', '\\'); if ($hasHeaders && $headers === null) { $headers = $row; diff --git a/WebFiori/Cli/Templates/TemplateManager.php b/WebFiori/Cli/Templates/TemplateManager.php new file mode 100644 index 0000000..aa757f3 --- /dev/null +++ b/WebFiori/Cli/Templates/TemplateManager.php @@ -0,0 +1,55 @@ +templatesPath = $templatesPath ?? __DIR__ . '/stubs'; + } + + /** + * Get template content by name. + */ + public function getTemplate(string $name): string { + $templateFile = $this->templatesPath . '/' . $name . '.stub'; + + if (!file_exists($templateFile)) { + throw new \InvalidArgumentException("Template '$name' not found at: $templateFile"); + } + + return file_get_contents($templateFile); + } + + /** + * Get available templates. + */ + public function getAvailableTemplates(): array { + $templates = []; + $files = glob($this->templatesPath . '/*.stub'); + + foreach ($files as $file) { + $templates[] = basename($file, '.stub'); + } + + return $templates; + } + + /** + * Process template with variables. + */ + public function processTemplate(string $template, array $variables): string { + $content = $this->getTemplate($template); + + foreach ($variables as $key => $value) { + $content = str_replace('{{' . $key . '}}', $value, $content); + } + + return $content; + } +} diff --git a/WebFiori/Cli/Templates/stubs/basic.stub b/WebFiori/Cli/Templates/stubs/basic.stub new file mode 100644 index 0000000..85e8b45 --- /dev/null +++ b/WebFiori/Cli/Templates/stubs/basic.stub @@ -0,0 +1,23 @@ +println('🚀 Executing {{command_name}} command...'); + + // TODO: Implement your command logic here + + $this->success('✅ Command completed successfully!'); + return 0; + } +} diff --git a/WebFiori/Cli/Templates/stubs/crud.stub b/WebFiori/Cli/Templates/stubs/crud.stub new file mode 100644 index 0000000..52c8b94 --- /dev/null +++ b/WebFiori/Cli/Templates/stubs/crud.stub @@ -0,0 +1,73 @@ + [ + ArgumentOption::DESCRIPTION => 'CRUD action to perform', + ArgumentOption::OPTIONAL => true, + ArgumentOption::VALUES => ['create', 'read', 'show', 'update', 'delete', 'list'], + ArgumentOption::DEFAULT => 'list' + ] + ]), '{{command_description}}'); + } + + public function exec(): int { + $action = $this->getArgValue('--action') ?? 'list'; + + switch ($action) { + case 'create': + return $this->createRecord(); + case 'read': + case 'show': + return $this->showRecord(); + case 'update': + return $this->updateRecord(); + case 'delete': + return $this->deleteRecord(); + case 'list': + default: + return $this->listRecords(); + } + } + + private function createRecord(): int { + $this->info('Creating new record...'); + // TODO: Implement create logic + $this->success('✅ Record created successfully!'); + return 0; + } + + private function showRecord(): int { + $this->info('Showing record...'); + // TODO: Implement show logic + return 0; + } + + private function updateRecord(): int { + $this->info('Updating record...'); + // TODO: Implement update logic + $this->success('✅ Record updated successfully!'); + return 0; + } + + private function deleteRecord(): int { + $this->info('Deleting record...'); + // TODO: Implement delete logic + $this->success('✅ Record deleted successfully!'); + return 0; + } + + private function listRecords(): int { + $this->info('Listing records...'); + // TODO: Implement list logic + return 0; + } +} diff --git a/WebFiori/Cli/Templates/stubs/interactive.stub b/WebFiori/Cli/Templates/stubs/interactive.stub new file mode 100644 index 0000000..22cb7b2 --- /dev/null +++ b/WebFiori/Cli/Templates/stubs/interactive.stub @@ -0,0 +1,43 @@ +println('🔧 Interactive Command Setup'); + $this->println('=========================='); + + // Get user input + $name = $this->getInput('Enter name: '); + $email = $this->getInput('Enter email: ', null, new InputValidator(function($email) { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + }, 'Please enter a valid email address!')); + + // Confirm action + if ($this->confirm('Proceed with the operation?')) { + $this->processData($name, $email); + $this->success('✅ Operation completed successfully!'); + } else { + $this->info('Operation cancelled.'); + } + + return 0; + } + + /** + * Process the collected data. + */ + private function processData(string $name, string $email): void { + // TODO: Implement data processing logic + $this->info("Processing data for: $name ($email)"); + } +} diff --git a/examples/01-basic-hello-world/README.md b/examples/01-basic-hello-world/README.md index 1a03ffa..fed2549 100644 --- a/examples/01-basic-hello-world/README.md +++ b/examples/01-basic-hello-world/README.md @@ -128,3 +128,18 @@ class HelloCommand extends Command { ``` This example serves as the foundation for understanding WebFiori CLI basics before moving to more advanced features. + +## Related Examples + +### Next Steps +- **[02-arguments-and-options](../02-arguments-and-options/)** - Learn advanced argument handling and validation +- **[03-user-input](../03-user-input/)** - Add interactive user input to your commands +- **[04-output-formatting](../04-output-formatting/)** - Enhance output with colors and formatting + +### Advanced Features +- **[10-multi-command-app](../10-multi-command-app/)** - Build complete CLI applications +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands automatically + +### Similar Concepts +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive command workflows +- **[11-masked-input](../11-masked-input/)** - Secure input handling diff --git a/examples/02-arguments-and-options/README.md b/examples/02-arguments-and-options/README.md index 0c3b8a9..c7358a1 100644 --- a/examples/02-arguments-and-options/README.md +++ b/examples/02-arguments-and-options/README.md @@ -350,3 +350,22 @@ class UserProfileCommand extends Command { ``` This example demonstrates advanced CLI application development with proper validation, error handling, and user experience design. + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Start here for basic command concepts + +### Next Steps +- **[03-user-input](../03-user-input/)** - Interactive input and validation +- **[11-masked-input](../11-masked-input/)** - Secure input for sensitive data +- **[04-output-formatting](../04-output-formatting/)** - Enhanced output styling + +### Advanced Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications +- **[09-database-ops](../09-database-ops/)** - Database operations with validation +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with arguments + +### Similar Concepts +- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interfaces +- **[08-file-processing](../08-file-processing/)** - File operations with validation diff --git a/examples/03-user-input/README.md b/examples/03-user-input/README.md index 72a9cad..b30caad 100644 --- a/examples/03-user-input/README.md +++ b/examples/03-user-input/README.md @@ -373,3 +373,25 @@ private function collectBasicInfo() { - **User Experience**: Rich formatting with emojis and clear section divisions This example demonstrates advanced user input handling suitable for complex CLI applications requiring data collection and validation. + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Argument handling and validation + +### Enhanced Input Methods +- **[11-masked-input](../11-masked-input/)** - Secure input for passwords and sensitive data +- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interactive workflows + +### Output Enhancement +- **[04-output-formatting](../04-output-formatting/)** - Colors, styles, and formatting +- **[06-table-display](../06-table-display/)** - Structured data presentation +- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full CLI applications with user management +- **[09-database-ops](../09-database-ops/)** - Database operations with user input + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate interactive commands automatically diff --git a/examples/04-output-formatting/README.md b/examples/04-output-formatting/README.md index b57f0c7..a9038e1 100644 --- a/examples/04-output-formatting/README.md +++ b/examples/04-output-formatting/README.md @@ -562,3 +562,25 @@ private function showSpinnerAnimation(): void { ``` This example demonstrates professional CLI output formatting suitable for creating visually appealing and user-friendly command-line applications. + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command and output concepts + +### Enhanced Output Features +- **[06-table-display](../06-table-display/)** - Structured data in formatted tables +- **[07-progress-bars](../07-progress-bars/)** - Professional progress indicators +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus with formatting + +### Input with Formatting +- **[03-user-input](../03-user-input/)** - User input with formatted prompts +- **[11-masked-input](../11-masked-input/)** - Secure input with visual feedback + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with consistent formatting +- **[09-database-ops](../09-database-ops/)** - Database operations with formatted output +- **[08-file-processing](../08-file-processing/)** - File operations with status formatting + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with formatting templates diff --git a/examples/05-interactive-commands/README.md b/examples/05-interactive-commands/README.md index a41aef9..bce5db7 100644 --- a/examples/05-interactive-commands/README.md +++ b/examples/05-interactive-commands/README.md @@ -316,3 +316,26 @@ private function showContextHelp(): void { - **[03-user-input](../03-user-input/)**: Input validation and handling - **[04-output-formatting](../04-output-formatting/)**: ANSI colors and formatting +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[03-user-input](../03-user-input/)** - User input and validation fundamentals +- **[04-output-formatting](../04-output-formatting/)** - ANSI colors and formatting + +### Enhanced Interactive Features +- **[11-masked-input](../11-masked-input/)** - Secure input for sensitive operations +- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments and options + +### Visual Enhancements +- **[06-table-display](../06-table-display/)** - Display data in formatted tables +- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full CLI applications with menus +- **[09-database-ops](../09-database-ops/)** - Database management with interactive menus +- **[08-file-processing](../08-file-processing/)** - File operations with user interaction + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate interactive commands automatically + diff --git a/examples/06-table-display/README.md b/examples/06-table-display/README.md index 14c6b63..b7d2cce 100644 --- a/examples/06-table-display/README.md +++ b/examples/06-table-display/README.md @@ -298,3 +298,25 @@ php main.php table-demo --help - **[05-interactive-commands](../05-interactive-commands/)** - Interactive menu systems - **[08-file-processing](../08-file-processing/)** - File data processing - **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications + +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting basics + +### Enhanced Display Features +- **[07-progress-bars](../07-progress-bars/)** - Visual progress indicators +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus with tables + +### Data Sources +- **[08-file-processing](../08-file-processing/)** - Process files and display results in tables +- **[09-database-ops](../09-database-ops/)** - Database queries with table output +- **[03-user-input](../03-user-input/)** - Collect data and display in tables + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with data display +- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with formatted output + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with table display diff --git a/examples/07-progress-bars/README.md b/examples/07-progress-bars/README.md index 72769e2..1b3b6a4 100644 --- a/examples/07-progress-bars/README.md +++ b/examples/07-progress-bars/README.md @@ -302,3 +302,24 @@ php main.php progress-demo --style=custom --items=20 --delay=50 - **[06-table-display](../06-table-display/)** - Data presentation techniques - **[08-file-processing](../08-file-processing/)** - File operations with progress - **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting + +### Visual Enhancement +- **[06-table-display](../06-table-display/)** - Structured data display +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive workflows with progress + +### Long-Running Operations +- **[08-file-processing](../08-file-processing/)** - File operations with progress tracking +- **[09-database-ops](../09-database-ops/)** - Database operations with progress indicators +- **[03-user-input](../03-user-input/)** - Multi-step processes with progress + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with progress feedback +- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with progress reporting + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with progress bars diff --git a/examples/08-file-processing/README.md b/examples/08-file-processing/README.md index c530980..d3b0f60 100644 --- a/examples/08-file-processing/README.md +++ b/examples/08-file-processing/README.md @@ -274,3 +274,25 @@ class FileProcessCommand extends Command { - **[04-output-formatting](../04-output-formatting/)** - Text formatting and colors - **[07-progress-bars](../07-progress-bars/)** - Progress tracking for file operations - **[10-multi-command-app](../10-multi-command-app/)** - Complete CLI applications +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - File path arguments and validation + +### Enhanced Processing +- **[07-progress-bars](../07-progress-bars/)** - Visual progress for file operations +- **[06-table-display](../06-table-display/)** - Display file data in formatted tables +- **[04-output-formatting](../04-output-formatting/)** - Formatted status messages + +### User Interaction +- **[03-user-input](../03-user-input/)** - Interactive file selection +- **[05-interactive-commands](../05-interactive-commands/)** - File operation menus +- **[11-masked-input](../11-masked-input/)** - Secure file path input + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with file management +- **[09-database-ops](../09-database-ops/)** - Database import/export from files + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate file processing commands diff --git a/examples/09-database-ops/README.md b/examples/09-database-ops/README.md index 6788a8c..a33f8b5 100644 --- a/examples/09-database-ops/README.md +++ b/examples/09-database-ops/README.md @@ -599,3 +599,25 @@ class QueryOptimizer { } } ``` +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Database connection arguments + +### Enhanced Features +- **[06-table-display](../06-table-display/)** - Display query results in tables +- **[07-progress-bars](../07-progress-bars/)** - Progress for long database operations +- **[05-interactive-commands](../05-interactive-commands/)** - Database management menus + +### User Interaction +- **[03-user-input](../03-user-input/)** - Interactive database configuration +- **[11-masked-input](../11-masked-input/)** - Secure database password input +- **[04-output-formatting](../04-output-formatting/)** - Formatted database status + +### Data Processing +- **[08-file-processing](../08-file-processing/)** - Import/export database data +- **[10-multi-command-app](../10-multi-command-app/)** - Complete database CLI applications + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate database operation commands diff --git a/examples/10-multi-command-app/README.md b/examples/10-multi-command-app/README.md index 7b1facd..afb40ad 100644 --- a/examples/10-multi-command-app/README.md +++ b/examples/10-multi-command-app/README.md @@ -713,3 +713,23 @@ class ApiClient { } } ``` +## Related Examples + +### Building Blocks (Start Here) +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments and validation +- **[03-user-input](../03-user-input/)** - User input and interaction + +### Feature Integration +- **[04-output-formatting](../04-output-formatting/)** - Professional output styling +- **[05-interactive-commands](../05-interactive-commands/)** - Menu-driven interfaces +- **[06-table-display](../06-table-display/)** - Data presentation in tables +- **[07-progress-bars](../07-progress-bars/)** - Visual progress feedback +- **[11-masked-input](../11-masked-input/)** - Secure input handling + +### Specialized Operations +- **[08-file-processing](../08-file-processing/)** - File management commands +- **[09-database-ops](../09-database-ops/)** - Database operation commands + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands for your application diff --git a/examples/11-masked-input/README.md b/examples/11-masked-input/README.md new file mode 100644 index 0000000..92bf0cb --- /dev/null +++ b/examples/11-masked-input/README.md @@ -0,0 +1,187 @@ +# Masked Input Example + +This example demonstrates the **masked input functionality** in WebFiori CLI, which allows secure entry of sensitive data like passwords, PINs, and tokens. + +## Features Demonstrated + +- **Basic Password Input**: Default asterisk (*) masking with validation +- **Custom Mask Characters**: Use different characters (•, #, X, -) for masking +- **Input Validation**: Enforce security requirements and format validation +- **Default Values**: Optional default values for sensitive fields +- **Confirmation Prompts**: Verify critical inputs by asking twice + +## Running the Example + +### Basic Usage +```bash +php main.php secure-input +``` + +### Run Specific Demos +```bash +# Password demo only +php main.php secure-input --demo=password + +# PIN demo with custom mask +php main.php secure-input --demo=pin + +# Token demo with default value +php main.php secure-input --demo=token + +# All demos (default) +php main.php secure-input --demo=all +``` + +## Code Examples + +### Basic Masked Input +```php +// Simple password input with default * masking +$password = $this->getMaskedInput('Enter password: '); +``` + +### Custom Mask Character +```php +// Use # characters for PIN masking +$pin = $this->getMaskedInput('Enter PIN: ', null, null, '#'); +``` + +### With Validation +```php +$validator = new InputValidator(function($password) { + return strlen($password) >= 8 && + preg_match('/[A-Z]/', $password) && + preg_match('/[0-9]/', $password); +}, 'Password must be 8+ chars with uppercase and number!'); + +$password = $this->getMaskedInput('Password: ', null, $validator); +``` + +### With Default Value +```php +// Provide a default token value +$token = $this->getMaskedInput('API Token: ', 'default-token', null, '•'); +``` + +## Method Signature + +```php +public function getMaskedInput( + string $prompt, // The prompt to display + ?string $default = null, // Optional default value + ?InputValidator $validator = null, // Optional input validator + string $mask = '*' // Mask character (default: *) +): ?string +``` + +## Security Features + +### Input Masking +- Characters are masked as you type +- Only mask characters are displayed in terminal +- Actual input is captured securely +- Supports backspace for corrections + +### Validation Support +- Enforce minimum length requirements +- Validate character patterns (uppercase, numbers, symbols) +- Custom validation logic +- Automatic retry on validation failure + +### Safe Handling +- Input is trimmed automatically +- Empty prompts return null safely +- Works with existing stream abstraction +- Compatible with testing framework + +## Use Cases + +### 1. User Authentication +```php +$password = $this->getMaskedInput('Login Password: '); +$confirmPassword = $this->getMaskedInput('Confirm Password: '); + +if ($password !== $confirmPassword) { + $this->error('Passwords do not match!'); + return 1; +} +``` + +### 2. API Configuration +```php +$apiKey = $this->getMaskedInput('API Key: ', null, null, '•'); +$secret = $this->getMaskedInput('API Secret: ', null, null, '-'); +``` + +### 3. Database Setup +```php +$dbPassword = $this->getMaskedInput('Database Password: '); + +$validator = new InputValidator(function($host) { + return filter_var($host, FILTER_VALIDATE_IP) || + filter_var($host, FILTER_VALIDATE_DOMAIN); +}, 'Invalid host format!'); + +$dbHost = $this->getInput('Database Host: ', 'localhost', $validator); +``` + +### 4. Secure Token Entry +```php +$jwtSecret = $this->getMaskedInput('JWT Secret: ', null, + new InputValidator(function($secret) { + return strlen($secret) >= 32; + }, 'JWT secret must be at least 32 characters!') +); +``` + +## Interactive Demo Features + +The example includes several interactive demonstrations: + +1. **Password Demo**: Shows validation with security requirements +2. **PIN Demo**: Demonstrates custom mask characters (#) +3. **Token Demo**: Shows default values with bullet (•) masking +4. **Advanced Demo**: Multiple scenarios including confirmation prompts + +## Testing + +The masked input functionality is fully testable using the existing `CommandTestCase` framework: + +```php +$output = $this->executeSingleCommand($command, [], ['secret123']); +$this->assertContains('Password received: secret123', $output); +``` + +## Best Practices + +1. **Always validate sensitive input** for security requirements +2. **Use appropriate mask characters** for different data types +3. **Implement confirmation prompts** for critical operations +4. **Never log or display** the actual sensitive values +5. **Provide clear error messages** for validation failures + +--- + +**Ready to secure your CLI applications?** Try the different demo modes to see masked input in action! +## Related Examples + +### Prerequisites +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic command structure +- **[03-user-input](../03-user-input/)** - User input fundamentals + +### Enhanced Input Methods +- **[02-arguments-and-options](../02-arguments-and-options/)** - Command arguments with validation +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive menus and workflows + +### Visual Enhancement +- **[04-output-formatting](../04-output-formatting/)** - Colors and formatting for prompts +- **[06-table-display](../06-table-display/)** - Display collected data in tables +- **[07-progress-bars](../07-progress-bars/)** - Progress indicators for data processing + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Full applications with secure authentication +- **[09-database-ops](../09-database-ops/)** - Database operations with secure credentials +- **[08-file-processing](../08-file-processing/)** - File operations with secure paths + +### Development Tools +- **[12-command-scaffolding](../12-command-scaffolding/)** - Generate commands with masked input diff --git a/examples/11-masked-input/SecureInputCommand.php b/examples/11-masked-input/SecureInputCommand.php new file mode 100644 index 0000000..b6b6fdb --- /dev/null +++ b/examples/11-masked-input/SecureInputCommand.php @@ -0,0 +1,143 @@ + [ + ArgumentOption::DESCRIPTION => 'Type of demo to run', + ArgumentOption::OPTIONAL => true, + ArgumentOption::VALUES => ['password', 'pin', 'token', 'all'], + ArgumentOption::DEFAULT => 'all' + ] + ], 'Demonstrates secure masked input functionality'); + } + + public function exec(): int { + $demo = $this->getArgValue('--demo') ?? 'all'; + + $this->println('🔒 WebFiori CLI - Masked Input Demo'); + $this->println('==================================='); + $this->println(); + + switch ($demo) { + case 'password': + $this->passwordDemo(); + break; + case 'pin': + $this->pinDemo(); + break; + case 'token': + $this->tokenDemo(); + break; + case 'all': + default: + $this->passwordDemo(); + $this->println(); + $this->pinDemo(); + $this->println(); + $this->tokenDemo(); + $this->println(); + $this->advancedDemo(); + break; + } + + $this->println(); + $this->success('✅ Demo completed successfully!'); + + return 0; + } + + /** + * Demonstrates basic password input with validation. + */ + private function passwordDemo(): void { + $this->info('📝 Password Demo - Basic masked input with validation'); + $this->println('Enter a password (minimum 8 characters):'); + + $validator = new InputValidator(function($password) { + if (strlen($password) < 8) { + return false; + } + if (!preg_match('/[A-Z]/', $password)) { + return false; + } + if (!preg_match('/[0-9]/', $password)) { + return false; + } + return true; + }, 'Password must be at least 8 characters with uppercase letter and number!'); + + $password = $this->getMaskedInput('Password: ', '*', null, $validator); + + $this->success("✅ Password accepted! Length: " . strlen($password)); + $this->println(" Captured value: $password"); + } + + /** + * Demonstrates PIN input with custom mask character. + */ + private function pinDemo(): void { + $this->info('🔢 PIN Demo - Custom mask character'); + $this->println('Enter a 4-digit PIN (will be masked with # characters):'); + + $validator = new InputValidator(function($pin) { + return strlen($pin) === 4 && ctype_digit($pin); + }, 'PIN must be exactly 4 digits!'); + + $pin = $this->getMaskedInput('PIN: ', '#', null, $validator); + + $this->success("✅ PIN accepted!"); + $this->println(" Captured value: $pin"); + } + + /** + * Demonstrates token input with default value. + */ + private function tokenDemo(): void { + $this->info('🎫 Token Demo - With default value'); + $this->println('Enter API token (or press Enter for demo token):'); + + $token = $this->getMaskedInput('API Token: ', '•', 'demo-token-12345'); + + $this->success("✅ Token set!"); + $this->println(" Captured value: $token"); + } + + /** + * Demonstrates advanced scenarios. + */ + private function advancedDemo(): void { + $this->info('🚀 Advanced Demo - Multiple scenarios'); + + // Database password with confirmation + $this->println('Setting up database connection:'); + + $dbPassword = $this->getMaskedInput('Database Password: '); + $confirmPassword = $this->getMaskedInput('Confirm Password: '); + + if ($dbPassword !== $confirmPassword) { + $this->error('❌ Passwords do not match!'); + $this->println(" First: $dbPassword"); + $this->println(" Second: $confirmPassword"); + return; + } + + $this->success('✅ Database password confirmed'); + $this->println(" Captured value: $dbPassword"); + } +} diff --git a/examples/11-masked-input/main.php b/examples/11-masked-input/main.php new file mode 100644 index 0000000..969d23c --- /dev/null +++ b/examples/11-masked-input/main.php @@ -0,0 +1,22 @@ +register(new SecureInputCommand()); + +// Start the application +exit($runner->start()); diff --git a/examples/12-command-scaffolding/README.md b/examples/12-command-scaffolding/README.md new file mode 100644 index 0000000..09d94ca --- /dev/null +++ b/examples/12-command-scaffolding/README.md @@ -0,0 +1,302 @@ +# Command Scaffolding Tools + +This example demonstrates the **command scaffolding functionality** in WebFiori CLI, which allows developers to quickly generate new command classes with proper structure, documentation, and templates. + +## Features + +- **Multiple Templates**: Basic, Interactive, CRUD, and File Processor templates +- **Smart Naming**: Automatic class name generation from command names +- **Namespace Support**: Generate commands with custom namespaces +- **Argument Generation**: Automatically create command arguments +- **Validation**: Input validation for command and class names +- **Overwrite Protection**: Confirmation prompts for existing files + +## Running the Example + +### Basic Command Generation +```bash +# Generate a basic command +php main.php make:command --name=hello-world + +# Generate with custom class name +php main.php make:command --name=user:create --class=CreateUserCommand + +# Generate with namespace +php main.php make:command --name=process-data --namespace="App\\Commands" +``` + +### Template-Based Generation +```bash +# Interactive command template +php main.php make:command --name=setup-wizard --template=interactive + +# CRUD operations template +php main.php make:command --name=user-manager --template=crud + +# File processor template +php main.php make:command --name=file-converter --template=file-processor +``` + +### Advanced Options +```bash +# Generate with arguments +php main.php make:command --name=backup-db --args="database,output-path,compress" + +# Custom output directory +php main.php make:command --name=deploy --path=src/Commands + +# All options combined +php main.php make:command \ + --name=api:sync \ + --class=ApiSyncCommand \ + --namespace="MyApp\\Commands" \ + --template=interactive \ + --args="endpoint,token,timeout" \ + --path=app/Commands +``` + +## Available Templates + +### 1. Basic Template +Simple command structure with minimal boilerplate: +```php +class HelloWorldCommand extends Command { + public function __construct() { + parent::__construct('hello-world', [], 'Description'); + } + + public function exec(): int { + $this->println('🚀 Executing hello-world command...'); + // TODO: Implement your command logic here + $this->success('✅ Command completed successfully!'); + return 0; + } +} +``` + +### 2. Interactive Template +Command with user input and validation: +```php +public function exec(): int { + $name = $this->getInput('Enter name: '); + $email = $this->getInput('Enter email: ', null, new InputValidator(function($email) { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + }, 'Please enter a valid email address!')); + + if ($this->confirm('Proceed with the operation?')) { + $this->processData($name, $email); + $this->success('✅ Operation completed successfully!'); + } + + return 0; +} +``` + +### 3. CRUD Template +Full CRUD operations structure: +```php +public function exec(): int { + $action = $this->getArgValue('--action') ?? 'list'; + + switch ($action) { + case 'create': return $this->createRecord(); + case 'read': return $this->showRecord(); + case 'update': return $this->updateRecord(); + case 'delete': return $this->deleteRecord(); + case 'list': + default: return $this->listRecords(); + } +} +``` + +### 4. File Processor Template +File processing with error handling: +```php +public function exec(): int { + $inputFile = $this->getArgValue('--input'); + + if (!$inputFile || !file_exists($inputFile)) { + $this->error('Input file is required and must exist!'); + return 1; + } + + try { + $this->processFile($inputFile, $this->getArgValue('--output')); + $this->success('✅ File processed successfully!'); + return 0; + } catch (\Exception $e) { + $this->error('❌ Error: ' . $e->getMessage()); + return 1; + } +} +``` + +## Generated Command Structure + +All generated commands include: + +### Proper Documentation +```php +/** + * CommandName - Generated CLI command. + * + * Description for command-name command + */ +class CommandNameCommand extends Command { +``` + +### Constructor with Arguments +```php +public function __construct() { + parent::__construct('command-name', [ + '--input-file' => [ + ArgumentOption::DESCRIPTION => 'Description for input file', + ArgumentOption::OPTIONAL => true + ] + ], 'Command description'); +} +``` + +### Structured Exec Method +```php +public function exec(): int { + // Template-specific implementation + return 0; +} +``` + +### Additional Helper Methods +Template-specific helper methods for common operations. + +## Smart Naming Conventions + +The scaffolding tool automatically converts command names to proper class names: + +| Command Name | Generated Class Name | +|--------------|---------------------| +| `hello-world` | `HelloWorldCommand` | +| `user:create` | `UserCreateCommand` | +| `api_sync` | `ApiSyncCommand` | +| `process-data-file` | `ProcessDataFileCommand` | + +## Validation Features + +### Command Name Validation +- Must start with a letter +- Can contain lowercase letters, numbers, hyphens, colons, underscores +- Examples: `hello`, `user:create`, `process-data` + +### Class Name Validation +- Must be valid PHP class name (PascalCase) +- Automatically generated if not provided +- Examples: `HelloCommand`, `UserCreateCommand` + +### File Overwrite Protection +```bash +$ php main.php make:command --name=existing-command +File /path/to/ExistingCommand.php already exists. Overwrite? (y/n): n +❌ Failed to generate command: File already exists and overwrite was declined. +``` + +## Integration with Your Application + +After generating commands, integrate them into your application: + +### 1. Register the Command +```php +use App\Commands\GeneratedCommand; + +$runner = new Runner(); +$runner->register(new GeneratedCommand()); +``` + +### 2. Implement Logic +Edit the generated `exec()` method to add your specific functionality. + +### 3. Add Tests +Create unit tests for your generated commands using `CommandTestCase`. + +## Best Practices + +### Naming Conventions +- Use kebab-case for command names: `user-create`, `data-export` +- Use namespaces for organization: `user:create`, `db:migrate` +- Keep names descriptive but concise + +### Template Selection +- **Basic**: Simple commands with minimal logic +- **Interactive**: Commands requiring user input +- **CRUD**: Data management commands +- **File Processor**: File manipulation commands + +### Organization +``` +app/ +├── Commands/ +│ ├── User/ +│ │ ├── CreateUserCommand.php +│ │ └── DeleteUserCommand.php +│ ├── Database/ +│ │ ├── MigrateCommand.php +│ │ └── SeedCommand.php +│ └── File/ +│ ├── ProcessCommand.php +│ └── ConvertCommand.php +``` + +## Example Workflow + +### 1. Generate User Management Commands +```bash +# Create user command +php main.php make:command --name=user:create --template=interactive --namespace="App\\Commands\\User" + +# List users command +php main.php make:command --name=user:list --template=crud --namespace="App\\Commands\\User" + +# Delete user command +php main.php make:command --name=user:delete --namespace="App\\Commands\\User" +``` + +### 2. Generate File Processing Commands +```bash +# CSV processor +php main.php make:command --name=csv:process --template=file-processor --args="input,output,delimiter" + +# Image converter +php main.php make:command --name=image:convert --template=file-processor --args="input,format,quality" +``` + +### 3. Generate API Commands +```bash +# API sync command +php main.php make:command --name=api:sync --template=interactive --args="endpoint,token" + +# Data export command +php main.php make:command --name=data:export --args="format,output,filter" +``` + +--- + +**Ready to boost your development speed?** Use the scaffolding tools to generate well-structured commands in seconds! +## Related Examples + +### Generated Command Examples +- **[01-basic-hello-world](../01-basic-hello-world/)** - Basic template structure +- **[02-arguments-and-options](../02-arguments-and-options/)** - Commands with arguments (CRUD template) +- **[03-user-input](../03-user-input/)** - Interactive commands (Interactive template) +- **[08-file-processing](../08-file-processing/)** - File operations (File Processor template) + +### Enhanced Features for Generated Commands +- **[04-output-formatting](../04-output-formatting/)** - Add formatting to generated commands +- **[05-interactive-commands](../05-interactive-commands/)** - Interactive workflows +- **[06-table-display](../06-table-display/)** - Data display in generated commands +- **[07-progress-bars](../07-progress-bars/)** - Progress indicators +- **[11-masked-input](../11-masked-input/)** - Secure input in generated commands + +### Complete Applications +- **[10-multi-command-app](../10-multi-command-app/)** - Applications built with scaffolded commands +- **[09-database-ops](../09-database-ops/)** - Database commands (perfect for CRUD template) + +### Development Workflow +Use this scaffolding tool to quickly generate commands for any of the above examples! diff --git a/examples/12-command-scaffolding/main.php b/examples/12-command-scaffolding/main.php new file mode 100644 index 0000000..f568443 --- /dev/null +++ b/examples/12-command-scaffolding/main.php @@ -0,0 +1,23 @@ +register(new MakeCommand()); + +// Start the application +exit($runner->start()); diff --git a/examples/README.md b/examples/README.md index f71ef77..62526f6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -17,7 +17,7 @@ Building more sophisticated CLI applications. - **[05-interactive-commands](05-interactive-commands/)** - Creating interactive command experiences - **[07-progress-bars](07-progress-bars/)** - Visual progress indicators for long operations - +- **[11-masked-input](11-masked-input/)** - Secure input with character masking ### 🔴 **Advanced Examples** Complex scenarios and advanced features. @@ -89,7 +89,7 @@ Explore examples 10-13 for real-world applications: |---------|----------|-------------| | **Command Creation** | 01, 02, 10 | Basic to advanced command structures | | **Arguments & Options** | 02, 13 | Parameter handling and validation | -| **User Input** | 03, 05 | Interactive input and validation | +| **User Input** | 03, 05, 11 | Interactive input, validation, and secure entry | | **Output Formatting** | 04, 07 | Colors, styles, and progress bars | | **Interactive Workflows** | 05, 10 | Menu systems and wizards | | **Progress Indicators** | 07, 10, 13 | Visual feedback for operations | diff --git a/tests/WebFiori/Tests/Cli/MakeCommandTest.php b/tests/WebFiori/Tests/Cli/MakeCommandTest.php new file mode 100644 index 0000000..7ce555b --- /dev/null +++ b/tests/WebFiori/Tests/Cli/MakeCommandTest.php @@ -0,0 +1,236 @@ +testOutputDir)) { + $this->removeDirectory($this->testOutputDir); + } + } + + protected function tearDown(): void { + parent::tearDown(); + // Clean up test directory + if (is_dir($this->testOutputDir)) { + $this->removeDirectory($this->testOutputDir); + } + } + + /** + * Test basic command generation. + * + * @test + */ + public function testBasicCommandGeneration() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'test-command', + '--class' => 'TestCommand', + '--path' => $this->testOutputDir + ], ['', 'y']); // Empty namespace, then confirm overwrite if needed + + // Check for success message (flexible matching) + $found = false; + foreach ($output as $line) { + if (strpos($line, 'Command generated successfully!') !== false) { + $found = true; + break; + } + } + $this->assertTrue($found, 'Success message not found in output'); + $this->assertEquals(0, $this->getExitCode()); + + // Check if file was created + $expectedFile = $this->testOutputDir . '/TestCommand.php'; + $this->assertTrue(file_exists($expectedFile), "Command file should be created"); + + // Check file content + $content = file_get_contents($expectedFile); + $this->assertStringContainsString('class TestCommand extends Command', $content); + $this->assertStringContainsString("'test-command'", $content); + } + + /** + * Test command generation with namespace. + * + * @test + */ + public function testCommandGenerationWithNamespace() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'user:create', + '--namespace' => 'App\\Commands', + '--path' => $this->testOutputDir + ]); + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/UserCreateCommand.php'; + $this->assertTrue(file_exists($expectedFile)); + + $content = file_get_contents($expectedFile); + $this->assertStringContainsString('namespace App\\Commands;', $content); + $this->assertStringContainsString('class UserCreateCommand extends Command', $content); + } + + /** + * Test interactive template generation. + * + * @test + */ + public function testInteractiveTemplate() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'setup-wizard', + '--template' => 'interactive', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/SetupWizardCommand.php'; + $content = file_get_contents($expectedFile); + + $this->assertStringContainsString('InputValidator', $content); + $this->assertStringContainsString('getInput(', $content); + $this->assertStringContainsString('confirm(', $content); + } + + /** + * Test CRUD template generation. + * + * @test + */ + public function testCrudTemplate() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'user-manager', + '--template' => 'crud', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/UserManagerCommand.php'; + $content = file_get_contents($expectedFile); + + $this->assertStringContainsString('createRecord()', $content); + $this->assertStringContainsString('updateRecord()', $content); + $this->assertStringContainsString('deleteRecord()', $content); + $this->assertStringContainsString('listRecords()', $content); + } + + /** + * Test command generation with arguments. + * + * @test + */ + public function testCommandWithArguments() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'process-data', + '--args' => 'input file,output format,verbose mode', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(0, $this->getExitCode()); + + $expectedFile = $this->testOutputDir . '/ProcessDataCommand.php'; + $content = file_get_contents($expectedFile); + + $this->assertStringContainsString('--input-file', $content); + $this->assertStringContainsString('--output-format', $content); + $this->assertStringContainsString('--verbose-mode', $content); + } + + /** + * Test invalid command name validation. + * + * @test + */ + public function testInvalidCommandName() { + $command = new MakeCommand(); + + $output = $this->executeSingleCommand($command, [ + '--name' => 'Invalid Command Name!', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + $this->assertEquals(1, $this->getExitCode()); + // Check for validation error message (flexible matching) + $found = false; + foreach ($output as $line) { + if (strpos($line, 'Command name must start with a letter') !== false) { + $found = true; + break; + } + } + $this->assertTrue($found, 'Validation error message not found in output'); + } + + /** + * Test file overwrite confirmation. + * + * @test + */ + public function testFileOverwriteConfirmation() { + $command = new MakeCommand(); + + // Create file first + $this->executeSingleCommand($command, [ + '--name' => 'existing-command', + '--path' => $this->testOutputDir + ], ['']); // Empty namespace input + + // Try to create again with 'no' confirmation + $output = $this->executeSingleCommand($command, [ + '--name' => 'existing-command', + '--path' => $this->testOutputDir + ], ['', 'n']); // Empty namespace, then 'no' to overwrite + + $this->assertEquals(1, $this->getExitCode()); + // Check for overwrite declined message (flexible matching) + $found = false; + foreach ($output as $line) { + if (strpos($line, 'File already exists and overwrite was declined') !== false) { + $found = true; + break; + } + } + $this->assertTrue($found, 'Overwrite declined message not found in output'); + } + + /** + * Helper method to remove directory recursively. + */ + private function removeDirectory(string $dir): void { + if (!is_dir($dir)) { + return; + } + + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +} diff --git a/tests/WebFiori/Tests/Cli/MaskedInputTest.php b/tests/WebFiori/Tests/Cli/MaskedInputTest.php new file mode 100644 index 0000000..6061bf4 --- /dev/null +++ b/tests/WebFiori/Tests/Cli/MaskedInputTest.php @@ -0,0 +1,190 @@ +getMaskedInput('Enter password: '); + $this->println("Password received: $input"); + return 0; + } + }; + + $output = $this->executeSingleCommand($command, [], ['secret123']); + + $this->assertContains("Enter password:\n", $output); + $this->assertContains("Password received: secret123\n", $output); + $this->assertEquals(0, $this->getExitCode()); + } + + /** + * Test masked input with default value. + * + * @test + */ + public function testMaskedInputWithDefault() { + $command = new class extends \WebFiori\Cli\Command { + public function __construct() { + parent::__construct('test-masked-default'); + } + + public function exec(): int { + $input = $this->getMaskedInput('Enter token: ', '*', 'default-token'); + $this->println("Token: $input"); + return 0; + } + }; + + // Test with empty input (should use default) + $output = $this->executeSingleCommand($command, [], ['']); + + $this->assertContains("Enter token: Enter = 'default-token'\n", $output); + $this->assertContains("Token: default-token\n", $output); + } + + /** + * Test masked input with custom mask character. + * + * @test + */ + public function testMaskedInputWithCustomMask() { + $command = new class extends \WebFiori\Cli\Command { + public function __construct() { + parent::__construct('test-custom-mask'); + } + + public function exec(): int { + $input = $this->getMaskedInput('Enter PIN: ', '#'); + $this->println("PIN: $input"); + return 0; + } + }; + + $output = $this->executeSingleCommand($command, [], ['1234']); + + $this->assertContains("Enter PIN:\n", $output); + $this->assertContains("PIN: 1234\n", $output); + } + + /** + * Test masked input with validation. + * + * @test + */ + public function testMaskedInputWithValidation() { + $command = new class extends \WebFiori\Cli\Command { + public function __construct() { + parent::__construct('test-masked-validation'); + } + + public function exec(): int { + $validator = new InputValidator(function($input) { + return strlen($input) >= 8; + }, 'Password must be at least 8 characters long!'); + + $input = $this->getMaskedInput('Enter password: ', '*', null, $validator); + $this->println("Valid password received"); + return 0; + } + }; + + // Test with invalid input first, then valid + $output = $this->executeSingleCommand($command, [], ['short', 'validpassword']); + + $this->assertContains("Error: Password must be at least 8 characters long!\n", $output); + $this->assertContains("Valid password received\n", $output); + } + + /** + * Test masked input with empty prompt. + * + * @test + */ + public function testMaskedInputWithEmptyPrompt() { + $command = new class extends \WebFiori\Cli\Command { + public function __construct() { + parent::__construct('test-empty-prompt'); + } + + public function exec(): int { + $input = $this->getMaskedInput(''); + $result = $input === null ? 'null' : $input; + $this->println("Result: $result"); + return 0; + } + }; + + $output = $this->executeSingleCommand($command); + + $this->assertContains("Result: null\n", $output); + } + + /** + * Test masked input with whitespace handling. + * + * @test + */ + public function testMaskedInputWhitespaceHandling() { + $command = new class extends \WebFiori\Cli\Command { + public function __construct() { + parent::__construct('test-whitespace'); + } + + public function exec(): int { + $input = $this->getMaskedInput('Enter value: '); + $this->println("Value: '$input'"); + return 0; + } + }; + + // Test with leading/trailing spaces + $output = $this->executeSingleCommand($command, [], [' spaced ']); + + $this->assertContains("Value: 'spaced'\n", $output); // Should be trimmed + } + + /** + * Test masked input with special characters. + * + * @test + */ + public function testMaskedInputWithSpecialCharacters() { + $command = new class extends \WebFiori\Cli\Command { + public function __construct() { + parent::__construct('test-special-chars'); + } + + public function exec(): int { + $input = $this->getMaskedInput('Enter complex password: '); + $this->println("Password length: " . strlen($input)); + return 0; + } + }; + + $complexPassword = 'P@ssw0rd!#$%'; + $output = $this->executeSingleCommand($command, [], [$complexPassword]); + + $this->assertContains("Password length: " . strlen($complexPassword) . "\n", $output); + } +}