Properly splitting a string in UNIX shell - bash

I need to be able to split a string so that each string are passed as variable in my shell.
I tried something like this:
$ cat test.sh
#!/bin/sh
COMPOPT="CC=clang CXX=clang++"
$COMPOPT cmake ../gdcm
I also tried a bash specific solution, but with no luck so far:
$ cat test.sh
#!/bin/bash -x
COMPOPT="CC=clang CXX=clang++"
ARRAY=($COMPOPT)
"${ARRAY[0]}" "${ARRAY[1]}" cmake ../gdcm
I always get the non-informative error message:
./test.sh: 5: ./t.sh: CC=clang: not found
Of course if I try directly from the running shell this works:
$ CC=clang CXX=clang++ cmake ../gdcm

Another eval-free solution is to use the env program:
env "${ARRAY[#]}" cmake ../gdm
which provides a level of indirection to the usual FOO=BAR command syntax.

When you say:
$COMPOPT cmake ../gdcm
the shell would attempt to execute the value of the variable as a command.
The evil eval is rather handy in such cases. Say:
eval $COMPOPT cmake ../gdcm

Though devnull's answer works but uses eval and that has known pitfalls.
Here is a way it can be done without invoking eval:
#!/bin/sh
COMPOPT="CC=clang CXX=clang++"
sh -c "$COMPOPT cmake ../gdcm"
i.e. pass the whole command line to sh (or bash).

Related

Bash: Insert unescaped string/characters from variable into command

In bash (GNU bash, version 3.2.57), I would like to substitute the exact content of a variable (unescaped) into a following command.
To illustrate what I mean, given the following string variable:
s="2>&1 > /dev/null"
If I try to insert that exact string into a command:
bash --version $s || echo "will install bash"
(this command is just a simple example for the sake of the question)
The command actually executed looks like this:
bash --version '2>&1' '>' /dev/null
The inserted strings are escaped, which I don't want.
What I would like instead is to somehow insert the content of s, unescaped, into the executed command, so that the executed command is this one:
bash --version 2>&1 > /dev/null
How could I achieve that ?
How could I achieve that ?
Instead of a variable, use a function.
run_this_silent() {
"$#" 2>&1 >/dev/null
}
run_this_silent bash --version
It is not possible to store redirections in a variable without using eval (or the equivalent bash -c COMMAND), and eval is a bad solution in pretty much every case imaginable. If you want to unconditionally silence a command (or a hundred commands) it's better to explicitly add the redirects to each of them.

Assigning a variable in a shell script for use outside of the script

I have a shell script that sets a variable. I can access it inside the script, but I can't outside of it. Is it possible to make the variable global?
Accessing the variable before it's created returns nothing, as expected:
$ echo $mac
$
Creating the script to create the variable:
#!/bin/bash
mac=$(cat \/sys\/class\/net\/eth0\/address)
echo $mac
exit 0
Running the script gives the current mac address, as expected:
$ ./mac.sh
12:34:56:ab:cd:ef
$
Accessing the variable after its created returns nothing, NOT expected:
$ echo $mac
$
Is there a way I can access this variable at the command line and in other scripts?
A child process can't affect the parent process like that.
You have to use the . (dot) command — or, if you like C shell notations, the source command — to read the script (hence . script or source script):
. ./mac.sh
source ./mac.sh
Or you generate the assignment on standard output and use eval $(script) to set the variable:
$ cat mac.sh
#!/bin/bash
echo mac=$(cat /sys/class/net/eth0/address)
$ bash mac.sh
mac=12:34:56:ab:cd:ef
$ eval $(bash mac.sh)
$ echo $mac
12:34:56:ab:cd:ef
$
Note that if you use no slashes in specifying the script for the dot or source command, then the shell searches for the script in the directories listed in $PATH. The script does not have to be executable; readable is sufficient (and being read-only is beneficial in that you can't run the script accidentally).
It's not clear what all the backslashes in the pathname were supposed to do other than confuse; they're unnecessary.
See ssh-agent for precedent in generating a script like that.

Create alias from stdout

I am trying to dynamically create alias' from the output of another command line tool.
For example:
> MyScript
blender="/opt/apps/blender/blender/2.79/blender"
someOtherAlias="ls -l"
I am trying the following code:
MyScript | {
while IFS= read -r line;
do
`echo alias $line`;
done;
}
But when I run this, I get the following error:
bash: alias: -l": not found
Just trying to run this command by itself gives me the same error:
> `echo 'alias someOtherAlias="ls -l"'`
bash: alias: -l": not found
But obviously the following command does work:
alias someOtherAlias="ls -l"
I've tried to find someone else who may have done this before, but none of my searches have come up with anything.
I would appreciate any and all help. Thanks!
See how bash (and posix shells) command parsing and quoting works and see difference between syntax and literal argument: for example '.."..' "..'.." are litteral quotes in an argument whereas " or ' are shell syntax and are not part of argument
also, enabling tacing with set -x may help to understand :
set -x
`echo 'alias someOtherAlias="ls -l"'`
++ echo 'alias someOtherAlias="ls -l"'
+ alias 'someOtherAlias="ls' '-l"'
bash: alias: -l": not found
bash sees 3 words : alias, someOtherAlias="ls and -l".
and alias loops over its arguments if they contain a = it create an alias otherwise it displays what alias argument is as -l" is not an alias it shows the error.
Note also as backquotes means command is run in a subshell (can be seen with mutiple + in trace) it will have no effect in current shell.
eval may be use to reinterpret literal as bash syntax (or to parse again a string).
So following should work, but be careful using eval on arbitrary arguments (from user input) can run arbitrary command.
eval 'alias someOtherAlias="ls -l"'
Finally also as bash commands after pipe are also run in subshell.
while IFS= read -r line;
do
`echo alias $line`;
done <MyScript

How do you invoke alias from ruby code for zsh shell?

I would like to invoke alias from ruby code so as to test the alias which I programmatically inserted into the dotfile. Say for example, the alias is the following:
alias something="echo somethingelse"
I searched the web and found the solution for bash:
#solution for bash
system %(
source ~/.bash_profile
shopt -s expand_aliases
something
)
However, this does not work for zsh.
I tried to invoke the alias using the following code (and a combination of other commands) but to no avail.
system %(
exec zsh #this seems to source .zshrc
something #this does not work
)
I would like it to work for zsh too. How can I get it working for zsh? Does anyone have a suggestion? Thanks in advance!
I have also tested the following but they don't work:
system %(
# exec /bin/zsh #this causes the subsequent lines to not run.
source ~/.zshrc #this causes the error lines to be printed
# setopt aliases #don't think it helps
something #trying to invoke this which is already in zshrc
)
The error messages:
/Users/ytbryan/.zprezto/init.zsh: line 14: autoload: command not
found
/Users/ytbryan/.zprezto/init.zsh: line 15: print: command not
found
/Users/ytbryan/.zshrc: line 42: `#': not a valid identifier
One approach is to run Zsh code via zsh -c. Aliases are not expanded when run from zsh -c, but the builtin aliases array is still accessible, so one can still expand aliases by manually retrieving expansions from the array and manually performing word splitting. This should cover most of the commonly seen aliases. For more advanced aliases (that involves process substitution, parameter expansion, command substitution, arithmetic expansion, brace expansion, filename expansion or filename generation, or that is more than a simple command), one might need to use eval (but one needs to be very cautious when using eval, and never ever use it when input comes from an untrusted source, or from a trusted but possibly tempered-with source).
Example code that could be embedded in Ruby system calls:
> zsh -c 'alias foo="print bar"; ${=aliases[foo]}'
bar
> zsh -c 'alias foo=print; ${=aliases[foo]} $#' -- 1 2 3
1 2 3
> zsh -c 'alias foo="print a b c | grep -o a"; ${=aliases[foo]}' # simple case where naive expansion fails
a b c | grep -o a
> zsh -c 'alias foo="print a b c | grep -o a"; eval "$aliases[foo]"' # eval comes to rescue, but be extra careful
a
Note that source works in zsh -c, so the alias definitions above could be sourced from any file just fine.

How can I use Bash syntax in Makefile targets?

I often find Bash syntax very helpful, e.g. process substitution like in diff <(sort file1) <(sort file2).
Is it possible to use such Bash commands in a Makefile? I'm thinking of something like this:
file-differences:
diff <(sort file1) <(sort file2) > $#
In my GNU Make 3.80 this will give an error since it uses the shell instead of bash to execute the commands.
From the GNU Make documentation,
5.3.2 Choosing the Shell
------------------------
The program used as the shell is taken from the variable `SHELL'. If
this variable is not set in your makefile, the program `/bin/sh' is
used as the shell.
So put SHELL := /bin/bash at the top of your makefile, and you should be good to go.
BTW: You can also do this for one target, at least for GNU Make. Each target can have its own variable assignments, like this:
all: a b
a:
#echo "a is $$0"
b: SHELL:=/bin/bash # HERE: this is setting the shell for b only
b:
#echo "b is $$0"
That'll print:
a is /bin/sh
b is /bin/bash
See "Target-specific Variable Values" in the documentation for more details. That line can go anywhere in the Makefile, it doesn't have to be immediately before the target.
You can call bash directly, use the -c flag:
bash -c "diff <(sort file1) <(sort file2) > $#"
Of course, you may not be able to redirect to the variable $#, but when I tried to do this, I got -bash: $#: ambiguous redirect as an error message, so you may want to look into that before you get too into this (though I'm using bash 3.2.something, so maybe yours works differently).
One way that also works is putting it this way in the first line of the your target:
your-target: $(eval SHELL:=/bin/bash)
#echo "here shell is $$0"
If portability is important you may not want to depend on a specific shell in your Makefile. Not all environments have bash available.
You can call bash directly within your Makefile instead of using the default shell:
bash -c "ls -al"
instead of:
ls -al
There is a way to do this without explicitly setting your SHELL variable to point to bash. This can be useful if you have many makefiles since SHELL isn't inherited by subsequent makefiles or taken from the environment. You also need to be sure that anyone who compiles your code configures their system this way.
If you run sudo dpkg-reconfigure dash and answer 'no' to the prompt, your system will not use dash as the default shell. It will then point to bash (at least in Ubuntu). Note that using dash as your system shell is a bit more efficient though.
It's not a direct answer to the question, makeit is limited Makefile replacement with bash syntax and it can be useful in some cases (I'm the author)
rules can be defined as bash-functions
auto-completion feature
Basic idea is to have while loop in the end of the script:
while [ $# != 0 ]; do
if [ "$(type -t $1)" == 'function' ]; then
$1
else
exit 1
fi
shift
done
https://asciinema.org/a/435159

Resources