Running script with relative paths from another directory
Asked Answered
P

1

10

I'm writing a script to compile a code. The sources are in many different directories. I'm going to release the source and I want it to be compiled by many other people. Thus in my script I use relative paths.
If someone runs the script on his machine, from the directory the script is in, like ./script everything is ok. But, if the script is being run from another directory, like ./path/to/script then the paths are not correct and the script doesn't work.
How can I overcome this?

Phobos answered 6/5, 2017 at 17:45 Comment(1)
Never use relative paths in scripts unless you are setting the current directory in the code itself. Use absolute paths or append the required directories to PATH in the beginning of the script.Faculty
E
24

A simple starting point might be to add this to the top of your script:

#!/bin/bash
scriptdir="$(dirname "$0")"
cd "$scriptdir"

All following code will occur as if it was run inside the script dir.

Note that this is about causing the script to run

  • From the folder where the script is located, and
  • Regardless of where the script is called from initially

Which is what I suspect you want given that it's a script for compiling, thus it would placed in a very specific folder in the src tree (like a configure script or a form of makefile for example)

Rationale

  • $0 is the full path of the running script.

    • From the bash man (my added formatting):

      Special Parameters

      The shell treats several parameters specially. These parameters may only be referenced; assignment to them is not allowed.
         ....

      0   Expands to the name of the shell or shell script. This is set at shell initialization.
         If bash is invoked with a file of commands, $0 is set to the name of that file.

         If bash is started with the -c option, then $0 is set to the first argument after
         the string to be executed, if one is present. Otherwise, it is set to the file name
         used to invoke bash, as given by argument zero.

  • dirname returns the path of its argument

  • cd changes the current directory

Therefore, every script with this code at the top, will be running as if ./script was typed while currently in that directory.

Coming at it from another angle

For measures of quality control and redundant error checking, you may want to implement a wrapper function that checks for a specific folder structure or presence of certain files that indicates everything is correct. (Say for whatever reason that dirname and cd command didn't work - which I have an example of for Mac down the bottom of this post). This concept is two fold:

  1. You're doing stuff to set the current directory
  2. You're checking that the stuff you did, worked.

For example:

runcheck () {
versioncheck () {

head -n1 version | grep -q "### myapp V"

}

backout () {
echo "Problem verifying source paths.  Are you sure your archive is complete?"
exit
}

[ -d ./src ] && [ -d ./docs ] && [ -d ../myapp ] && (versioncheck) || backout 
    return
}

That code would check the following:

  • The directory src exists in the same path as the executable script
  • The directory docs exists in the same path as the executable script
  • The directory myapp exists in the path one level down from the executable script
  • That there's a file called version in the same path as the executable script, and that it's first line contains the string ### myapp V (for example, for a version file which might have a top line that reads: ### myapp V1.4 ###)

You could then place the command runcheck at any point in your script where it's important to verify you're in the right place:

Complete implementation example:

#!/bin/bash

scriptdir="$(dirname "$0")"
cd "$scriptdir"

runcheck () {
versioncheck () {
    head -n1 version | grep -q "### myapp V"
}   

backout () {
    echo "Problem verifying source paths.  Are you sure your archive is complete?"
    exit
}

[ -d ./src ] && [ -d ./docs ] && [ -d ../myapp ] && (versioncheck) || backout 
    return
}

runcheck #initial check at start of script

## bunch of code
## goes here

runcheck #just checking again

## bunch of code
## goes here

runcheck #final check before really doing something bad

## end of script

Side Note / Supplement: This will work for bash when no thorough checking needs to occur to account for symbolic linking of the scriptfile, etc.. (which again... in portable source code tarballs, etc, I highly doubt is the case).

I suggest reading this thread: Getting the source directory of a Bash script from within if you want or ever need a thorough understanding on this topic for more comprehensive applications of it.

Again I re-inforce using what you know tailored for what you need - for example it might be considered generally more "robust" to use dirname "$(readlink -f "$0")", however, on Mac OS X, that would give you readlink: illegal option -- f , and is of no real benefit for portable scripts but more applicable for referencing locations of installed binaries that may be symlinked and/or contained in $PATH dirs

Eusebiaeusebio answered 6/5, 2017 at 18:52 Comment(8)
Thank you, very simple and does the job. If I have multiple scripts that call each other, is it safe that each one of them will have these opening lines? Will each script have its own unique scriptdir variable?Phobos
@Phobos Yes, each individual running instance, whether it's the same script or other, with those lines at the top will have $scriptdir apply only to that file. Even if for some crazy reason, $scriptdir was an exported environment variable in the parent shell, the setting of $scriptdir within the script will still update it's value for that script (not the parent shell environment)Eusebiaeusebio
@Phobos I've added an additional supplement that might help check the setting of the scriptdir and subsequent current directory all worked well before executing any actual commands. You would obviously modify this to suit your projectEusebiaeusebio
Congratulations for the completeness of your reply.Literati
Might cd "$(dirname $0)" be more compact and clearMarlinmarline
@VicenteAdolfoBoleaSánchez Not in my view. 1. The variable name makes it very clear what's happening. 2. You may want to refer to the $scriptdir multiple times in which case the command doesn't have to be subshelled each time and can be referred to itself in a subshellEusebiaeusebio
Lovely answer!!Alcheringa
how to make same but not for all paths in the script? some will be like this, and the passed as arguments will be related to pwdAccomplish

© 2022 - 2024 — McMap. All rights reserved.