eloyesp
eloyesp

Reputation: 3285

Make a bash function fail when on empty

I want to write a function that always have a non empty output or fails, but I'm missing a command that read stdin and pipe it to stdout if non-empty or fails like:

example() {
  do_something_interesting_here $1 | cat_or_fails
}

The idea is that if the command cat_or_fails is given an empty input it fails (so the function fails) or the input is output without any changes (like cat).

But I could not find any standard utility capable of that trick, or may be I'm not sure how to use those tools.

Upvotes: 2

Views: 480

Answers (4)

kvantour
kvantour

Reputation: 26481

There are various ways of doing this, but the simplest is using awk:

function cat_or_fail () { awk '1;END{exit !NR}' "$@"; }

Here, awk returns a non-zero status if not a single line is read.

Alternatively, a tool available in the moreutils package. The command ifne allows one to execute a command, depending if /dev/stdin is empty or not:

  • ifne command: run command if the standard input is not empty
  • ifne -n command: run command if the standard input is empty. If the standard input is not empty, it sends standard input to standard output.

The latter is essentially what the OP expects. The function cat_or_fail would look like

function cat_or_fail () { ifne -n false; }

Which, if the OP wants to have a similar behaviour as cat, you could write it as:

function cat_or_fail () { cat -- "${@}" | ifne -n false; }

The latter can take files as arguments as well as read input from pipes similar to cat.

If you want to expand this to command_or_fail, where command is executed using standard input, or fail. You have to work a bit differently.

The return status of ifne is unaffected by the content of the standard input (empty or not). So you can not use it in an and-or sequence list (foo && bar || baz).

In order to create the requested behaviour, you need to use the pipefail option

$ set -o pipefail
$ function command_or_fail() { ifne -n false | ifne "$@"; }
$ echo foo | command_or_fail cat -n
1 foo
$ echo $?
0
$ </dev/null | command_or_fail cat -n
$ echo $?
1

Upvotes: 1

Charles Duffy
Charles Duffy

Reputation: 295510

A simple algorithm to accomplish this is trivial and obvious: Try to read one byte. If it works, write that byte back out and then run cat. If it fails, exit with a nonzero status.

Both below variants can be used in the manner described in the question (... | cat_or_fails).


A very simple implementation (that doesn't try to handle binary files starting with the NUL character) would look like:

cat_or_fails() {
  local firstbyte
  IFS= read -r -n 1 firstbyte || return
  printf '%s' "${firstbyte:-$'\n'}"
  cat
}

A slightly less simple implementation that does try to handle binary files correctly might look like:

cat_or_fails() {
  local firstbyte
  IFS= read -r -d '' -n 1 firstbyte || return
  if [[ $firstbyte ]]; then
    printf '%s' "$firstbyte"
  else
    printf '\0'
  fi
  cat
}

Upvotes: 1

Gilbert
Gilbert

Reputation: 3776

My usual trick to assure that at least one line of a text file is found follows. It is all standard shell stuff... bash, ksh, zsh.

example() {
  set -o pipefail     #assure failure on do_something is not suppressed
  do_something_interesting_here $1 | 
       grep .           #assure no output returns error
}

If you need pipefail to cancel when the function returns:

example() {
 (   #isolate pipefail in subshell
  set -o pipefail     #assure failure on do_something is not suppressed
  do_something_interesting_here $1 | 
       grep .           #assure no output returns error
 )
}

Upvotes: 1

eloyesp
eloyesp

Reputation: 3285

Following the idea of @William Pursell in the comment it seems that grep can do the trick using something like:

example() {
  do_something_interesting_here $1 | grep "[[:alnum:]]"
}

As mentioned in the comments, it will consume any empty lines (or any line that don't match the regex given).

Upvotes: 1

Related Questions