26

I'm using a crontab to run a maintenance script for my minecraft server. Most of the time it works fine, unless the crontab tries to use the restart script. If I run the restart script manually, there aren't any issues. Because I believe it's got to do with path names, I'm trying to make sure it's always doing any minecraft command FROM the minecraft directory. So I'm encasing the command in pushd/popd:

os.system("pushd /directory/path/here")
os.system("command to sent to minecraft")
os.system("popd")

Below is an interactive session taking minecraft out of the equation. A simple "ls" test. As you can see, it does not at all run the os.system command from the pushd directory, but instead from /etc/ which is the directory in which I was running python to illustrate my point.Clearly pushd isn't working via python, so I'm wondering how else I can achieve this. Thanks!

>>> def test():
...     import os
...     os.system("pushd /home/[path_goes_here]/minecraft")
...     os.system("ls")
...     os.system("popd")
... 
>>> test()
~/minecraft /etc
DIR_COLORS    cron.weekly  gcrypt         inputrc    localtime   mime.types         ntp       ppp         rc3.d       sasldb2         smrsh      vsftpd.ftpusers
DIR_COLORS.xterm  crontab      gpm-root.conf      iproute2   login.defs  mke2fs.conf            ntp.conf      printcap        rc4.d       screenrc        snmp       vsftpd.tpsave
X11       csh.cshrc    group          issue      logrotate.conf  modprobe.d         odbc.ini      profile         rc5.d       scsi_id.config  squirrelmail   vz
adjtime       csh.login    group-         issue.net  logrotate.d     motd               odbcinst.ini  profile.d       rc6.d       securetty       ssh        warnquota.conf
aliases       cyrus.conf   host.conf      java       lvm         mtab               openldap      protocols       redhat-release  security        stunnel        webalizer.conf
alsa          dbus-1       hosts          jvm        lynx-site.cfg   multipath.conf         opt       quotagrpadmins  resolv.conf     selinux         sudoers        wgetrc
alternatives      default      hosts.allow    jvm-commmon    lynx.cfg    my.cnf             pam.d         quotatab        rndc.key        sensors.conf    sysconfig      xinetd.conf
bashrc        depmod.d     hosts.deny     jwhois.conf    mail        named.caching-nameserver.conf  passwd        rc          rpc         services        sysctl.conf    xinetd.d
blkid         dev.d        httpd          krb5.conf  mail.rc     named.conf         passwd-       rc.d        rpm         sestatus.conf   termcap        yum
cron.d        environment  imapd.conf     ld.so.cache    mailcap     named.rfc1912.zones        pear.conf     rc.local        rsyslog.conf    setuptool.d     udev       yum.conf
cron.daily    exports      imapd.conf.tpsave  ld.so.conf     mailman     netplug            php.d         rc.sysinit      rwtab       shadow          updatedb.conf  yum.repos.d
cron.deny     filesystems  init.d         ld.so.conf.d   makedev.d   netplug.d          php.ini       rc0.d       rwtab.d         shadow-         vimrc
cron.hourly   fonts        initlog.conf   libaudit.conf  man.config  nscd.conf          pki       rc1.d       samba       shells          virc
cron.monthly      fstab        inittab        libuser.conf   maven       nsswitch.conf          postfix       rc2.d       sasl2       skel        vsftpd
sh: line 0: popd: directory stack empty

=== (CentOS server with python 2.4)

Will Vousden
  • 32,488
  • 9
  • 84
  • 95
Thomas Thorogood
  • 2,150
  • 3
  • 24
  • 30
  • I'm a little confused by the line that goes "~/minecraft /etc." To me it looks like a simple case of the fact that `os.system` spawns a subshell... doing `bash -c "pushd directory"`, `bash -c "popd"` will give you the same result... why not just use `os.chdir`? – photoionized May 31 '11 at 23:02
  • 1
    nvm about the confusion line, it's the output of your `pushd` being executed, but the analysis still stands, your commands aren't working because `os.system` spawns a subshell. – photoionized May 31 '11 at 23:04
  • ... and once that subshell is complete, the pushd/popd context is rendered meaningless. – macetw Nov 07 '16 at 13:53

7 Answers7

100

In Python 2.5 and later, I think a better method would be using a context manager, like so:

import contextlib
import os


@contextlib.contextmanager
def pushd(new_dir):
    previous_dir = os.getcwd()
    os.chdir(new_dir)
    try:
        yield
    finally:
        os.chdir(previous_dir)

You can then use it like the following:

with pushd('somewhere'):
    print os.getcwd() # "somewhere"

print os.getcwd() # "wherever you started"

By using a context manager you will be exception and return value safe: your code will always cd back to where it started from, even if you throw an exception or return from inside the context block.

You can also nest pushd calls in nested blocks, without having to rely on a global directory stack:

with pushd('somewhere'):
    # do something
    with pushd('another/place'):
        # do something else
    # do something back in "somewhere"
bryant1410
  • 5,540
  • 4
  • 39
  • 40
spiralman
  • 1,071
  • 2
  • 7
  • 4
  • 3
    I like the idea, that's much more elegant and pythonic :) – Congbin Guo May 18 '13 at 03:36
  • 6
    That's was I was looking for. However this code doesn't pop if an exception is raised in the with statement, because @contextmanager doesn't handle exceptions https://docs.python.org/2/library/contextlib.html You need to surround the yield with a try...finally – Maxime Nov 02 '15 at 03:45
  • 2
    @MaximeViargues there's also the contextlib `closing` method which takes care of the try...finally requirement for you https://docs.python.org/2/library/contextlib.html#contextlib.closing – rjmoggach Jan 11 '16 at 18:29
  • 2
    Only the name is not well chosen, IMHO ; because you do not have the push/pop symmetric like in a sh script since the popd is implicit – wap26 Dec 02 '16 at 09:49
17

Each shell command runs in a separate process. It spawns a shell, executes the pushd command, and then the shell exits.

Just write the commands in the same shell script:

os.system("cd /directory/path/here; run the commands")

A nicer (perhaps) way is with the subprocess module:

from subprocess import Popen
Popen("run the commands", shell=True, cwd="/directory/path/here")
Josh Lee
  • 171,072
  • 38
  • 269
  • 275
  • I just tried the second method; I've only recently started using subprocess (new programmer here. clearly.) and I'd like to learn that better. It works, but then i have to CTRL+C to get the python prompt back. Odd. – Thomas Thorogood May 31 '11 at 23:06
  • 1
    It's worth noting that using `pushd` on a Windows machine with a UNC path as the argument, causes Windows to automatically map a network drive to the path. Using `cd` does not work with UNC paths, nor does `Popen`. If that functionality is required, using `Popen(r'pushd \\server\folder & dir & popd', shell=True)` works. Also note that `;` won't separate commands in Windows, but `&` will. – Grismar Feb 18 '19 at 00:21
  • I've seen that mapping a windows drive can take up to 30 seconds. Do you really want to wait that long for your python script to run? – greggT Feb 21 '20 at 17:04
  • If that's what it takes for it to be more reliable and only done during a build or initialization, then yes? – nlhnt Aug 12 '22 at 11:47
8

pushd and popd have some added functionality: they store previous working directories in a stack - in other words, you can pushd five times, do some stuff, and popd five times to end up where you started. You're not using that here, but it might be useful for others searching for the questions like this. This is how you can emulate it:

# initialise a directory stack
pushstack = list()

def pushdir(dirname):
  global pushstack
  pushstack.append(os.getcwd())
  os.chdir(dirname)

def popdir():
  global pushstack
  os.chdir(pushstack.pop())
naught101
  • 18,687
  • 19
  • 90
  • 138
4

I don't think you can call pushd from within an os.system() call:

>>> import os
>>> ret = os.system("pushd /tmp")
sh: pushd: not found

Maybe just maybe your system actually provides a pushd binary that triggers a shell internal function (I think I've seen this on FreeBSD beforeFreeBSD has some tricks like this, but not for pushd), but the current working directory of a process cannot be influenced by other processes -- so your first system() starts a shell, runs a hypothetical pushd, starts a shell, runs ls, starts a shell, runs a hypothetical popd... none of which influence each other.

You can use os.chdir("/home/path/") to change path: http://docs.python.org/library/os.html#os-file-dir

sarnold
  • 102,305
  • 22
  • 181
  • 238
3

No need to use pushd -- just use os.chdir:

>>> import os
>>> os.getcwd()
'/Users/me'
>>> os.chdir('..')
>>> os.getcwd()
'/Users'
>>> os.chdir('me')
>>> os.getcwd()
'/Users/me'
senderle
  • 145,869
  • 36
  • 209
  • 233
1

Or make a class to use with 'with'

import os

class pushd: # pylint: disable=invalid-name
    __slots__ = ('_pushstack',)

    def __init__(self, dirname):
        self._pushstack = list()
        self.pushd(dirname)

    def __enter__(self):
        return self

    def __exit__(self, exec_type, exec_val, exc_tb) -> bool:
        # skip all the intermediate directories, just go back to the original one.
        if self._pushstack:
            os.chdir(self._pushstack.pop(0)))
        if exec_type:
            return False
        return True

    def popd(self) -> None:
        if len(self._pushstack):
            os.chdir(self._pushstack.pop())

    def pushd(self, dirname) -> None:
        self._pushstack.append(os.getcwd())
        os.chdir(dirname)


    with pushd(dirname) as d:
        ... do stuff in that dirname
        d.pushd("../..")
        d.popd()
Gary
  • 11
  • 3
0

If you really need a stack, i.e. if you want to do several pushd and popd, see naught101 above.

If not, simply do:

olddir = os.getcwd()
os.chdir('/directory/path/here')
os.system("command to sent to minecraft")
os.chdir(olddir)