7

With the os module in Python we can easily access environment variables through the dict os.environ. However, I found out that os.environ does not just hold variables, but also globally defined shell functions (e.g. from the module software package).

Is it possible from within Python to find out whether a given entry in os.environ actually is a function and not a variable? Please note that a shell-agnostic solution is preferred, but I could settle for a Bash-specific solution as well.

  • Is a python function, or a bash shell function? – VooDooNOFX Nov 28 '13 at 07:00
  • You can check a Python variable to see if it's callable as described here: http://stackoverflow.com/questions/624926/how-to-detect-whether-a-python-variable-is-a-function- but I'm interested in what you're doing to add shell functions to `os.environ`. That seems odd to me. – Peter DeGlopper Nov 28 '13 at 07:00
  • As posted below, if you export shell functions with `export -f fn` or `declare -fx fn` then function `fn` will end up in `os.environ`. – Michael Schlottke-Lakemper Nov 28 '13 at 07:04
  • @VooDooNOFX: A bash shell function is what I try to figure out. – Michael Schlottke-Lakemper Nov 28 '13 at 07:04
  • @Thrustmaster: `export tt='() aw, dayum!; echo $tt` leaves me with `() aw, dayum!`, so in this case we would get a false positive. Since this is a script that will be deployed on very heterogenous systems I cannot rely on not having any variable content start with `()`. – Michael Schlottke-Lakemper Nov 28 '13 at 07:09
  • @MichaelSchlottke But that's the only way to do. If I do `export tt=$'() { echo 1\n}'; bash -c tt`, I get a 1. SO that's the only one you can rely on. – glglgl Nov 28 '13 at 08:49

3 Answers3

7

This feature is bash-specific, so a test for an exported shell function needs to do what Bash does. Experimentation and source code show that Bash recognizes an environment variable as a shell function at startup by the presence of a () { prefix in its value — if the prefix is missing, or even slightly altered, the variable is treated as an ordinary data variable.

Therefore, the equivalent Python check would look like this:

def is_env_shell_func(name):
    return os.environ[name].startswith('() {')
user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • Again, I think this will return `True` for any variable that starts with `() {`, not just functions. – Michael Schlottke-Lakemper Nov 28 '13 at 07:15
  • @MichaelSchlottke That's a feature, because this is exactly what [the shell does](http://git.savannah.gnu.org/cgit/bash.git/tree/variables.c#n341) to recognize such variables. I've now amended the answer to mention this. – user4815162342 Nov 28 '13 at 07:20
  • Sorry, I don't follow... if I create a variable as `export not_a_function='() { wololo }' and run `declare -f not_a_function`, it tells by way of return code `1` that `not_a_function` is, in fact, not a function. But maybe I misunderstand you. – Michael Schlottke-Lakemper Nov 28 '13 at 07:23
  • @MichaelSchlottke What I get in Bash 4.2.45 is `error importing function definition for `not_a_function'` when starting a subshell, so Bash obviously **tries** to define the function. And if you fix the definition so it works, by including a newline before the closing brace, then the definition tests and works just fine. – user4815162342 Nov 28 '13 at 07:26
  • Would you be so kind to post the lines of code you used to trigger the described behavior? I am not able to reproduce it with Bash 3.2 or 4.1. – Michael Schlottke-Lakemper Nov 28 '13 at 07:29
  • @MichaelSchlottke I've added them to the answer. – user4815162342 Nov 28 '13 at 07:39
  • @MichaelSchlottke Also, judging by the source of `initialize_shell_variables` in Bash 4.1.1, it does the exact same thing. – user4815162342 Nov 28 '13 at 07:53
  • Aha! Now I finally get it :) It seems that you have to invoke `bash` again to make it work as a function, i.e. if you just export it, the current shell does not yet recognize it as a function. – Michael Schlottke-Lakemper Nov 28 '13 at 08:03
  • @MichaelSchlottke Yes; I've now added "at startup" to the relevant sentence of the answer. – user4815162342 Nov 28 '13 at 12:40
3

One solution that I find to work (but that is ridiculously clumsy) is the following:

import subprocess

var = 'my_variable_name_i_want_to_check'
p = subprocess.Popen('declare -f ' + var, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.communicate()

if p.returncode == 0:
    print('function')
else:
    print('variable')
2

Are you sure shell functions are there in os.environ?

{master>}% function test_fn() {
function> echo "Hello";
function> }
{master>}% test_fn
Hello
{master>}% python
Python 2.7.3 (default, Jan  2 2013, 13:56:14) 
[GCC 4.7.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> os.environ['test_fn']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python2.7/UserDict.py", line 23, in __getitem__
    raise KeyError(key)
KeyError: 'test_fn'
>>> os.environ.keys()
['SSH_ASKPASS', 'PS_FORMAT', 'GIT_PS1_SHOWDIRTYSTATE', 'GNOME_DESKTOP_SESSION_ID', 'WINDOWPATH', 'LOGNAME', 'USER', 'GNOME_KEYRING_CONTROL', 'HOME', 'PS1', 'DISPLAY', 'PATH', 'LANG', 'TERM', 'SHELL', 'SSH_AGENT_PID', 'XAUTHORITY', 'LANGUAGE', 'GIT_PS1_SHOWSTASHSTATE', 'SHLVL', 'GIT_PS1_SHOWUPSTREAM', 'WINDOWID', 'EDITOR', 'MANPATH', 'GIT_PS1_SHOWCOLORHINTS', 'GPG_AGENT_INFO', 'USERNAME', 'WORKON_HOME', 'COLORTERM', 'WORDCHARS', 'SSH_AUTH_SOCK', 'TMUX', 'GDMSESSION', 'XDG_SESSION_COOKIE', 'LS_OPTIONS', 'DBUS_SESSION_BUS_ADDRESS', '_', 'VIRTUALENVWRAPPER_HOOK_DIR', 'VIRTUALENVWRAPPER_PROJECT_FILENAME', 'DESKTOP_SESSION', 'GIT_PS1_SHOWUNTRACKEDFILES', 'GNOME_KEYRING_PID', 'WINDOW_MANAGER', 'ZBEEP', 'PYTHONSTARTUP', 'OLDPWD', 'SESSION_MANAGER', 'XDG_DATA_DIRS', 'PWD', 'CFLAGS', 'VIRTUALENVWRAPPER_LOG_DIR', 'LS_COLORS', 'TMUX_PANE']
>>> 
Noufal Ibrahim
  • 71,383
  • 13
  • 135
  • 169