Format of 'select' generated menu

Oracle Linux 5.6 64-bit

Given the below snippet

ORACLE_SID=''
PS3='Select target (test) database being refreshed: '
#
while [[ $ORACLE_SID = "" ]]; do
  select ORACLE_SID in `egrep -i '^FS|^HR' /etc/oratab |\
      awk -F\: '{print $1}'|sort` ; do
    if [[ $ORACLE_SID = "" ]]; then
         echo
         echo "Please enter a valid number.  Retry.";
         echo
    elif [[ $ORACLE_SID = "None of the above" ]]; then
         exit ;
    else {
          break ;
         }
    fi
    break
    done
done

Produces a menu that looks like this, exactly as expected.

1) fsaaaaa
2) fsbbbbb
3) fsccccc
4) hrddddd
Select target (test) database being refreshed:

But now I want to add a "none of the above" item to the menu.
So add it in, thus:

while [[ $ORACLE_SID = "" ]]; do
  select ORACLE_SID in `egrep -i '^FS|^HR' /etc/oratab |\
      awk -F\: '{print $1}'|sort` "None of the above" ; do
    if [[ $ORACLE_SID = "" ]]; then
         echo
         echo "Please enter a valid number.  Retry.";
         echo
    elif [[ $ORACLE_SID = "None of the above" ]]; then
         exit ;
    else {
          break ;
         }
    fi
    break
    done
done

And the menu went from a single column to three columns:

1) fsaaaaa           3) fsccccc           5) None of the above
2) fsbbbbb           4) hrddddd
Select target (test) database being refreshed:

It behaves exactly as I planned, but I don't understand the change of format.

What shell are you using? I know that bash tries to format it elegantly if it can, depending on available space, number of, and size of options.

Just using sh. First line of script is

#!/bin/sh

I thought at first it might be an issue with the interaction with IFS, but I added the same "none of the above" option to another block elsewhere in the script ... one that only produces 3 choices (plus my 'none of the above') and it formats in a single column.

I could try it with bash and see what happens. Hopefully there won't be other slight differences that impact some of the other code ... Worst comes to worst, I can live with it like it is, but I'd like to learn something from it as well.

---------- Post updated at 12:44 PM ---------- Previous update was at 12:05 PM ----------

Just as a follow up, I just noticed that /bin/sh is just a symbolic pointing to /bin/bash, so I guess my script is being processed by bash

/bin/sh just means "any shell compatible with generic bourne". It could be bash, ksh, or even old-fashioned, honest-to-goodness, written-in-1977 Bourne written by Steven Bourne himself.

So if you're using anything except generic Bourne features, you should probably specify what shell you're using more exactly.

The output of select can differ from shell to shell. If it's readable, I don't really see an issue.

As noted in my edit, I discovered that /bin/sh is a symbolic pointing specifically to /bin/bash.

My shell scripting is pretty much self-taught; consequently, a lot of the subtleties of the differences between the various shells escape me. I'm aware there are differences, but the specifics in any given instance aren't always clear.

"If it's readable ...."
I agree. Which is why I said I can live with it but would like to use this as a learning experience (see comment on 'self taught' -- :wink: )

How about

export COLUMNS=20
$ select A in A B C D E F G "None of the above "; do :; done

RudiC,

That fixed the display to what I was after.
FWIW, I found no reference to this in any discussion or example of 'select'. Of course, when you showed that, the first thing I did was go back to my references to see what I could find about COLUMNS. The only thing I could find was in my copy of Unix in a Nutshell, where it says "Screen's column width; used in line edit modes and select lists." That's it. Doesn't really explain how it is used, particularly in select lists. I did read in another reference that its default value is 80. Can you explain a bit more about the interaction going on here?

Many commands and programs interacting with terminals try to find out the terminal geometry by several means. The COLUMNS environment variable is one of those, containing the width of the terminal. select then tries to fit as many menu items across as would fit into the terminal width. If you reduce that artificially, it will print just one item across.

That makes sense, but I'm still can't seem to get my head around how that works in the specifics of this case.

If I shorten my literal last entry to simply 'None', and don't specifically set COLUMNS:

oracle:11g$ cat doit
#!/bin/sh
echo
ORACLE_SID=''
PS3='Target (test) database: '
#export COLUMNS=20
echo COLUMNS = $COLUMNS
#
while [[ $ORACLE_SID = "" ]]; do
  select ORACLE_SID in `egrep -i '^FS|^HR' /etc/oratab |\
      awk -F\: '{print $1}'|sort` 'None'; do
    if [[ $ORACLE_SID = "" ]]; then
         echo
         echo "Please enter a valid number.  Retry.";
         echo
    elif [[ $ORACLE_SID = 'None of the above' ]]; then
         exit ;
    else {
          break ;
         }
    fi
    break
    done
done
#
unset PS3
exit

oracle:11g$ doit

COLUMNS =
1) fs91dvvb
2) fs91uavb
3) fs9devvb
4) hr91tsvb
5) None
Target (test) database:

But if I simply lengthen that literal;

#!/bin/sh
echo
ORACLE_SID=''
PS3='Target (test) database: '
#export COLUMNS=20
echo COLUMNS = $COLUMNS
#
while [[ $ORACLE_SID = "" ]]; do
  select ORACLE_SID in `egrep -i '^FS|^HR' /etc/oratab |\
      awk -F\: '{print $1}'|sort` 'None of the above'; do
    if [[ $ORACLE_SID = "" ]]; then
         echo
         echo "Please enter a valid number.  Retry.";
         echo
    elif [[ $ORACLE_SID = 'None of the above' ]]; then
         exit ;
    else {
          break ;
         }
    fi
    break
    done
done
#
unset PS3
exit

oracle:11g$ doit

COLUMNS =
1) fs91dvvb           3) fs9devvb           5) None of the above
2) fs91uavb           4) hr91tsvb
Target (test) database:

FWIW, note in both cases when the script echoed the value of $COLUMNS, it was null. But if I do it from the command line

oracle:11g$ echo $COLUMNS
80

oracle:11g$

I would have expected the script to have inherited the value of 80 from the process that launched it.

And finally with the longer literal menu item, but with COLUMNS explicitly set:

oracle:11g$ cat doit
#!/bin/sh
echo
ORACLE_SID=''
PS3='Target (test) database: '
export COLUMNS=20
echo COLUMNS = $COLUMNS
#
while [[ $ORACLE_SID = "" ]]; do
  select ORACLE_SID in `egrep -i '^FS|^HR' /etc/oratab |\
      awk -F\: '{print $1}'|sort` 'None of the above'; do
    if [[ $ORACLE_SID = "" ]]; then
         echo
         echo "Please enter a valid number.  Retry.";
         echo
    elif [[ $ORACLE_SID = 'None of the above' ]]; then
         exit ;
    else {
          break ;
         }
    fi
    break
    done
done
#
unset PS3
exit

oracle:11g$ doit

COLUMNS = 20
1) fs91dvvb
2) fs91uavb
3) fs9devvb
4) hr91tsvb
5) None of the above
Target (test) database:

The output of all of those looks readable, is the thing. Neither too many lines, nor too wide, to be visible. Not 100% consistent when you give it different options and configurations, but that's kind of the point -- it's a command that's designed to automatically format itself to be readable, and so far you haven't managed to make it not be so. What, exactly, is actually wrong?

Yes, it's all readable and usable. Yes, the different options produce different results and that is to be expected. As I've stated earlier in the thread, what I'm after is some real understanding of exactly what the interactions are and how they work. I'm trying to learn something usable from this experience. I'm not the kind of guy who simply accepts "here, use this code, it works in this case". I've always been driven to peel back the layers and understand why it works the way it does so that I can apply that knowledge appropriately to other situations.

So, I don't understand why

  • with the default setting of COLUMNS, the list with a short literal ('None') presented as a single column, but with a longer literal ('None of the above') it presented in three columns.

  • setting the value of COLUMNS to 20 caused the previous 3 column list to be presented as one column.

  • echoing $COLUMNS from the command prompt produced a value (80), but echoing it from within the shell script called from the same command prompt resulted in what appears to be nulls. I would have expected (and all of my previous experience confirms) that the shell script would have inherited the entire environment from the calling process. So I would expect the default value of COLUMNS within the script to be the same as the value at the command prompt that called the script.

You mix up the defintion of columns.
While $COLUMNS represents the value of characters possible to be presented on one line,
the columns of 'select' contain more than 1 char per line.

While its true that a script inherits the values of variables from the caller-environmnet (eg: terminal),
you can set it to something diffrent and change it back after its use.
Example (in the script):

OLD_COL=$COLUMNS
COLUMNS=20
select CHOICE in A B C D E F G;do echo $CHOICE;break;done
COLUMNS=$OLD_COL

So you dont mix up the output that follows your 'select' call.

hth

So if it set COLUMNS=20 it shouldn't present a menu in multiple columns if such presentation would require more than 20 characters (including spacing) per line? Again, that makes sense in explanation, but not in observation. With the default value of 80 (or is it null? see comment on that, regarding environment inheritance, below), and all of my menu items of short (< 8 chars each) it presented in a single column. But by the above understanding it should have presented multiple columns. Only when one of the menu items was a longer value did the presentation go to multiple values.

I'm afraid the meaning of that is escaping me.

Yes, I know I can save and reset it. But my point was that I was not observing the script to actually inherit from the caller.

oracle:11g$ cat doit2
#!/bin/sh
echo COLUMNS = $COLUMNS
exit

oracle:11g$ echo $COLUMNS
80

oracle:11g$ ./doit2
COLUMNS =

oracle:11g$ echo $COLUMNS
80

oracle:11g$

Hi, edstevens.

I like to know answers, too. However, some questions are more important to me than others -- we all set priorities.

So some of the ways to get answers for:

1) Keep asking the question here in hopes that someone will answer,

2) Put on your scientist hat, and run lots of experiments, trying to see the pattern of behavior for various inputs,

3) Put on your programmer hat, and look at the code.

In case you are interested in pursuing 3), the bash 4.2 has a routine sequence:

execute_cmd.c
  execute_select_command
    select_query
      print_select_list

The latter section of code takes into account many items of interest:

static void
print_select_list (list, list_len, max_elem_len, indices_len)
     WORD_LIST *list;
     int list_len, max_elem_len, indices_len;
{
  int ind, row, elem_len, pos, cols, rows;
  int first_column_indices_len, other_indices_len;

  if (list == 0)
    {
      putc ('\n', stderr);
      return;
    }

  cols = max_elem_len ? COLS / max_elem_len : 1;
  if (cols == 0)
    cols = 1;
  rows = list_len ? list_len / cols + (list_len % cols != 0) : 1;
  cols = list_len ? list_len / rows + (list_len % rows != 0) : 1;

  if (rows == 1)
    {
      rows = cols;
      cols = 1;
    }

  first_column_indices_len = NUMBER_LEN (rows);
  other_indices_len = indices_len;

  for (row = 0; row < rows; row++)
    {
      ind = row;
      pos = 0;
      while (1)
	{
	  indices_len = (pos == 0) ? first_column_indices_len : other_indices_len;
	  elem_len = print_index_and_element (indices_len, ind + 1, list);
	  elem_len += indices_len + RP_SPACE_LEN;
	  ind += rows;
	  if (ind >= list_len)
	    break;
	  indent (pos + elem_len, pos + max_elem_len);
	  pos += max_elem_len;
	}
      putc ('\n', stderr);
    }
}

static int
print_index_and_element (len, ind, list)
      int len, ind;
      WORD_LIST *list;
{
  register WORD_LIST *l;
  register int i;

  if (list == 0)
    return (0);
  for (i = ind, l = list; l && --i; l = l->next)
    ;
  fprintf (stderr, "%*d%s%s", len, ind, RP_SPACE, l->word->word);
  return (displen (l->word->word));
}

static void
indent (from, to)
     int from, to;
{
  while (from < to)
    {
      if ((to / tabsize) > (from / tabsize))
    {
      putc ('\t', stderr);
      from += tabsize - from % tabsize;
    }
      else
    {
      putc (' ', stderr);
      from++;
    }
    }
}

Best wishes ... cheers, drl

1 Like

True enough.

You've pointed me into layers I've never been to before, but am not afraid to explore. I'll chew on that and consider this thread to have run its course. Thanks.

Hi.

It is not easy to understand code that has been extracted from a larger source. Here is a shell script that drives a portion of the bash-select-code, but rendered in perl so that you can run it. It calculates the rows and columns as bash would for the select. It does not pretty-print the selections, it just does the calculations. It assumes that you have a file that corresponds to the strings you would use with select. It allows a second parameter, the setting for COLUMNS so that you can see the effects of changing that. Here are a few runs with varying files and widths, the last with some arithmetic and logical internal results displayed. Note that the shell script calls the perl script, so that it is easily posted here, however, you can copy/paste the perl code and run it as desired.

#!/usr/bin/env bash

# @(#) s1	Demonstrate driver for bash select row-col calculations

# Utility functions: print-as-echo, print-line-with-visual-space, debug.
# export PATH="/usr/local/bin:/usr/bin:/bin"
LC_ALL=C ; LANG=C ; export LC_ALL LANG
pe() { for _i;do printf "%s" "$_i";done; printf "\n"; }
pl() { pe;pe "-----" ;pe "$*"; }
db() { ( printf " db, ";for _i;do printf "%s" "$_i";done;printf "\n" ) >&2 ; }
db() { : ; }
C=$HOME/bin/context && [ -f $C ] && $C perl

FILE=${1-data1}

pl " Results, $FILE:"
./p2 $FILE 80

FILE=data2
pl " Results, $FILE:"
./p2 $FILE 80

pl " Results, $FILE 20:"
./p2 $FILE 20

pl " Results, $FILE 80, details:"
./p2 -d $FILE 80

exit 0

producing:

$ ./s1

Environment: LC_ALL = C, LANG = C
(Versions displayed with local utility "version")
OS, ker|rel, machine: Linux, 2.6.26-2-amd64, x86_64
Distribution        : Debian 5.0.8 (lenny, workstation) 
bash GNU bash 3.2.39
perl 5.10.0

-----
 Results, data1:

For data file data1, containing:
fsaaaaa
fsbbbbb
fsccccc
hrddddd
 Maximum element length = 7

 Final calculation, COLUMNS (width) = 80:
 Rows = 4
 Cols = 1

-----
 Results, data2:

For data file data2, containing:
fsaaaaa
fsbbbbb
fsccccc
hrddddd
None of the above
 Maximum element length = 17

 Final calculation, COLUMNS (width) = 80:
 Rows = 2
 Cols = 3

-----
 Results, data2 20:

For data file data2, containing:
fsaaaaa
fsbbbbb
fsccccc
hrddddd
None of the above
 Maximum element length = 17

 Final calculation, COLUMNS (width) = 20:
 Rows = 5
 Cols = 1

-----
 Results, data2 80, details:
debug: value, filename, COLUMNS = data2, 80
debug: value, array a length 5 "fsaaaaa fsbbbbb fsccccc hrddddd None of the above", longest = 17
debug: value initial cols = 4
debug: value before  , rows, cols = 2, 4
debug: value after   , rows, cols = 2, 3
debug: value adjusted, rows, cols = 2, 3

For data file data2, containing:
fsaaaaa
fsbbbbb
fsccccc
hrddddd
None of the above
 Maximum element length = 17

 Final calculation, COLUMNS (width) = 80:
 Rows = 2
 Cols = 3

And here it the perl code, p2:

#!/usr/bin/env perl

# @(#) p2	Demonstrate rows, cols calculations for bash "select".

use warnings;
use strict;

# Short circuit for debug.
# Use with d(), keys: Action, Location, Value.
our ($debug);
$debug = 0;
if ( @ARGV && $ARGV[0] =~ /-de*b*u*g*/ ) {
  $debug = 1;
  shift;
}

# Read list, calculate longest line.

my ( $filename, $COLUMNS, $f, $longest, $t1, @a, @list );
my ( $list_len, $max_elem_len, $cols, $rows, $string );

$filename = shift || "data1";
$COLUMNS  = shift || "80";
d("value, filename, COLUMNS = $filename, $COLUMNS");

open( $f, "<", $filename ) || die "Cannot open file $filename\n";

$longest = 0;
while (<$f>) {
  chomp;
  $t1 = length($_);
  $longest = $t1 > $longest ? $t1 : $longest;
  push @a, $_;
}
$list_len = scalar(@a);
d("value, array a length $list_len \"@a\", longest = $longest");
@list         = @a;
$max_elem_len = $longest;

$cols = $max_elem_len ? $COLUMNS / $max_elem_len : 1;
$cols = int($cols);
d("value initial cols = $cols");
$cols = 1 if $cols == 0;

$rows = $list_len ? $list_len / $cols + ( $list_len % $cols != 0 ) : 1;
$rows = int($rows);

d("value before  , rows, cols = $rows, $cols");
$cols = $list_len ? $list_len / $rows + ( $list_len % $rows != 0 ) : 1;
$cols = int($cols);
d("value after   , rows, cols = $rows, $cols");

if ( $rows == 1 ) {
  $rows = $cols;
  $cols = 1;
}
d("value adjusted, rows, cols = $rows, $cols");

print "\n";
print "For data file $filename, containing:\n";
foreach $string (@list) {

  print "$string\n";
}
print " Maximum element length = $max_elem_len\n";
print "\n";

print " Final calculation, COLUMNS (width) = $COLUMNS:\n";
print " Rows = $rows\n";
print " Cols = $cols\n";

exit;

sub d {

  # Debug - print strings if debug does not fail.
  if ( not $debug ) {
    return;
  }
  elsif ( scalar(@_) == 0 ) {
    print "\n";
  }
  else {
    print STDERR join "", "debug: ", @_, "\n";
  }
}

Best wishes ... cheers, drl

1 Like

COLUMNS might be getting set in /etc/profile or ~/.bashrc or something similar.

Is it meaningfull to set COLUMNS hardcoded to any and every terminal(-window) just so select looks 'pretty'?
AFAIK all the rest of the terminal output depends on that too, as in line breaks and such.

Perhaps COLUMNS is not exported, try export COLUMNS before running doit2

If COLUMNS is not set scripts/programs could be using tput cols or similar to query the terminal for it's actual width.

Also see bash shopt checkwinsize , this keeps COLUMNS and LINES updated automatically, this is useful for an xterm where the users could resize the window after the shell has started.

That's a rather loaded question. I don't even know which layout you consider "prettiest" yet.

No, you should not hardcode COLUMNS to make select prettier. Doing this may cause inconsistent, unreliable, or unreadable results across different systems, terminals, and shells.

If the end user wants to hardcode their COLUMNS that's another matter. They'd be the one to know how big their terminal is.