241

I run a python shell from crontab every minute:

* * * * * /home/udi/foo/bar.py

/home/udi/foo has some necessary subdirectories, like /home/udi/foo/log and /home/udi/foo/config, which /home/udi/foo/bar.py refers to.

The problem is that crontab runs the script from a different working directory, so trying to open ./log/bar.log fails.

Is there a nice way to tell the script to change the working directory to the script's own directory? I would fancy a solution that would work for any script location, rather than explicitly telling the script where it is.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
Adam Matan
  • 128,757
  • 147
  • 397
  • 562
  • unrelated to `crontab` use-case: both `sys.argv[0]` and `__file__` fail if script is run using `execfile()`; [`inspect`-based solution](http://stackoverflow.com/a/22881871/4279) could be used instead. – jfs Apr 05 '14 at 19:59
  • Answers do not belong in questions -- the acceptance and voting mechanisms should be used to indicate which answer is preferred. Putting an answer in a question prevents that answer from being voted on, commented on separately from the question itself, &c; see [What is the appropriate action when the answer to a question is added to the question itself?](https://meta.stackoverflow.com/questions/267434/what-is-the-appropriate-action-when-the-answer-to-a-question-is-added-to-the-que) on [meta]. – Charles Duffy Jun 25 '23 at 14:21

5 Answers5

289

This will change your current working directory to so that opening relative paths will work:

import os
os.chdir("/home/udi/foo")

However, you asked how to change into whatever directory your Python script is located, even if you don't know what directory that will be when you're writing your script. To do this, you can use the os.path functions:

import os

abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)

This takes the filename of your script, converts it to an absolute path, then extracts the directory of that path, then changes into that directory.

Eli Courtwright
  • 186,300
  • 67
  • 213
  • 256
  • 3
    Equals hardcoding the directory. – Ikke Sep 16 '09 at 13:28
  • I added info on how to change into the script's directory; I posted a partial answer while I looked up the os.path functions, which I couldn't remember off the top of my head. – Eli Courtwright Sep 16 '09 at 13:37
  • 2
    If you are running it from a symlink, this will not work. Use `__file__` instead of `sys.argv[0]`. – Chris Down Oct 19 '11 at 12:30
  • 2
    Why the abspath step? Why not simply `os.chdir(os.path.dirname(__file__))`? – Colonel Panic Jul 28 '13 at 11:50
  • @ColonelPanic: In most cases you won't need an absolute path, but it will help in some edge cases such as if some other code already changed the working directory, or if your script is being run without a working directory at all like in a cron job or something. – Eli Courtwright Jul 29 '13 at 14:06
  • 8
    `__file__` fails in "frozen" programs (created using py2exe, PyInstaller, cx_Freeze). `sys.argv[0]` works. @ChrisDown: If you want to follow symlinks; `os.path.realpath()` could be used. – jfs Apr 05 '14 at 20:12
  • 4
    @EliCourtwright If `__file__` is not already an absolute path, and the user has changed the working directory, then `os.path.abspath` will fail anyway. – Arthur Tacca Jan 17 '17 at 16:16
  • 1
    One liner is better in this case.`os.chdir(os.path.dirname(os.path.abspath(__file__)))` – fx-kirin Jun 07 '17 at 04:44
  • 1
    Ready-to-copy-paste line that helps for some scripts: `import os; os.chdir(os.path.dirname(os.path.abspath(__file__)))` – Basj Feb 07 '18 at 19:16
  • @ChrisDown dang! I've actullay given up this solution because __file__ doesn't work for symlinks for me. – Kukuster Aug 02 '21 at 20:27
  • __file__ is just the name of the file not it's path.. 2nd part of answer will just give cwd/ I.e. is wrong ... – spinkus Oct 08 '22 at 13:50
64

You can get a shorter version by using sys.path[0].

os.chdir(sys.path[0])

From http://docs.python.org/library/sys.html#sys.path

As initialized upon program startup, the first item of this list, path[0], is the directory containing the script that was used to invoke the Python interpreter

xverges
  • 4,608
  • 1
  • 39
  • 60
  • 1
    Be aware that this does not work if you `import git`. The first path will point to /lib/site-packages/git/ext/gitdb. At least this happens to me – marco Sep 30 '20 at 08:54
  • 1
    Thanks, @marco. I just created a PR for this https://github.com/gitpython-developers/GitPython/pull/1068 – xverges Oct 04 '20 at 19:02
27

Don't do this.

Your scripts and your data should not be mashed into one big directory. Put your code in some known location (site-packages or /var/opt/udi or something) separate from your data. Use good version control on your code to be sure that you have current and previous versions separated from each other so you can fall back to previous versions and test future versions.

Bottom line: Do not mingle code and data.

Data is precious. Code comes and goes.

Provide the working directory as a command-line argument value. You can provide a default as an environment variable. Don't deduce it (or guess at it)

Make it a required argument value and do this.

import sys
import os
working= os.environ.get("WORKING_DIRECTORY","/some/default")
if len(sys.argv) > 1: working = sys.argv[1]
os.chdir( working )

Do not "assume" a directory based on the location of your software. It will not work out well in the long run.

S.Lott
  • 384,516
  • 81
  • 508
  • 779
  • 13
    I think you're right about separating code and data for large software packages, but it seems quite far-fetched for a small maintenance script. I totally agree about the version control. – Adam Matan Sep 16 '09 at 14:04
  • 3
    S. Lott is right. Always keep data and code separated, unless data is not transitory. For example, if you have icons, that is data, but it's not transitory, and it makes sense to consider it relative to the software bundle (whatever that means) – Stefano Borini Sep 16 '09 at 14:44
  • 7
    @Udi Pasmon: Not far-fetched at all. It's the "small maintenance scripts" that get organizations into deep trouble. Years from now, this "small maintenance script" and it's children and derivations and data files will be a nightmare to disentangle and reimplement. Keep data as far from code as possible -- pass parameters for everything -- assume nothing. – S.Lott Sep 16 '09 at 16:01
  • 1
    +1 I thought I wanted to do as the OP, but after reading your advice I instead modified my script. Now it requires a parameter to specify the location of a log file. – Iain Samuel McLean Elder Jun 21 '13 at 14:48
  • 1
    +1. It is easier to create a package (rpm) for a python script if data directories can be customized easily. – jfs Apr 05 '14 at 20:18
  • I'm using this to pull in configuration files that are shared with a different script in a nearby directory. – user313114 Dec 18 '14 at 22:14
  • @user313114 should be using an entrypoint. https://github.com/RichardBronosky/entrypoint_demo – Bruno Bronosky Nov 28 '16 at 18:01
24

Change your crontab command to

* * * * * (cd /home/udi/foo/ || exit 1; ./bar.py)

The (...) starts a sub-shell that your crond executes as a single command. The || exit 1 causes your cronjob to fail in case that the directory is unavailable.

Though the other solutions may be more elegant in the long run for your specific scripts, my example could still be useful in cases where you can't modify the program or command that you want to execute.

Ruud Althuizen
  • 558
  • 4
  • 10
  • 2
    This is an extremely sound solution. I usually find myself editing other peoples answers to add things like the `|| exit 1`. It's refreshing to see this. Although I do have to wonder why you wouldn't just do `cd /home/udi/foo/ && ./bar.py` – Bruno Bronosky Nov 28 '16 at 18:05
  • 3
    @BrunoBronosky With the explicit `exit 1` your crond will be notified of an error, and in most cases will send an email notification of the failure. – Ruud Althuizen Jan 04 '17 at 14:18
  • @RuudAlthuizen `cd /home/udi/foo/ && ./bar.py` will do the same. – amit kumar Apr 09 '22 at 05:30
0

Just in case, you could also use pathlib

import os
from pathlib import Path

file = Path(__file__)
parent = file.parent
os.chdir(parent)
# print(file, parent, os.getcwd())

Or in single line:

os.chdir(Path(__file__).parent)
Haribk
  • 131
  • 7