109

How do I check if a program exists from a python script?

Let's say you want to check if wget or curl are available. We'll assume that they should be in path.

It would be the best to see a multiplatform solution but for the moment, Linux is enough.

Hints:

  • running the command and checking for return code is not always enough as some tools do return non 0 result even when you try --version.
  • nothing should be visible on screen when checking for the command

Also, I would appreciate a solution that that is more general, like is_tool(name)

sorin
  • 161,544
  • 178
  • 535
  • 806

9 Answers9

213

shutil.which

Let me recommend an option that has not been discussed yet: a Python implementation of which, specifically shutil.which. It was introduced in Python 3.3 and is cross-platform, supporting Linux, Mac, and Windows. It is also available in Python 2.x via whichcraft. You can also just rip the code for which right out of whichcraft here and insert it into your program.

def is_tool(name):
    """Check whether `name` is on PATH and marked as executable."""

    # from whichcraft import which
    from shutil import which

    return which(name) is not None

distutils.spawn.find_executable

Another option that has already been mentioned is distutils.spawn.find_executable.

find_executable's docstring is as follows:

Tries to find 'executable' in the directories listed in 'path'

So if you pay attention, you'll note that the name of the function is somewhat misleading. Unlike which, find_executable does not actually verify that executable is marked as executable, only that it is on the PATH. So it's entirely possible (however unlikely) that find_executable indicates a program is available when it is not.

For example, suppose you have a file /usr/bin/wget that is not marked executable. Running wget from the shell will result in the following error: bash: /usr/bin/wget: Permission denied. which('wget') is not None will return False, yet find_executable('wget') is not None will return True. You can probably get away with using either function, but this is just something to be aware of with find_executable.

def is_tool(name):
    """Check whether `name` is on PATH."""

    from distutils.spawn import find_executable

    return find_executable(name) is not None
j3h
  • 692
  • 5
  • 10
Six
  • 5,122
  • 3
  • 29
  • 38
  • 7
    Link to docs for `shutil.which()`: https://docs.python.org/3/library/shutil.html#shutil.which – Nick Chammas Feb 06 '16 at 02:46
  • up vote for distutils.spawn.find_executable! Thanks man! – Barmaley Sep 08 '16 at 20:31
  • If the Python function follows the shell command... `which` is not a good choice. You should probably use `command -v` instead. Also see [How to check if a program exists from a Bash script?](https://stackoverflow.com/q/592620/608639) and [How to check if command exists in a shell script?](https://stackoverflow.com/q/7522712/608639) – jww Apr 21 '19 at 03:45
  • 1
    Check the source to `shutil.which` https://github.com/python/cpython/blob/3e986de0d65e78901b55d4e500b1d05c847b6d5e/Lib/shutil.py#L1291 or https://github.com/pydanny/whichcraft/blob/master/whichcraft.py#L20. It's pure Python and doesn't call the `which` binary so none of the arguments made in those answers are relevant. – Six Apr 21 '19 at 04:53
  • Could someone help me understand the `return which(name) is not None` line? – Peter Schorn Feb 10 '20 at 06:28
  • @PeterSchorn find_executable(name) returns None if it doesnt find anything. so in None is not None will be `False`, hence `return False`. when it is not `None`, therefore the condition becomes `True` and returns `True`. – Hossein Mar 10 '20 at 09:28
54

The easiest way is to try to run the program with the desired parameters, and handle the exception if it doesn't exist:

try:
    subprocess.call(["wget", "your", "parameters", "here"])
except FileNotFoundError:
    # handle file not found error.

This is a common pattern in Python: EAFP

In Python 2, you had to catch OsError instead, since the more fine-grained exception classes for OS errors did not exist yet:

try:
    subprocess.call(["wget", "your", "parameters", "here"])
except OSError as e:
    if e.errno == errno.ENOENT:
        # handle file not found error.
    else:
        # Something else went wrong while trying to run `wget`
        raise
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • right. for programs you know how to use, this is probably the best way to go about it. After all: expecting success often leads to cleaner code... – Daren Thomas Jun 26 '12 at 15:06
  • 1
    @DarenThomas: For programs you don't know how to use, the information whether they exist or not doesn't seem to be too useful. :) – Sven Marnach Jun 26 '12 at 15:07
  • I like your approach, but it does pollute the stdout and stderr. Also, it is not a function ;) – sorin Jun 26 '12 at 15:08
  • @SorinSbarnea: How does this pollute stdout and stderr? Since I cannot think of any other reason for testing whether `wget` exists, I'm assuming you are going to run it anyway. – Sven Marnach Jun 26 '12 at 15:14
  • 1
    Your solution will display output of the executed command, look at my solution, based on yours, which is totally quiet. http://stackoverflow.com/a/11210902/99834 – sorin Jun 26 '12 at 15:38
  • 1
    @SorinSbarnea: My approach is a completely different one. The only reason you might be testing if a command exists is that you are planning to run it. My recommendation is: Don't test in advance, just run it, and catch the exception if things fail. This does not pollute stdout or stderr in any way, and it will lead to cleaner code and less overhead. – Sven Marnach Jun 26 '12 at 15:53
  • @SvenMarnach "The only reason you might be testing if a command exists is that you are planning to run it." A different example: if you are making an install script that will skip some steps if they are unnecessary, then you may want to simply check if the commands already exist, then if not, install. Not the best solution, but sometimes good enough. The install script doesn't run those, just makes sure that they are available. – Ctrl-C Nov 06 '18 at 15:02
  • @Ctrl-C Makes sense as a use case. I recommend the answer by Six instead – `shutil.which` wasn't available at the time I wrote this answer. – Sven Marnach Nov 06 '18 at 15:34
  • `errno` is its own module so the test should be `if e.errno == errno.ENOENT:` – Mike Jul 26 '19 at 19:25
  • @Mike Yeah, probably. In Python 2, it was possible to access the `errno` module via `os`, but that was probably never meant to be exposed – I'll update the answer. – Sven Marnach Jul 26 '19 at 20:48
  • 1
    At least in Python 3, an arguably cleaner way is to catch `FileNotFoundError` directly. saves the if – fabian789 Dec 14 '20 at 16:10
  • 1
    @fabian789 Good point, updated the answer. – Sven Marnach Dec 14 '20 at 16:53
15

You could use a subprocess call to the binary needed with :

  • "which" : *nix
  • "where" : Win 2003 and later (Xp has an addon)

to get the executable path (supposing it is in the environment path).

import os 
import platform
import subprocess

cmd = "where" if platform.system() == "Windows" else "which"
try: 
    subprocess.call([cmd, your_executable_to_check_here])
except: 
    print "No executable"

or just use Ned Batchelder's wh.py script, that is a "which" cross platform implementation:

http://nedbatchelder.com/code/utilities/wh_py.html

J_Zar
  • 2,034
  • 2
  • 21
  • 34
  • 1
    call doesn't throw an exception when the program doesn't exist, instead the call returns non-zero – Photon Jan 29 '16 at 09:07
13

I would probably shell out to which wget or which curl and check that the result ends in the name of the program you are using. The magic of unix :)

Actually, all you need to do is check the return code of which. So... using our trusty subprocess module:

import subprocess

rc = subprocess.call(['which', 'wget'])
if rc == 0:
    print('wget installed!')
else:
    print('wget missing in path!')

Note that I tested this on windows with cygwin... If you want to figure out how to implement which in pure python, i suggest you check here: http://pypi.python.org/pypi/pycoreutils (oh dear - it seems they don't supply which. Time for a friendly nudge?)

UPDATE: On Windows, you can use where instead of which for a similar effect.

Guillaume Jacquenot
  • 11,217
  • 6
  • 43
  • 49
Daren Thomas
  • 67,947
  • 40
  • 154
  • 200
13
import subprocess
import os

def is_tool(name):
    try:
        devnull = open(os.devnull)
        subprocess.Popen([name], stdout=devnull, stderr=devnull).communicate()
    except OSError as e:
        if e.errno == os.errno.ENOENT:
            return False
    return True
sorin
  • 161,544
  • 178
  • 535
  • 806
  • 3
    This would leave the subprocess running indefinitely if it fills the pipe buffers of either stdout or stderr. If you want to run the process *at all* just to check if it exists, you should use open `os.devnull` and use it as stdout and stderr. – Sven Marnach Jun 26 '12 at 15:50
  • not likely to happen but you are right, thanks. – sorin Jun 26 '12 at 16:35
  • 1
    Many tools output usage information when called without parameters, which could easily fill the pipe buffers. I was wrong with my initial comment anyway – I missed the call to `comunnicate()`, which was beyond the right margin of the code box, and I did not scroll far enough to the right. The method `Popen.communicate()` takes care of avoiding any deadlocks. – Sven Marnach Jun 26 '12 at 16:44
  • Make sure to open /dev/null for *writing* ie: ``open(os.devnull, "w")`` – yadutaf May 11 '15 at 22:20
  • 3
    Also just a FYI for people using Python 3.3 and later, you can use `subprocess.DEVNULL` rather than `open(os.devnull)`. – Six Dec 09 '15 at 10:33
  • 1
    You will also need to close `os.devnull` after opening. Otherwise if executing many times, there will be too many handles open and ultimately it will exceed the limit. – Astitva Srivastava Jul 03 '19 at 06:30
12

I'd go for:

import distutils.spawn

def is_tool(name):
  return distutils.spawn.find_executable(name) is not None
Martin Richard
  • 1,533
  • 12
  • 14
  • `distutils.spawn` works well in Linux and Mac OS X. But in the latter one, if you make a `app`, and you double click to execute, `distutils.spawn` always returns `None`. – muammar May 19 '16 at 08:08
4

I'd change @sorin's answer as follows, the reason is it would check the name of the program without passing the absolute path of the program

from subprocess import Popen, PIPE

def check_program_exists(name):
    p = Popen(['/usr/bin/which', name], stdout=PIPE, stderr=PIPE)
    p.communicate()
    return p.returncode == 0
Nicole Finnie
  • 1,370
  • 13
  • 12
1
import os
import subprocess


def is_tool(prog):
    for dir in os.environ['PATH'].split(os.pathsep):
        if os.path.exists(os.path.join(dir, prog)):
            try:
                subprocess.call([os.path.join(dir, prog)],
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT)
            except OSError, e:
                return False
            return True
    return False
ryanday
  • 2,506
  • 18
  • 25
0

A slight modification to @SvenMarnach's code that addresses the issue of printing to the standard output stream. If you use the subprocess.check_output() function rather than subprocess.call() then you can handle the string that is normally printed to standard out in your code and still catch exceptions and the exit status code.

If you want to suppress the standard output stream in the terminal, don’t print the std out string that is returned from check_output:

import subprocess
import os
try:
    stdout_string = subprocess.check_output(["wget", "--help"], stderr=subprocess.STDOUT)
    # print(stdout_string)
except subprocess.CalledProcessError as cpe:
    print(cpe.returncode)
    print(cpe.output)
except OSError as e:
    if e.errno == os.errno.ENOENT:
        print(e)
    else:
        # Something else went wrong while trying to run `wget`
        print(e)

The non-zero exit status code and output string are raised in the CalledProcessError as subprocess.CalledProcessError.returncode and subprocess.CalledProcessError.output so you can do whatever you'd like with them.

If you want to print the executable's standard output to the terminal, print the string that is returned:

import subprocess
import os
try:
    stdout_string = subprocess.check_output(["wget", "--help"], stderr=subprocess.STDOUT)
    print(stdout_string)
except subprocess.CalledProcessError as cpe:
    print(cpe.returncode)
    print(cpe.output)
except OSError as e:
    if e.errno == os.errno.ENOENT:
        print(e)
    else:
        # Something else went wrong while trying to run `wget`
        print(e)

print() adds an extra newline to the string. If you want to eliminate that (and write std error to the std err stream instead of the std out stream as shown with the print() statements above), use sys.stdout.write(string) and sys.stderr.write(string) instead of print():

import subprocess
import os
import sys
try:
    stdout_string = subprocess.check_output(["bogus"], stderr=subprocess.STDOUT)
    sys.stdout.write(stdout_string)
except subprocess.CalledProcessError as cpe:
    sys.stderr.write(cpe.returncode)
    sys.stderr.write(cpe.output)
except OSError as e:
    if e.errno == os.errno.ENOENT:
        sys.stderr.write(e.strerror)
    else:
        # Something else went wrong while trying to run `wget`
        sys.stderr.write(e.strerror)
Chris Simpkins
  • 1,534
  • 2
  • 11
  • 13