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

View File

@ -47,6 +47,7 @@ php src/SFTPsync.php --host <host> --user <user> --password <password> [--port <
- `--password <password>`: required (or prompted)
- `--port <port>`: optional, default `22`
- `--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-delete <file_or_dir>`: 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.

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
{
@ -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 <file_or_dir>');
$parsed['skip'][] = normalizeSkipRule($skipValue);
@ -306,6 +311,7 @@ Options:
--password <password> Required
--port <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 <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
-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<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>>}
* @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,no_print_skip:bool,skip:list<string>,skip_delete:list<string>,actions:list<array<string,string>>}
*/
function askForMissingRequiredOptions(array $config): array
{