Add --no-print-skip option for suppressing SKIP log output
Introduce a new CLI flag --no-print-skip to hide SKIP status lines during sync/delete operations while preserving existing skip behavior and summary counters. Update help output, README, and AGENTS.md to document the new option.
This commit is contained in:
@ -18,6 +18,7 @@
|
|||||||
- `2` argument/usage error
|
- `2` argument/usage error
|
||||||
- Missing `--host`, `--user`, `--password` should still support interactive prompt mode.
|
- Missing `--host`, `--user`, `--password` should still support interactive prompt mode.
|
||||||
- `--skip` and `--skip-delete` matching semantics should remain stable.
|
- `--skip` and `--skip-delete` matching semantics should remain stable.
|
||||||
|
- `--no-print-skip` must suppress only `SKIP` log lines, without changing skip decisions or summary counters.
|
||||||
- `--delete-dir` safety guard against dangerous paths (`/`, empty path, dot paths) must remain intact.
|
- `--delete-dir` safety guard against dangerous paths (`/`, empty path, dot paths) must remain intact.
|
||||||
|
|
||||||
## Coding Conventions
|
## Coding Conventions
|
||||||
|
|||||||
@ -47,6 +47,7 @@ php src/SFTPsync.php --host <host> --user <user> --password <password> [--port <
|
|||||||
- `--password <password>`: required (or prompted)
|
- `--password <password>`: required (or prompted)
|
||||||
- `--port <port>`: optional, default `22`
|
- `--port <port>`: optional, default `22`
|
||||||
- `--print-relative`: show paths relative to action root in logs
|
- `--print-relative`: show paths relative to action root in logs
|
||||||
|
- `--no-print-skip`: suppress `SKIP` status lines during execution
|
||||||
- `--skip <file_or_dir>`: repeatable, applied to `--sync` and `--sync-down`
|
- `--skip <file_or_dir>`: repeatable, applied to `--sync` and `--sync-down`
|
||||||
- `--skip-delete <file_or_dir>`: repeatable, applied to `--delete` and `--delete-dir`
|
- `--skip-delete <file_or_dir>`: repeatable, applied to `--delete` and `--delete-dir`
|
||||||
- `-h`, `--help`: show help
|
- `-h`, `--help`: show help
|
||||||
@ -108,3 +109,5 @@ After upload/download, mtime is propagated to the target when possible.
|
|||||||
## Output
|
## Output
|
||||||
|
|
||||||
The script prints status lines (`MKDIR`, `UPLOAD`, `DOWNLOAD`, `DELETE`, `RMDIR`, `SKIP`, `ERROR`) and a final summary with operation counters.
|
The script prints status lines (`MKDIR`, `UPLOAD`, `DOWNLOAD`, `DELETE`, `RMDIR`, `SKIP`, `ERROR`) and a final summary with operation counters.
|
||||||
|
|
||||||
|
Use `--no-print-skip` if you want to keep skipped-item accounting in the final summary but hide individual `SKIP` log lines during the run.
|
||||||
|
|||||||
@ -150,7 +150,7 @@ final class SftpAdapter
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,skip:list<string>,skip_delete:list<string>,actions:list<array<string,string>>}
|
* @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,no_print_skip:bool,skip:list<string>,skip_delete:list<string>,actions:list<array<string,string>>}
|
||||||
*/
|
*/
|
||||||
function parseArguments(array $argv): array
|
function parseArguments(array $argv): array
|
||||||
{
|
{
|
||||||
@ -161,6 +161,7 @@ function parseArguments(array $argv): array
|
|||||||
'password' => '',
|
'password' => '',
|
||||||
'port' => 22,
|
'port' => 22,
|
||||||
'print_relative' => false,
|
'print_relative' => false,
|
||||||
|
'no_print_skip' => false,
|
||||||
'skip' => [],
|
'skip' => [],
|
||||||
'skip_delete' => [],
|
'skip_delete' => [],
|
||||||
'actions' => [],
|
'actions' => [],
|
||||||
@ -204,6 +205,10 @@ function parseArguments(array $argv): array
|
|||||||
$parsed['print_relative'] = true;
|
$parsed['print_relative'] = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case '--no-print-skip':
|
||||||
|
$parsed['no_print_skip'] = true;
|
||||||
|
break;
|
||||||
|
|
||||||
case '--skip':
|
case '--skip':
|
||||||
$skipValue = readCliValue($argv, ++$i, '--skip <file_or_dir>');
|
$skipValue = readCliValue($argv, ++$i, '--skip <file_or_dir>');
|
||||||
$parsed['skip'][] = normalizeSkipRule($skipValue);
|
$parsed['skip'][] = normalizeSkipRule($skipValue);
|
||||||
@ -306,6 +311,7 @@ Options:
|
|||||||
--password <password> Required
|
--password <password> Required
|
||||||
--port <port> Optional, default 22
|
--port <port> Optional, default 22
|
||||||
--print-relative Show logged paths relative to action local/remote roots
|
--print-relative Show logged paths relative to action local/remote roots
|
||||||
|
--no-print-skip Do not print SKIP logs during synchronization
|
||||||
--skip <file_or_dir> Repeatable, skip matching names/paths in --sync and --sync-down
|
--skip <file_or_dir> Repeatable, skip matching names/paths in --sync and --sync-down
|
||||||
--skip-delete <file_or_dir> Repeatable, skip matching names/paths in --delete and --delete-dir
|
--skip-delete <file_or_dir> Repeatable, skip matching names/paths in --delete and --delete-dir
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
@ -342,7 +348,7 @@ function run(array $config): int
|
|||||||
|
|
||||||
foreach ($config['actions'] as $action) {
|
foreach ($config['actions'] as $action) {
|
||||||
try {
|
try {
|
||||||
runAction($adapter, $action, $config['skip'], $config['skip_delete'], $config['print_relative']);
|
runAction($adapter, $action, $config['skip'], $config['skip_delete'], $config['print_relative'], $config['no_print_skip']);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
logErrorAction(describeAction($action) . ': ' . $e->getMessage());
|
logErrorAction(describeAction($action) . ': ' . $e->getMessage());
|
||||||
return EXIT_RUNTIME_ERROR;
|
return EXIT_RUNTIME_ERROR;
|
||||||
@ -358,23 +364,24 @@ function runAction(
|
|||||||
array $action,
|
array $action,
|
||||||
array $skipRules,
|
array $skipRules,
|
||||||
array $skipDeleteRules,
|
array $skipDeleteRules,
|
||||||
bool $printRelative
|
bool $printRelative,
|
||||||
|
bool $noPrintSkip
|
||||||
): void {
|
): void {
|
||||||
switch ($action['type']) {
|
switch ($action['type']) {
|
||||||
case 'sync':
|
case 'sync':
|
||||||
syncUp($adapter, $action['local_dir'], $action['remote_dir'], $skipRules, $printRelative);
|
syncUp($adapter, $action['local_dir'], $action['remote_dir'], $skipRules, $printRelative, $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case 'sync-down':
|
case 'sync-down':
|
||||||
syncDown($adapter, $action['remote_dir'], $action['local_dir'], $skipRules, $printRelative);
|
syncDown($adapter, $action['remote_dir'], $action['local_dir'], $skipRules, $printRelative, $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deleteRemoteFile($adapter, $action['remote_file'], $skipDeleteRules, $printRelative);
|
deleteRemoteFile($adapter, $action['remote_file'], $skipDeleteRules, $printRelative, $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case 'delete-dir':
|
case 'delete-dir':
|
||||||
deleteRemoteDir($adapter, $action['remote_dir'], $skipDeleteRules, $printRelative);
|
deleteRemoteDir($adapter, $action['remote_dir'], $skipDeleteRules, $printRelative, $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -387,7 +394,8 @@ function syncUp(
|
|||||||
string $localDir,
|
string $localDir,
|
||||||
string $remoteDir,
|
string $remoteDir,
|
||||||
array $skipRules,
|
array $skipRules,
|
||||||
bool $printRelative
|
bool $printRelative,
|
||||||
|
bool $noPrintSkip
|
||||||
): void {
|
): void {
|
||||||
if (!is_dir($localDir)) {
|
if (!is_dir($localDir)) {
|
||||||
throw new RuntimeException('Local directory does not exist: ' . $localDir);
|
throw new RuntimeException('Local directory does not exist: ' . $localDir);
|
||||||
@ -412,14 +420,14 @@ function syncUp(
|
|||||||
$directoryIterator = new RecursiveDirectoryIterator($localBase, FilesystemIterator::SKIP_DOTS);
|
$directoryIterator = new RecursiveDirectoryIterator($localBase, FilesystemIterator::SKIP_DOTS);
|
||||||
$filteredIterator = new RecursiveCallbackFilterIterator(
|
$filteredIterator = new RecursiveCallbackFilterIterator(
|
||||||
$directoryIterator,
|
$directoryIterator,
|
||||||
static function (SplFileInfo $current, mixed $key, $iterator) use ($normalizedLocalBase, $baseLen, $skipRules, $localBase, $printRelative): bool {
|
static function (SplFileInfo $current, mixed $key, $iterator) use ($normalizedLocalBase, $baseLen, $skipRules, $localBase, $printRelative, $noPrintSkip): bool {
|
||||||
$localPath = normalizeLocalPath($current->getPathname());
|
$localPath = normalizeLocalPath($current->getPathname());
|
||||||
$relative = ltrim(substr($localPath, $baseLen), '/');
|
$relative = ltrim(substr($localPath, $baseLen), '/');
|
||||||
if ($relative === '') {
|
if ($relative === '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (isPathSkipped($relative, $skipRules)) {
|
if (isPathSkipped($relative, $skipRules)) {
|
||||||
logAction('SKIP', formatLocalLogPath($localPath, $localBase, $printRelative) . ' (matches --skip)');
|
logSkipAction(formatLocalLogPath($localPath, $localBase, $printRelative) . ' (matches --skip)', $noPrintSkip);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -466,7 +474,7 @@ function syncUp(
|
|||||||
$remoteDisplayPath = formatRemoteLogPath($remoteFile, $remoteBase, $printRelative);
|
$remoteDisplayPath = formatRemoteLogPath($remoteFile, $remoteBase, $printRelative);
|
||||||
|
|
||||||
if (!shouldTransfer((int)$localSize, $localMtime, $remoteSize, $remoteMtime, $remoteStat !== null)) {
|
if (!shouldTransfer((int)$localSize, $localMtime, $remoteSize, $remoteMtime, $remoteStat !== null)) {
|
||||||
logAction('SKIP', $localDisplayPath . ' -> ' . $remoteDisplayPath);
|
logSkipAction($localDisplayPath . ' -> ' . $remoteDisplayPath, $noPrintSkip);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,7 +491,8 @@ function syncDown(
|
|||||||
string $remoteDir,
|
string $remoteDir,
|
||||||
string $localDir,
|
string $localDir,
|
||||||
array $skipRules,
|
array $skipRules,
|
||||||
bool $printRelative
|
bool $printRelative,
|
||||||
|
bool $noPrintSkip
|
||||||
): void {
|
): void {
|
||||||
$remoteBase = normalizeRemotePath($remoteDir);
|
$remoteBase = normalizeRemotePath($remoteDir);
|
||||||
if ($remoteBase === '') {
|
if ($remoteBase === '') {
|
||||||
@ -501,7 +510,7 @@ function syncDown(
|
|||||||
logAction('MKDIR', formatLocalLogPath($createdPath, $localBase, $printRelative));
|
logAction('MKDIR', formatLocalLogPath($createdPath, $localBase, $printRelative));
|
||||||
});
|
});
|
||||||
|
|
||||||
syncDownRecursive($adapter, $remoteBase, $localDir, $skipRules, $remoteBase, $localBase, $printRelative);
|
syncDownRecursive($adapter, $remoteBase, $localDir, $skipRules, $remoteBase, $localBase, $printRelative, $noPrintSkip);
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncDownRecursive(
|
function syncDownRecursive(
|
||||||
@ -512,6 +521,7 @@ function syncDownRecursive(
|
|||||||
string $remoteDisplayBase,
|
string $remoteDisplayBase,
|
||||||
string $localDisplayBase,
|
string $localDisplayBase,
|
||||||
bool $printRelative,
|
bool $printRelative,
|
||||||
|
bool $noPrintSkip,
|
||||||
string $relativeBase = ''
|
string $relativeBase = ''
|
||||||
): void {
|
): void {
|
||||||
foreach ($adapter->listDir($remoteDir) as $item) {
|
foreach ($adapter->listDir($remoteDir) as $item) {
|
||||||
@ -520,7 +530,7 @@ function syncDownRecursive(
|
|||||||
$localPath = rtrim($localDir, DIRECTORY_SEPARATOR . '/\\') . DIRECTORY_SEPARATOR . $item;
|
$localPath = rtrim($localDir, DIRECTORY_SEPARATOR . '/\\') . DIRECTORY_SEPARATOR . $item;
|
||||||
|
|
||||||
if (isPathSkipped($relativePath, $skipRules)) {
|
if (isPathSkipped($relativePath, $skipRules)) {
|
||||||
logAction('SKIP', formatRemoteLogPath($remotePath, $remoteDisplayBase, $printRelative) . ' (matches --skip)');
|
logSkipAction(formatRemoteLogPath($remotePath, $remoteDisplayBase, $printRelative) . ' (matches --skip)', $noPrintSkip);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,6 +546,7 @@ function syncDownRecursive(
|
|||||||
$remoteDisplayBase,
|
$remoteDisplayBase,
|
||||||
$localDisplayBase,
|
$localDisplayBase,
|
||||||
$printRelative,
|
$printRelative,
|
||||||
|
$noPrintSkip,
|
||||||
$relativePath
|
$relativePath
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@ -569,11 +580,11 @@ function syncDownRecursive(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldTransfer($remoteSize, $remoteMtime, $localSize, $localMtime, $localExists)) {
|
if (!shouldTransfer($remoteSize, $remoteMtime, $localSize, $localMtime, $localExists)) {
|
||||||
logAction(
|
logSkipAction(
|
||||||
'SKIP',
|
|
||||||
formatRemoteLogPath($remotePath, $remoteDisplayBase, $printRelative)
|
formatRemoteLogPath($remotePath, $remoteDisplayBase, $printRelative)
|
||||||
. ' -> '
|
. ' -> '
|
||||||
. formatLocalLogPath($localPath, $localDisplayBase, $printRelative)
|
. formatLocalLogPath($localPath, $localDisplayBase, $printRelative),
|
||||||
|
$noPrintSkip
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -600,7 +611,8 @@ function deleteRemoteFile(
|
|||||||
SftpAdapter $adapter,
|
SftpAdapter $adapter,
|
||||||
string $remoteFile,
|
string $remoteFile,
|
||||||
array $skipDeleteRules,
|
array $skipDeleteRules,
|
||||||
bool $printRelative
|
bool $printRelative,
|
||||||
|
bool $noPrintSkip
|
||||||
): void {
|
): void {
|
||||||
$remoteFile = normalizeRemotePath($remoteFile);
|
$remoteFile = normalizeRemotePath($remoteFile);
|
||||||
if ($remoteFile === '') {
|
if ($remoteFile === '') {
|
||||||
@ -609,13 +621,13 @@ function deleteRemoteFile(
|
|||||||
$displayBase = dirnameRemotePath($remoteFile);
|
$displayBase = dirnameRemotePath($remoteFile);
|
||||||
$displayPath = formatRemoteLogPath($remoteFile, $displayBase, $printRelative);
|
$displayPath = formatRemoteLogPath($remoteFile, $displayBase, $printRelative);
|
||||||
if (isPathSkipped($remoteFile, $skipDeleteRules)) {
|
if (isPathSkipped($remoteFile, $skipDeleteRules)) {
|
||||||
logAction('SKIP', $displayPath . ' (matches --skip-delete)');
|
logSkipAction($displayPath . ' (matches --skip-delete)', $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stat = $adapter->stat($remoteFile);
|
$stat = $adapter->stat($remoteFile);
|
||||||
if ($stat === null) {
|
if ($stat === null) {
|
||||||
logAction('SKIP', $displayPath . ' (not found)');
|
logSkipAction($displayPath . ' (not found)', $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($adapter->isDir($remoteFile)) {
|
if ($adapter->isDir($remoteFile)) {
|
||||||
@ -630,7 +642,8 @@ function deleteRemoteDir(
|
|||||||
SftpAdapter $adapter,
|
SftpAdapter $adapter,
|
||||||
string $remoteDir,
|
string $remoteDir,
|
||||||
array $skipDeleteRules,
|
array $skipDeleteRules,
|
||||||
bool $printRelative
|
bool $printRelative,
|
||||||
|
bool $noPrintSkip
|
||||||
): void {
|
): void {
|
||||||
$remoteDir = normalizeRemotePath($remoteDir);
|
$remoteDir = normalizeRemotePath($remoteDir);
|
||||||
if (isDangerousRemoteDir($remoteDir)) {
|
if (isDangerousRemoteDir($remoteDir)) {
|
||||||
@ -639,26 +652,26 @@ function deleteRemoteDir(
|
|||||||
$displayBase = $remoteDir;
|
$displayBase = $remoteDir;
|
||||||
$displayPath = formatRemoteLogPath($remoteDir, $displayBase, $printRelative);
|
$displayPath = formatRemoteLogPath($remoteDir, $displayBase, $printRelative);
|
||||||
if (isPathSkipped($remoteDir, $skipDeleteRules)) {
|
if (isPathSkipped($remoteDir, $skipDeleteRules)) {
|
||||||
logAction('SKIP', $displayPath . ' (matches --skip-delete)');
|
logSkipAction($displayPath . ' (matches --skip-delete)', $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$adapter->exists($remoteDir)) {
|
if (!$adapter->exists($remoteDir)) {
|
||||||
logAction('SKIP', $displayPath . ' (not found)');
|
logSkipAction($displayPath . ' (not found)', $noPrintSkip);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!$adapter->isDir($remoteDir)) {
|
if (!$adapter->isDir($remoteDir)) {
|
||||||
throw new RuntimeException('Remote path is not a directory: ' . $displayPath);
|
throw new RuntimeException('Remote path is not a directory: ' . $displayPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
$canRemoveRoot = deleteRemoteDirRecursive($adapter, $remoteDir, $skipDeleteRules, $displayBase, $printRelative);
|
$canRemoveRoot = deleteRemoteDirRecursive($adapter, $remoteDir, $skipDeleteRules, $displayBase, $printRelative, $noPrintSkip);
|
||||||
if ($canRemoveRoot) {
|
if ($canRemoveRoot) {
|
||||||
$adapter->removeEmptyDir($remoteDir);
|
$adapter->removeEmptyDir($remoteDir);
|
||||||
logAction('RMDIR', $displayPath);
|
logAction('RMDIR', $displayPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logAction('SKIP', $displayPath . ' (contains --skip-delete entries)');
|
logSkipAction($displayPath . ' (contains --skip-delete entries)', $noPrintSkip);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteRemoteDirRecursive(
|
function deleteRemoteDirRecursive(
|
||||||
@ -667,6 +680,7 @@ function deleteRemoteDirRecursive(
|
|||||||
array $skipDeleteRules,
|
array $skipDeleteRules,
|
||||||
string $displayBase,
|
string $displayBase,
|
||||||
bool $printRelative,
|
bool $printRelative,
|
||||||
|
bool $noPrintSkip,
|
||||||
string $relativeBase = ''
|
string $relativeBase = ''
|
||||||
): bool {
|
): bool {
|
||||||
$canRemoveCurrent = true;
|
$canRemoveCurrent = true;
|
||||||
@ -675,7 +689,7 @@ function deleteRemoteDirRecursive(
|
|||||||
$child = joinRemotePath($remoteDir, $item);
|
$child = joinRemotePath($remoteDir, $item);
|
||||||
$childDisplay = formatRemoteLogPath($child, $displayBase, $printRelative);
|
$childDisplay = formatRemoteLogPath($child, $displayBase, $printRelative);
|
||||||
if (isPathSkipped($relativePath, $skipDeleteRules)) {
|
if (isPathSkipped($relativePath, $skipDeleteRules)) {
|
||||||
logAction('SKIP', $childDisplay . ' (matches --skip-delete)');
|
logSkipAction($childDisplay . ' (matches --skip-delete)', $noPrintSkip);
|
||||||
$canRemoveCurrent = false;
|
$canRemoveCurrent = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -687,6 +701,7 @@ function deleteRemoteDirRecursive(
|
|||||||
$skipDeleteRules,
|
$skipDeleteRules,
|
||||||
$displayBase,
|
$displayBase,
|
||||||
$printRelative,
|
$printRelative,
|
||||||
|
$noPrintSkip,
|
||||||
$relativePath
|
$relativePath
|
||||||
);
|
);
|
||||||
if ($canRemoveChild) {
|
if ($canRemoveChild) {
|
||||||
@ -948,6 +963,16 @@ function logAction(string $status, string $message): void
|
|||||||
writeStatusLine(STDOUT, $status, $message);
|
writeStatusLine(STDOUT, $status, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logSkipAction(string $message, bool $noPrintSkip): void
|
||||||
|
{
|
||||||
|
incrementStat('SKIP');
|
||||||
|
if ($noPrintSkip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeStatusLine(STDOUT, 'SKIP', $message);
|
||||||
|
}
|
||||||
|
|
||||||
function logError(string $message): void
|
function logError(string $message): void
|
||||||
{
|
{
|
||||||
incrementStat('ERROR');
|
incrementStat('ERROR');
|
||||||
@ -1096,8 +1121,8 @@ function main(array $argv): int
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,skip:list<string>,skip_delete:list<string>,actions:list<array<string,string>>} $config
|
* @param array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,no_print_skip:bool,skip:list<string>,skip_delete:list<string>,actions:list<array<string,string>>} $config
|
||||||
* @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,skip:list<string>,skip_delete:list<string>,actions:list<array<string,string>>}
|
* @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,no_print_skip:bool,skip:list<string>,skip_delete:list<string>,actions:list<array<string,string>>}
|
||||||
*/
|
*/
|
||||||
function askForMissingRequiredOptions(array $config): array
|
function askForMissingRequiredOptions(array $config): array
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user