funwithlinux blog

How to Dynamically Redirect Standard Input in a Bash Script Without Duplicating Commands

In Bash scripting, standard input (stdin) redirection is a powerful tool for controlling where a command reads input from—whether from a file, terminal, or another process. However, a common challenge arises when scripts need to handle dynamic input sources (e.g., optional input files or user input) without duplicating code. For example, if your script should process input from a file if provided, or from stdin otherwise, you might be tempted to write separate code paths for each case. This leads to duplicated commands, which are harder to maintain and error-prone.

This blog post explores techniques to dynamically redirect stdin in Bash scripts while keeping your code DRY (Don’t Repeat Yourself). We’ll cover practical methods, examples, and best practices to handle dynamic input sources cleanly and efficiently.

2025-12

Table of Contents#

  1. Understanding Standard Input in Bash
  2. The Problem: Duplicating Commands with Static Redirection
  3. Dynamic Input Redirection: Solutions
  4. Advanced Techniques and Best Practices
  5. Conclusion
  6. References

1. Understanding Standard Input in Bash#

Before diving into dynamic redirection, let’s recap how stdin works in Bash:

  • Standard Input (stdin) is a stream of data provided to a command, typically via the terminal (keyboard) by default. It is assigned file descriptor (fd) 0.
  • Redirection allows you to change the source of stdin using the < operator. For example:
    # Read input from "data.txt" instead of the terminal  
    grep "pattern" < data.txt  
  • Many commands accept - as a special argument to indicate "read from stdin." For example:
    # Read from stdin (equivalent to not using < at all)  
    grep "pattern" -  

2. The Problem: Duplicating Commands with Static Redirection#

Consider a script that processes input from a file (if provided) or stdin (if not). A naive approach might check for the presence of a file argument and duplicate the processing command:

#!/bin/bash  
 
# Naive approach: Duplicates the "process_data" command  
if [ $# -ge 1 ]; then  
  # Case 1: Input from file  
  process_data < "$1"  # Duplicated code  
else  
  # Case 2: Input from stdin  
  process_data         # Duplicated code  
fi  

This works, but it duplicates the process_data command. If process_data is complex (e.g., a long pipeline with flags), updating it requires changing both instances—risking inconsistencies and bugs.

3. Dynamic Input Redirection: Solutions#

The goal is to define the input source once and reuse it across a single command. Below are three robust methods to achieve this.

Method 1: Use a Variable with - for Stdin#

The simplest approach is to store the input source in a variable, using - as a placeholder for stdin. Most commands interpret - as "read from stdin," so you can redirect from this variable unconditionally.

How It Works:#

  • If a file argument is provided, set input_source to the file path.
  • If no argument is provided, set input_source to - (stdin).
  • Redirect the command from input_source once.

Example Script:#

#!/bin/bash  
 
# Set input source: use first argument, or "-" (stdin) if none  
input_source="${1:--}"  
 
# Validate: If input is not stdin, check if the file exists  
if [ "$input_source" != "-" ] && [ ! -f "$input_source" ]; then  
  echo "Error: File '$input_source' does not exist." >&2  
  exit 1  
fi  
 
# Process input ONCE (no duplication!)  
process_data < "$input_source"  

Key Notes:#

  • ${1:--} is Bash parameter expansion: If $1 is empty/unset, use -.
  • Works with most commands (e.g., grep, awk, sed) that accept - for stdin.
  • Portability: Supported in Bash, Zsh, and modern shells. For POSIX compliance, replace - with /dev/stdin (Linux) or /dev/fd/0 (BSD/macOS), but - is more widely compatible.

Method 2: Conditional Redirection with exec in a Subshell#

The exec command can redirect file descriptors for the current shell. By wrapping exec in a subshell, you can conditionally redirect stdin (fd 0) without affecting the parent script.

How It Works:#

  • Use a subshell (...) to isolate the redirection.
  • If a file is provided, redirect stdin (fd 0) to the file using exec 0< "$file".
  • Run the processing command once (it will read from the redirected stdin).

Example Script:#

#!/bin/bash  
 
process_data() {  
  # Example processing: count lines and print the first 3  
  wc -l && head -n 3  
}  
 
# Use a subshell to isolate redirection  
(  
  if [ $# -ge 1 ]; then  
    # Validate file  
    if [ ! -f "$1" ]; then  
      echo "Error: '$1' is not a file." >&2  
      exit 1  
    fi  
    # Redirect stdin to the file within the subshell  
    exec 0< "$1"  
  fi  
 
  # Process input (reads from redirected stdin)  
  process_data  
)  

Key Notes:#

  • The subshell (...) ensures the exec redirection only affects commands inside the subshell.
  • Original stdin remains intact outside the subshell, making this safe for multi-step scripts.

Method 3: Temporary File Descriptors (Preserve Original Stdin)#

If your script needs to preserve the original stdin (e.g., for user prompts later), use a temporary file descriptor (e.g., fd 3) to avoid modifying fd 0.

How It Works:#

  • Open the input file on a temporary fd (e.g., 3) if provided; otherwise, point the temporary fd to stdin (fd 0).
  • Redirect the processing command’s stdin from the temporary fd.

Example Script:#

#!/bin/bash  
 
process_data() {  
  # Example: Read input and prompt user for confirmation  
  echo "Processing input:"  
  cat  # Read from stdin (will be redirected from temp fd)  
  read -p "Continue? [y/N] " -r  # Reads from ORIGINAL stdin (terminal)  
}  
 
input_fd=0  # Default to stdin (fd 0)  
 
if [ $# -ge 1 ]; then  
  # Validate file  
  if [ ! -f "$1" ]; then  
    echo "Error: '$1' is not a file." >&2  
    exit 1  
  fi  
  # Open file on fd 3  
  exec 3< "$1"  
  input_fd=3  # Use fd 3 as input source  
fi  
 
# Redirect stdin from input_fd for process_data  
process_data <& "$input_fd"  
 
# Cleanup: Close fd 3 if it was opened  
if [ "$input_fd" -eq 3 ]; then  
  exec 3<&-  
fi  

Key Notes:#

  • process_data <& "$input_fd" redirects stdin (fd 0) from input_fd (3 or 0).
  • Original stdin (terminal) remains accessible for read -p because we didn’t modify fd 0.

4. Advanced Techniques and Best Practices#

Handling Multiple Input Files#

To process multiple input sources (e.g., a list of files or stdin), loop over the input sources and apply dynamic redirection per file:

#!/bin/bash  
 
# Process each input source (file or stdin)  
for input_source in "${@:--}"; do  # Default to "-" (stdin) if no args  
  if [ "$input_source" != "-" ] && [ ! -f "$input_source" ]; then  
    echo "Skipping invalid file: $input_source" >&2  
    continue  
  fi  
  echo "=== Processing $input_source ==="  
  process_data < "$input_source"  # Reuse the same command  
done  

Error Handling#

  • Always validate file existence before redirecting to avoid "No such file or directory" errors.
  • Use set -o errexit to exit the script if a redirection fails:
    set -o errexit  # Exit on any error  
    if [ -n "$input_file" ]; then  
      exec 0< "$input_file"  # Fails if file doesn't exist, script exits  
    fi  

Portability#

  • Avoid relying on /dev/stdin for non-Linux systems (e.g., macOS uses /dev/fd/0). Use - instead, as it’s supported by most commands.
  • For POSIX-compliant scripts (not just Bash), avoid Bash-specific features like ${1:--}. Use a conditional instead:
    input_source="-"  
    if [ $# -ge 1 ]; then  
      input_source="$1"  
    fi  

5. Conclusion#

Dynamic stdin redirection eliminates code duplication and makes scripts more maintainable. Choose the method that best fits your use case:

  • Use Method 1 (variable with -) for simple scripts needing a single input file or stdin.
  • Use Method 2 (exec in a subshell) for full control over redirection without affecting the parent script.
  • Use Method 3 (temporary file descriptors) when you need to preserve the original stdin (e.g., for user input).

By adopting these techniques, you’ll write cleaner, more resilient Bash scripts.

6. References#