Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

I want to handle filenames as arguments in a bash script in a cleaner, more flexible way, taking 0, 1, or 2 arguments for input and output filenames.

  • when args = 0, read from stdin, write to stdout
  • when args = 1, read from $1, write to stdout
  • when args = 2, read from $1, write to $2

How can I make the bash script version cleaner, shorter?

Here is what I have now, which works, but is not clean,

#!/bin/bash
if [ $# -eq 0 ] ; then #echo "args 0"
    fgrep -v "stuff"
elif [ $# -eq 1 ] ; then #echo "args 1"
    f1=${1:-"null"}
    if [ ! -f $f1 ]; then echo "file $f1 dne"; exit 1; fi
    fgrep -v "stuff" $f1 
elif [ $# -eq 2 ]; then #echo "args 2"
    f1=${1:-"null"}
    if [ ! -f $f1 ]; then echo "file $f1 dne"; exit 1; fi
    f2=${2:-"null"}
    fgrep -v "stuff" $f1 > $f2
fi

The perl version is cleaner,

#!/bin/env perl
use strict; 
use warnings;
my $f1=$ARGV[0]||"-";
my $f2=$ARGV[1]||"-";
my ($fh, $ofh);
open($fh,"<$f1") or die "file $f1 failed";
open($ofh,">$f2") or die "file $f2 failed";
while(<$fh>) { if( !($_ =~ /stuff/) ) { print $ofh "$_"; } }
share|improve this question
    
The perl example either opens new files or reopens stdin and stdout. But I would prefer something that reopened stdin and stdout with filename arguments, when given. –  ChuckCottrill Oct 9 '13 at 14:50
add comment

5 Answers

up vote 5 down vote accepted

I'd make heavier use of I/O redirection:

#!/bin/bash
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
[[ $1 ]] && exec 3<$1 || exec 3<&0
[[ $2 ]] && exec 4>$2 || exec 4>&1
fgrep -v "stuff" <&3 >&4

Explanation

  • [[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1

    Test if an input file has been specified as a command line argument and if the file exists.

  • [[ $1 ]] && exec 3<$1 || exec 3<&0

    If $1 is set, i.e. an input file has been specified, the specified file is opened at file descriptor 3, otherwise stdin is duplicated at file descriptor 3.

  • [[ $2 ]] && exec 4>$2 || exec 4>&1

    Similarly if the $2 is set, i.e. an output file has been specified, the specified file is opened at file descriptor 4, otherwise stdout is duplicated at file descriptor 4.

  • fgrep -v "stuff" <&3 >&4

    Lastly fgrep is invoked, redirecting its stdin and stdout to the previously set file descriptors 3 and 4 respectively.

Reopening standard input and output

If you'd prefer not to open intermediate file descriptors, an alternative is to replace the file descriptors corresponding to stdin and stdout directly with the specified input and output files:

#!/bin/bash
[[ $1 ]] && [[ ! -f $1 ]] && echo "file $1 dne" && exit 1
[[ $1 ]] && exec 0<$1
[[ $2 ]] && exec 1>$2
fgrep -v "stuff"

A drawback with this approach is that you loose the ability to differentiate output from the script itself from the output of the command which is the target for the redirection. In the original approach, you can direct script output to the unmodified stdin and stdout, which in turn might have been redirected by the caller of the script. The specified input and output files could still be accessed via the corresponding file descriptors, which are distinct from the script stdin and stdout.

share|improve this answer
    
This behaves more like the perl script, opening new files or reopening stdin and stdout with new file descriptors. –  ChuckCottrill Oct 9 '13 at 14:51
1  
@ChuckCottrill Updated the answer with an example of reopening stdin and stdout. Personally, I'd generally still prefer the original approach for the reasons explained in the updated answer. –  Thomas Nyman Oct 9 '13 at 16:29
    
Agreed, that is useful. I was looking for something idiomatic that I could use for convenience to 'wrap' some scripts. –  ChuckCottrill Oct 9 '13 at 22:09
add comment

How about:

  input="${1:-/dev/stdin}"
  output="${2:-/dev/stdout}"
  err="${3:-/dev/stderr}"

  foobar <"$input" >"$output" 2>"$err"

You should note that /dev/std(in|out|err) are not in the POSIX standard so this will only work on systems that support these special files.

This also assumes sane input: it doesn't check for the existence of files before redirecting.

share|improve this answer
    
I considered using /dev/std{in|out|err}, just hoping for something more clever... –  ChuckCottrill Oct 9 '13 at 22:07
add comment

if you don't mind that the output is always redirected to stdout, you can use the following one-liner:

cat $1 |fgrep -v "stuff" | tee  
share|improve this answer
    
I'm looking more for an idiom that reopens stdin and stdout when filenames are given. –  ChuckCottrill Oct 9 '13 at 14:53
add comment

You can use exec <foo to redirect the script's input from the file foo, and likewise exec >foo for the output.

#!/bin/sh
set -e
if [ $# -ne 0 ]; then
  exec <"$1"
  shift
fi
if [ $# -ne 0 ]; then
  exec >"$1"
  shift
fi
fgrep -v "stuff"

This style is terse but does not lend itself well to detailed error reporting.

share|improve this answer
add comment

I don't know if this is 'cleaner', but here are some suggestions (this is not tested code). The use of exec (per Thomas Nyman) can lead to safety issues and should be treated with care.

First place repetitive code in a function.

# die <message>
function die(){
    echo "Fatal error: $1, exiting ..." >&2
    exit 1
}

# is_file <file-path>
function is_file(){
    [[ -n "$1" && -f "$1" ]] && return 0
    die 'file not found'
}

Here, instead of using fgrep, cat is your friend. Then use select case:

case $# in
    0) cat ;;                                  # accepts stdin to stdout.
    1) is_file "$1"; cat "$1" ;;               # puts $1 to stdout.
    2) is_file "$1"; cat "$1" > "$2" ;;        # puts $1 to $2.
    *) die 'too many arguments' ;;
esac

Another alternative (that is clean and very compact) is to load the instructions in an array, and then access it via the value of $#, something like a function pointer. Given the function is_file above, the Bash code is something like:

# action array.
readonly do_stuff=(
    'cat'                                  # 0 arg.
    'is_file \"$1\"; cat \"$1\"'           # 1 arg.
    'is_file \"$1\"; cat \"$1\" > \"$2\";' # 2 args.
)

# Main - just do:
[[ $# -le 2 ]] && ${do_stuff[$#]} || die 'too many arguments' 

I'm not 100% on the syntax, but the double quotes need to be escaped. Best to double quote variables that contain file paths.

An added note, when writing to $2 - should probably check that file does not exist or it will be overwritten.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.