Skip to content

Instantly share code, notes, and snippets.

@zanbaldwin
Last active October 1, 2018 13:58
Show Gist options
  • Save zanbaldwin/c5c35c255b44b6a0e10a3fa1934faac0 to your computer and use it in GitHub Desktop.
Save zanbaldwin/c5c35c255b44b6a0e10a3fa1934faac0 to your computer and use it in GitHub Desktop.
Figuring out file-descriptors in PHP console applications

Prior to PHP 7.2+, there is no complete, unified way to find out how input and output flows through PHP console applications. Current polyfills for PHP versions below 7.2 (such as symfony/polyfill-php72) cannot properly test for a TTY (as there is nothing in PHP core that links userland code to the appropriate functions in unistd.h, the header file that provides access to the POSIX operating system API) - they all perform the same logic that is in isCharacterDevice() as a it's-the-best-that-can-be-done-for-now attempt.

If you need to know if PHP output is going to a TTY, you require PHP 7.2 (and the stream_isatty() function) - however, if you assume that a TTY is the default, and only need to know - for example - if your output is being piped to another process then this implementation is sufficient.

This implementation does provide a isTTY() method, it just falls back to the polyfill method when run on versions of PHP less than 7.2.

Usage

$stream = \STDOUT;
$mode = new \Darsyn\FD\StreamMode($stream);
if ($mode->isNamedPipe()) {
    $this->setVerbose(false);
}

Example Outputs

$ php script.php
> STDIN:  Character Device
> STDOUT: Character Device
> STDERR: Character Device

# Directing output to /dev/null also reports as "Character Device" so isn't
# enough to determine if output will be seen, discarded, or is a TTY.
# Additionally use stream_isatty() function (available PHP 7.2+) for this.
$ php script.php 2>/dev/null
> STDIN:  Character Device
> STDOUT: Character Device
> STDERR: Character Device

$ php script.php | cat
> STDIN:  Character Device
> STDOUT: Named Pipe
> STDERR: Character Device

$ echo "blah" | php script.php > /tmp/output.txt
> STDIN:  Named Pipe
> STDOUT: Regular File
> STDERR: Character Device

$ php script.php < /tmp/input.txt > /tmp/output.txt
> STDIN:  Regular File
> STDOUT: Regular File
> STDERR: Character Device

$ php script.php 2> /tmp/error.log
> STDIN:  Character Device
> STDOUT: Character Device
> STDERR: Regular File

$ php script.php 2>&1 >/tmp/output.txt
> STDIN:  Character Device
> STDOUT: Regular File
> STDERR: Named Pipe
<?php declare(strict_types=1);
namespace Darsyn\FD;
class StreamMode implements StreamModeInterface
{
/** @var resource $fp */
private $fp;
/** @var integer $mode */
private $mode;
/** @var boolean|null $tty */
private $tty;
/**
* @param resource $fp
*/
public function __construct($fp)
{
if (!\is_resource($fp)) {
throw new \TypeError(sprintf(
'%s() requires argument of type "resource", "%s" given.',
__FUNCTION__,
\is_object($fp) ? \get_class($fp) : \gettype($fp)
));
}
if (\get_resource_type($fp) !== 'stream') {
throw new \InvalidArgumentException(sprintf(
'%s() requires a stream resource argument, unsupported resource type given.',
__FUNCTION__
));
}
$stat = \fstat($fp);
if (!\is_array($stat)) {
throw new \RuntimeException('Could not get information about file pointer.');
}
// S_IFMT
$this->mode = $stat['mode'] & 0170000;
if (\function_exists('stream_isatty')) {
$this->tty = \stream_isatty($fp);
}
}
/** {@inheritdoc} */
public function isRegularFile(): bool
{
// S_IFREG
return $this->mode === 0100000;
}
/** {@inheritdoc} */
public function isDirectory(): bool
{
// S_IFDIR
return $this->mode === 0040000;
}
/** {@inheritdoc} */
public function isSymbolicLink(): bool
{
// S_IFLNK
return $this->mode === 0120000;
}
/** {@inheritdoc} */
public function isNamedPipe(): bool
{
// S_IFIFO
return $this->mode === 0010000;
}
public function isSocket(): bool
{
// S_IFSOCK
return $this->mode === 0140000;
}
/** {@inheritdoc} */
public function isCharacterDevice(): bool
{
// S_IFCHR
return $this->mode === 0020000;
}
/** {@inheritdoc} */
public function isTTY(): bool
{
return $this->tty ?: $this->isCharacterDevice();
}
/** {@inheritdoc} */
public function isBlockDevice(): bool
{
// S_IFBLK
return $this->mode === 0060000;
}
}
<?php declare(strict_types=1);
namespace Darsyn\FD;
interface StreamModeInterface
{
/** @return boolean */
public function isRegularFile(): bool;
/** @return boolean */
public function isDirectory(): bool;
/** @return boolean */
public function isSymbolicLink(): bool;
/**
* A named pipe is referred to as FIFO (first-in, first-out) in computing, and is used for inter-process
* communication. It's an extension of traditional pipes (communication between parent and child processes).
* @return boolean
*/
public function isNamedPipe(): bool;
/** @return boolean */
public function isSocket(): bool;
/**
* Character devices behave like pipes, serial ports, etc (writing or reading to them is an immediate action). The
* name "character device" comes from the fact that each character is handled individually.
* In the context of a PHP script running in the console, this *usually* indicates that the script is being run in
* the console (a TTY). If running PHP 7.2+, you might want to also check stream_isatty().
* @return boolean
*/
public function isCharacterDevice(): bool;
/**
* Due to access to unistd.h in PHP core, it must be understood that this method will only work properly in PHP
* 7.2+. If used on a PHP version below that, it will return the same result as isCharacterDevice() as a "good
* enough" solution. On versions prior to 7.2, this will incorrectly report character devices (such as
* redirecting to `/dev/null`) as TTY.
* @return boolean
*/
public function isTTY(): bool;
/**
* Block devices get their name because the corresponding hardware typically reads and writes a whole block at a
* time.
* @return boolean
*/
public function isBlockDevice(): bool;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment