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:
2026-03-25 12:39:42 +01:00
parent 4f3bc7fb94
commit 4e331d262a
3 changed files with 57 additions and 28 deletions

View File

@ -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

View File

@ -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.

View File

@ -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
{ {