2

My bash script sources a script file (call it file2.sh) according to an argument. (It is either sourced or not.) The script file2.sh contains a function "foo" (call it a modified or improved version of foo).

The script also sources another file (file1.sh) which contains the original function "foo". This one (file1.sh) is always sourced (and it has other functions that are required).

I need the function "foo" in file2.sh to override the "foo" in file1.sh (if file2.sh is sourced).

Furthermore, I need to do this in any scripts that my main script calls. Several of those called script files also source file1.sh. They are expecting the original "foo" function. But I need to make them call the improved "foo" without modifying them.

In other words, if my main script includes file2.sh, I want any script (which is called by my main script) that sources file1.sh to use "foo" from file2.sh instead. And I can't change any of the other files except my main script. (Or if I do change them, I need them to work correctly when file2.sh is not sourced by the main script.)

  • Why do your files have .sh extensions if they are bash scripts? Are they sh or bash scripts? – Stéphane Chazelas Aug 3 '13 at 9:19
  • @Stephane Chazelas: These are bash scripts. What is the right file extension? And what is wrong with the ".sh" extension? – MountainX Aug 3 '13 at 19:22
  • If they are not written in sh syntax, I'd find using a .sh extension misleading. If using bash specific syntax, a .bash extension would make more sense. bash and shells in general don't care anyway how the files are named, it's just a cosmetic issue. – Stéphane Chazelas Aug 3 '13 at 20:42
4

You can flag your function as 'read only' in file2.sh.

Note: this will cause warnings when file1.sh later tries to define (redefine) the function.

Those warnings will appear on stderr and could cause trouble. I don't know if they can be disabled.

Further note: this MIGHT cause the scripts to fail, if they are checking the return status of the function definition. I think that there is also a bash option that can be set that would cause a non-zero return status anywhere to abort execution of the script. Good luck!

Can you modify file1.sh? Simply using conditionals to check if the function is defined before defining it would be a more robust solution.

Here is an example of usage of 'readonly'.

hobbes@metalbaby:~/scratch$ mkdir bash-source-test
hobbes@metalbaby:~/scratch$ cd bash-source-test
hobbes@metalbaby:~/scratch/bash-source-test$ cat > file2.sh
#!/bin/bash
fname(){
  echo "file2"
}

readonly -f fname
hobbes@metalbaby:~/scratch/bash-source-test$ cat > file1.sh
#!/bin/bash
fname(){
  echo "file1"
}

readonly -f fname
hobbes@metalbaby:~/scratch/bash-source-test$ cat >top.sh
#!/bin/bash 
if [ $1 ]; then
source file2.sh
fi  
source file1.sh

fname
hobbes@metalbaby:~/scratch/bash-source-test$ chmod +x *.sh
hobbes@metalbaby:~/scratch/bash-source-test$ ./top.sh 
file1
hobbes@metalbaby:~/scratch/bash-source-test$ ./top.sh hello
file1.sh: line 4: fname: readonly function
file2
hobbes@metalbaby:~/scratch/bash-source-test$ 
  • both of your suggestions are very helpful. I will experiment with both of them. – MountainX Aug 3 '13 at 1:36
  • can you give an example of using conditionals to check if the function is defined before defining it? I'd like to try that approach. (I'll also google around and see if I find an example.) – MountainX Aug 3 '13 at 1:38
  • @MountainX, see stackoverflow.com/questions/85880/… for various techniques for determining if a given name exists as a function. – daveloyall Aug 12 '13 at 14:45
  • A recent edit removed the bash warning message regarding the attempt to alter a readonly function. I rolled back the edit because the purpose of my answer is to demonstrate the mechanics of read-only functions. – daveloyall Jan 15 '15 at 18:57
1

If those scripts are indeed interpreted by bash (and not in sh emulation mode), and if file1.sh defines but doesn't use foo, you could redefine the source and . commands in such a way that upon sourcing file1.sh, it makes sure file1.sh doesn't redefine foo, for instance by aliasing it away to something else:

$ cat file1.sh
foo() { echo original foo; }
$ cat file2.sh
foo() { echo new foo; }
$ cat main.sh
file2_sourced=false
source() {
  case ${1##*/} in
    (file2.sh)
      file2_sourced=true;;
    (file1.sh)
      if "$file2_sourced"; then
        alias foo=original_foo

        builtin source "$@"; local "ret=$?"
        unalias foo
        return "$ret"
      fi;;
  esac
  builtin source "$@"
}
.() { source "$@"; }

source file2.sh
source file1.sh

foo
original_foo

$ bash main.sh
new foo
original foo

Or possibly more appropriate:

source() {
  shopt -s expand_aliases
  if [ "${1##*/}" = file1.sh ] && command -v foo > /dev/null 2>&1; then
    # foo already defined, make file1.sh define original_foo this time
    alias foo=original_foo
    builtin source "$@"; local "ret=$?"
    unalias foo
    return "$ret"
  else
    builtin source "$@"
  fi
}

That is: only prevent file1.sh from re-defining foo.

Now, if those other scripts that main.sh calls are executed as opposed to being sourced, you'd need to export those source and . functions in main.sh:

export -f source .

Actually, it's also possible to do it without modifying main.sh, by calling it as:

env BASHOPTS=expand_aliases source='() {
  if [ "${1##*/}" = file1.sh ] && command -v foo > /dev/null 2>&1; then
    # foo already defined, make file1.sh define original_foo this time
    alias foo=original_foo
    builtin source "$@"; local "ret=$?"
    unalias foo
    return "$ret"
  else
    builtin source "$@"
  fi
}' .='() { source "$@"; }' bash main.sh
  • Isn't simply sourcing the files in order as in my answer enough? I have the sneaky suspicion that it isn't since both you and @daveloyall chose much more complex routes. – terdon Aug 3 '13 at 15:34
  • @terdon, my understanding was that the OP had no control on the order the file1.sh and file2.sh were sourced and they could be sourced multiple time from scripts he could not modify (why we don't know). – Stéphane Chazelas Aug 3 '13 at 17:13
1

I have never written anything but small bash scripts for basic sysadmin and automation tasks so I have very little experience with sourcing functions from other files. This may therefore be very naive or not portable or something but, at least on my system and in most programming languages I know, if you redefine something it stays redefined.

In other words, as long as you make sure you source file1.sh before file2.sh and export the function afterwards, you should always be exporting the function from file2.sh if it has been loaded and from file1.sh if it has not:

$ cat file1.sh 
#!/bin/bash
fname(){
  echo "BBBBB"
}

$ cat file2.sh 
#!/bin/bash
fname(){
  echo "CCCCC"
}

$ cat foobar.sh 
#!/bin/bash
source file1.sh
if [ $1 ]; then 
    source file2.sh;
fi
export -f fname;
./run_function.sh

$ cat run_function.sh 
#!/bin/bash
fname

Any scripts called (eg ./run_function.sh) will have whichever fname was defined last, so they will load the one from file2.sh if it has been sourced and from file.sh if it has not:

$ ./foobar.sh
BBBBB
$ ./foobar.sh hello
CCCCC
0

I think probably the easiest answer here is to get the function out of those script files.

#file[12]
#foo() { : removed; }
. ./foo.fn
#and rest of script

#foo.fn
foo() { : old version is always defined; }
[ -z "${NEWFOO:+1}" ] ||
foo() { : new version defined only if $NEWFOO; }

#your caller script
export NEWFOO=1
case $srcarg in (./file1) unset NEWFOO;; esac
#or however ^that part^ is usually done
. "$srcarg"
#and rest of script

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.