2

I want to make sure my script will work when the user uses a syntax like this:

script.sh firstVariable < SecondVariable

For some reason I can't get this to work.

I want $1=firstVariable And $2=SecondVariable

But for some reason my script thinks only firstVariable exists?

Joey
  • 154
  • 5
  • 18
  • 3
    `<` is the redirection operator. It connects the `stdin` of your script to file `SecondVariable`. – choroba Jan 05 '16 at 11:47
  • Why is the `<`? parameters are separated by space when passed to an executable on the command line. – marekful Jan 05 '16 at 11:48
  • So how do i make stdin of SecondVariable equal to $2? – Joey Jan 05 '16 at 11:51
  • @marekful, the space is allowed. I don't consider it good form, but it's entirely valid syntax. – Charles Duffy Jan 05 '16 at 18:00
  • @Joey, what does "stdin of SecondVariable equal to $2" even mean? "SecondVariable" is, in the context of this command line, a filename; you can of course access the file's contents, by reading from your script's stdin. – Charles Duffy Jan 05 '16 at 18:34

2 Answers2

5

This is a classic X-Y problem. The goal is to write a utility in which

utility file1 file2

and

utility file1 < file2

have the same behaviour. It seems tempting to find a way to somehow translate the second invocation into the first one by (somehow) figuring out the "name" of stdin, and then using that name the same way as the second argument would be used. Unfortunately, that's not possible. The redirection happens before the utility is invoked, and there is no portable way to get the "name" of an open file descriptor. (Indeed, it might not even have a name, in the case of other_cmd | utility file1.)

So the solution is to focus on what is being asked for: make the two behaviours consistent. This is the case with most standard utilities (grep, cat, sort, etc.): if the input file is not specified, the utility uses stdin.

In many unix implementations, stdin does actually have a name: /dev/stdin. In such systems, the above can be achieved trivially:

utility() {
  utility_implementation "$1" "${2:-/dev/stdin}"
}

where utility_implementation actually does whatever is required to be done. The syntax of the second argument is normal default parameter expansion; it represents the value of $2 if $2 is present and non-empty, and otherwise the string /dev/stdin. (If you leave out the - so that it is "${2:/dev/stdin}", then it won't do the substitution if $2 is present and empty, which might be better.)

Another way to solve the problem is to ensure that the first syntax becomes the same as the second syntax, so that the input is always coming from stdin even with a named file. The obvious simple approach:

utility() {
  if (( $# < 2 )); then
    utility_implementation "$1"
  else
    utility_implementation "$1" < "$2"
  fi
}

Another way to do this uses the exec command with just a redirection to redirect the shell's own stdin. Note that we have to do this inside a subshell ((...) instead of {...}) so that the redirection does not apply to the shell which invokes the function:

utility() (
  if (( $# > 1 )) then; exec < "$2"; fi
  # implementation goes here. $1 is file1 and stdin
  # is now redirected to $2 if $2 was provided.
  # ...
)
Community
  • 1
  • 1
rici
  • 234,347
  • 28
  • 237
  • 341
1

To make the stdin of the second variable the final argument to the script(so if you have one arg then < second arg, it will be the second), you can use the below

#!/bin/bash

##read loop to read in stdin
while read -r line
do

  ## This just checks if the variable is empty, so a newline isn't appended on the front
  [[ -z $Vars ]] && Vars="$line" && continue

  ## Appends every line read to variable
  Vars="$Vars"$'\n'"$line"

  ## While read loop using stdin
done < /dev/stdin

 ##Set re-sets the arguments to the script to the original arguments and then the new argument we derived from stdin
set - "$@" "$Vars"

## Echo the new arguments
echo "$@"
123
  • 10,778
  • 2
  • 22
  • 45
  • wow this is great but `echo "$@"` of course echos every line of $2 instead of just the file name (it kind off works like cat now) – Joey Jan 05 '16 at 12:12
  • @joey if you want it to just read as the filename why do you want to use stdin ? – 123 Jan 05 '16 at 12:29
  • I have to it's for an exercise for school. The program should work when i use either script.sh var1 var2 or script.sh var1 < var2 – Joey Jan 05 '16 at 13:24
  • @joey Tell your teacher it's a terrible exercise and has no use at all. – 123 Jan 05 '16 at 13:44
  • 1
    @Joey You can't (well you can but not portably I don't think) get the *name* of what's attached to stdin. Nor would doing that be of any real use. What's the *actual* question that you think requires this? What does the script need to do? – Etan Reisner Jan 05 '16 at 13:47
  • Assuming bash 4, `readarray -t lines; IFS=$'\n'; Vars="$*"` is a much terser formulation of that loop. – Charles Duffy Jan 05 '16 at 18:02
  • 1
    BTW, what do you expect `< /dev/stdin` to do that isn't already the case by default? – Charles Duffy Jan 05 '16 at 18:04
  • 1
    @CharlesDuffy: One might also be tempted to write `lines=$(cat)` although `cat` is not builtin. Or `lines=$( – rici Jan 05 '16 at 19:35
  • @CharlesDuffy I don't have bash 4 unfortunately, also, i thought it made it clearer where the data was coming from. Terseness is not always best for examples for beginners. I think the downvotes are a tad harsh. – 123 Jan 06 '16 at 08:31
  • @123, the downvote was not related to anything in my comment, but rather that the approach as a whole -- including my `readarray` alternative implementation of that approach -- is objectionable: I would *strenuously* object if anyone submitted this kind of hack to a codebase I maintained, as opposed to using one of the several more idiomatic approaches for handling input from multiple sources given in rici's answer. – Charles Duffy Jan 06 '16 at 16:06