How do I write folder-agnostic shell script on Mac as I can do in Windows?

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.


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.