writing command line scripts in php: part 2; reading STDIN
Tags: php
Published: 2022-04-04 14:29:05

although not a popular choice, php can be a very effective language for writing command line scripts. it is feature-rich, and if you are longer on php skills than, say, bash, or if you have already-existing php code you would like to incorporate into a command line script, it can be an excellent choice.

in this series of articles, we will be going through the various techniques and constructs that will help us build quality scripts in php. 

this installment focuses on reading from standard input. 

## previous installments
this is the second installment of the series. the first installment covered parsing command line arguments, preflighting our scripts to ensure they can run in the current environment, and handling some niceties of script design.

the articles that compose this series (so far) are:

- [pt 1. arguments, preflights and more ](https://dev.to/gbhorwood/writing-command-line-scripts-in-php-part-1-3jpb)
- [pt 2. handling STDIN input](https://dev.to/gbhorwood/writing-command-line-scripts-in-php-part-2-reading-stdin-2enf)
- [pt 3. interactive input](https://dev.to/gbhorwood/writing-command-line-scripts-in-php-part-3-interactive-input-34)


## the flyover
we will be designing our example script here to read data piped into it from the `STDIN` stream. this feature requires us to do two things:

- test if there is content in `STDIN` waiting to be read by our script
- actually reading the `STDIN` input

if you are not familiar with linux data streams such as `STDIN` or `STDOUT`, it is a good idea to [spend a few minutes reading up on them](https://www.howtogeek.com/435903/what-are-stdin-stdout-and-stderr-on-linux/) first.

## reading piped input
command line scripts often take their input as piped in from `STDIN`. consider this short little pipeline that finds all the users on the system that use the [fish shell](https://fishshell.com/) instead of `bash`:

```bash
$ cat /etc/passwd | grep "bin/fish"
gbhorwood:x:1000:1000:grant horwood,,,:/home/gbhorwood:/usr/bin/fish
```

the `cat` command dumps the contents of the file to `STDOUT`. normally, `STDOUT` goes to our terminal so we can read it, however in this example the `|` operator (pipe) traps the contents of `STDOUT` and uses it as input for the next command to the right. the pipe operator, essentially, 'pipes' output from the command on the left to the input for the command on the right. that's why it's called a 'pipe'. 

this is a handy feature and one we want to implement in our php script. so let's do that with this function, which we will add to `ourfancyscript.php`:

```php
#!/usr/bin/env php
<?php

/**
 * Read contents piped in from STDIN stream
 *
 * @return  String
 */
function read_piped_input()
{
    $piped_input = null;
    while ($line = fgets(STDIN)) { // noted STDIN here is not a string
        $piped_input .= $line;
    }

    return (string) $piped_input;
} 

/**
 * Entry point
 */
$my_piped_in_content = read_piped_input();
print "piped input is:".PHP_EOL;
print $my_piped_in_content;
```

let's look a that `read_piped_input()` function. the core functionlity here is using [`fgets`](https://www.php.net/manual/en/function.fgets.php) to read from the `STDIN` pointer on a loop, line-by-line, until the content is exhausted. those lines are concatenated together, and returned. mission accomplished.

let's see how it runs.
```bash
echo "this is our piped input" | ./ourfancyscript.php
piped input is:
this is our piped input
```

exactly what we expect.

## testing for piped input
except, there's a problem.

if we run `ourfancyscript.php` *without* any input on `STDIN`, it hangs. why? because it's patiently waiting for input that never comes.

to solve this, we are going to write a function that tests whether or not there is any input on `STDIN` and only read from the pipe if it returns true.

```php
/**
 * Test if there is input waiting on STDIN
 *
 * @return bool
 */
function test_piped_input()
{
    $streams = [STDIN]; // note STDIN here is not a string
    $write_array = [];
    $except_array = [];
    $seconds = 0; // zero seconds on timeout since this is just for testing stream change
    $streamCount = @stream_select($streams, $write_array, $except_array, $seconds);

    return (boolean) $streamCount;
}
```
the key to this function is the [`stream_select`](https://www.php.net/manual/en/function.stream-select.php) command. `stream_select` basically waits for the state of a stream to change, timing out after `$seconds` have passed.

we pass to it `STDIN` as the only element of an array, since that's the stream we're interested in, and set the timeout `$seconds` to 0. we use zero seconds because `STDIN` input is present (or not) before we even run our script. there's no sense waiting around for it; it's either there or it isn't.

if there is data piped in to our command, `STDIN` has, by definition, 'changed' and `stream_select` returns a non-zero number. we know we have data waiting for us! if there is no data, the stream is unchanged and the return is 0.

## putting it together
now that we have `test_piped_input()` and `read_piped_input()`, we can put them together in our script:

```php
/**
 * Entry point
 */
if (test_piped_input()) {
    $my_stdin_content = read_piped_input();
    print "piped input is:".PHP_EOL;
    print $my_stdin_content;
}
```
if we now run `ourfancyscript.php` without a piped-in stream, it proceeds. if we do pipe in data, it handles it.

let's look at the full script now:

```php
#!/usr/bin/env php
<?php

/**
 * Test if there is input waiting on STDIN
 *
 * @return bool
 */
function test_piped_input()
{
    $streams = [STDIN]; // note STDIN here is not a string
    $write_array = [];
    $except_array = [];
    $seconds = 0; // zero seconds on timeout since this is just for testing stream change
    $streamCount = @stream_select($streams, $write_array, $except_array, $seconds);

    return (boolean) $streamCount;
}

/**
 * Read contents piped in from STDIN stream
 *
 * @return  String
 */
function read_piped_input()
{
    $piped_input = null;
    while ($line = fgets(STDIN)) {
        $piped_input .= $line;
    }

    return (string) $piped_input;
}

/**
 * Entry point
 */
if (test_piped_input()) {
    $my_stdin_content = read_piped_input();
    print "piped input is:".PHP_EOL;
    print $my_stdin_content;
}
```

## next steps
there's still a lot more ground to cover in effectively geting input to our script. in future installments we will be looking at interactive input.