Question

I would like to write a set of shell scripts which would execute smoothly on Mac disregarding where they are called from.

I am currently struggling about how to implement this.

On windows, I could write three batch scripts

A.bat in folder A B.bat in folder Subfolder, which is subfolder of A and which path is A/Subfolder C.bat in folder Subfolder, which is subfolder of A and which path is A/Subfolder

In A.bat I could write

call Subfolder/B.bat

in B.Bat I could write

call C.bat

and both scripts A and B would execute successfully.

On Mac, however, I have to prepend ./ to every script so that it is recognized as script.

So when I write in a.sh code:

Subfolder/b.sh

and then in b.sh code

./c.sh

when I execute ./b.sh from terminal it is executed successfully.

However, when I execute ./a.sh from a parent folder in terminal, it fails with error

./a.sh
Subfolder/b.sh: line 1: ./c.sh: No such file or directory

Is this possible on Mac to write a code in b.sh which would execute disregarding if it is called from the current folder or from the parent folder?

Was it helpful?

Solution

There are two concepts relevant to understand what's going on here:

  • when running binaries or shell scripts as name-of-executable, the shell looks into $PATH for the list of directories the binary/shell script could be stored it. The first match found is then used to run the binary/script, if no match is found you get an error message. If instead you run it as ./name-of-executable or /path/to/executable $PATH is not searched but the path is taken either relative to the current directory (if it starts with ./ or ../) or absolute
  • each process (including each shell script) has a default directory it runs in and which gets inherited to any child processes started

So in your case if you run ./a.sh the current directory remains the one a.sh is stored in even when subfolder/b.sh is running, which then lets ./c.sh fail.

The easy way out of this to always change directories before calling child processes (and of course changing back afterwards). So in a.sh you would write

cd subfolder
./b.sh
cd ..

which would allow b.sh to call ./c.sh within the same subfolder without problems.

OTHER TIPS

I'll claim there are three relevant concepts you are (/may be) running into trouble with here:

  • In a unix-style shell, when you use a command name that doesn't include a "/", it's looked for in the directories in the PATH environment variable (places like /bin, /usr/bin, etc), not anyplace relative to where you are (or some current script is). On the other hand, if it does contain a "/", it's treated as a path for the command/script to run. Using ./ in front of a script name is just a way of specifying a path to a file in the current directory.

  • In unix (including macOS), when you specify a relative path (to a document, script, or whatever), it's resolved relative to the process's current working directory; in a script, this is generally not the directory the script is in, but the directory the user (or whatever) was in when it ran the script.

    So if you're in /Users/patlatus and you run a script in /Users/patlatus/Documents/scriptproject, and the script refers to Subfolder/b.sh, it'll look for /Users/patlatus/Subfolder/b.sh. Finding files relative to the script is tricky, and not always possible (or even well-defined), but bash you can usually derive it from $BASH_SOURCE. Something like this:

    scriptdir=$(dirname "$BASH_SOURCE")    # Find the current script's directory
    "$scriptdir/Subfolder/b.sh"            # Run a script in a subdirectory of that
    
  • When you run a script just with its path (or name), it'll run as a subprocess. This means it inherits copies of any variables exported from the parent script, but not unexported variables, and any changes it makes to variables will not propagate back to the parent script's shell. This is different from how call works in a batch script; call is closer to running another script with the source command (or its synonym .), which runs it in the same shell process (with full access to variables etc). If you want this, use the . or source command:

    scriptdir=$(dirname "$BASH_SOURCE")    # Find the current script's directory
    . "$scriptdir/Subfolder/b.sh"          # Source a script in a subdirectory of that
    

    Note that since the sourced script shares variables, if it redefines scriptdir as its directory, that'll affect its value in the calling script as well. Note that the . command has nothing whatsoever to do with the "." in ./scriptname -- that's just a relative path starting from the current directory, not a command.

Licensed under: CC-BY-SA with attribution
Not affiliated with apple.stackexchange
scroll top