3

A script on AIX which checks the number of parameters passed and complains if they are not correct:-

if [ "$#" -ge 1 ]; then
  ...
else
  echo 'Usage: myscript <a> [b] [c]'
fi

The script sets some environment variables so it is sourced. By that I mean I type the following on my command line. There is no second script, I am not sourcing a script from within another script.

. ./myscript.sh

If I pass it no parameters, I expect it to print out the usage statement but instead it runs using the parameters passed to the previous command that was sourced (not necessarily even this script!).

To double check this I added this line at the beginning of my script to prove that this was the case.

echo $# $@

If I pass it no parameters, it prints out the number and the list of parameters from a previously sourced script.

How do I get it to say $# is zero when I pass it zero parameters and source it?

The smallest possible recreate is a script thus:-

myscript.sh

echo $# $@

and then run it thus:-

. ./myscript.sh a b c

which prints out

3 a b c

then run it again without any parameters thus:-

. ./myscript.sh

which prints out the same

3 a b c

when you would expect it to print out

0

If you don't source the script and just run it thus:-

./myscript.sh

then it works as expected and prints out

0

I hope that clarifies the issue at hand.

Since I seem to be getting lots of answers to explain why the above doesn't work and no answers to tell me what I should do instead, I thought I would have one more attempt at trying to explain what I am trying to do.

Requirement

I need to write a script that expects at least one parameter and complains if it is not invoked with a said parameter (see the first block of code in my question above), and this script needs to set some environment variables. Because of this second requirement I have assumed that I need to source the script so that the environment variables are still set when the script completes. How can you write a script that can tell when it has not been passed parameters that is also able to set an environment variable?

  • Possible duplicate of Passing variables to a bash script when sourcing it – Patrick May 3 '18 at 12:05
  • 3
    This should work. Which shell are you using? Post a small, complete pair of scripts (the sourcing script and the sourced script) that reproduces the problem. – Gilles May 3 '18 at 12:25
  • @Patrick That's not a duplicate: it explains that what Morag is trying to do should work. – Gilles May 3 '18 at 12:26
  • @Gilles - I don't understand what you mean by a "pair of scripts". There is only one script which I run from the command line with the command shown in my question . ./myscript.sh. If I don't provide any parameters, e.g. . ./myscript.sh abc then the script runs with parameters from a previously sourced command. – Morag Hughson May 3 '18 at 21:32
  • I seem to be getting lots of explanations about why what I have done doesn't work, but no answers to tell me what I should do instead. Perhaps I didn't ask the question very well. I have edited my question to try and make it clearer what I am trying to do. Perhaps then someone can tell me how I should have coded my script. – Morag Hughson May 5 '18 at 0:55
3

In

. path/to/script at least one argument

As others have said, depending on the shell, those extra arguments are either ignored (Bourne, Almquist) or replace the current set of positional parameters ($@, $1, $2...) for the interpretation of that script (most other shells). POSIX doesn't specify the behaviour when extra arguments are passed to .. In that case, there is also some variation between shells as to whether any change to the positional parameters (like with set a b) made within the script will remain after . finishes.

In:

. path/to/script

however, the behaviour is specified. The positional parameters are required not to be affected, so will stay the same as before for the interpretation of the script.

If you want the positional parameters to be an empty list during the interpretation of that script, you'll need to reset it before hand, or use a function:

set -- # or shift "$#"
. path/to/script

(though the positional parameters are lost). Or use a function:

dot() { file=$1; shift; . "$file"; }
dot path/to/myscript

At the time the . command is executed, the positional parameters will be that of the function. After the shift, it will be the arguments passed to the function minus the first one, so in the above, an empty list; but that means that with that function you can portably pass an arbitrary list of positional parameters to the sourced script as in:

dot path/to/myscript extra args

Where those extra args will be available as $1 and $2 within the sourced script.

And if myscript calls set to modify the list of positional parameters, that will only affect the function's positional parameters, and those changes will not persist after the function returns.

  • Thank you for your answer. I seem to be getting lots of explanations about why what I have done doesn't work, but no answers to tell me what I should do instead. Perhaps I didn't ask the question very well. I will edit my question to try and make it clearer what I am trying to do. Perhaps then someone can tell me how I should have coded my script. – Morag Hughson May 5 '18 at 0:46
  • I've read and re-read your answer a few times now and I'm struggling to understand how to put it into practice. The reason I want to test whether the correct parameters were passed to the script is because I can't trust the user running it to do the correct thing. If they have to type another command prior to running it, I can't trust them to do that either. With the function - is this something inside my script that will somehow fix the way the parameters are given to the script? – Morag Hughson May 7 '18 at 9:37
  • 1
    @MoragHughson, the gist of it is that you cannot pass arbitrary lists of arguments to a sourced script on the command line (. script arbitrary list), so you need to document a different way to pass arguments to your script, for instance using that function trick, or using a dedicate variable instead of positional arguments. – Stéphane Chazelas May 7 '18 at 9:41
  • What you just wrote should be an answer. That was the impression I have been getting from all these complicated explanations that I didn't understand how they solved my problem. But no-one was saying it explicitly. If you write that as an answer I will accept it. Or put that as the first sentence in this answer. – Morag Hughson May 7 '18 at 9:47
2

Bash's help text for .:

.: . filename [arguments]
    Execute commands from a file in the current shell.

    Read and execute commands from FILENAME in the current shell.  The
    entries in $PATH are used to find the directory containing FILENAME.
    If any ARGUMENTS are supplied, they become the positional parameters
    when FILENAME is executed.

Note the phrasing: "if any arguments are supplied". Without arguments, the current positional parameters stay.

$ cat /tmp/sourceme.sh     
printf "%d: " "$#"
printf "<%s> " "$@"
printf "\n"
$ bash -c 'set -- aa bb; . /tmp/sourceme.sh'
2: <aa> <bb> 
$ bash -c 'set -- aa bb; . /tmp/sourceme.sh foo bar'
2: <foo> <bar> 

All other shells I tested (zsh, ksh) behave similarly. Except for dash, which always leaves the original positional parameters in place.

$ dash -c 'set -- aa bb; . /tmp/sourceme.sh foo bar'
2: <aa> <bb> 

I can't see any mention about resetting the positional parameters in the POSIX definition of . so it would seem that giving arguments to . is nonstandard to begin with.


Instead of a sourced script, you could use a function, as they can accept arguments in a standard shell. In this case you'd source a file that defines the function.

E.g. a file (define_fun.sh) with the function definition:

#!/bin/sh
f() {
    echo "args: $# $@"     # just a test
    if [ "$#" = 0 ]; then 
        echo please give at least one argument
        return 1
    fi;
    ENVVAR=$1          # this should be visible to the whole shell  
    export ENVVAR      # and any commands called later
} 

And then use it:

$ . define_fun.sh      # source the file to load the func in the current shell

$ f                    # actually run it: this'll complain
$ f 123 456            # this should set ENVVAR
$ echo $ENVVAR         # and this will print '123'

You could instead put the function definition in one of the shell's initialization files (~/.profile? Depends on the shell.)

  • Thank you for your answer - are you telling me that I need to use this -c 'set -- construct for it to work? I'm a little confused by whether your answer is a solution for me or an explanation of why it will never work? I have updated my original question with a few more examples to hopefully clear up any confusion about what I was asking. – Morag Hughson May 3 '18 at 21:41
  • @MoragHughson, it's an explanation of why you don't get the result you expect: you're expecting behaviour that's not in the standard, and which your shell apparently doesn't implement. The set -- a b is there to set the positional parameters of the shell sourcing the script, just so that we can see what values come from the outside of the sourced script. – ilkkachu May 4 '18 at 9:49
  • @MoragHughson, the additions you made to the question don't show what the positional parameters are before you used . myscript.sh, so we can't tell what happens. I would expect that they contain a, b, c, before you called . myscript.sh the first time, so that's what you get if your shell doesn't support giving arguments to .. – ilkkachu May 4 '18 at 9:52
  • I do apologise, I thought I had written that I ran the script like this:- . ./myscript a b c and then I ran the script a second time like this:- . ./myscript and on that second invocation I got the parameters from the first invocation of the script when I expected to get zero. – Morag Hughson May 5 '18 at 0:35
  • Perhaps part of my problem is I don't understand where this -c set -- operator comes into the picture? I'm not using that anywhere. I don't know what it is for. I'm just passing parameters into my script by typing them after the script file name. – Morag Hughson May 5 '18 at 0:40
0

Passing arguments to the dot builtin command is a Korn Shell extension that is not granted to work in other shells.

Since the POSIX standard does not mention $# and $@ for the dot command, the arguments to the current shell are visible to the dot command, so if you call your shell e.g. as:

myshell -s a b c

and then call

. ./myscript.sh

this is expected to print

3 a b c
  • 1
    I think they are not passing arguments to dot yet the dotted script is picking up the existing args (which OP didn’t expect) – Jeff Schaller May 3 '18 at 17:46
  • When I pass parameters to my script, e.g. . ./myscript.sh a b c it works fine. My question is about when I don't pass parameters. – Morag Hughson May 3 '18 at 21:43
  • pubs.opengroup.org/onlinepubs/9699919799/utilities/… is the official standard for the command. From a POSIX view, $# is expected to be 0 in all cases (unless the shell implements an extension. In special, in your second example, $# is expected to be 0, except if you called your shell ac $shell -c 1a b c. – schily May 4 '18 at 10:06
  • I seem to be getting lots of explanations about why what I have done doesn't work, but no answers to tell me what I should do instead. Perhaps I didn't ask the question very well. I have edited my question to try and make it clearer what I am trying to do. Perhaps then someone can tell me how I should have coded my script. – Morag Hughson May 5 '18 at 0:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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