Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bash: function + source + declare = boom

Tags:

function

bash

Here is a problem:

In my bash scripts I want to source several file with some checks, so I have:

if [ -r foo ] ; then
  source foo
else
  logger -t $0 -p crit "unable to source foo"
  exit 1
fi 

if [ -r bar ] ; then
  source bar
else
  logger -t $0 -p crit "unable to source bar"
  exit 1
fi 

# ... etc ...

Naively I tried to create a function that do:

 function safe_source() {
   if [ -r $1 ] ; then
     source $1
   else
     logger -t $0 -p crit "unable to source $1"
     exit 1
   fi 
 }

 safe_source foo
 safe_source bar
 # ... etc ...

But there is a snag there.

If one of the files foo, bar, etc. have a global such as --

declare GLOBAL_VAR=42

-- it will effectively become:

function safe_source() {
  # ...
  declare GLOBAL_VAR=42
  # ...
}

thus a global variable becomes local.

The question:

An alias in bash seems too weak for this, so must I unroll the above function, and repeat myself, or is there a more elegant approach?

... and yes, I agree that Python, Perl, Ruby would make my life easier, but when working with legacy system, one doesn't always have the privilege of choosing the best tool.

like image 880
Chen Levy Avatar asked May 23 '10 11:05

Chen Levy


2 Answers

It's a bit late answer, but now declare supports a -g parameter, which makes a variable global (when used inside function). Same works in sourced file.

If you need a global (read exported) variable, use:

declare -g DATA="Hello World, meow!"
like image 103
Reishin Avatar answered Oct 07 '22 22:10

Reishin


Yes, Bash's 'eval' command can make this work. 'eval' isn't very elegant, and it sometimes can be difficult to understand and debug code that uses it. I usually try to avoid it, but Bash often leaves you with no other choice (like the situation that prompted your question). You'll have to weigh the pros and cons of using 'eval' for yourself.

Some background on 'eval'

If you're not familiar with 'eval', it's a Bash built-in command that expects you to pass it a string as its parameter. 'eval' dynamically interprets and executes your string as a command in its own right, in the current shell context and scope. Here's a basic example of a common use (dynamic variable assignment):

$>  a_var_name="color"
$>  eval ${a_var_name}="blue"
$>  echo -e "The color is ${color}."
The color is blue.

See the Advanced Bash Scripting Guide for more info and examples: http://tldp.org/LDP/abs/html/internal.html#EVALREF

Solving your 'source' problem

To make 'eval' handle your sourcing issue, you'd start by rewriting your function, 'safe_source()'. Instead of actually executing the command, 'safe_source()' should just PRINT the command as a string on STDOUT:

function safe_source() { echo eval " \
  if [ -r $1 ] ; then \
    source $1 ; \
  else \
    logger -t $0 -p crit \"unable to source $1\" ; \
    exit 1 ; \
  fi \
"; }

Also, you'll need to change your function invocations, slightly, to actually execute the 'eval' command:

`safe_source foo`
`safe_source bar`

(Those are backticks/backquotes, BTW.)

How it works

In short:

  • We converted the function into a command-string emitter.
  • Our new function emits an 'eval' command invocation string.
  • Our new backticks call the new function in a subshell context, returning the 'eval' command string output by the function back up to the main script.
  • The main script executes the 'eval' command string, captured by the backticks, in the main script context.
  • The 'eval' command string re-parses and executes the 'eval' command string in the main script context, running the whole if-then-else block, including (if the file exists) executing the 'source' command.

It's kind of complex. Like I said, 'eval' is not exactly elegant. In particular, there are a couple of special things you should notice about the changes we made:

  • The entire IF-THEN-ELSE block has becomes one whole double-quoted string, with backslashes at the end of each line "hiding" the newlines.
  • Some of the shell special characters like '"') have been backslash-escaped, while others ('$') have been left un-escaped.
  • 'echo eval' has been prepended to the whole command string.
  • Extra semicolons have been appended to all of the lines where a command gets executed to terminate them, a role that the (now-hidden) newlines originally performed.
  • The function invocation has been wrapped in backticks.

Most of these changes are motived by the fact that 'eval' won't handle newlines. It can only deal with multiple commands if we combine them into a single line delimited by semicolons, instead. The new function's line breaks are purely a formatting convenience for the human eye.

If any of this is unclear, run your script with Bash's '-x' (debug execution) flag turned on, and that should give you a better picture of exactly what's happening. For instance, in the function context, the function actually produces the 'eval' command string by executing this command:

echo eval ' if [ -r <INCL_FILE> ] ; then source <INCL_FILE> ; else logger -t <SCRIPT_NAME> -p crit "unable to source <INCL_FILE>" ; exit 1 ; fi '

Then, in the main context, the main script executes this:

eval if '[' -r <INCL_FILE> ']' ';' then source <INCL_FILE> ';' else logger -t <SCRIPT_NAME> -p crit '"unable' to source '<INCL_FILE>"' ';' exit 1 ';' fi

Finally, again in the main context, the eval command executes these two commands if exists:

'[' -r <INCL_FILE> ']'
source <INCL_FILE>

Good luck.

like image 33
Ryan B. Lynch Avatar answered Oct 07 '22 23:10

Ryan B. Lynch