0

How can I populate a variable passed to bash script, which is called from another bash script? I want to know about options and best practices.

Let me elaborate why the many questions answered by either setting a exit status or echoing are not sufficient:

  • exit n: n is restricted to 0 <= n <= 255
  • echo foo doesn't allow me to echo any relevant information in that script, alternate screen buffer also won't help with this.

I have figured out one possible solution:

#outer.sh
source inner.sh
populate result
echo "Evaluated: $result"

#inner.sh
function populate {
    local __popvar=$1
    eval $__popvar="'RETURN VALUE'"
}

I dislike this solution for three reasons:

  1. The need to source the inner script, polluting global scope with helper functions.
  2. The need to eval, especially because of the confusing multi-quotes.
  3. Verbosity. I first need to source, and then call one function, whereas I'd much rather like to bash inner.sh result.

Further information on the inner script

The inner script is supposed to write to the alternate screen buffer. On this buffer, the user is able to select an option from an array (selection via arrow keys or ijkl style, confirmation with space or enter). This option should be returned from the script somehow. Returning the index is not an option, as the number of elements in the array can exceed 256. Code:

#!/usr/bin/bash

prompt=$1; shift
options=( "$@" )

c_opts=${#options[@]}
selected=0

# switch to alternate screen and trap the kill signal to switch back
tput smcup

trap ctrl_c INT
function ctrl_c {
  tput rmcup
  exit 1
}


function print_opts {
  for (( i = 0; i < $c_opts; i++ )); do
    if [[ i -eq $selected ]]; then
      echo -e "\t\e[7m ${options[i]} \e[0m"
    else
      echo -e "\t ${options[i]} "
    fi
  done
}

function reset_term {
  for (( i = 0; i < $c_opts; i++ )); do
    tput cuu1  # move cursor up 1 line
    tput el    # delete current line
  done

}

function reprint_opts {
  reset_term
  print_opts
}

echo $prompt
print_opts

while read -sN1 key; do

  read -sN1 -t 0.0001 k1
  read -sN1 -t 0.0001 k2
  read -sN1 -t 0.0001 k3
  key+="${k1}${k2}${k3}"

  # colemak layout
  case "$key" in
    n|u|$'\e[A'|$'\e0A'|$'\e[D'|$'\e0D')   # up or left
      ((selected > 0)) && ((selected--));;

    e|i|$'\e[B'|$'\e0B'|$'\e[C'|$'\e0C')   # down or right
      ((selected < $c_opts-1)) && ((selected++));;

    $'\e[1~'|$'\e0H'|$'\e[H')  # home key
      selected=0;;

    $'\e[4~'|$'\e0F'|$'\e[F')  # end key
      ((selected = $c_opts-1));;

    ' '|$'\x0a')  # enter or space
      tput rmcup && echo ${options[$selected]} && exit 0;;

    q|$'\e')  # q or escape
      tput rmcup && exit 0;;

  esac

  reprint_opts
done

Based on @JohnKugelman's comment, the script should be called as follows:

prompt="Your options are:"

options=(
  "Option A"
  "Option B"
  "Option C"
  "Option D"
)


result=$( exec 3>&1; bash select-menu.sh "$prompt" "${options[@]}" 2>&1 1>&3; exec 3>&- )
echo $result

This does seem an appealing solution, but it does not fix the problem. The Selection menu that is to be printed on the alternate screen buffer is not printed. Input however works correctly and the selection is stored in result.

To get a sense of the desired behavior you can replace the last two lines in the calling script like so:

bash select-menu.sh "$prompt" "${options[@]}"
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
tifrel
  • 421
  • 8
  • 20
  • 3
    I don't understand the argument against `echo`. Isn't `result=$(inner.sh)` a good solution? – mouviciel May 31 '18 at 11:16
  • Have a look at `man bash` and search for `declare -n`. – ghoti May 31 '18 at 11:23
  • The problem is if the inner script contains something like `echo "Your options are 1) optA 2) optB 2) optC" && read -p "Please type the corresponding number" no`. Edited question. – tifrel May 31 '18 at 11:24
  • In my special case, I want to pass an array to inner.sh. The user then should choose from the options inside the array. This is implemented via switching to the alternate screen buffer and then rewriting the screen. Selection is to be handled via arrow keys. Maybe a popup menu in the terminal would be adequate to describe what is happening. The inner script should then return the selected option from the array. Returning the index via `exit` is definitely not an option as I may exceed 256 values in that array. – tifrel May 31 '18 at 11:28
  • Isn't this just "how do I perform indirect assignment without `eval`?" -- a question we already have answered many times over? Nothing about the process changes just because you're doing it after `source`ing a child script. – Charles Duffy May 31 '18 at 11:41
  • See also the relevant section of [BashFAQ #6](https://mywiki.wooledge.org/BashFAQ/006#Assigning_indirect.2Freference_variables). – Charles Duffy May 31 '18 at 11:46
  • btw, there's no reason to involve a function for this; you can do all the work (`printf -v "$1" %s "result"`) from the outer context of the sourced script. – Charles Duffy May 31 '18 at 11:46
  • @JohnKugelman I just tried it (putting `> /dev/std(err|out)` behind echoing statements) and it kind of works, but the switching to and from the alternate screen buffer do not work anymore. Besides that it is an excellent alternative. – tifrel May 31 '18 at 11:47
  • 1
    Let's not close this as a duplicate of https://stackoverflow.com/questions/9938649/indirect-variable-assignment-in-bash. Variable indirection is one possible answer, but it's not the only one. – John Kugelman May 31 '18 at 11:48
  • I further edited the question, to clarify my intents. It seems to me that @JohnKugelman is giving suggestions in the right direction. @mouviciel I hope you now understand what my difficulties with a normal `echo` are. One more option I guess would be storing the result in a file and then reading it, but in this case I'd much rather `source` the script and call the defined functions to avoid unnecessary disk IO. – tifrel May 31 '18 at 12:47
  • @JohnKugelman I did just that, calling it with: `result=$( bash select-menu.sh "$prompt" "${options[@]}" ); for n in "" "" "" "" ""; do; echo $n; done; echo "Evaluated: $result"`. I can now at least get to the alternate screen. It causes glitches on the standard buffer and I do not retrieve a value from it. – tifrel May 31 '18 at 14:02
  • @JohnKugelman, ...your answer actually helps me understand what the OP was asking (distinct from the existing questions about indirect assignment); I've tried to edit the title to make that more immediately clear to others as well. – Charles Duffy Jun 01 '18 at 03:35

1 Answers1

2

Don't rule out result=$(inner.sh) just yet. If you want to display interactive prompts or dialogs in the script, do those on stderr, and have it write only the answer to stdout. Then you can have your cake and eat it too: interactive prompts and the result saved to a variable.

For example, dialog does exactly this if you use --output-fd 1 to tell it to write its answer to stdout. It uses curses to draw a dialog to the alternate screen but does it all on stderr.

$ value=$(dialog --keep-tite --output-fd 1 --inputbox title 10 40)
<dialog box shown>
<type "hello">
hello

(via Ask Ubuntu: How to get dialog box input directed to a variable?)

The script you posted can be made to do the same thing. It currently writes to stdout. Put exec 3>&1 1>&2 at the top so it'll write to stderr instead, and change the final echo ${options[$selected]} to echo ${options[$selected]} >&3 to write the answer to stdout. That'd get rid of the need for the caller to juggle file descriptors.


That said, prompts are not very UNIX-y. Consider eschewing interactivity entirely in favor of command-line arguments, configuration files, or environment variables. These options are better for power users who know how to use your script and want to automate it themselves.

My main purpose here is to commit a latest-stable-config from a selection of my last backups, which in my opinion needs the human judgement of when to consider a backup as appropriately stable.

The way I'd personally handle it is by writing a script with a couple of modes. Let's call it backups. backups --list would display a list of backups. You pick one and then call backups --commit <id> which would commit the named config. backups with no arguments would display usage for the unfamiliar user.

$ backups
Usage: backups --list
   or: backups --commit <id>

Manages a selection of backups. Use --list to list available backups
and --commit to commit the latest stable config.
$ backups --list
4ac6  10 minutes ago
18f2  1 day ago
3019  7 days ago
$ backups --commit 4ac6
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • Gave the solution from the linked question a try (accepted answer, not the one you actually linked). I also updated my question and gave clarification and context on what the called script is doing (basically a generalized select from a list dialog). I do not have dialog installed and never made use of it, but it seems to me that it is not fit for my purpose. Redirection of outputs however seems to be quite close to what I need. Thanks for your help so far. – tifrel May 31 '18 at 13:02
  • In response to being non-UNIX-y, my main purpose here is to commit a `latest-stable-config` from a selection of my last backups, which in my opinion needs the human judgement of when to consider a backup as appropriately stable. Actually I am redirecting the result to another script, that automates the commit, so you could consider it a layer of human intervention baked on top of something automated. – tifrel May 31 '18 at 13:23
  • @tillyboy Either use `dialog` or do the same meaning output normal output to stderr and the answer to stdout. That's how it works. – hek2mgl May 31 '18 at 14:16