Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 22 additions & 8 deletions src/basename.php
Original file line number Diff line number Diff line change
@@ -1,32 +1,46 @@
<?php

// PHP Coreutils - Copyright (c) 2026 Daniel Dias Rodrigues
// Distributed under the MIT License; see LICENSE.

require_once __DIR__ . '/lib/parser.php';
require_once __DIR__ . '/lib/pathnames.php';

/** Strip directory components and an optional suffix; cwd is intentionally unused. */
function _basename(array $input, ?string $cwd = null): array {
function _basename(array $input, ?string $cwd = null): array
{
$result = coreutilsResult('basename', ['entries' => []]);
[$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) {
Expand Down
1 change: 1 addition & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

# PHP Coreutils
# A lightweight, pure-PHP implementation of classic Unix core utilities,
# designed for portability and environments without shell access.
Expand Down
38 changes: 24 additions & 14 deletions src/cp.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

# PHP Coreutils
# A lightweight, pure-PHP implementation of classic Unix core utilities,
# designed for portability and environments without shell access.
Expand Down Expand Up @@ -30,12 +31,17 @@
require_once __DIR__ . '/lib/filesystem.php';

/** Copy local files or recursively copy directories; never prints output. */
function cp(array $input, ?string $cwd = null): array {
function cp(array $input, ?string $cwd = null): array
{
$result = coreutilsResult('cp', ['copied' => [], '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;
Expand Down Expand Up @@ -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));
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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;
}
Expand All @@ -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;
Expand All @@ -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));
Expand Down
12 changes: 9 additions & 3 deletions src/dirname.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<?php

// PHP Coreutils - Copyright (c) 2026 Daniel Dias Rodrigues
// Distributed under the MIT License; see LICENSE.

require_once __DIR__ . '/lib/parser.php';
require_once __DIR__ . '/lib/pathnames.php';

/** Strip the last path component; cwd is intentionally unused. */
function _dirname(array $input, ?string $cwd = null): array {
function _dirname(array $input, ?string $cwd = null): array
{
$result = coreutilsResult('dirname', ['entries' => []]);
[$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;
Expand Down
53 changes: 39 additions & 14 deletions src/find.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<?php

// PHP Coreutils - Copyright (c) 2026 Daniel Dias Rodrigues
// Distributed under the MIT License; see LICENSE.

require_once __DIR__ . '/lib/parser.php';
require_once __DIR__ . '/lib/filesystem.php';

/** List matching entries without output, changing cwd or traversing symbolic links. */
function find(array $input, ?string $cwd = null): array {
function find(array $input, ?string $cwd = null): array
{
$result = coreutilsResult('find', ['entries' => []]);
[$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;
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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];
}
Expand Down
24 changes: 17 additions & 7 deletions src/lib/errorHandling.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php

# PHP Coreutils
# A lightweight, pure-PHP implementation of classic Unix core utilities,
# designed for portability and environments without shell access.
Expand Down Expand Up @@ -27,11 +28,13 @@
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

/** Build an error without producing output. */
function coreutilsError(string $command, string $code, string $message, ?string $path = null): array {
function coreutilsError(string $command, string $code, string $message, ?string $path = null): array
{
return ['code' => $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,
Expand All @@ -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;
Expand All @@ -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
);
}
Loading
Loading