tags:

views:

61

answers:

1

Here is my script

#!/usr/bin/env ruby
if __FILE__ == $0
    `cd ..` 
    puts `ls` 
end

which runs fine, but when it exits, I'm back where I started. How can I "export" the changes I've made to the environment?

+4  A: 

That's because ` operator is not for complicated scripting. It's useful for running single command (and reading its output). There is no shell behind it to store environment changes between its calls and after your Ruby script is terminated.

On Linux systems each process has its own "current directory" path (it could be found in /proc/‹pid›/cwd). Changing directory in a process does not affect parent processes (shell you run program from). If cd built-in were binary it could change only its own current directory, not one of the parent process (that's why cd command could be built-in only).


NOTE

If Ruby script must be executed from a shell and must affect the shell's current directory path, it's possible to make a "trick". Instead of running commands from Ruby itself, print that commands to stdout and then source it to the shell you were running Ruby script from. Commands will not be executed by a separate process of a shell, so all cds will take effect in the current shell instance.

So, instead of

ruby run_commands.rb

write in your shell-script something like that:

source <(ruby prepare_and_print_commands.rb)

Shell class

But there is a convenient tool for command line scripting in Ruby — Shell class! It has predefined shortenings for frequently used commands (such as cd, pwd, cat, echo, etc.) and allows to define your own (it supports commands and aliases)! Also it transparently supports redirecting of input/output using |, >, <, >>, << Ruby operators.

Working with Shell is self-explanatory most of the time. Take a look at several simple examples.

Creating a "shell" object and changing directory

sh = Shell.new
sh.cd '~/tmp'
# or
sh = Shell.cd('~/tmp')

Working in the current directory

puts "Working directory: #{sh.pwd}"
(sh.echo 'test') | (sh.tee 'test') > STDOUT
# Redirecting possible to "left" as well as to "right".
(sh.cat < 'test') > 'triple_test'
# '>>' and '<<' are also supported.
sh.cat('test', 'test') >> 'triple_test'

(Note that parentheses are necessary sometimes because of the precedence of "redirecting" operators. Also command output would not be printed to stdout by default, so you need to specify that use > STDOUT, or > STDERR if needed.)

Testing file properties

puts sh.test('e', 'test')
# or
puts sh[:e, 'test']
puts sh[:exists?, 'test']
puts sh[:exists?, 'nonexistent']

(Works similar to test function in a usual shell.)

Defining custom commands and aliases

#                        name    command line to execute
Shell.def_system_command 'list', 'ls -1'

#                   name   cmd   command's arguments
Shell.alias_command "lsC", "ls", "-CBF"
Shell.alias_command("lsC", "ls") { |*opts| ["-CBF", *opts] }

Name of the defined command later could be used to run it exactly the same way as it works for predefined echo or cat, for example.

Using directory stack

sh.pushd '/tmp'
sh.list > STDOUT
(sh.echo sh.pwd) > STDOUT
sh.popd
sh.list > STDOUT
(sh.echo sh.pwd) > STDOUT

(Here custom list command defined above is used.)

By the way, there is useful chdir command to run several commands in a directory and return to previous working directory after that.

puts sh.pwd
sh.chdir('/tmp') do
    puts sh.pwd
end
puts sh.pwd

Skip shell object for a group of commands

# Code above, rewritten to drop out 'sh.' in front of each command.
sh.transact do
    pushd '/tmp'
    list > STDOUT
    (echo pwd) > STDOUT
    popd
    list > STDOUT
    (echo pwd) > STDOUT
end

Additional features

In addition Shell has:

  • foreach method to iterate through lines in a file, or through list of files in a directory (depending on whether given path points to a file or a directory),
  • jobs and kill commands to control processes,
  • a bunch of commands for operating with files (like basename, chmod, chown, delete, dirname, rename, stat, symlink),
  • a list of File methods' synonyms (directory?, executable?, exists?, readable?, etc.),
  • equivalents to the FileTest class methods' (syscopy, copy, move, compare, safe_unlink, makedirs, install).
ib
"There is no shell behind it to store environment changes between its calls"... yes there is. That's why the state is maintained between two commands (updated the question to demonstrate this). But how do I keep state "after your Ruby script is terminated"? Thanks.
Yar
What system and Ruby version do you use? Both 1.8.7 and 1.9.0 versions of Ruby under Debian fails to run `cd ..` because there is no `cd` command in the system, it's built-in shell command. `` ` operator executes command directly, without sending it to particular shell (at least, these implementations works so)!
ib
1.8.7, but Darwin (OSX). Just confirmed that this is different from Ubuntu, at least. However, on Linux you can do "system 'cd ..'" and that works.
Yar
Sorry, it returns false on Ubuntu. On Mac it returns true.
Yar
I've added an explanation of the "current directory" concept on Linux systems in the answer (the second paragraph). So, at least on Linux, you cannot affect current directory of a parent process (relative to your script).
ib
It looks like `system` call in Ruby is implemented differently on Linux and Mac OS X. As far as I know (I'm using Linux most of the time), current directory is per-process based not only on Linux, but on Unix-like systems.
ib
Thank @ib, so you're saying that there is no way for a script -- even a shell script -- to affect your current dir? Not even with export PWD or something?
Yar
Yes. Child process (i.e. shell running a script) just cannot affect other processes' "current directory" path (including its parent process). The only "trick" would be to change your scripts so that to run the chunk of a code that cd's somewhere, inside _the same_ (parent) process. For example, if you have a Bash script `a.sh` that cd's to `/tmp`, running `a.sh` from the current shell would not affect current directory, but `source a.sh` would cause not to execute `a.sh` independently, but to run commands stored in `a.sh` in the current shell, in place.
ib
@ib, in fact, I did that in my answer below. Thanks for your help.
Yar
@ib, thanks, didn't notice the trick in your answer. Nice stuff, I'll delete mine and mark yours as best answer. Sorry to put you through all this work :)
Yar
Ugh, this ` | source /dev/stdin` doesn't work on OSX. It doesn't error, but it doesn't work either.
Yar
Then you could try `source <(ruby prepare_and_print_commands.rb)`. That's because some shells runs sub-process to source file and `/dev/stdin` is changed during that process. Both variants works on Linux (tried Zsh and Bash).
ib
By the way, what shell do you use to run `source ...`? (I'm asking because some archaic shells like `sh` might not work with such source redirection.)
ib