Can I portably define a variable based on a function argument? - shell

I'd like to define a function defvar which defines a variable, e.g.
defvar FOO BAR ## equivalent to FOO=BAR
However, the naive way of defining such a function does not work (presumedly because the shell ends up executing the whole string as a single command?)
defvar () { $1=$2; } ## no dice
One way around this is to explicitly perform the assignment as part of an export call, e.g.
defvar () { export $1=$2; } ## works, but not portable to some older shells
which works in my local bash shell, but it sounds like this may not be portable, and may not work on some older shells.
Is there a portable mechanism for accomplishing this?

We can solve this using eval:
defvar () {
eval "$1='$2'"
}
We construct a statement to evaluate using the function arguments, e.g. "$1='$2'", and then evaluate the constructed string (thereby performing the assignment).
Since eval is defined by the POSIX standard, it's about as portable as it gets. It seems to even work on the heirloom bourne shell, so I imagine this will work on even the older shells out there.
That said, using eval is kind of icky, since someone could write something like:
defvar FOO "'; echo Ouch; '"
That is, it's easy to use that to execute arbitrary code, so either this should only be run with trusted inputs, or sanitized / validated input.

Related

Define bash aliases to run git as specific user

Is it possible to define a function in a bash script which generically defines git-aliases for different users in order to let users apply their changes on a shared system so that the commits contains their username and email?
alias git_as_user1='GIT_AUTHOR_NAME="User1_pre User1_sur" GIT_AUTHOR_EMAIL="user1#company.de" GIT_SSH="/home/account/ssh_user_wrapper.sh" GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL git'
I came up with the following function, but it does not evaluate args at the time of the alias definition but later on, when the alias is called.
This is unintended and renders the approach useless.
function alias_git_as ()
{
alias git_as_$1='GIT_AUTHOR_NAME=$1 GIT_AUTHOR_EMAIL=$2
}
In .basrc:
alias_git_as "login" "Surname Prename" "user#company.de"
-> Won't work !!! -> Defines the alias git_as_login, but the second and third arg are dismissed. When a certain user runs git_as_login from his terminal he would need to pass "Surname Prename" "user#company.de" again. But the args should be captured at time the alias is defined.
Two issues:
You use single quotes, but those suppress expansion; to make expansion happen early, you need double quotes instead.
Your original code only takes two arguments, but your example usage uses three.
Also, to make this work with names with spaces, we use the bash 5.x feature ${var#Q} below.
# define the function
alias_git_as() { alias "git_as_$1=GIT_AUTHOR_NAME=${2#Q} GIT_AUTHOR_EMAIL=${3#Q}"; }
# use the function
alias_git_as "login" "Surname Prename" "user#company.de"
# use the invoked alias
git_as_login
See this working at https://ideone.com/PV09NG
A version that's compatible with older versions of bash while still retaining support for unusual author names may instead look like:
alias_git_as() {
local alias_def
printf -v alias_def 'git_as_%s=GIT_AUTHOR_NAME=%q GIT_AUTHOR_EMAIL=%q' "$1" "$2" "$3"
alias "$alias_def"
}

Conditional to move forward depending on what shell is used

I'm trying to write a .functions dotfile, with the purpose of loading it (source $HOME/.functions) in my bash, zsh and fish configuration files. I already did it with another (.aliases), successfully. However now I am facing a problem derived from fish not being posix-compliant.
The thing is that aliases share syntax among the three shells, but when it comes to functions fish has its own syntax (function my_func; #code; end instead of function my_func { #code; }). As an example, consider:
Fish:
function say_hello
echo "hello";
end
Bash/Zsh:
say_hello() {
echo "hello";
}
This disables me from just writing them in the file "as is", so I was thinking of writing a conditional such as if [ "$0" = "bash" ] || [ "$0" = "zsh" ]; then #functions_POSIX; else #functions_fish; fi. However, this conditional syntax is also not available in fish!
That's where I'm stuck rn. I would rather not have separate files for each shell.
Thank you in advance.
The only workable answer, in my opinion, is to separate the definitions.
Even if you figure out some way to hack around the fact that fish checks the syntax for the entire file (so wherever you put a bash function definition it will give a syntax error without executing anything), this won't yield a readable file that's nice to edit. You'll just be fighting the hacks.
And function definitions can't be shared anyway, as it's not just a simple search-and-replace of fi to end - the semantics are different, e.g. a command substitution will only be split on newlines, the various special variables ($#) work in different ways, arrays are entirely different, etc...
That means making it a single file isn't workable or helpful, so make your functions scripts instead (if they don't modify the shell's environment) or make a wrapper around a script that does the environment changing, or just write them twice.

How do I pass a command parameter in a variable holding the command?

I want to produce the same output as this:
bash utilities.bash "is_net_connected"
But I don't know how to pass "is_net_connected" if command and file is stored in a variable like this:
T=$(bash utilities.bash)
I've tried these but it doesn't seem to work. It's not picking up ${1} in utilities.bash.
$(T) "is_net_connected"
$(T "is_net_connected")
Not the best way to inport but I'm trying to avoid cluttering my main script with function blocks.
T=$(bash utilities.bash) doesn't save the command; it runs the command and saves its output. You want to define a function instead.
T () {
bash utilities.bash "$#"
}
# Or on one line,
# T () { bash utilities.bash "$#"; }
Now
T "is_net_connected"
will run bash utilities.bash with whatever arguments were passed to T. In a case like this, an alias would work the same: alias T='bash utilities.bash'. However, any changes to what T should do will probably require switching from an alias to a function anyway, so you may as well use the function to start. (Plus, you would have to explicitly enable alias expansion in your script.)
You might be tempted to use
T="bash utilities.bash"
$T is_net_connected
Don't be. Unquoted parameter expansions are bad practice that only work in select situations, and you will get bitten eventually if you try to use them with more complicated commands. Use a function; that's why the language supports them.

Bash functions returning values meanwhile altering global variables

I'm just struggling with bash functions, and trying to return string values meanwhile some global variable is modified inside the function. An example:
MyGlobal="some value"
function ReturnAndAlter () {
MyGlobal="changed global value"
echo "Returned string"
}
str=$(ReturnAndAlter)
echo $str # prints out 'Returned value' as expected
echo $MyGlobal # prints out the initial value, not changed
This is because $(...) (and also `...` if used instead) cause the function to have its own environment, so the global variable is never affected.
I found a very dirty workaround by returning the value into another global variable and calling the function only using its name, but think that there should be a cleaner way to do it.
My dirty solution:
MyGlobal="some value"
ret_val=""
function ReturnAndAlter () {
ret_val="Returned string"
MyGlobal="changed value"
}
ReturnAndAlter # call the bare function
str=$ret_val # and assign using the auxiliary global ret_val
echo $str
echo $MyGlobal # Here both global variables are updated.
Any new ideas? Some way of calling functions that I'm missing?
Setting global variables is the only way a function has of communicating directly with the shell that calls it. The practice of "returning" a value by capturing the standard output is a bit of a hack necessitated by the shell's semantics, which are geared towards making it easy to call other programs, not making it easy to do things in the shell itself.
So, don't worry; no, you aren't missing any cool tricks. You're doing what the shell allows you to do.
The $(…) (command expansion) is run in a sub-shell.
All changes inside the sub-shell are lost when the sub-shell close.
It is usually a bad idea to use both printing a result and changing a variable inside a function. Either make all variables or just use one printed string.
There is no other solution.

Exclamation point in bash function name - should I?

I'm writing a bash script and it's really convenient to use an exclamation point in a function name.
Example:
function hello! {
echo goodbye
}
function hello {
echo hello
}
And it works!
After looking through the specs, I found this:
name
A word consisting solely of letters, numbers, and underscores, and beginning with a letter or underscore. Names are used as shell variable and function names. Also referred to as an identifier.
I feel like I'm breaking the rules here. Is this wrong? Will this mess something up in the future? What's actually going on?
Since it violates the Bash spec, I'd say you're exploiting a bug in Bash, so your code might not work when the bug is fixed. Drop the !
Out of burning curiousity, why is it so much more convenient to use the exclamation point in your function name?
Generally, for portability reasons, you may not want to use the bang; just because the interpreter on that particular OS accepts it, if you need to deploy that script elsewhere, other interpreters of slightly different flavors/versions may not be as accepting.
I'm not sure about the implications in this case, but if the specification states something this clearly, I'd say anything beyond that is undefined behavior and should be avoided.
It's not a good idea to use ! in a function name if you want your code to be portable. bash --posix or invoking bash as "sh" both reject "hello!" as a function name. But I suspect that bash silently permits aberrant function names ("hello?" "hello-" and "hello/" also work, to name a few) because one important use of functions is allowing the user to override normal commands and these commands (e.g. ls, rm, etc.) can contain any sort of character allowed by the filesystem.
Note that "hello!" as a variable name doesn't work.
If your function is meant to be invoked by an user as a command from the terminal, i. e. it's defined in .bashrc, then you could give it a longer name and create an alias with the bang at the end.
Example:
function cd_mkdir {
DEST=$1
mkdir -p "$DEST"
cd "$DEST"
}
alias cd!=cd_mkdir
Now, while in terminal I can invoke this as:
cd! foo/bar/baz
And the foo, bar and baz directories get created if they don't exists.
The exclamation mark at the end is a nice and easy mnemonic of "shouting" the familiar command to be force executed, even if the original variant of the command couldn't.

Resources