Exactly what does env do in Bash? - bash

I get this behavior when using Bash (under Cygwin):
$ printf '\u00d5'
\u00d5
$ env printf '\u00d5' # This results in the behavior I want
Õ
It does not matter if I use UTF-8 or ISO-8859-1 encoding in the terminal.
My questions are: Exactly what does env do? Why do I need it in this specific case?

env is not in bash, it's a standalone executable, it is used to set or clear environment variable before running program. In your particular case, it's running the binary printf instead of the shell builtin. You can achieve the same result by using the absolute path:
/usr/bin/printf '\u00d5'
The least intrusive method is perhaps the following: redefine the printf function and have Bash handle the rest. Source a file that contains the following:
function printf()
{
$(which printf) "$#"
}
or as a one-liner function printf() { $(which printf) "$#"; }. Of course you could replace the $(which printf) for a /usr/bin/printf ...
Then simply use printf as you're used to. Your script stays the same and you could even introduce a condition to define the function only on certain Bash versions.
AFAIK you can also leave out the function, but I find it adds to readability.
[EDIT: the function keyword is a bash extension; printf () { ...; } is the POSIX syntax. If you do use the function keyword, the () after the function name are optional.]
Commonly, env is also used in the hash-bang lines of scripts that strive to be portable. The reason being that env is nearly always at /usr/bin/env, while bash isn't always at /bin/bash as many a hash-bang line implies. Example:
#!/usr/bin/env bash
also works for other programs/interpreters:
#!/usr/bin/env python

Related

Export a function from zsh to bash script

I have a function defined in my .zshenv like this:
function my_func() {
echo $1
}
I then execute a bash script from the zsh which its content is:
type my_func
I got an error: /tmp/test.bash: line 3: type: my_func: not found
While if I type type my_func from the zsh I got: my_func is a shell function from
Is there a way to use zsh defined function in a bash script? It seems to work for the exported variables
How bash does it
Bash itself can export its functions to other bash shells. It does so by exporting a string environmental variable of the form:
BASH_FUNC_functionNameHere%%=() { functionBodyHere; }
So in theory you could use the following zsh-command to export your function for bash:
export "BASH_FUNC_my_func%%=() { $(echo; typeset -f my_func | tail -n+2) }"
However, this does not work because zsh doesn't allow %% in identifiers.
Workaround
Depending on how you start bash, you might be able to inject the function definition into the bash process.
When running a bash script like bash myScript or ./myScript you can use bash -c "$(typeset -f my_func); export -f my_func; myScript" instead.
When starting an interactive bash shell using bash, you can use bash -c "$(typeset -f my_func); export -f my_func; exec bash" instead.
The right way
Either way, your function has to be a polyglot. That is, the source code of the function has to be understood by both zsh and bash. Above approach is not really viable if you want to export many functions or want to call many bash scripts.
It would be easier to define each of your functions inside its own script file and add the locations of those scripts to $PATH. That way you can call your "functions" from every shell and they will always work independently from your current shell.
Replacing the functions by script files only works if your functions don't want to modify the parent shell. cd or setting variables has no effect on the caller. If you want to do stuff like this, you can still use a script file, but then have to source it using . myFunctionFile. For sourcing, the source code has to be a polyglot again.

Converting a BASH script to run on SH (via BusyBox)

I have an Asus router running a recent version of FreshTomato - that comes with BusyBox.
I need to run a script that was made with BASH in mind - it is an adaptation of this script - but it fails to run with this error: line 41: syntax error: bad substitution
Checking the script with shellcheck.net yields these errors:
Line 41:
for optionvarname in ${!foreign_option_*} ; do
^-- SC3053: In POSIX sh, indirect expansion is undefined.
^-- SC3056: In POSIX sh, name matching prefixes are undefined.
Line 42:
option="${!optionvarname}"
^-- SC3053: In POSIX sh, indirect expansion is undefined.
These are the lines that are causing problems:
for optionvarname in ${!foreign_option_*} ; do # line 41
option="${!optionvarname}" # line 42
# do some stuff with $option...
done
If my understanding is correct, the original script simply does something with all variables that have a name starting with foreign_option_
However, as far as I could determine, both ${!foreign_option_*} and ${!optionvarname} constructs are BASH-specific and not POSIX compliant, so there is no direct "bash to sh" code conversion possible.
I have tried to create a /bin/bash symlink that points to busybox, but I got the Read-only file system error.
So, how can I get this script to run on my router? I see only two options, but I cant figure out how to implement either:
Make BusyBox interpret the script as BASH instead of SH - can I use a specific shebang for this?
Seems like the fastest option, but only if BusyBox has a "complete" implementation of BASH
Alter the script code to not use BASH specifics.
This is safer, but since there is no "collect al variables starting with X" for SH, how can I do it?
how can I get this script to run on my router?
That easy, either:
install bash on your router or
port the script to busybox/posix compatible shell.
Make BusyBox interpret the script as BASH instead of SH - can I use a specific shebang for this?
That doesn't make sense. Busybox comes with ash shell interpreter and bash is bash. Bash can interpret bash extensions, ash can't interpret them. You can't "make busybox interpret bash" - cars don't fly, planes are for that. If you want to make a car fly, you add wings to it and make it faster. The answer to Make BusyBox interpret the script as BASH instead of SH would be: patch busybox and implement all bash extensions in it.
Shebang is used to run a file under different interpreter. Using #!/bin/bash would invoke bash, which would be unrelated to anything busybox related and busybox wouldn't be involved in it.
how can I do it?
Decide on a unrealistic maximum, iterate over variables named foreign_option_{1...some_max}, for each variable see if it is set, if it is set, cotinue the script.
for i in $(seq 100); do
optionvarname="foreign_option_${i}"
# https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash
if eval "[ -z \"\${${optionvarname}+x}\" ]"; then continue; fi;
With enough luck maybe you can use the set output. The following will fail if any variable contains a value as newline + the string that matches the regex:
for optionvarname in $(set | grep -o '^foreign_option_[0-9]\+=' | sed 's/=//'); then
Indirect expansion can be easily replaced by eval:
eval "option=\"\$${optionvarname}\""
If you really cannot install Bash on that router, here is one possible workaround, which seems to work for me in BusyBox on a Qnap NAS :
foreign_option_one=1
foreign_option_two=2
for x in one two; do
opt_var=foreign_option_${x}
eval "opt_value=\$$opt_var"
echo "$opt_var = $opt_value"
done
(But you will probably encounter more problems with moving a Bash script to busybox, so you might want to first consider alternatives like replacing the router)

Build a shell command in a string for later execution portably

I am trying to do the following command in bash and dash:
x="env PATH=\"$PATH:/dir with space\""
cmd="ls"
"$x" $cmd
This fails with
-bash: env PATH="/opt/local/bin:/opt/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/git/bin:/usr/local/go/bin:/dir
with space": No such file or directory
Note the following works:
env PATH="$PATH:/dir with space" $cmd
The reason I am assigning to variable x env, is because it is part of a larger command wrapper to $cmd, which is also a complicated variable.
It's more complex than the initial example. I have logic in setting these variables,, instead of repeating them each time. Eventually the invocation is as shown here:
path_value="$PATH"
invocation="env PATH=\"$path_value\" $other_val1 $other_val2"
base="python python_script.py --opt1=a,b,c script_args"
add_on="$base more_arg1 more_arg2"
"$invocation" $base
You can use shell array to store and reuse:
x=(env PATH="$PATH:/dir with space")
cmd="ls"
"${x[#]}" "$cmd"
anubhava's helpful array-based bash answer is the best choice if you can assume only bash (which appeared to be the case initially).
Given that you must also support dash, which almost exclusively supports POSIX sh features only, arrays are not an option.
Assuming that you fully control or trust the values that you use to build your command line in a string, you can use eval, which should generally be avoided:
path_value="$PATH"
invocation="env PATH=\"$path_value\" $other_val1 $other_val2"
base="python python_script.py --opt1=a,b,c script_args"
add_on="$base more_arg1 more_arg2"
eval "$invocation $add_on"

Bash variable space in directory path

How can I remove a white space when I use a variable in a directory path.
For example, I'm trying to do
alias test='echo /david/$1'
and when I try
test hhh
this produces
/david/ hhh
with a white space before the variable. It seems very simple but I can't find a solution.
alias doesn't do parameter expansion. At all. Use a function instead.
test (){
echo "/david/$1"
}
man bash:
There is no mechanism for using arguments in the replacement text. If arguments are needed, a shell function should be used (see FUNCTIONS below). [...] For almost every purpose, aliases are superseded by shell functions.
As a part of alias expansion a space is added at the end of the expanded string (otherwise no argument could be added, like alias ll='ls -l'. So ll -a would be expanded to ls -l-a which would be wrong). So I see no solution to this problem anything else then to use function as Ignacio proposed.
Anyway using test as function or alias is not the best thing to do as it is a bash built-in command (if no aliased or named a function). You can check how a mnemonic will be interpreted using the type bash built-in.
I defined an alias and a function called test:
$ type test
type test
test is aliased to `echo /xxx/'
$ type -a test
type -a test
test is aliased to `echo /xxx/'
test is a function
test ()
{
echo "/yyy/$1"
}
test is a shell builtin
test is /usr/bin/test

How do I include an environment variable inside an alias for bash?

I am pretty new to bash, and I want to include an env for bash aliases.. I want to do something like the following
alias foo="bar $(baz)"
So that I could do something like the following
> baz=40
> foo
and foo will expand to the command bar 40. Currently the above does not work because $(baz) is expanded while making the alias. Do I have to wrap this inside a function or something?
You need to use single quotes (') to prevent bash from expanding the variable when creating the alias:
$ alias foo='echo "$bar"'
$ bar="hello"
$ foo
hello
Aliases don't have an "environment". An alias is simply a "dumb" text substitution. In the question, an environment variable isn't being used - only a shell variable. If you want to use the environment, use a function. In this case, there is no advantage to an alias over a function.
$ alias foo='echo "$bar"'
$ bar=hi foo
This produces no output because the environment set for a simple command doesn't apply to expansions.
$ alias foo=$'eval \'echo "$bar"\''
$ bar=hi foo
hi
If a function were used instead, there wouldn't be a problem.
$ foo() { echo "$bar"; }
$ bar=hi foo
hi
When in doubt, always use a function.
Edit
Technically, the above is bash-only. Doing this in a fully portable way is nearly impossible.
In dash, mksh, bash POSIX mode, and other POSIX shells you can do:
foo() { echo "$bar"; }
bar=hi command eval foo
However, this won't work in ksh93 or zsh. (I've already reported a bug for ksh93 but it may never be fixed.) In mksh and ksh93 you should instead define functions using the function keyword, but that isn't POSIX. I'm not aware of any solution that will work everywhere.
To make matters worse, extra exceptions are being added to POSIX 2008-TC1 so that the way environment assignments work will be even more complicated. I suggest not using them unless you really know what you're doing.
I do the following to delay expansion of environment variables in aliases until they are run,
alias foo="ls \${FOO_DIR}"
for example to show the contents of the dynamically-defined value of FOO_DIR.

Resources