2

If I have a program written in a language other than bash (say python), how can I change environment variables or the current working directory inside it such that it reflects in the calling shell?

I want to use this to write a 'command line helper' that simplifies common operations. For example, a smart cd. When I simple type in the name of a directory into my prompt, it should cd into it.

[~/]$ Downloads
[~/Downloads]$

or even

[~/]$ project5
[~/projects/project5]$

I then found How to change current working directory inside command_not_found_handle (which is exactly one of the things I wanted to do) , which introduced me to shopt -s autocd. However, this still doesn't handle the case where the supplied directory is not in ./.

In addition, if I want to do things like setting the http_proxy variable from a python script, or even update the PATH variable, what are my options?

P. S. I understand that there probably isn't an obvious way to write a magical command inside a python script that automatically updates environment variables in the calling shell. I'm looking for a working solution, not necessarily one that's elegant.

Community
  • 1
  • 1
Vivek Ghaisas
  • 961
  • 1
  • 9
  • 24
  • Here are ways to solve the problem conventionally: setting `PS1=[\w]` will work for the dir and adding `export http_proxy=http://foo.example.com:8080/` to `/etc/profile.d/foo.sh`. – Brian Cain Oct 22 '14 at 15:21
  • I've edited your title and tagging: A subprocess in a different language isn't strictly a subshell -- a subshell is what you get from a fork with no exec, whereas getting into a Python interpreter requires not just a fork but also an `exec` call. – Charles Duffy Oct 22 '14 at 15:24
  • @BrianCain, setting `PS1` is not what I want to do. I want to `cd` into that directory. Also, `profile.d` solutions: 1) Will need root access to do on the fly, 2) Will only run when a new shall starts. – Vivek Ghaisas Oct 22 '14 at 15:24
  • @CharlesDuffy, fair enough. However, I didn't want to specify python. Will re-edit. – Vivek Ghaisas Oct 22 '14 at 15:27
  • @CharlesDuffy, I don't mind a bash solution either. That would be a subshell, would it not? Feel free to remove the `/subshell` part from the title if I'm wrong. – Vivek Ghaisas Oct 22 '14 at 15:28
  • 1
    Oh, I see. `CDPATH` comes pretty close to this feature. You still have to type `cd project5` though. – Brian Cain Oct 22 '14 at 15:28
  • It would be a subshell IF it were implemented inside the same shell with no exec call. If you're running a script in such a way that its shebang matters, it's not a subshell. – Charles Duffy Oct 22 '14 at 15:31
  • @CharlesDuffy I see. My bad. Will update the title. Thanks for the new information. :) – Vivek Ghaisas Oct 22 '14 at 15:32
  • Wouldn't it be a much simpler and cleaner design to make your “smart shell” the *parent* process that handles some inputs directly and spawns a child shell for those it doesn't handle? The child would automatically inherit the CWD, environment, open file descriptors and what not else. Frankly, I don't even think it is possible at all to do what you describe (if I understand correctly, which I'm uncertain) in the reverse child-to-parent design. – 5gon12eder Oct 22 '14 at 15:37
  • ...another easy rule you can distinguish between the situations: Because a subshell has a fork with no exec, it inherits non-exported variables, whereas a subprocess gets only exported variables, even if it happens to be a shell. – Charles Duffy Oct 22 '14 at 15:38
  • By the way -- you can always have the command_not_found handle call your external (Python or whatever) process and process its output using eval in the manner given in my answer. I'd actually appreciate some pointers/feedback on how it fails to match the question. :) – Charles Duffy Oct 22 '14 at 15:41
  • @CharlesDuffy,your answer does match what I want done. I simply intended to wait a little longer before marking it as the correct answer. – Vivek Ghaisas Oct 22 '14 at 16:31
  • @5gon12eder True, it would be a cleaner design. I shall certainly consider doing that, but as of now it doesn't fit in the larger system that I plan to put it in. – Vivek Ghaisas Oct 22 '14 at 16:32

2 Answers2

4

This can only be done with the parent shell's involvement and assistance. For a real-world example of a program that does this, you can look at how ssh-agent is supposed to be used:

eval "$(ssh-agent -s)"

...reads the output from ssh-agent and runs it in the current shell (-s specifies Bourne-compatible output, vs csh).


If you're using Python, be sure to use pipes.quote() (or, for Python 3.x, shlex.quote()) to process your output safely:

import pipes
dirname='/path/to/directory with spaces'
foo_val='value with * wildcards * that need escaping and \t\t tabs!'
print 'cd %s; export FOO=%s;' % (pipes.quote(dirname), pipes.quote(foo_val))

...as careless use can otherwise lead to shell injection attacks.


By contrast, if you're writing this as an external script in bash, be sure to use printf %q for safe escaping (though note that its output is targeted for other bash shells, not for POSIX sh compliance):

#!/bin/bash
dirname='/path/to/directory with spaces'
foo_val='value with * wildcards * that need escaping and \t\t tabs!'
printf 'cd %q; export FOO=%q;' "$dirname" "$foo_val"

If, as it appears from your question, you want your command to appear to be written as a native shell function, I would suggest wrapping it in one (this practice can also be used with command_not_found_handle). For instance, installation can involve putting something like the following in one's .bashrc:

my_command() {
  eval "$(command /path/to/my_command.py "$@")"
}

...that way users aren't required to type eval.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
1

Essentially, Charles Duffy hit the nail on the head, I present here another spin on the issue.

What you're basically asking about is interprocess communication: You have a process, which may or may not be a subprocess of the shell (I don't think that matters too much), and you want that process to communicate information to the original shell (just another process, btw), and have it change its state.

One possibility is to use signals. For example, in your shell you could have:

trap 'cd /tmp; pwd;' SIGUSR2

Now:

  1. Type echo $$ in your shell, this will give you a number, PID
  2. cd to a directory in your shell (any directory other than /tmp)
  3. Go to another shell (in another window or what have you), and type: kill SIGUSR2 PID
  4. You will find that you are in /tmp in your original shell.

So that's an example of the communication channel. The devil of course is in the details. There are two halves to your problem: How to get the shell to communicate to your program (the command_not_found_handle would do that nicely if that would work for you), and how to get your program to communicate to the shell. Below, I cover the latter issue:

You could, for example, have a trap statement in the original shell:

trap 'eval $(/path/to/my/fancy/command $(pwd) $$)' SIGUSR2

...your fancy command will be given the current working directory of the original shell as the first argument, and the process id of the shell (so it knows who to signal), and it can act upon it. If your command sends an executable shell command string to the eval command, it will be executed in the environment of the original shell.

For example:

trap 'eval $(/tmp/doit $$ $(pwd)); pwd;' SIGUSR2

/tmp/doit is the fancy command. It could be any executable type [Python, C, Perl, etc.]), the key is that it spits out a string that the shell can evaluate. In /tmp/doit, I have provided a bash script:

#!/bin/bash
echo "echo PID: $1 original directory: $2; cd /tmp"

(I make sure the file is executable with: chmod 755 /tmp/doit). Now if I type:

cd; echo $$

Then, in another shell, take the number output ("NNNNN") by the above echo and do:

kill -s SIGUSR2 NNNNN

...then suddenly I will see something like this pop up in the original shell:

PID: NNNNN original directory: /home/myhomepath
/tmp

and if I type "pwd" in my original shell, I will see that I'm in /tmp.

The guy who wanted command_not_found_handle to do something in the current shell environment could have used signals to get the effect he wanted. Here I was running the kill manually but there's no reason why a shell function couldn't do it.

Doing fancy work on the frontend, whereby you re-interpret or pre-interpret the user's input to the shell, may require that the user runs a frontend program that could be pretty complicated, depending on what you want to do. The old school "expect" program is ideal for something like this, but not too many youngsters pick up TCL these days :-) .

Mike S
  • 1,235
  • 12
  • 19