From 4e331d262a7d4a36c8e038ab72b03e7560f46165 Mon Sep 17 00:00:00 2001 From: igor Date: Wed, 25 Mar 2026 12:39:42 +0100 Subject: [PATCH] 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. --- AGENTS.md | 1 + README.md | 3 ++ src/SFTPsync.php | 81 +++++++++++++++++++++++++++++++----------------- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2cbb188..f79b36f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,7 @@ - `2` argument/usage error - Missing `--host`, `--user`, `--password` should still support interactive prompt mode. - `--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. ## Coding Conventions diff --git a/README.md b/README.md index 41002c9..af168b2 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ php src/SFTPsync.php --host --user --password [--port < - `--password `: required (or prompted) - `--port `: optional, default `22` - `--print-relative`: show paths relative to action root in logs +- `--no-print-skip`: suppress `SKIP` status lines during execution - `--skip `: repeatable, applied to `--sync` and `--sync-down` - `--skip-delete `: repeatable, applied to `--delete` and `--delete-dir` - `-h`, `--help`: show help @@ -108,3 +109,5 @@ After upload/download, mtime is propagated to the target when possible. ## Output 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. diff --git a/src/SFTPsync.php b/src/SFTPsync.php index 42fb8c6..2d0653a 100644 --- a/src/SFTPsync.php +++ b/src/SFTPsync.php @@ -150,7 +150,7 @@ final class SftpAdapter } /** - * @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,skip:list,skip_delete:list,actions:list>} + * @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,no_print_skip:bool,skip:list,skip_delete:list,actions:list>} */ function parseArguments(array $argv): array { @@ -161,6 +161,7 @@ function parseArguments(array $argv): array 'password' => '', 'port' => 22, 'print_relative' => false, + 'no_print_skip' => false, 'skip' => [], 'skip_delete' => [], 'actions' => [], @@ -204,6 +205,10 @@ function parseArguments(array $argv): array $parsed['print_relative'] = true; break; + case '--no-print-skip': + $parsed['no_print_skip'] = true; + break; + case '--skip': $skipValue = readCliValue($argv, ++$i, '--skip '); $parsed['skip'][] = normalizeSkipRule($skipValue); @@ -306,6 +311,7 @@ Options: --password Required --port Optional, default 22 --print-relative Show logged paths relative to action local/remote roots + --no-print-skip Do not print SKIP logs during synchronization --skip Repeatable, skip matching names/paths in --sync and --sync-down --skip-delete Repeatable, skip matching names/paths in --delete and --delete-dir -h, --help Show this help @@ -342,7 +348,7 @@ function run(array $config): int foreach ($config['actions'] as $action) { 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) { logErrorAction(describeAction($action) . ': ' . $e->getMessage()); return EXIT_RUNTIME_ERROR; @@ -358,23 +364,24 @@ function runAction( array $action, array $skipRules, array $skipDeleteRules, - bool $printRelative + bool $printRelative, + bool $noPrintSkip ): void { switch ($action['type']) { case 'sync': - syncUp($adapter, $action['local_dir'], $action['remote_dir'], $skipRules, $printRelative); + syncUp($adapter, $action['local_dir'], $action['remote_dir'], $skipRules, $printRelative, $noPrintSkip); return; 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; case 'delete': - deleteRemoteFile($adapter, $action['remote_file'], $skipDeleteRules, $printRelative); + deleteRemoteFile($adapter, $action['remote_file'], $skipDeleteRules, $printRelative, $noPrintSkip); return; case 'delete-dir': - deleteRemoteDir($adapter, $action['remote_dir'], $skipDeleteRules, $printRelative); + deleteRemoteDir($adapter, $action['remote_dir'], $skipDeleteRules, $printRelative, $noPrintSkip); return; default: @@ -387,7 +394,8 @@ function syncUp( string $localDir, string $remoteDir, array $skipRules, - bool $printRelative + bool $printRelative, + bool $noPrintSkip ): void { if (!is_dir($localDir)) { throw new RuntimeException('Local directory does not exist: ' . $localDir); @@ -412,14 +420,14 @@ function syncUp( $directoryIterator = new RecursiveDirectoryIterator($localBase, FilesystemIterator::SKIP_DOTS); $filteredIterator = new RecursiveCallbackFilterIterator( $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()); $relative = ltrim(substr($localPath, $baseLen), '/'); if ($relative === '') { return true; } if (isPathSkipped($relative, $skipRules)) { - logAction('SKIP', formatLocalLogPath($localPath, $localBase, $printRelative) . ' (matches --skip)'); + logSkipAction(formatLocalLogPath($localPath, $localBase, $printRelative) . ' (matches --skip)', $noPrintSkip); return false; } return true; @@ -466,7 +474,7 @@ function syncUp( $remoteDisplayPath = formatRemoteLogPath($remoteFile, $remoteBase, $printRelative); if (!shouldTransfer((int)$localSize, $localMtime, $remoteSize, $remoteMtime, $remoteStat !== null)) { - logAction('SKIP', $localDisplayPath . ' -> ' . $remoteDisplayPath); + logSkipAction($localDisplayPath . ' -> ' . $remoteDisplayPath, $noPrintSkip); continue; } @@ -483,7 +491,8 @@ function syncDown( string $remoteDir, string $localDir, array $skipRules, - bool $printRelative + bool $printRelative, + bool $noPrintSkip ): void { $remoteBase = normalizeRemotePath($remoteDir); if ($remoteBase === '') { @@ -501,7 +510,7 @@ function syncDown( 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( @@ -512,6 +521,7 @@ function syncDownRecursive( string $remoteDisplayBase, string $localDisplayBase, bool $printRelative, + bool $noPrintSkip, string $relativeBase = '' ): void { foreach ($adapter->listDir($remoteDir) as $item) { @@ -520,7 +530,7 @@ function syncDownRecursive( $localPath = rtrim($localDir, DIRECTORY_SEPARATOR . '/\\') . DIRECTORY_SEPARATOR . $item; if (isPathSkipped($relativePath, $skipRules)) { - logAction('SKIP', formatRemoteLogPath($remotePath, $remoteDisplayBase, $printRelative) . ' (matches --skip)'); + logSkipAction(formatRemoteLogPath($remotePath, $remoteDisplayBase, $printRelative) . ' (matches --skip)', $noPrintSkip); continue; } @@ -536,6 +546,7 @@ function syncDownRecursive( $remoteDisplayBase, $localDisplayBase, $printRelative, + $noPrintSkip, $relativePath ); continue; @@ -569,11 +580,11 @@ function syncDownRecursive( } if (!shouldTransfer($remoteSize, $remoteMtime, $localSize, $localMtime, $localExists)) { - logAction( - 'SKIP', + logSkipAction( formatRemoteLogPath($remotePath, $remoteDisplayBase, $printRelative) . ' -> ' - . formatLocalLogPath($localPath, $localDisplayBase, $printRelative) + . formatLocalLogPath($localPath, $localDisplayBase, $printRelative), + $noPrintSkip ); continue; } @@ -600,7 +611,8 @@ function deleteRemoteFile( SftpAdapter $adapter, string $remoteFile, array $skipDeleteRules, - bool $printRelative + bool $printRelative, + bool $noPrintSkip ): void { $remoteFile = normalizeRemotePath($remoteFile); if ($remoteFile === '') { @@ -609,13 +621,13 @@ function deleteRemoteFile( $displayBase = dirnameRemotePath($remoteFile); $displayPath = formatRemoteLogPath($remoteFile, $displayBase, $printRelative); if (isPathSkipped($remoteFile, $skipDeleteRules)) { - logAction('SKIP', $displayPath . ' (matches --skip-delete)'); + logSkipAction($displayPath . ' (matches --skip-delete)', $noPrintSkip); return; } $stat = $adapter->stat($remoteFile); if ($stat === null) { - logAction('SKIP', $displayPath . ' (not found)'); + logSkipAction($displayPath . ' (not found)', $noPrintSkip); return; } if ($adapter->isDir($remoteFile)) { @@ -630,7 +642,8 @@ function deleteRemoteDir( SftpAdapter $adapter, string $remoteDir, array $skipDeleteRules, - bool $printRelative + bool $printRelative, + bool $noPrintSkip ): void { $remoteDir = normalizeRemotePath($remoteDir); if (isDangerousRemoteDir($remoteDir)) { @@ -639,26 +652,26 @@ function deleteRemoteDir( $displayBase = $remoteDir; $displayPath = formatRemoteLogPath($remoteDir, $displayBase, $printRelative); if (isPathSkipped($remoteDir, $skipDeleteRules)) { - logAction('SKIP', $displayPath . ' (matches --skip-delete)'); + logSkipAction($displayPath . ' (matches --skip-delete)', $noPrintSkip); return; } if (!$adapter->exists($remoteDir)) { - logAction('SKIP', $displayPath . ' (not found)'); + logSkipAction($displayPath . ' (not found)', $noPrintSkip); return; } if (!$adapter->isDir($remoteDir)) { 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) { $adapter->removeEmptyDir($remoteDir); logAction('RMDIR', $displayPath); return; } - logAction('SKIP', $displayPath . ' (contains --skip-delete entries)'); + logSkipAction($displayPath . ' (contains --skip-delete entries)', $noPrintSkip); } function deleteRemoteDirRecursive( @@ -667,6 +680,7 @@ function deleteRemoteDirRecursive( array $skipDeleteRules, string $displayBase, bool $printRelative, + bool $noPrintSkip, string $relativeBase = '' ): bool { $canRemoveCurrent = true; @@ -675,7 +689,7 @@ function deleteRemoteDirRecursive( $child = joinRemotePath($remoteDir, $item); $childDisplay = formatRemoteLogPath($child, $displayBase, $printRelative); if (isPathSkipped($relativePath, $skipDeleteRules)) { - logAction('SKIP', $childDisplay . ' (matches --skip-delete)'); + logSkipAction($childDisplay . ' (matches --skip-delete)', $noPrintSkip); $canRemoveCurrent = false; continue; } @@ -687,6 +701,7 @@ function deleteRemoteDirRecursive( $skipDeleteRules, $displayBase, $printRelative, + $noPrintSkip, $relativePath ); if ($canRemoveChild) { @@ -948,6 +963,16 @@ function logAction(string $status, string $message): void 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 { 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,skip_delete:list,actions:list>} $config - * @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,skip:list,skip_delete:list,actions:list>} + * @param array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,no_print_skip:bool,skip:list,skip_delete:list,actions:list>} $config + * @return array{help:bool,host:string,user:string,password:string,port:int,print_relative:bool,no_print_skip:bool,skip:list,skip_delete:list,actions:list>} */ function askForMissingRequiredOptions(array $config): array {