1

Please notice that I'm talking about passing arguments by-ref to a script, not to a function. I already checked here and here and they discuss about passing arguments using functions.

What I want is to call a script passing an argument to it and having the argument becoming a variable in the shell with the value assigned inside the script (which is exactly what happens with the read RESPONSE command, where RESPONSE becomes a variable in the current shell with the value assigned by the read command).

I know that using functions like this (using local -n):

#!/bin/bash

function boo() {
  local -n ref=$1
  ref="new"
}

SOME_VAR="old"
echo $SOME_VAR
boo SOME_VAR
echo $SOME_VAR

or like this (using eval):

function booeval() {
  ref=$1
  eval $(echo $ref=\"new\")
}

SOME_VAR="old"
echo $SOME_VAR
booeval SOME_VAR
echo $SOME_VAR

work as expected. But local (or declare/typedef) doesn't work outside of a function and eval seems to run in a subshell that when the script is over the variable doesn't survive in the shell. I wanted to have something like this:

$ cat /tmp/test.sh
#!/bin/bash

ref=$1
eval $(echo "export $ref=\"new value\"")
$ /tmp/test.sh VAR
$ echo $VAR

$

Any ideas?

Adriano_Pinaffo
  • 1,429
  • 4
  • 23
  • 46
  • 3
    You can do this by running the script with the `source` command (which more-or-less runs its contents like a function), but not while running the script normally. When you run a script normally, it runs in a subprocess, which has no access to the parent shell's variables (except environment variables, but even there it has copies which cannot affect the originals). Also, arguments are inherently just text strings, not anything more complex or flexible. – Gordon Davisson Aug 05 '23 at 22:18
  • @GordonDavisson the source thing will not work from the shell. I mean, it would, but I can't rely on the user to run the source command before running the program – Adriano_Pinaffo Aug 06 '23 at 18:44
  • 1
    @Adriano_Pinaffo, one option is for your program to check if it is being sourced and exit with a helpful error message if it is not. See [BashFAQ/109 (How can I tell whether my script was sourced (dotted in) or executed?)](https://mywiki.wooledge.org/BashFAQ/109). – pjh Aug 06 '23 at 20:49

3 Answers3

2

As you are no doubt aware, what is assigned in the subshell stays in the subshell.

As noted in the comments, one solution is to not have a subshell: If the script is sourced then assignments will be retained. However this requires that the script be carefully written to not have unwanted side-effects.

If invoking the script normally (as a subshell) you might be able to work around the problem with convoluted IPC to pass values around but it seems to me you could bypass complexity entirely by simply having the script output the value instead of attempting to assign it to anything:

var=$(script)

You can wrap this using your first approach, so that the variable name can be passed in:

assign_to(){
    local -n ref=$1
    shift
    ref=$("$@")
}

For example:

$ myref=myvar
$ assign_to $myref date +%Y
$ declare -p myvar
declare -- myvar="2023"
$
jhnc
  • 11,310
  • 1
  • 9
  • 26
  • But here you used local -n inside of a function, which is something that will not work in a script, because of the whole subshell we talked about. I want something like the `read` command which runs on the subshell but assigns a value to a variable that was passed in the main shell – Adriano_Pinaffo Aug 06 '23 at 18:43
  • 1
    @Adriano_Pinaffo, [read](https://www.gnu.org/software/bash/manual/bash.html#index-read) is a [Bash builtin command](https://www.gnu.org/software/bash/manual/bash.html#Bash-Builtins). It does not run in a subshell. That is why it can modify shell variables. – pjh Aug 06 '23 at 20:42
  • My function runs in the parent so it doesn't matter that it would fail if it were run inside the child. As I said in my third paragraph, you are welcome to try to do something more convoluted. – jhnc Aug 06 '23 at 22:50
  • slightly more powerful (script call can include redirections, etc): `myref=myvar; mapfile -d '' $myref < <(date +%Y); declare -p myvar` – jhnc Aug 06 '23 at 22:54
  • @pjh it does make sense that it is a bash command and not a program called by bash. The command is in the bash man page. Thanks for pointing it out – Adriano_Pinaffo Aug 07 '23 at 23:41
0

A 'by reference' is not supported by bash.

There are a number of ways around it, using f.e. STDIO or using a separate file.

The STDIO works simple if a single variable is used:

parent_shell_var=$(bash script)

and just echo the value you want exported up.

A file can be used in this way: Parent script:

#!/bin/bash
hop=0
echo $hop
bash child_script tempfile
. tempfile
rm tempfile
echo $hop

child_script:

#!/bin/bash
echo "hop=1" > "$1"

These are very simplistic examples substitutes for the by-ref passing of arguments. These work for separate scripts as well as for subscripts.

Ljm Dullaart
  • 4,273
  • 2
  • 14
  • 31
0

Thank you all for the effort in answering the question. I will end up going with the gdb option to set the environmental variable using the parameters passed as arguments. As long as I do sudo, it will work just fine.

This is not intended to be a solution to be used without a lot of caution as you would be calling internal bash functions and things could be messed up. But in general it will run just fine.

Checking some ways to unset read-only variables, I stumbled upon using gdb to call the unbind_variable() function directly. So, I applied the same logic and set a new variable from the script. It "kinda" works, but not smoothly:

$ cat /tmp/test.sh
#!/bin/bash

ref="$1"
value="Value assigned from inside the script"
bashpid=$(ps -oppid,pid,cmd | grep $0 | grep -v grep | cut -f1 -d' ')

sudo gdb -ex 'call (int) setenv("'"$ref"'","'"$value"'",1)' --pid=$bashpid --batch 2>/dev/null 1>&2

And when I call it:

$ declare -p VARPASSED
bash: declare: VARPASSED: not found
$ /tmp/test.sh VARPASSED
$ declare -p VARPASSED
declare -x VARPASSED="Value assigned from inside the script"

Now the variable passed is correctly assigned to the current shell. The problem is that gdb requires sudo to attach to the current shell, which is a little bit annoying. How did they do with the read command?

PS: please cut some slack on the way to get the bash pid, I know there are much more efficient ways to do it. That was just not my focus ;)

Adriano_Pinaffo
  • 1,429
  • 4
  • 23
  • 46
  • 1
    Requiring the user to `eval` a command's output to set variables in the parent is good enough for the rest of the world, including widely deployed tools like `ssh-agent` -- so in the real world this is a constraint users put up with; in return, they get tools that use standard interfaces and work portably, including in environments where gdb isn't usable (think MacOS, or systems where ptrace is disabled for security reasons). – Charles Duffy Aug 07 '23 at 23:51
  • `eval` cannot set variables in the parent, that is the problem – Adriano_Pinaffo Aug 08 '23 at 11:23
  • That's why ssh-agent and similar tools require -- as I said -- **the user** to eval their output. If you've never used ssh-agent, look at how it works. Its development team has gotten away for decades with telling users that unless they run `eval "$(ssh-agent -s)"` their variables won't get set. Why do you think you can't do the same? – Charles Duffy Aug 08 '23 at 13:09
  • I really don't see the comparison here. `ssh-agent -s` generates a string for you to be consumed by eval... that is not what I want – Adriano_Pinaffo Aug 10 '23 at 18:26
  • It's not what you _want_, but it's the conventional, standard way to have a child process pass environment variables to a parent shell. Users are accustomed to it and will put up with it. If, as an information security person, someone asked me to give a process sudo or chroot (or exempt it from SELinux policies, or so forth) so it could attach to its parent and call `setenv()`, I'd laugh them out of the room. – Charles Duffy Aug 10 '23 at 19:01
  • Technically speaking you're not passing anything from the child process to the parent, you're just displaying a string to stdout, that's it. Now, regarding `setenv()` if it is open-source, you can read the code and see what it is setting – Adriano_Pinaffo Aug 11 '23 at 08:48
  • "Technically speaking", if you document that the parent process is expected to connect a child's stdout to a FIFO, read that FIFO and parse it's contents, that's a calling convention every bit as much as "register a will contain the output" or "the output will be left on the stack at position X" is a calling convention. I can be pedantic with the best of them, but the hair you're trying to split makes no sense. – Charles Duffy Aug 11 '23 at 13:41
  • 1
    As for code review being an option -- the issue is not just needing to review and establish trust in the code that invokes gdb (though that's essential -- not just to make sure it's not actively malicious but also to make sure that it doesn't unintentionally enable injection attacks **which the code in this answer is in fact vulnerable to**), but also everything at the same privilege level as the invoking code. – Charles Duffy Aug 11 '23 at 13:49