diff --git a/src/basename.php b/src/basename.php index e294907..038b516 100644 --- a/src/basename.php +++ b/src/basename.php @@ -1,4 +1,5 @@ []]); [$options, $args, $errors] = coreutilsValidateInput('basename', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('basename'); return $result; } $multiple = ($options['multiple'] ?? false) || array_key_exists('suffix', $options); if (!$args || (!$multiple && count($args) > 2)) { - coreutilsAddError($result, coreutilsError('basename', 'invalid-operands', - $args ? 'expected NAME [SUFFIX]; use -a for multiple names' : 'missing operand'), 2); + coreutilsAddError($result, coreutilsError( + 'basename', + 'invalid-operands', + $args ? 'expected NAME [SUFFIX]; use -a for multiple names' : 'missing operand' + ), 2); return $result; } $suffix = $options['suffix'] ?? ''; - if (!$multiple && count($args) === 2) $suffix = array_pop($args); + if (!$multiple && count($args) === 2) { + $suffix = array_pop($args); + } if (strpos($suffix, "\0") !== false) { - coreutilsAddError($result, coreutilsError('basename', 'invalid-value', - 'suffix must not contain NUL bytes'), 2); + coreutilsAddError($result, coreutilsError( + 'basename', + 'invalid-value', + 'suffix must not contain NUL bytes' + ), 2); return $result; } foreach ($args as $arg) { diff --git a/src/bootstrap.php b/src/bootstrap.php index b001ba5..42daa26 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -1,4 +1,5 @@ [], 'created' => [], 'skipped' => []]); [$options, $args, $errors] = coreutilsValidateInput('cp', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($errors) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($errors) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('cp'); return $result; @@ -80,7 +86,8 @@ function cp(array $input, ?string $cwd = null): array { } /** Separator-aware comparison of canonical paths, including Windows case folding. */ -function coreutilsCopyWithin(string $path, string $directory): bool { +function coreutilsCopyWithin(string $path, string $directory): bool +{ if (DIRECTORY_SEPARATOR === '\\') { $path = strtolower(str_replace('\\', '/', $path)); $directory = strtolower(str_replace('\\', '/', $directory)); @@ -89,7 +96,8 @@ function coreutilsCopyWithin(string $path, string $directory): bool { return $path === $directory || strpos($path, $directory . '/') === 0; } -function coreutilsCopyEntry(string $source, string $target, array &$result, array &$written): void { +function coreutilsCopyEntry(string $source, string $target, array &$result, array &$written): void +{ $recursive = $result['options']['recursive'] ?? false; $stat = coreutilsLstat($source, $warning); if ($stat === false) { @@ -99,7 +107,7 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra $type = $stat['mode'] & 0170000; // Ordinary cp follows a source link; recursive cp reproduces the link itself. if ($type === 0120000 && !$recursive) { - $stat = coreutilsFsCall(fn() => stat($source), $warning); + $stat = coreutilsFsCall(fn () => stat($source), $warning); if ($stat === false) { coreutilsAddError($result, coreutilsFsError('cp', 'stat link target', $source, $warning)); return; @@ -114,16 +122,18 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra coreutilsAddError($result, coreutilsError('cp', 'recursive-required', "omitting directory '$source'; use -r", $source)); return; } - $parent = coreutilsFsCall(fn() => realpath(dirname($target))); + $parent = coreutilsFsCall(fn () => realpath(dirname($target))); if ($parent === false || !coreutilsIsDirectory($parent)) { coreutilsAddError($result, coreutilsError('cp', 'invalid-parent', "destination parent does not exist: '$target'", $target)); return; } $key = $parent . DIRECTORY_SEPARATOR . basename($target); - if (DIRECTORY_SEPARATOR === '\\') $key = strtolower($key); + if (DIRECTORY_SEPARATOR === '\\') { + $key = strtolower($key); + } $targetStat = coreutilsLstat($target); $targetType = $targetStat === false ? null : $targetStat['mode'] & 0170000; - $canonical = coreutilsFsCall(fn() => realpath($source)); + $canonical = coreutilsFsCall(fn () => realpath($source)); $sameInode = $targetStat !== false && $stat['ino'] !== 0 && $stat['ino'] === $targetStat['ino'] && $stat['dev'] === $targetStat['dev']; if ($sameInode || ($canonical !== false && coreutilsCopyWithin($key, $canonical) @@ -148,13 +158,13 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra return; } if ($type === 0040000) { - $entries = coreutilsFsCall(fn() => scandir($source), $warning); + $entries = coreutilsFsCall(fn () => scandir($source), $warning); if ($entries === false) { coreutilsAddError($result, coreutilsFsError('cp', 'read directory', $source, $warning)); return; } if ($targetStat === false) { - if (!coreutilsFsCall(fn() => mkdir($target, 0777), $warning)) { + if (!coreutilsFsCall(fn () => mkdir($target, 0777), $warning)) { coreutilsAddError($result, coreutilsFsError('cp', 'create directory', $target, $warning)); return; } @@ -168,7 +178,7 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra return; } if ($type === 0120000) { - $link = coreutilsFsCall(fn() => readlink($source), $warning); + $link = coreutilsFsCall(fn () => readlink($source), $warning); if ($link === false) { coreutilsAddError($result, coreutilsFsError('cp', 'read link', $source, $warning)); return; @@ -177,9 +187,9 @@ function coreutilsCopyEntry(string $source, string $target, array &$result, arra coreutilsAddError($result, coreutilsError('cp', 'unsupported-link', 'symlink() is unavailable', $source)); return; } - $success = coreutilsFsCall(fn() => symlink($link, $target), $warning); + $success = coreutilsFsCall(fn () => symlink($link, $target), $warning); } else { - $success = coreutilsFsCall(fn() => copy($source, $target), $warning); + $success = coreutilsFsCall(fn () => copy($source, $target), $warning); } if (!$success) { coreutilsAddError($result, coreutilsFsError('cp', 'copy to ' . $target, $source, $warning)); diff --git a/src/dirname.php b/src/dirname.php index 4a201a1..c18157a 100644 --- a/src/dirname.php +++ b/src/dirname.php @@ -1,4 +1,5 @@ []]); [$options, $args, $errors] = coreutilsValidateInput('dirname', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('dirname'); return $result; diff --git a/src/find.php b/src/find.php index ca1bfa9..df31438 100644 --- a/src/find.php +++ b/src/find.php @@ -1,4 +1,5 @@ []]); [$options, $args, $errors] = coreutilsValidateInput('find', $input); $result['options'] = $options; - foreach ($errors as $error) coreutilsAddError($result, $error, 2); - if ($result['status'] !== 0) return $result; + foreach ($errors as $error) { + coreutilsAddError($result, $error, 2); + } + if ($result['status'] !== 0) { + return $result; + } if ($options['help'] ?? false) { $result['help'] = coreutilsHelp('find'); return $result; @@ -22,8 +28,11 @@ function find(array $input, ?string $cwd = null): array { $accepted = explode(',', $list); foreach ($accepted as $type) { if (!in_array($type, ['f', 'd', 'l'], true)) { - coreutilsAddError($result, coreutilsError('find', 'invalid-type', - 'type must be f, d, l or a comma-separated list of these types'), 2); + coreutilsAddError($result, coreutilsError( + 'find', + 'invalid-type', + 'type must be f, d, l or a comma-separated list of these types' + ), 2); return $result; } } @@ -54,32 +63,48 @@ function find(array $input, ?string $cwd = null): array { } $type = $typeCodes[$stat['mode'] & 0170000] ?? '?'; if ($probe !== $path && $type !== 'd') { - coreutilsAddError($result, coreutilsError('find', 'invalid-path', - "cannot inspect '$name': trailing separator requires a real directory", $name)); + coreutilsAddError($result, coreutilsError( + 'find', + 'invalid-path', + "cannot inspect '$name': trailing separator requires a real directory", + $name + )); continue; } $matches = $types === null || in_array($type, $types, true); // Match the displayed entry's basename, including a starting . or .. . $leaf = rtrim($name, $separators); - if (DIRECTORY_SEPARATOR === '\\') $leaf = str_replace('\\', '/', $leaf); + if (DIRECTORY_SEPARATOR === '\\') { + $leaf = str_replace('\\', '/', $leaf); + } $slash = strrpos($leaf, '/'); - if ($slash !== false) $leaf = substr($leaf, $slash + 1); - if ($leaf === '') $leaf = DIRECTORY_SEPARATOR; + if ($slash !== false) { + $leaf = substr($leaf, $slash + 1); + } + if ($leaf === '') { + $leaf = DIRECTORY_SEPARATOR; + } foreach ($patterns as $pattern) { - if (!$matches) break; + if (!$matches) { + break; + } $matches = fnmatch($pattern, $leaf); } if ($matches) { $result['data']['entries'][] = ['name' => $name, 'path' => $path, 'type' => $type]; } - if ($type !== 'd') continue; - $children = coreutilsFsCall(fn() => scandir($path, SCANDIR_SORT_ASCENDING), $warning); + if ($type !== 'd') { + continue; + } + $children = coreutilsFsCall(fn () => scandir($path, SCANDIR_SORT_ASCENDING), $warning); if ($children === false) { coreutilsAddError($result, coreutilsFsError('find', 'read directory', $name, $warning)); continue; } foreach (array_reverse($children) as $child) { - if ($child === '.' || $child === '..') continue; + if ($child === '.' || $child === '..') { + continue; + } $stack[] = [rtrim($name, $separators) . DIRECTORY_SEPARATOR . $child, rtrim($path, $separators) . DIRECTORY_SEPARATOR . $child]; } diff --git a/src/lib/errorHandling.php b/src/lib/errorHandling.php index aaca5d8..4c94066 100644 --- a/src/lib/errorHandling.php +++ b/src/lib/errorHandling.php @@ -1,4 +1,5 @@ $code, 'message' => $command . ': ' . $message, 'path' => $path]; } -function coreutilsResult(string $command, array $data = []): array { +function coreutilsResult(string $command, array $data = []): array +{ return [ 'command' => $command, 'status' => 0, @@ -42,13 +45,15 @@ function coreutilsResult(string $command, array $data = []): array { ]; } -function coreutilsAddError(array &$result, array $error, int $status = 1): void { +function coreutilsAddError(array &$result, array $error, int $status = 1): void +{ $result['errors'][] = $error; $result['status'] = max($result['status'], $status); } /** Capture filesystem warnings locally; always restore the caller's handler. */ -function coreutilsFsCall(callable $operation, ?string &$warning = null) { +function coreutilsFsCall(callable $operation, ?string &$warning = null) +{ $warning = null; set_error_handler(function ($severity, $message) use (&$warning) { $warning = $message; @@ -61,9 +66,14 @@ function coreutilsFsCall(callable $operation, ?string &$warning = null) { } } -function coreutilsFsError(string $command, string $operation, string $path, ?string $warning): array { +function coreutilsFsError(string $command, string $operation, string $path, ?string $warning): array +{ // Keep the actual diagnostic; do not label every failure "Permission denied". $detail = $warning === null ? 'Operation failed' : preg_replace('/^[^:]+\(\):\s*/', '', $warning); - return coreutilsError($command, 'filesystem-error', - "cannot $operation '$path': $detail", $path); + return coreutilsError( + $command, + 'filesystem-error', + "cannot $operation '$path': $detail", + $path + ); } diff --git a/src/lib/filesystem.php b/src/lib/filesystem.php index 6eafe29..311205b 100644 --- a/src/lib/filesystem.php +++ b/src/lib/filesystem.php @@ -1,4 +1,5 @@ realpath($cwd), $warning); - if ($resolved === false || !coreutilsFsCall(fn() => is_dir($resolved), $warning)) { + $resolved = coreutilsFsCall(fn () => realpath($cwd), $warning); + if ($resolved === false || !coreutilsFsCall(fn () => is_dir($resolved), $warning)) { $warning = $warning ?? 'Working directory does not exist or is inaccessible'; return false; } @@ -44,9 +48,12 @@ function coreutilsWorkingDirectory(?string $cwd, ?string &$warning = null) { } /** Preserve the final component and .. semantics: realpath() would dereference links. */ -function coreutilsResolvePath(string $path, string $cwd): string { +function coreutilsResolvePath(string $path, string $cwd): string +{ if (DIRECTORY_SEPARATOR === '\\') { - if (preg_match('~^[a-zA-Z]:[/\\\\]|^[/\\\\]{2}~', $path)) return $path; + if (preg_match('~^[a-zA-Z]:[/\\\\]|^[/\\\\]{2}~', $path)) { + return $path; + } if ($path[0] === '/' || $path[0] === '\\') { // A root-relative Windows path belongs to the drive/share of the explicit cwd. preg_match('~^(?:[a-zA-Z]:|[/\\\\]{2}[^/\\\\]+[/\\\\][^/\\\\]+)~', $cwd, $root); @@ -58,12 +65,14 @@ function coreutilsResolvePath(string $path, string $cwd): string { return rtrim($cwd, DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/') . DIRECTORY_SEPARATOR . $path; } -function coreutilsLstat(string $path, ?string &$warning = null) { +function coreutilsLstat(string $path, ?string &$warning = null) +{ clearstatcache(true, $path); - return coreutilsFsCall(fn() => lstat($path), $warning); + return coreutilsFsCall(fn () => lstat($path), $warning); } -function coreutilsIsDirectory(string $path): bool { +function coreutilsIsDirectory(string $path): bool +{ clearstatcache(true, $path); - return coreutilsFsCall(fn() => is_dir($path)); + return coreutilsFsCall(fn () => is_dir($path)); } diff --git a/src/lib/format.php b/src/lib/format.php index 60d3c74..1f3eec8 100644 --- a/src/lib/format.php +++ b/src/lib/format.php @@ -1,4 +1,5 @@ $function($id)) : false; + $record = function_exists($function) ? coreutilsFsCall(fn () => $function($id)) : false; return $cache[$id] = $record === false ? (string) $id : (string) $record['name']; } -function coreutilsFormatEntries(array $entries, array $settings, $formatter, array &$users, array &$groups): array { +function coreutilsFormatEntries(array $entries, array $settings, $formatter, array &$users, array &$groups): array +{ $rows = []; $widths = []; foreach ($entries as $entry) { @@ -70,15 +79,23 @@ function coreutilsFormatEntries(array $entries, array $settings, $formatter, arr continue; } $row = [$entry['permissions'], (string) $entry['nlink']]; - if ($settings['owner']) $row[] = coreutilsIdentity($entry['uid'], false, $users); - if ($settings['group']) $row[] = coreutilsIdentity($entry['gid'], true, $groups); + if ($settings['owner']) { + $row[] = coreutilsIdentity($entry['uid'], false, $users); + } + if ($settings['group']) { + $row[] = coreutilsIdentity($entry['gid'], true, $groups); + } $row[] = $settings['size'] === 'bytes' ? (string) $entry['size'] : coreutilsHumanSize($entry['size'], $settings['size'] === 'si'); $row[] = coreutilsFormatDate($entry['mtime'], $formatter); - if ($entry['target'] !== null) $name .= ' -> ' . coreutilsQuoteName($entry['target']); + if ($entry['target'] !== null) { + $name .= ' -> ' . coreutilsQuoteName($entry['target']); + } $row[] = $name; foreach ($row as $i => $cell) { - if ($i !== count($row) - 1) $widths[$i] = max($widths[$i] ?? 0, strlen($cell)); + if ($i !== count($row) - 1) { + $widths[$i] = max($widths[$i] ?? 0, strlen($cell)); + } } $rows[] = $row; } @@ -94,9 +111,12 @@ function coreutilsFormatEntries(array $entries, array $settings, $formatter, arr } /** Format both diagnostics and output as plain text; the original result stays reusable. */ -function coreutilsText(array $result): string { +function coreutilsText(array $result): string +{ $lines = array_column($result['errors'], 'message'); - if ($result['help'] !== null) return implode("\n", $lines) . ($lines ? "\n" : '') . $result['help']; + if ($result['help'] !== null) { + return implode("\n", $lines) . ($lines ? "\n" : '') . $result['help']; + } if (in_array($result['command'], ['basename', 'dirname'], true)) { $separator = ($result['options']['zero'] ?? false) ? "\0" : "\n"; $values = array_column($result['data']['entries'], 'output'); @@ -112,9 +132,15 @@ function coreutilsText(array $result): string { $lines = array_merge($lines, coreutilsFormatEntries($files, $settings, $formatter, $users, $groups)); $headers = count($files) + count($directories) > 1; foreach ($directories as $index => $directory) { - if ($files || $index > 0) $lines[] = ''; - if ($headers) $lines[] = coreutilsQuoteName($directory['name']) . ':'; - if (!$directory['readable']) continue; + if ($files || $index > 0) { + $lines[] = ''; + } + if ($headers) { + $lines[] = coreutilsQuoteName($directory['name']) . ':'; + } + if (!$directory['readable']) { + continue; + } if ($settings['long']) { $blocks = $directory['blocks']; $total = $blocks === null ? '?' : ($settings['size'] === 'bytes' @@ -151,7 +177,8 @@ function coreutilsText(array $result): string { } /** Escape the complete presentation, including names, link targets and errors. */ -function coreutilsHtml(array $result): string { +function coreutilsHtml(array $result): string +{ return '
'
. htmlspecialchars(coreutilsText($result), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '';
}
diff --git a/src/lib/locale.php b/src/lib/locale.php
index 75f3f1e..62933e7 100644
--- a/src/lib/locale.php
+++ b/src/lib/locale.php
@@ -1,4 +1,5 @@
compare($left, $right);
return $comparison === false ? strcmp($left, $right) : $comparison;
}
-function coreutilsDateFormatter(?string $locale) {
- if (!class_exists('IntlDateFormatter')) return null;
+function coreutilsDateFormatter(?string $locale)
+{
+ if (!class_exists('IntlDateFormatter')) {
+ return null;
+ }
try {
return new IntlDateFormatter(
$locale === null || $locale === 'C' || $locale === 'POSIX' ? 'en_US_POSIX' : $locale,
@@ -59,7 +65,8 @@ function coreutilsDateFormatter(?string $locale) {
}
}
-function coreutilsFormatDate(int $timestamp, $formatter = null): string {
+function coreutilsFormatDate(int $timestamp, $formatter = null): string
+{
if ($formatter !== null) {
$formatted = $formatter->format($timestamp);
if ($formatted !== false) {
diff --git a/src/lib/options.php b/src/lib/options.php
index 74687cb..86b9b02 100644
--- a/src/lib/options.php
+++ b/src/lib/options.php
@@ -1,4 +1,5 @@
[short spelling, long spelling, requires a value]. */
-function coreutilsOptionDefinitions(string $command): array {
+function coreutilsOptionDefinitions(string $command): array
+{
$common = ['help' => [null, 'help', false]];
if ($command === 'basename') {
return [
@@ -99,7 +101,8 @@ function coreutilsOptionDefinitions(string $command): array {
return [];
}
-function coreutilsHelp(string $command): string {
+function coreutilsHelp(string $command): string
+{
if ($command === 'basename') {
return "Usage: basename [OPTION]... NAME [SUFFIX]\n"
. " or: basename -a [OPTION]... NAME...\n"
diff --git a/src/lib/parser.php b/src/lib/parser.php
index 962946b..5af4553 100644
--- a/src/lib/parser.php
+++ b/src/lib/parser.php
@@ -1,4 +1,5 @@
$command, 'options' => [], 'args' => [], 'errors' => []];
@@ -105,8 +108,12 @@ function parseCommand(string $input): array {
}
$short = $long = [];
foreach ($definitions as $name => [$s, $l, $takesValue]) {
- if ($s !== null) $short[$s] = $name;
- if ($l !== null) $long[$l] = $name;
+ if ($s !== null) {
+ $short[$s] = $name;
+ }
+ if ($l !== null) {
+ $long[$l] = $name;
+ }
}
$endOfOptions = false;
$findPredicates = false;
@@ -114,13 +121,19 @@ function parseCommand(string $input): array {
$token = $tokens[$i];
if ($endOfOptions || $token === '' || $token === '-' || $token[0] !== '-') {
if ($findPredicates) {
- $parsed['errors'][] = coreutilsError($command, 'unexpected-path',
- 'paths must precede -type and -name predicates', $token);
+ $parsed['errors'][] = coreutilsError(
+ $command,
+ 'unexpected-path',
+ 'paths must precede -type and -name predicates',
+ $token
+ );
continue;
}
$parsed['args'][] = $token;
// basename's second operand is a literal suffix, even if it starts with '-'.
- if ($command === 'basename') $endOfOptions = true;
+ if ($command === 'basename') {
+ $endOfOptions = true;
+ }
continue;
}
if ($token === '--') {
@@ -171,14 +184,17 @@ function parseCommand(string $input): array {
unset($parsed['options'][$name]);
$parsed['options'][$name] = $value;
}
- if ($takesValue) break;
+ if ($takesValue) {
+ break;
+ }
}
}
return $parsed;
}
/** Validate parsed or programmatically supplied input before any filesystem changes. */
-function coreutilsValidateInput(string $command, array $input): array {
+function coreutilsValidateInput(string $command, array $input): array
+{
$errors = $input['errors'] ?? [];
if (($input['command'] ?? $command) !== $command && !$errors) {
$errors[] = coreutilsError($command, 'invalid-command', 'input belongs to a different command');
diff --git a/src/lib/pathnames.php b/src/lib/pathnames.php
index 0faaac2..32a668f 100644
--- a/src/lib/pathnames.php
+++ b/src/lib/pathnames.php
@@ -1,13 +1,19 @@
scandir($parent));
+ if ($parent === $path) {
+ return false;
+ }
+ if (coreutilsLstat($parent) === false) {
+ return coreutilsRemovalMissing($parent);
+ }
+ if (!coreutilsIsDirectory($parent)) {
+ return false;
+ }
+ $names = coreutilsFsCall(fn () => scandir($parent));
return $names !== false && !in_array(basename($path), $names, true);
}
/** Refuse roots, dot operands, the explicit cwd and its ancestors. Final links stay links. */
-function coreutilsRemovalProtected(string $path, string $cwd, bool $directory): bool {
+function coreutilsRemovalProtected(string $path, string $cwd, bool $directory): bool
+{
$name = basename($path);
- if ($path === '' || $name === '.' || $name === '..') return true;
- if (!$directory) return false;
- $canonical = coreutilsFsCall(fn() => realpath($path));
- if ($canonical === false) return true;
- if (dirname($canonical) === $canonical) return true;
+ if ($path === '' || $name === '.' || $name === '..') {
+ return true;
+ }
+ if (!$directory) {
+ return false;
+ }
+ $canonical = coreutilsFsCall(fn () => realpath($path));
+ if ($canonical === false) {
+ return true;
+ }
+ if (dirname($canonical) === $canonical) {
+ return true;
+ }
if (DIRECTORY_SEPARATOR === '\\') {
$canonical = strtolower(str_replace('\\', '/', $canonical));
$cwd = strtolower(str_replace('\\', '/', $cwd));
@@ -54,7 +71,8 @@ function coreutilsRemovalProtected(string $path, string $cwd, bool $directory):
return $canonical === $cwd || strpos($cwd, rtrim($canonical, '/') . '/') === 0;
}
-function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void {
+function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void
+{
$command = $result['command'];
$separators = DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/';
$trimmed = rtrim($path, $separators);
@@ -86,19 +104,23 @@ function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void {
coreutilsAddError($result, coreutilsError($command, 'recursive-required', "cannot remove directory '$path'; use -r", $path));
return;
}
- $names = coreutilsFsCall(fn() => scandir($trimmed), $warning);
+ $names = coreutilsFsCall(fn () => scandir($trimmed), $warning);
if ($names === false) {
coreutilsAddError($result, coreutilsFsError($command, 'read directory', $path, $warning));
return;
}
$errorCount = count($result['errors']);
foreach ($names as $name) {
- if ($name !== '.' && $name !== '..') coreutilsRemoveEntry($trimmed . DIRECTORY_SEPARATOR . $name, $cwd, $result);
+ if ($name !== '.' && $name !== '..') {
+ coreutilsRemoveEntry($trimmed . DIRECTORY_SEPARATOR . $name, $cwd, $result);
+ }
+ }
+ if (count($result['errors']) !== $errorCount) {
+ return;
}
- if (count($result['errors']) !== $errorCount) return;
}
- $success = $directory ? coreutilsFsCall(fn() => rmdir($trimmed), $warning)
- : coreutilsFsCall(fn() => unlink($trimmed), $warning);
+ $success = $directory ? coreutilsFsCall(fn () => rmdir($trimmed), $warning)
+ : coreutilsFsCall(fn () => unlink($trimmed), $warning);
if (!$success) {
coreutilsAddError($result, coreutilsFsError($command, 'remove', $path, $warning));
return;
@@ -107,12 +129,17 @@ function coreutilsRemoveEntry(string $path, string $cwd, array &$result): void {
$result['data']['removed'][] = $trimmed;
}
-function coreutilsRemove(string $command, array $input, ?string $cwd): array {
+function coreutilsRemove(string $command, array $input, ?string $cwd): array
+{
$result = coreutilsResult($command, ['removed' => [], 'skipped' => []]);
[$options, $args, $errors] = coreutilsValidateInput($command, $input);
$result['options'] = $options;
- foreach ($errors as $error) coreutilsAddError($result, $error, 2);
- if ($errors) return $result;
+ foreach ($errors as $error) {
+ coreutilsAddError($result, $error, 2);
+ }
+ if ($errors) {
+ return $result;
+ }
if ($options['help'] ?? false) {
$result['help'] = coreutilsHelp($command);
return $result;
@@ -134,10 +161,14 @@ function coreutilsRemove(string $command, array $input, ?string $cwd): array {
$count = count($result['data']['removed']);
coreutilsRemoveEntry($path, $cwd, $result);
if ($command !== 'rmdir' || !($options['parents'] ?? false)
- || count($result['data']['removed']) === $count) break;
+ || count($result['data']['removed']) === $count) {
+ break;
+ }
$parent = dirname(rtrim($path, DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/'));
// Parent removal stops before the cwd or filesystem root, including aliases.
- if (coreutilsRemovalProtected($parent, $cwd, true)) break;
+ if (coreutilsRemovalProtected($parent, $cwd, true)) {
+ break;
+ }
$path = $parent;
}
}
diff --git a/src/ls.php b/src/ls.php
index 3148a9f..0498913 100644
--- a/src/ls.php
+++ b/src/ls.php
@@ -1,4 +1,5 @@
's', 0120000 => 'l', 0100000 => '-', 0060000 => 'b',
0040000 => 'd', 0020000 => 'c', 0010000 => 'p'];
$text = $types[$mode & 0170000] ?? '?';
@@ -44,10 +46,13 @@ function coreutilsSymbolicPerms(int $mode): string {
return $text;
}
-function coreutilsLsSettings(array $options): array {
+function coreutilsLsSettings(array $options): array
+{
$size = 'bytes';
foreach ($options as $name => $enabled) {
- if ($enabled && ($name === 'human-readable' || $name === 'si')) $size = $name;
+ if ($enabled && ($name === 'human-readable' || $name === 'si')) {
+ $size = $name;
+ }
}
return [
'long' => ($options['long'] ?? false) || ($options['omit-owner'] ?? false) || ($options['omit-group'] ?? false),
@@ -57,11 +62,12 @@ function coreutilsLsSettings(array $options): array {
];
}
-function coreutilsLsEntry(string $name, string $path, array $stat, array &$result): array {
+function coreutilsLsEntry(string $name, string $path, array $stat, array &$result): array
+{
$type = coreutilsSymbolicPerms($stat['mode']);
$target = null;
if ($type[0] === 'l') {
- $target = coreutilsFsCall(fn() => readlink($path), $warning);
+ $target = coreutilsFsCall(fn () => readlink($path), $warning);
if ($target === false) {
coreutilsAddError($result, coreutilsFsError('ls', 'read symbolic link', $name, $warning));
$target = null;
@@ -78,13 +84,18 @@ function coreutilsLsEntry(string $name, string $path, array $stat, array &$resul
}
/** Return metadata with raw names and sizes. Presentation is handled by coreutilsText/Html. */
-function ls(array $input, ?string $cwd = null, ?string $locale = null): array {
+function ls(array $input, ?string $cwd = null, ?string $locale = null): array
+{
$result = coreutilsResult('ls', ['files' => [], 'directories' => []]);
$result['locale'] = $locale;
[$options, $args, $errors] = coreutilsValidateInput('ls', $input);
$result['options'] = $options;
- foreach ($errors as $error) coreutilsAddError($result, $error, 2);
- if ($result['status'] !== 0) return $result;
+ foreach ($errors as $error) {
+ coreutilsAddError($result, $error, 2);
+ }
+ if ($result['status'] !== 0) {
+ return $result;
+ }
if ($options['help'] ?? false) {
$result['help'] = coreutilsHelp('ls');
return $result;
@@ -96,7 +107,7 @@ function ls(array $input, ?string $cwd = null, ?string $locale = null): array {
}
$settings = coreutilsLsSettings($options);
$collator = coreutilsCollator($locale);
- $compare = fn($a, $b) => coreutilsCompare($a['name'], $b['name'], $collator);
+ $compare = fn ($a, $b) => coreutilsCompare($a['name'], $b['name'], $collator);
$directories = [];
foreach ($args ?: ['.'] as $arg) {
$path = coreutilsResolvePath($arg, $base);
@@ -123,7 +134,7 @@ function ls(array $input, ?string $cwd = null, ?string $locale = null): array {
usort($directories, $compare);
foreach ($directories as $directory) {
$group = $directory + ['entries' => [], 'blocks' => 0, 'readable' => true];
- $items = coreutilsFsCall(fn() => scandir($directory['path'], SCANDIR_SORT_NONE), $warning);
+ $items = coreutilsFsCall(fn () => scandir($directory['path'], SCANDIR_SORT_NONE), $warning);
if ($items === false) {
coreutilsAddError($result, coreutilsFsError('ls', 'open directory', $directory['name'], $warning));
$group['blocks'] = null;
@@ -131,14 +142,17 @@ function ls(array $input, ?string $cwd = null, ?string $locale = null): array {
$result['data']['directories'][] = $group;
continue;
}
- $items = array_values(array_filter($items, fn($name) => ($options['all'] ?? false) || $name[0] !== '.'));
- usort($items, fn($a, $b) => coreutilsCompare($a, $b, $collator));
+ $items = array_values(array_filter($items, fn ($name) => ($options['all'] ?? false) || $name[0] !== '.'));
+ usort($items, fn ($a, $b) => coreutilsCompare($a, $b, $collator));
if ($options['group-directories-first'] ?? false) {
$dirs = $files = [];
foreach ($items as $name) {
$full = $directory['path'] . DIRECTORY_SEPARATOR . $name;
- if (coreutilsIsDirectory($full)) $dirs[] = $name;
- else $files[] = $name;
+ if (coreutilsIsDirectory($full)) {
+ $dirs[] = $name;
+ } else {
+ $files[] = $name;
+ }
}
$items = array_merge($dirs, $files);
}
diff --git a/src/mkdir.php b/src/mkdir.php
index 915fbb2..9d1cc1b 100644
--- a/src/mkdir.php
+++ b/src/mkdir.php
@@ -1,4 +1,5 @@
mkdir($path, 0777), $warning)) {
+ if ($parent !== $path && !coreutilsMakeParents($parent, $created, $warning)) {
+ return false;
+ }
+ if (coreutilsIsDirectory($path)) {
+ return true;
+ }
+ if (!coreutilsFsCall(fn () => mkdir($path, 0777), $warning)) {
return coreutilsIsDirectory($path); // Another caller may have created the parent.
}
$created[] = $path;
$stat = coreutilsLstat($path, $warning);
- if ($stat === false) return false;
+ if ($stat === false) {
+ return false;
+ }
if (DIRECTORY_SEPARATOR !== '\\' && ($stat['mode'] & 0300) !== 0300) {
$mode = ($stat['mode'] & 07777) | 0300;
- return coreutilsFsCall(fn() => chmod($path, $mode), $warning);
+ return coreutilsFsCall(fn () => chmod($path, $mode), $warning);
}
return true;
}
/** Return created/existing paths, errors and status. No output, chdir() or session access. */
-function _mkdir(array $input, ?string $cwd = null): array {
+function _mkdir(array $input, ?string $cwd = null): array
+{
$result = coreutilsResult('mkdir', ['created' => [], 'existing' => []]);
[$options, $args, $errors] = coreutilsValidateInput('mkdir', $input);
$result['options'] = $options;
- foreach ($errors as $error) coreutilsAddError($result, $error, 2);
- if ($result['status'] !== 0) return $result;
+ foreach ($errors as $error) {
+ coreutilsAddError($result, $error, 2);
+ }
+ if ($result['status'] !== 0) {
+ return $result;
+ }
if ($options['help'] ?? false) {
$result['help'] = coreutilsHelp('mkdir');
return $result;
@@ -85,7 +100,7 @@ function _mkdir(array $input, ?string $cwd = null): array {
coreutilsAddError($result, coreutilsFsError('mkdir', 'create directory', $arg, $warning));
continue;
}
- if (!coreutilsFsCall(fn() => mkdir($path, $mode), $warning)) {
+ if (!coreutilsFsCall(fn () => mkdir($path, $mode), $warning)) {
if ($recursive && coreutilsIsDirectory($path)) {
$result['data']['existing'][] = $path;
} else {
@@ -95,7 +110,7 @@ function _mkdir(array $input, ?string $cwd = null): array {
}
$result['data']['created'][] = $path;
// Only an explicit -m overrides the umask. Never chmod an existing directory.
- if ($explicitMode && !coreutilsFsCall(fn() => chmod($path, $mode), $warning)) {
+ if ($explicitMode && !coreutilsFsCall(fn () => chmod($path, $mode), $warning)) {
coreutilsAddError($result, coreutilsFsError('mkdir', 'set permissions on', $arg, $warning));
}
}
diff --git a/src/mv.php b/src/mv.php
index d4e9641..548ee0b 100644
--- a/src/mv.php
+++ b/src/mv.php
@@ -1,4 +1,5 @@
[], 'skipped' => []]);
[$options, $args, $errors] = coreutilsValidateInput('mv', $input);
$result['options'] = $options;
- foreach ($errors as $error) coreutilsAddError($result, $error, 2);
- if ($errors) return $result;
+ foreach ($errors as $error) {
+ coreutilsAddError($result, $error, 2);
+ }
+ if ($errors) {
+ return $result;
+ }
if ($options['help'] ?? false) {
$result['help'] = coreutilsHelp('mv');
return $result;
@@ -51,7 +57,9 @@ function mv(array $input, ?string $cwd = null): array {
}
$noClobber = false;
foreach ($options as $name => $enabled) {
- if ($enabled && ($name === 'force' || $name === 'no-clobber')) $noClobber = $name === 'no-clobber';
+ if ($enabled && ($name === 'force' || $name === 'no-clobber')) {
+ $noClobber = $name === 'no-clobber';
+ }
}
$separators = DIRECTORY_SEPARATOR === '\\' ? '/\\' : '/';
$destination = coreutilsResolvePath(array_pop($args), $cwd);
@@ -77,8 +85,8 @@ function mv(array $input, ?string $cwd = null): array {
continue;
}
$target = $directory ? rtrim($destination, $separators) . DIRECTORY_SEPARATOR . $name : $destination;
- $parent = coreutilsFsCall(fn() => realpath(dirname($target)));
- $sourceParent = coreutilsFsCall(fn() => realpath(dirname($source)));
+ $parent = coreutilsFsCall(fn () => realpath(dirname($target)));
+ $sourceParent = coreutilsFsCall(fn () => realpath(dirname($source)));
if ($parent === false || !coreutilsIsDirectory($parent)) {
coreutilsAddError($result, coreutilsError('mv', 'invalid-parent', "destination parent does not exist: '$target'", $target));
continue;
@@ -101,7 +109,7 @@ function mv(array $input, ?string $cwd = null): array {
continue;
}
if ($isDirectory) {
- $canonical = coreutilsFsCall(fn() => realpath($source));
+ $canonical = coreutilsFsCall(fn () => realpath($source));
$prefix = rtrim((string) $canonical, $separators) . DIRECTORY_SEPARATOR;
$comparisonParent = $parent . DIRECTORY_SEPARATOR;
if (DIRECTORY_SEPARATOR === '\\') {
@@ -117,12 +125,12 @@ function mv(array $input, ?string $cwd = null): array {
coreutilsAddError($result, coreutilsError('mv', 'type-mismatch', "cannot replace '$target' with a different entry type", $target));
continue;
}
- $parentStat = coreutilsFsCall(fn() => stat($parent));
+ $parentStat = coreutilsFsCall(fn () => stat($parent));
if ($parentStat === false || $stat['dev'] !== $parentStat['dev']) {
coreutilsAddError($result, coreutilsError('mv', 'cross-device', "cannot move '$operand': cross-filesystem moves are not supported", $operand));
continue;
}
- if (!coreutilsFsCall(fn() => rename($source, $target), $warning)) {
+ if (!coreutilsFsCall(fn () => rename($source, $target), $warning)) {
coreutilsAddError($result, coreutilsFsError('mv', 'move to ' . $target, $operand, $warning));
continue;
}
diff --git a/src/pwd.php b/src/pwd.php
index a74f379..4967b62 100644
--- a/src/pwd.php
+++ b/src/pwd.php
@@ -1,4 +1,5 @@
null]);
[$options, $args, $errors] = coreutilsValidateInput('pwd', $input);
$result['options'] = $options;
- foreach ($errors as $error) coreutilsAddError($result, $error, 2);
- if ($result['status'] !== 0) return $result;
+ foreach ($errors as $error) {
+ coreutilsAddError($result, $error, 2);
+ }
+ if ($result['status'] !== 0) {
+ return $result;
+ }
if ($options['help'] ?? false) {
$result['help'] = coreutilsHelp('pwd');
return $result;
}
if ($args) {
- coreutilsAddError($result, coreutilsError('pwd', 'unexpected-operand',
- 'this command does not accept operands'), 2);
+ coreutilsAddError($result, coreutilsError(
+ 'pwd',
+ 'unexpected-operand',
+ 'this command does not accept operands'
+ ), 2);
return $result;
}
$logical = false;
@@ -28,10 +37,13 @@ function pwd(array $input, ?string $cwd = null): array {
}
}
- $directory = $cwd ?? coreutilsFsCall(fn() => getcwd(), $warning);
+ $directory = $cwd ?? coreutilsFsCall(fn () => getcwd(), $warning);
if ($directory === false) {
- coreutilsAddError($result, coreutilsError('pwd', 'invalid-cwd',
- $warning ?? 'Cannot determine working directory'));
+ coreutilsAddError($result, coreutilsError(
+ 'pwd',
+ 'invalid-cwd',
+ $warning ?? 'Cannot determine working directory'
+ ));
return $result;
}
// Refresh cached resolutions when a link has changed between calls.
@@ -42,19 +54,27 @@ function pwd(array $input, ?string $cwd = null): array {
return $result;
}
$result['data']['path'] = $physical;
- if (!$logical) return $result;
+ if (!$logical) {
+ return $result;
+ }
// An explicit application cwd takes precedence over the process environment.
$candidate = $cwd ?? getenv('PWD');
- if (!is_string($candidate) || $candidate === '' || strpos($candidate, "\0") !== false) return $result;
+ if (!is_string($candidate) || $candidate === '' || strpos($candidate, "\0") !== false) {
+ return $result;
+ }
$windows = DIRECTORY_SEPARATOR === '\\';
$absolute = $windows
? preg_match('~^(?:[a-zA-Z]:[/\\\\]|[/\\\\]{2}[^/\\\\]+[/\\\\][^/\\\\]+)~', $candidate)
: $candidate[0] === '/';
- if (!$absolute) return $result;
+ if (!$absolute) {
+ return $result;
+ }
$components = explode('/', $windows ? str_replace('\\', '/', $candidate) : $candidate);
- if (in_array('.', $components, true) || in_array('..', $components, true)) return $result;
- $resolved = coreutilsFsCall(fn() => realpath($candidate));
+ if (in_array('.', $components, true) || in_array('..', $components, true)) {
+ return $result;
+ }
+ $resolved = coreutilsFsCall(fn () => realpath($candidate));
if ($resolved !== false && ($windows ? strcasecmp($resolved, $physical) === 0 : $resolved === $physical)) {
$result['data']['path'] = $candidate;
}
diff --git a/src/rm.php b/src/rm.php
index b24cd04..baee13b 100644
--- a/src/rm.php
+++ b/src/rm.php
@@ -1,4 +1,5 @@
fixture(function ($base) {
+
+$tests['cp copies binary contents without output or source changes'] = fn () => fixture(function ($base) {
$bytes = "a\0b\xff\n";
file_put_contents($base . '/source', $bytes);
- ob_start(); $result = cp(parseCommand('cp source target'), $base); same('', ob_get_clean());
- same(0, $result['status']); same($bytes, file_get_contents($base . '/source'));
+ ob_start();
+ $result = cp(parseCommand('cp source target'), $base);
+ same('', ob_get_clean());
+ same(0, $result['status']);
+ same($bytes, file_get_contents($base . '/source'));
same($bytes, file_get_contents($base . '/target'));
same([['source' => $base . '/source', 'destination' => $base . '/target']], $result['data']['copied']);
same('', coreutilsText($result));
});
-$tests['cp overwrites by default and skips with no-clobber'] = fn() => fixture(function ($base) {
- file_put_contents($base . '/a', 'new'); file_put_contents($base . '/b', 'old');
+$tests['cp overwrites by default and skips with no-clobber'] = fn () => fixture(function ($base) {
+ file_put_contents($base . '/a', 'new');
+ file_put_contents($base . '/b', 'old');
$result = cp(parseCommand('cp --no-clobber a b'), $base);
- same(0, $result['status']); same(1, count($result['data']['skipped'])); same('old', file_get_contents($base . '/b'));
- same(0, cp(parseCommand('cp a b'), $base)['status']); same('new', file_get_contents($base . '/b'));
+ same(0, $result['status']);
+ same(1, count($result['data']['skipped']));
+ same('old', file_get_contents($base . '/b'));
+ same(0, cp(parseCommand('cp a b'), $base)['status']);
+ same('new', file_get_contents($base . '/b'));
same('new', file_get_contents($base . '/a'));
});
-$tests['cp multiple sources require a directory and continue after errors'] = fn() => fixture(function ($base) {
- file_put_contents($base . '/a', 'a'); file_put_contents($base . '/b', 'b'); mkdir($base . '/out');
- same(1, cp(parseCommand('cp a b missing'), $base)['status']); check(!file_exists($base . '/missing'));
+$tests['cp multiple sources require a directory and continue after errors'] = fn () => fixture(function ($base) {
+ file_put_contents($base . '/a', 'a');
+ file_put_contents($base . '/b', 'b');
+ mkdir($base . '/out');
+ same(1, cp(parseCommand('cp a b missing'), $base)['status']);
+ check(!file_exists($base . '/missing'));
$result = cp(parseCommand('cp a absent b out'), $base);
- same(1, $result['status']); same(2, count($result['data']['copied']));
- same('a', file_get_contents($base . '/out/a')); same('b', file_get_contents($base . '/out/b'));
+ same(1, $result['status']);
+ same(2, count($result['data']['copied']));
+ same('a', file_get_contents($base . '/out/a'));
+ same('b', file_get_contents($base . '/out/b'));
});
-$tests['cp validates all input before writing and help ignores cwd'] = fn() => fixture(function ($base) {
+$tests['cp validates all input before writing and help ignores cwd'] = fn () => fixture(function ($base) {
file_put_contents($base . '/a', 'a');
foreach (['cp a', 'cp -z a b', 'cp a ""', 'cp a b https://example.com', 'cp a "unterminated'] as $command) {
- same(2, cp(parseCommand($command), $base)['status']); check(!file_exists($base . '/b'));
+ same(2, cp(parseCommand($command), $base)['status']);
+ check(!file_exists($base . '/b'));
}
same(2, cp(['args' => ['a', 'b'], 'options' => ['recursive' => 1]], $base)['status']);
same(1, cp(parseCommand('cp a b'), $base . '/missing')['status']);
- $result = cp(parseCommand('cp --help'), '/nonexistent'); same(0, $result['status']);
+ $result = cp(parseCommand('cp --help'), '/nonexistent');
+ same(0, $result['status']);
check(strpos(coreutilsText($result), 'Usage: cp') === 0);
});
-$tests['cp recursive copies hidden files and empty directories'] = fn() => fixture(function ($base) {
- mkdir($base . '/tree'); mkdir($base . '/tree/empty'); mkdir($base . '/tree/sub');
- file_put_contents($base . '/tree/.hidden', 'hidden'); file_put_contents($base . '/tree/sub/file', 'data');
- same(1, cp(parseCommand('cp tree out'), $base)['status']); check(!file_exists($base . '/out'));
- $result = cp(parseCommand('cp --recursive tree/ out'), $base); same(0, $result['status']);
- same(3, count($result['data']['created'])); same(2, count($result['data']['copied']));
- check(is_dir($base . '/out/empty')); same('hidden', file_get_contents($base . '/out/.hidden'));
+$tests['cp recursive copies hidden files and empty directories'] = fn () => fixture(function ($base) {
+ mkdir($base . '/tree');
+ mkdir($base . '/tree/empty');
+ mkdir($base . '/tree/sub');
+ file_put_contents($base . '/tree/.hidden', 'hidden');
+ file_put_contents($base . '/tree/sub/file', 'data');
+ same(1, cp(parseCommand('cp tree out'), $base)['status']);
+ check(!file_exists($base . '/out'));
+ $result = cp(parseCommand('cp --recursive tree/ out'), $base);
+ same(0, $result['status']);
+ same(3, count($result['data']['created']));
+ same(2, count($result['data']['copied']));
+ check(is_dir($base . '/out/empty'));
+ same('hidden', file_get_contents($base . '/out/.hidden'));
same('data', file_get_contents($base . '/out/sub/file'));
check(is_dir($base . '/tree/empty'));
});
-$tests['cp merges directories and no-clobber skips only existing leaves'] = fn() => fixture(function ($base) {
- mkdir($base . '/tree'); mkdir($base . '/out'); mkdir($base . '/out/tree');
- file_put_contents($base . '/tree/a', 'new'); file_put_contents($base . '/tree/b', 'b');
- file_put_contents($base . '/out/tree/a', 'old'); file_put_contents($base . '/out/tree/keep', 'keep');
- $result = cp(parseCommand('cp -rnv tree out'), $base); same(0, $result['status']);
- same(1, count($result['data']['skipped'])); same('old', file_get_contents($base . '/out/tree/a'));
- same('b', file_get_contents($base . '/out/tree/b')); same('keep', file_get_contents($base . '/out/tree/keep'));
- same(0, cp(parseCommand('cp -r tree out'), $base)['status']); same('new', file_get_contents($base . '/out/tree/a'));
+$tests['cp merges directories and no-clobber skips only existing leaves'] = fn () => fixture(function ($base) {
+ mkdir($base . '/tree');
+ mkdir($base . '/out');
+ mkdir($base . '/out/tree');
+ file_put_contents($base . '/tree/a', 'new');
+ file_put_contents($base . '/tree/b', 'b');
+ file_put_contents($base . '/out/tree/a', 'old');
+ file_put_contents($base . '/out/tree/keep', 'keep');
+ $result = cp(parseCommand('cp -rnv tree out'), $base);
+ same(0, $result['status']);
+ same(1, count($result['data']['skipped']));
+ same('old', file_get_contents($base . '/out/tree/a'));
+ same('b', file_get_contents($base . '/out/tree/b'));
+ same('keep', file_get_contents($base . '/out/tree/keep'));
+ same(0, cp(parseCommand('cp -r tree out'), $base)['status']);
+ same('new', file_get_contents($base . '/out/tree/a'));
});
-$tests['cp rejects self-copy and descendant destinations without mutation'] = fn() => fixture(function ($base) {
- file_put_contents($base . '/a', 'original'); mkdir($base . '/tree'); mkdir($base . '/tree/sub');
- same(1, cp(parseCommand('cp a a'), $base)['status']); same('original', file_get_contents($base . '/a'));
+$tests['cp rejects self-copy and descendant destinations without mutation'] = fn () => fixture(function ($base) {
+ file_put_contents($base . '/a', 'original');
+ mkdir($base . '/tree');
+ mkdir($base . '/tree/sub');
+ same(1, cp(parseCommand('cp a a'), $base)['status']);
+ same('original', file_get_contents($base . '/a'));
foreach (['cp -r tree tree', 'cp -r tree tree/sub', 'cp -r tree tree/new', 'cp -r . out'] as $command) {
same(1, cp(parseCommand($command), $base)['status']);
}
- check(!file_exists($base . '/tree/tree')); check(!file_exists($base . '/tree/new')); check(!file_exists($base . '/out'));
+ check(!file_exists($base . '/tree/tree'));
+ check(!file_exists($base . '/tree/new'));
+ check(!file_exists($base . '/out'));
});
-$tests['cp detects same-file hard links'] = fn() => fixture(function ($base) {
+$tests['cp detects same-file hard links'] = fn () => fixture(function ($base) {
file_put_contents($base . '/a', 'keep');
- skipUnless(function_exists('link') && coreutilsFsCall(fn() => link($base . '/a', $base . '/alias')), 'hard links unavailable');
- same(1, cp(parseCommand('cp a alias'), $base)['status']); same('keep', file_get_contents($base . '/a'));
+ skipUnless(function_exists('link') && coreutilsFsCall(fn () => link($base . '/a', $base . '/alias')), 'hard links unavailable');
+ same(1, cp(parseCommand('cp a alias'), $base)['status']);
+ same('keep', file_get_contents($base . '/a'));
});
-$tests['cp ordinary follows file links but recursive preserves links and cycles'] = fn() => fixture(function ($base) {
- symlinkSupport($base); file_put_contents($base . '/file', 'content'); symlink('file', $base . '/link');
- same(0, cp(parseCommand('cp link copy'), $base)['status']); check(!is_link($base . '/copy'));
+$tests['cp ordinary follows file links but recursive preserves links and cycles'] = fn () => fixture(function ($base) {
+ symlinkSupport($base);
+ file_put_contents($base . '/file', 'content');
+ symlink('file', $base . '/link');
+ same(0, cp(parseCommand('cp link copy'), $base)['status']);
+ check(!is_link($base . '/copy'));
same('content', file_get_contents($base . '/copy'));
- mkdir($base . '/tree'); symlink('missing', $base . '/tree/broken'); symlink('.', $base . '/tree/cycle');
+ mkdir($base . '/tree');
+ symlink('missing', $base . '/tree/broken');
+ symlink('.', $base . '/tree/cycle');
same(0, cp(parseCommand('cp -r tree out'), $base)['status']);
- same('missing', readlink($base . '/out/broken')); same('.', readlink($base . '/out/cycle'));
- same(0, cp(parseCommand('cp -r link duplicate-link'), $base)['status']); same('file', readlink($base . '/duplicate-link'));
- same(1, cp(parseCommand('cp tree/broken missing-copy'), $base)['status']); check(!file_exists($base . '/missing-copy'));
+ same('missing', readlink($base . '/out/broken'));
+ same('.', readlink($base . '/out/cycle'));
+ same(0, cp(parseCommand('cp -r link duplicate-link'), $base)['status']);
+ same('file', readlink($base . '/duplicate-link'));
+ same(1, cp(parseCommand('cp tree/broken missing-copy'), $base)['status']);
+ check(!file_exists($base . '/missing-copy'));
});
-$tests['cp refuses destination links and supports no-clobber for broken links'] = fn() => fixture(function ($base) {
- symlinkSupport($base); file_put_contents($base . '/a', 'new'); file_put_contents($base . '/b', 'old');
- symlink('b', $base . '/target'); symlink('missing', $base . '/broken');
- same(1, cp(parseCommand('cp a target'), $base)['status']); same('old', file_get_contents($base . '/b'));
- same(1, cp(parseCommand('cp a broken'), $base)['status']); check(!file_exists($base . '/missing'));
- $result = cp(parseCommand('cp -n a broken'), $base); same(0, $result['status']); same(1, count($result['data']['skipped']));
+$tests['cp refuses destination links and supports no-clobber for broken links'] = fn () => fixture(function ($base) {
+ symlinkSupport($base);
+ file_put_contents($base . '/a', 'new');
+ file_put_contents($base . '/b', 'old');
+ symlink('b', $base . '/target');
+ symlink('missing', $base . '/broken');
+ same(1, cp(parseCommand('cp a target'), $base)['status']);
+ same('old', file_get_contents($base . '/b'));
+ same(1, cp(parseCommand('cp a broken'), $base)['status']);
+ check(!file_exists($base . '/missing'));
+ $result = cp(parseCommand('cp -n a broken'), $base);
+ same(0, $result['status']);
+ same(1, count($result['data']['skipped']));
});
-$tests['cp blocks recursion through destination aliases and does not traverse nested target links'] = fn() => fixture(function ($base) {
- symlinkSupport($base); mkdir($base . '/tree'); mkdir($base . '/tree/sub'); symlink('tree/sub', $base . '/alias');
- same(1, cp(parseCommand('cp -r tree alias'), $base)['status']); check(!file_exists($base . '/tree/sub/tree'));
- mkdir($base . '/out'); mkdir($base . '/out/tree'); mkdir($base . '/elsewhere');
- symlink('../../elsewhere', $base . '/out/tree/sub'); file_put_contents($base . '/tree/sub/a', 'a');
- same(1, cp(parseCommand('cp -r tree out'), $base)['status']); check(!file_exists($base . '/elsewhere/a'));
+$tests['cp blocks recursion through destination aliases and does not traverse nested target links'] = fn () => fixture(function ($base) {
+ symlinkSupport($base);
+ mkdir($base . '/tree');
+ mkdir($base . '/tree/sub');
+ symlink('tree/sub', $base . '/alias');
+ same(1, cp(parseCommand('cp -r tree alias'), $base)['status']);
+ check(!file_exists($base . '/tree/sub/tree'));
+ mkdir($base . '/out');
+ mkdir($base . '/out/tree');
+ mkdir($base . '/elsewhere');
+ symlink('../../elsewhere', $base . '/out/tree/sub');
+ file_put_contents($base . '/tree/sub/a', 'a');
+ same(1, cp(parseCommand('cp -r tree out'), $base)['status']);
+ check(!file_exists($base . '/elsewhere/a'));
same(1, cp(parseCommand('cp -r alias/ copy'), $base)['status']);
});
-$tests['cp rejects type collisions, missing parents and special files'] = fn() => fixture(function ($base) {
- mkdir($base . '/tree'); mkdir($base . '/out'); mkdir($base . '/out/a'); file_put_contents($base . '/a', 'a');
- same(1, cp(parseCommand('cp a out'), $base)['status']); check(is_dir($base . '/out/a'));
- same(1, cp(parseCommand('cp -r tree a'), $base)['status']); same('a', file_get_contents($base . '/a'));
+$tests['cp rejects type collisions, missing parents and special files'] = fn () => fixture(function ($base) {
+ mkdir($base . '/tree');
+ mkdir($base . '/out');
+ mkdir($base . '/out/a');
+ file_put_contents($base . '/a', 'a');
+ same(1, cp(parseCommand('cp a out'), $base)['status']);
+ check(is_dir($base . '/out/a'));
+ same(1, cp(parseCommand('cp -r tree a'), $base)['status']);
+ same('a', file_get_contents($base . '/a'));
same(1, cp(parseCommand('cp a missing/file'), $base)['status']);
same(1, cp(parseCommand('cp a missing/'), $base)['status']);
- if (function_exists('posix_mkfifo') && coreutilsFsCall(fn() => posix_mkfifo($base . '/fifo', 0600))) {
+ if (function_exists('posix_mkfifo') && coreutilsFsCall(fn () => posix_mkfifo($base . '/fifo', 0600))) {
same(1, cp(parseCommand('cp fifo copy'), $base)['status']);
same(1, cp(parseCommand('cp a fifo'), $base)['status']);
}
});
-$tests['cp handles absolute paths, dash names, cwd and escaped verbose text'] = fn() => fixture(function ($base) {
+$tests['cp handles absolute paths, dash names, cwd and escaped verbose text'] = fn () => fixture(function ($base) {
file_put_contents($base . '/-a', 'a');
- $cwd = getcwd(); $locale = setlocale(LC_ALL, 0); $mask = umask();
- $result = cp(parseCommand('cp -v -- -a "