10

I've got a bash script I'd like to loop over the lines in stdin, or loop over each argument passed in.

Is there a clean way to write this so I don't have to have 2 loops?

#!/bin/bash

# if we have command line args... 
if [ -t 0 ]
then
  # loop over arguments
  for arg in "$@" 
  do
    # process each argument
  done
else
  # loop over lines from stdin
  while IFS= read -r line; do
    # process each line
  done
fi

EDIT: I'm looking for a generic solution that just uses a single loop, as I find I want to do this quite often, but have always wrote out 2 loops and then called a function instead. So maybe something that turns stdin into an array, so I could use a single loop instead?

4
  • 1
    What are you actually trying to accomplish ? Sounds like a typical XY question... Mar 23, 2018 at 13:51
  • What's the problem you're trying to solve? What kind of processing needs to happen in the loop?
    – Kusalananda
    Mar 23, 2018 at 14:15
  • thanks for the feedback! I write lots of little bash scripts that I like to be able to accepts args from the command line, or from stdin. The particular script i'm writing now is one that will concatenate arguments using a delimiter, so I can write both cat /tmp/it | concat and concat a b c and it'd join these arguments together in both cases, trimming each arg and putting a comma between items. e.g. a,b,c.
    – Brad Parks
    Mar 23, 2018 at 14:18
  • Just a note that instead of cat use of < /tmp/it is encouraged. e.g. < /tmp/it | while ... etc.
    – ocodo
    Jul 19, 2022 at 4:56

4 Answers 4

14

Create data for your while read loop:

#!/bin/sh

if [ "$#" -gt 0 ]; then
    # We have command line arguments.
    # Output them with newlines in-between.
    printf '%s\n' "$@"
else
    # No command line arguments.
    # Just pass stdin on.
    cat
fi |
while IFS= read -r string; do
    printf 'Got "%s"\n' "$string"
done

Note that your concat example can be done with the while read loop replaced by tr '\n' ',' or similar.

Also, the -t test says nothing about whether you have command line arguments or not.


Alternatively, to process both command line arguments and standard input (in that order):

#!/bin/sh

{
    if [ "$#" -gt 0 ]; then
        # We have command line arguments.
        # Output them with newlines in-between.
        printf '%s\n' "$@"
    fi

    if [ ! -t 0 ]; then
        # Pass stdin on.
        cat
    fi
} |
while IFS= read -r string; do
    printf 'Got "%s"\n' "$string"
done

Or, using short-cut notation that some people seems to like:

#!/bin/sh

{
    [ "$#" -gt 0 ] && printf '%s\n' "$@"
    [ ! -t 0 ]     && cat
} |
while IFS= read -r string; do
    printf 'Got "%s"\n' "$string"
done
2
  • Note that that changes the behaviour if there are arguments that contain newline characters. Mar 23, 2018 at 15:04
  • @StéphaneChazelas Yes, command line arguments with literal newlines in them will be broken up into multiple items when they arrive at the loop.
    – Kusalananda
    Mar 23, 2018 at 15:07
6

We can also use redirection of standard input:

#!/usr/bin/env bash
test -t 0 && exec < <(printf '%s\n' "$@")
while IFS= read -r line; do
    echo "$line"
done

test with :

test.sh Hello World
test.sh < /etc/passwd
1
  • as requested - succinct!
    – Brad Parks
    Jan 11, 2020 at 23:30
2

With bash specifically, you could do:

if [ -t 0 ]; then
  args=("$@")
else
  readarray -t args
fi
for i in "${args[@]}"; do
   ...
done
1

You can also get access to STDIN descriptor, so:

for i in $(cat /dev/stdin) ; do echo $i ; done

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .