3

In bash, suppose some function generates a simple array of unknown length. The array will be transformed and printed on screen: each line with its little index number next to it to pick from it. Space is tight, presentation must be flawless, toolset is… um, no frills, I'd say.

How can I find out the stringlength of index number itself (for formatting purposes) which is not a value on the array. For instance, if the array had 334,345 items (whose last item you'd print with echo ${hugearray[334344]}), then 6 would be what I'm looking for.

I have to stay within bash because there's no tput or other "fancy" ncurses formatting utils in the target system. Only bash itself.

${#WORD[@]} Gives me the total of items, from that I fashioned some rudimentary construct to get it:

$〉pkglist+=($(brew list -1)) # ← I'm testing on a Mac.
$〉printf "%s\n" ${pkglist[@]}
automake
…
xquartz

$〉echo ${#pkglist[@]}
68

$〉indexlength=${#pkglist[@]}
$〉echo ${#indexlength}
2

It works but it's not exactly elegant. In my defense, I'm not dev but I was kinda hoping there already was a better, simpler way to do it, nested indirections or some other bash voodoo. But the regular stuff that acts on the actual data was confusing enough as it is, I thought it would be best to ask before regretting something (the target system ONLY has the root account). Is there?

Thanks!

3
  • 1
    What's not elegant about using the simple ${#maxindex} expansion? Anything other than letting the shell calculate the number that way would make it more complicated and slow.
    – Kusalananda
    Apr 22 at 5:10
  • what's inelegant is that you have a variable called indexlength which contains the number of elements in the array, not the length of anything. It's only after taking the length of the value of that variable when you get the string length of the index. Note that, if you're going to print all the elements with their indexes, strictly speaking you don't need enough space to represent their number, just the max index. If there are e.g. 10 elements, the max index is 9, with a string length of just one.
    – ilkkachu
    Apr 22 at 5:57
  • and also, if the aim is to print all the options for the user to pick from, Bash has the select command for doing just that. Try e.g. arr=( foo{55..166} ); select item in "${arr[@]}"; do echo "you chose '$item'"; break; done. It also deals with printing the choices in multiple columns and aligning the numbers.
    – ilkkachu
    Apr 22 at 9:04

3 Answers 3

5

If you're on macos, you should have access to zsh, so you shouldn't have to use bash.

In zsh, array length is just $#array like in csh and you can nest parameter expansion operators, so you can do ${#${#array}}¹ to get the length of the decimal representation of the length of the array.

$ a=(qwe asdasd qewqw)
$ () {printf "%${#${#a}}d %s\n" "${@:^a}"} {1..$#a}
1 qwe
2 asdasd
3 qweqe
$ a=( {a..m} )
$ () {printf "%${#${#a}}d %s\n" "${@:^a}}" {1..$#a}
 1 a
 2 b
 3 c
 4 d
 5 e
 6 f
 7 g
 8 h
 9 i
10 j
11 k
12 l
13 m

Note that all of array=(...), array+=(...), {x..y} have been copied by bash from zsh (often indirectly via ksh). {1..$expansion} works in zsh or ksh but not in bash.

The ${a:^b} array-zipping operator and () {code} args anonymous functions are still specific to zsh.


If you have to use bash, doing it in two steps like you do is probably as good as it gets.

You could do it in one expansion with $(((l=${#array[@]}),SHLVL[l=\${#l}],l)), relying on the delayed evaluation of array indices, but that doesn't help with legibility and may not be future-proof.

Also note that in bash (like in ksh whose array design bash copied unfortunately), arrays are not really arrays but more like associative arrays with keys being positive integers and start at 0 (not 1 like in most other shells)

bash-5.1$ a=( [5]=123 [123]=qwe [4567]=asd )
bash-5.1$ typeset -p a
declare -a a=([5]="123" [123]="qwe" [4567]="asd")
bash-5.1$ echo "${#a[@]}"
3

3 is the number of elements in the array, but not the highest key value for which you need:

bash-5.1$ keys=("${!a[@]}")
bash-5.1$ echo "${keys[@]: -1}"
4567

So if you want to print aligned keys and values, you need something like

printarray() {
  local -n arr="$1"
  local -a keys=("${!arr[@]}")
  local highest_key="${keys[@]: -1}"
  local key_width="${#highest_key}"
  local key
  for key in "${keys[@]}"; do
    printf '%*d %s\n' "$key_width" "$key" "${arr[key]}"
  done
}
bash-5.1$ printarray a
   5 123
 123 qwe
4567 asd

(won't work with the ancient bash found on macos; local -n needs bash 4.3 or newer)


¹ ${#$#array} doesn't work as it is taken as ${#${$#array}}, the length of $$ after it has been stripped of array from its start using the ${var#pattern} Korn-style operator.

1

Would this be some sort of BASH vodoo, as you called it? - no additional variable
: $((${#pkglist[*]}-1)); echo ${#_}; but $_ can lead to debugging problems - long scripts

11
  • 1
    @ikkachu you are right. Apr 22 at 19:30
  • _ is a variable, even though one used for many things internally by bash. Apr 22 at 19:56
  • 1
    : "${!pkglist[@]}"; echo "${#_}" would be a more useful use of the _ variable and allow you to handle sparse arrays. Apr 22 at 20:02
  • @Stéphane Chazelas i upvoted this. Yours is definitive better, but why do we need double quotes, when we have an indexed array, or do you mean possibility of IFS= number. i can live with an extra variable indexlength, i only posted this, because it seems to me, that the questioner dont like it. _ is a variable, but no additional extra variable. Apr 22 at 20:36
  • About quoting, yes I don't see the point of asking the shell to split+glob it by leaving it unquoted. See also the related paragraph of Security implications of forgetting to quote a variable in bash/POSIX shells. But the main point was more about passing the sorted list of keys to the : utility so $_ gets the last (so highest) one. Apr 23 at 6:58
-1

EDIT: I apologize; as @ilkkachu kindly reminded me, the simplest way to get the length of a variable is with ${#variable}, not using external tools. Please ignore this answer; I will delete it shortly. Shame on me!

To determine how many digits has a number you could use the logarithm of base 10 of the number plus one and then dropping the decimal part, but the binary calculator bc has a handy function to do that:

echo "length(334344)" | bc

will print 6 as desired. If the argument of length is not an integer, it will count the real number of digits given without the decimal comma (i.e., length(3.4560) will give 5).

To insert a Bash variable in the echo use Bash variable expansion inside the double quotes:

x=334344
echo "length($x)" | bc

We can write that as a function for easy calling:

function num_digits {
#determine hoy many digits has a number
#param number
    local number="$1"
    echo "length($number)" | bc
}

then use it as follows:

lendigits=$(num_digits 334344)

or, with a variable

x=334344
lendigits=$(num_digits $x)

Note: If bc is not already installed in your Linux distro, you can add it using the distro's package manager.

Note 2: Of course you can use the expression directly without a function:

x=334344
lendigits=$(echo "length($x)" | bc)

but is less readable and I prefer a slighty longer script where special operations are identified and encapsulated in a function that later I can copy to use in other scripts.

2
  • 3
    But doesn't length(foo) here do exactly the same as the ${#foo} used in the question? Except that here, you're relying on an external tool, which might not be available if the toolset is limited (and they did say Bash only), and running an external tool is way more inefficient than doing the same thing internally.
    – ilkkachu
    Apr 22 at 6:03
  • 1
    @ilkkachu, you are totally right! I'm surprised I forgot so easily the basics. I'll left my answer a little while to let this message be read, and then delete the answer. Is really refreshing to be reminded of the need to explore all possibilities before selecting one.
    – Fjor
    Apr 23 at 17:57

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .