Exporting environment variables both to bash as csh using a bash script with functions - linux

I have a bash shell-script with a function which exports an environment variable.
For sake of argument lets use the following example:
#!/bin/bash
function my_function()
{
export my_env_var=$1
}
Since the whole purpose is to export the variable to the main shell I source it.
When the main shell is bash this works fine:
<bash-shell>
> source ~/tmp/my_test.sh
> my_function test
> echo $my_env_var
test
But other customers use csh and there things start to fail if I use the same command with the same script, since csh does not know functions :-(
<csh-shell>
% source ~/tmp/my_test.sh
Badly placed ()'s
I already tried to wrap it in a wrapper-script:
#!/bin/sh
bash -c 'source ~/tmp/my_test.sh; my_function test`
echo my_env_var = $my_env_var
But my_env_var is not exported in this way:
<csh-shell>
% source ~/tmp/my_test2.sh
my_env_var: Undefined variable.
Where it is known in the bash shell (as can be seen by changing the 2nd script to:
#!/bin/sh
bash -c 'source ~/tmp/my_test.sh; my_function test; echo my_env_var in bash = $my_env_var`
echo my_env_var = $my_env_var
<csh-shell>
% source ~/tmp/my_test2.sh
my_env_var in bash = test
my_env_var: Undefined variable.
What am I missing / doing wrong so the script exports the variable when it is called from bash and when it is called from csh?

The Bourne shell and csh are not compatible; many commands are different, and csh misses many features (it doesn't have functions at all). Plus, sooner or later you're going to have someone who uses fish, which is different yet still. The only way to make a non-trivial script work for both is to write it twice.
That said, if you want to set some environment variables then the general strategy is to create a script which outputs the required commands; this can be in any language (shell, Python, C); for example:
#!/bin/sh
# ... do work here ...
var="foo"
# Getting the shell in a cross-platform way isn't too easy. This was only tested
# on Linux. Can add a "-c" or "-f" flag if you need cross-platform support.
shell=$(ps -ho comm $(ps -ho ppid $$))
case "$shell" in
(csh|tcsh) echo "setenv VAR $var" ;;
(fish) echo "set -Ux VAR $var" ;;
(*) echo "export VAR=$var"
esac
And when you run it, it outputs the appropriate commands:
% ./work
export VAR=foo
% tcsh
> ./work
setenv VAR foo
> fish
martin#x270 ~> ./work
set -Ux VAR foo
And to actually set it, eval the output like so:
% eval $(./work)
% echo $VAR
foo
% tcsh
> eval `./work`
> echo $VAR
foo
> fish
martin#x270 ~> eval (./work)
martin#x270 ~> echo $VAR
foo
The downside of this is that informational messages, warnings, etc. will also get eval'd; to solve this make sure to always output these to stderr:
echo >&2 "warning: foo"
If you don't want to run eval you can also use something slightly more complicated which prints VAR=foo and then create a Bourne and csh wrapper script to parse those lines, but "output the variables you want to set, instead of directly setting them" is the general approach to take to make something work in multiple incompatible shells.

Related

Bash script to append argument to $PATH

I'm trying to write as simple a bash script as possible to append one argument to the $PATH environment variable if argument isn't already part of the $PATH. I know there are other simple ways to do it by not using a bash script; however, I want to use a bash script. I've experimented with export but I haven't had any luck. Right now my simple code looks like this:
#!/bin/bash
if [[ "$(echo $PATH)" != *"$1"* ]]
then
PATH=$PATH:$1
fi
But:
$ ./script /home/scripts
$ echo $PATH
(returns unaltered PATH)
try with src or .:
src ./script /home/scripts
. ./script /home/scripts
It's because your script runs on its own interpreter and this interpreter instance (which is where the variable $PATH is getting set) dies when the script dies. You have to ask your current interpreter to run the script instead (that's what src or . are used for)

Bash script: Can't set a variable using which

In my terminal,
prog="cat"
name=$(which $prog)
echo $name
prints /bin/cat
But in my script:
pro="$1"
prog=$(which $pro)
echo "pro is $pro"
echo "prog is "$prog""
running scriptname cat prints
pro is cat
prog is
How do I make which work? it should print prog is /bin/cat
which(1) is an external program used to search PATH for an executable. It behaves differently on different systems and you can't rely on a useful exit code; use (from most to least portable) command -v or type -P (to find the path) or hash (to check) instead.
try printf '%s\n' "$PATH" inside your script as well as outside of it. maybe the command you're looking for is not in the PATH used in the script?
That is almost certainly the cause.

What's the point of eval/bash -c as opposed to just evaluating a variable?

Suppose you have the following command stored in a variable:
COMMAND='echo hello'
What's the difference between
$ eval "$COMMAND"
hello
$ bash -c "$COMMAND"
hello
$ $COMMAND
hello
? Why is the last version almost never used if it is shorter and (as far as I can see) does exactly the same thing?
The third form is not at all like the other two -- but to understand why, we need to go into the order of operations when bash in interpreting a command, and look at which of those are followed when each method is in use.
Bash Parsing Stages
Quote Processing
Splitting Into Commands
Special Operator Parsing
Expansions
Word Splitting
Globbing
Execution
Using eval "$string"
eval "$string" follows all the above steps starting from #1. Thus:
Literal quotes within the string become syntactic quotes
Special operators such as >() are processed
Expansions such as $foo are honored
Results of those expansions are split on characters into whitespace into separate words
Those words are expanded as globs if they parse as same and have available matches, and finally the command is executed.
Using sh -c "$string"
...performs the same as eval does, but in a new shell launched as a separate process; thus, changes to variable state, current directory, etc. will expire when this new process exits. (Note, too, that that new shell may be a different interpreter supporting a different language; ie. sh -c "foo" will not support the same syntax that bash, ksh, zsh, etc. do).
Using $string
...starts at step 5, "Word Splitting".
What does this mean?
Quotes are not honored.
printf '%s\n' "two words" will thus parse as printf %s\n "two words", as opposed to the usual/expected behavior of printf %s\n two words (with the quotes being consumed by the shell).
Splitting into multiple commands (on ;s, &s, or similar) does not take place.
Thus:
s='echo foo && echo bar'
$s
...will emit the following output:
foo && echo bar
...instead of the following, which would otherwise be expected:
foo
bar
Special operators and expansions are not honored.
No $(foo), no $foo, no <(foo), etc.
Redirections are not honored.
>foo or 2>&1 is just another word created by string-splitting, rather than a shell directive.
$ bash -c "$COMMAND"
This version starts up a new bash interpreter, runs the command, and then exits, returning control to the original shell. You don't need to be running bash at all in the first place to do this, you can start a bash interpreter from tcsh, for example. You might also do this from a bash script to start with a fresh environment or to keep from polluting your current environment.
EDIT:
As #CharlesDuffy points out starting a new bash shell in this way will clear shell variables but environment variables will be inherited by the spawned shell process.
Using eval causes the shell to parse your command twice. In the example you gave, executing $COMMAND directly or doing an eval are equivalent, but have a look at the answer here to get a more thorough idea of what eval is good (or bad) for.
There are at least times when they are different. Consider the following:
$ cmd="echo \$var"
$ var=hello
$ $cmd
$var
$ eval $cmd
hello
$ bash -c "$cmd"
$ var=world bash -c "$cmd"
world
which shows the different points at which variable expansion is performed. It's even more clear if we do set -x first
$ set -x
$ $cmd
+ echo '$var'
$var
$ eval $cmd
+ eval echo '$var'
++ echo hello
hello
$ bash -c "$cmd"
+ bash -c 'echo $var'
$ var=world bash -c "$cmd"
+ var=world
+ bash -c 'echo $var'
world
We can see here much of what Charles Duffy talks about in his excellent answer. For example, attempting to execute the variable directly prints $var because parameter expansion and those earlier steps had already been done, and so we don't get the value of var, as we do with eval.
The bash -c option only inherits exported variables from the parent shell, and since I didn't export var it's not available to the new shell.

Running two commands in same variable in Bash CGI

In my Bash CGI script, I take a command passed as GET parameter and execute it. This could be:
CMD='ls -al'
$CMD
Which works fine and produces expected output. But if I try to pass two commands with
CMD='ls -al; echo hello'
$CMD
or
CMD='ls -al && echo hello'
$CMD
neither command gets executed.
How can I run multiple commands from the same line/variable in my bash CGI?
You can execute variables as bash code using bash:
# UNSAFE, DO NOT USE
cmd='ls -al; echo hello'
bash -c "$cmd"
Alternatively, depending on the context you want to run it in, you can use eval "$cmd" to run it as if it was a line in your own script, rather than a separate piece of shell code to execute:
# UNSAFE, DO NOT USE
cmd='ls -al; echo hello'
eval "$cmd"
Both of these methods have serious implications for security and correctness, so I felt I had to add warnings to prevent them from being copied out of context.
For your remote shell or root kit specifically meant to run insecure user input, you can ignore the warnings.

Setting environment variables for multiple commands in bash one-liner

Let's say I have following command
$> MYENVVAR=myfolder echo $MYENVVAR && MYENVVAR=myfolder ls $MYENVVAR
I mean that MYENVVAR=myfolder repeats
Is it possible to set it once for both "&&" separated commands while keeping the command on one line?
Assuming you actually need it as an environment variable (even though the example code does not really need an environment variable; some shell variables are not environment variables):
(export MYENVVAR=myfolder; echo $MYENVVAR && ls $MYENVVAR)
If you don't need it as an environment variable, then:
(MYENVVAR=myfolder; echo $MYENVVAR && ls $MYENVVAR)
The parentheses create a sub-shell; environment variables (and plain variables) set in the sub-shell do not affect the parent shell. In both commands shown, the variable is set once and then used twice, once by each of the two commands.
Parentheses spawn a new process, where you can set its own variables:
( MYENVVAR=myfolder; echo 1: $MYENVVAR; ); echo 2: $MYENVVAR;
1: myfolder
2:
Wrapping the commands into a string and using eval on them is one way not yet mentioned:
a=abc eval 'echo $a; echo $a'
a=abc eval 'echo $a && echo $a'
Or, if you want to use a general-purpose many-to-many mapping between environment variables and commands, without the need to quote your commands, you can use my trap-based function below:
envMulti()
{
shopt -s extdebug;
PROMPT_COMMAND="$(trap -p DEBUG | tee >(read -n 1 || echo "trap - DEBUG")); $(shopt -p extdebug); PROMPT_COMMAND=$PROMPT_COMMAND";
eval "trap \"\
[[ \\\"\\\$BASH_COMMAND\\\" =~ ^trap ]] \
|| { eval \\\"$# \\\$BASH_COMMAND\\\"; false; }\" DEBUG";
}
Usage:
envMulti a=aaa b=bbb; eval 'echo $a'; eval 'echo $b'
Note: the eval 'echo...'s above have nothing to do with my script; you can never do a=aaa echo $a directly, because the $a gets expanded too early.
Or use it with env if you prefer (it actually prefixes any commands with anything):
echo -e '#!/bin/bash\n\necho $a' > echoScript.sh
chmod +x echoScript.sh
envMulti env a=aaa; ./echoScript.sh; ./echoScript.sh
Note: created a test script just to demonstrate usage with env, which can't accept built-ins like eval as used in the earlier demo.
Oh, and the above were all intended for running your own shell commands by-hand. If you do anything other than that, make sure you know all the cautions about using eval -- i.e. make sure you trust the source of the commands, etc.
Did you consider using export like
export MYENVVAR=myfolder
then type your commands like echo $MYENVVAR (that would work even in sub-shells) etc

Resources