5

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 "$_"; } }
1
  • 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. Oct 9, 2013 at 14:50

5 Answers 5

8

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.

3
  • This behaves more like the perl script, opening new files or reopening stdin and stdout with new file descriptors. Oct 9, 2013 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. Oct 9, 2013 at 16:29
  • Agreed, that is useful. I was looking for something idiomatic that I could use for convenience to 'wrap' some scripts. Oct 9, 2013 at 22:09
3

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.

1
  • I considered using /dev/std{in|out|err}, just hoping for something more clever... Oct 9, 2013 at 22:07
1

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  
1
  • I'm looking more for an idiom that reopens stdin and stdout when filenames are given. Oct 9, 2013 at 14:53
1

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.

0

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.

You must log in to answer this question.

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