pipe stdio to a function

70 Views Asked by At

Noob question!

I've got a function on the command line that takes parameters

sqr() { echo $(( $1 * $1 )) ; }

sqr 4 echos 16. fine.

Now I want to pipe an output to this function

echo 4 | sqr

I get " syntax error: operand expected (error token is "* ") " - clearly $1 is empty

I've tried using xargs with various options to pass the stdio to the function but I get "No such file or directory" errors. Am I missing something obvious?

4

There are 4 best solutions below

4
pmf On BEST ANSWER

One way could be using parameter expansion on $1 to have cat flush STDIN as fallback:

$ sqr() { n=${1:-$(cat)}; echo $((n*n)); }

$ sqr 6
36

$ echo 5 | sqr
25

$ echo 5 | sqr 6  # $1 takes precedence over STDIN
36
3
Gilles Quénot On

The issue with xargs is that it use a subshell, so your function is not inherited in the new shell.

If you create a new command accessible in PATH, then you could use xargs and add a feature to be able to use STDIN or arguments dynamically::

~/bin/sqr:

#!/bin/bash

if [[ $1 ]]; then
    arg=$1
else
    arg=$(cat)
fi
echo $(( $arg * $arg ))

chmod +x ~/bin/sqr
PATH=~/bin:$PATH

Now:

$ echo 6 | xargs -n1 sqr
36

$ echo 6 | sqr 
36

$ sqr 6
36

The part arg=$(cat) slurp STDIN in the variable arg.

2
chepner On

Parameters and standard input are two fairly different ways for a caller to communicate with a function:

sqr_param () {
    local n
    n=$1
    echo $(( $n * $n ))
}

sqr_stdin () {
    local n
    read -r n
    echo $(( $n * $n ))
}

Writing them both out in a fairly verbose manner lets you see that one (the "impure") version can be trivially implemented in terms of the other ("pure") version.

# This one focuses on math
sqr () {
    echo $(( $1 * $1 ))
}

# This one provides an interface with the outside world
sqr_stdin () {
    local n
    read -r n
    sqr "$n"
}

I would resist the urge to try to implement a single function that can handle either an argument or standard input. "So much complexity in software comes from trying to make one thing do two things."

3
Ed Morton On
$ sqr() {
    local n tmpfd
    exec {tmpfd}<&0             # Save value of FD 0 (stdin) in "tmpfd"
    (( $# )) && exec <<<"$1"    # If an argument passed in create a
                                # temp file, store that value in it,
                                # then set FD 0 to point to that file.
    IFS= read -r n              # Read a value from FD 0
    exec 0<&"$tmpfd"            # Restore FD 0 to the value in "tmpfd"
    echo $(( $n * $n ))         # Print the result of the math
}

$ sqr 4
16

$ echo 4 | sqr
16

The above uses a named file descriptor tmpfd, available in bash 4.1 or later. See https://mywiki.wooledge.org/FileDescriptor for more info on file descriptors.

Alternatively, if you define the function body in a subshell then you don't need to save and restore FD 0 since the exec directive will only apply within the subshell:

$ sqr() {
  (                             # Start a subshell
    local n
    (( $# )) && exec <<<"$1"    # If an argument passed in create a
                                # temp file, store that value in it,
                                # then set FD 0 to point to that file.
    IFS= read -r n              # Read a value from FD 0
    echo $(( $n * $n ))         # Print the result of the math
  )                             # End the subshell
}

$ sqr 4
16

$ echo 4 | sqr
16