For loop with Alphabet

This works perfectly on OSX

#!/bin/bash
chars=( {a..z} )
n=3
for ((i=0; i<n; i++))
do
  echo "${chars[i]}"
done

But when I run it on Ubuntu, I get the following error.

ForLoopAlphabetTest.sh: 2: ForLoopAlphabetTest.sh: Syntax error: "(" unexpected

I can't seem to solve the issue. Any suggestions?


Solution 1:

Presumably, you are running the script as:

sh ForLoopAlphabetTest.sh

In Ubuntu, sh is symlinked to dash; as dash has no concept of arrays, you are getting the syntax error for (.

The script works perfectly on bash, so it would be fine if you were running it as bash's argument:

bash ForLoopAlphabetTest.sh

Now, you have the bash shebang on the script, so you could make the script executable (chmod u+x ForLoopAlphabetTest.sh), and run it as:

/path/to/ForLoopAlphabetTest.sh

or from the script's directory:

./ForLoopAlphabetTest.sh

Also note that, your script contains brace expansion {a..z}, and C-style for construct: for (( ... )) which are also not supported by dash; so if your goal is portability, you should look at POSIX sh syntaxes only.

Solution 2:

Your script uses three features of the Bash shell that are not provided by all Bourne-style shells. As heemayl says, you can simply run that script with bash instead of sh. Your hashbang line at the top (#!/bin/bash) specifies bash but is only effective if you execute the script, as heemayl explained. If you pass the name of the script to sh, sh won't automatically call bash, but will simply run the script. This is because once your script is actually running, the hashbang line has no effect.

Your other alternative, if you need to write fully portable scripts that don't depend on Bash features, is to change your script so that it works without them. The Bash features you use are:

  • An array. This came first, so this is what produced the error when you tried to run your script with the Dash shell. The parenthesized expression ( {a..z} ), which you assign to chars, creates an array, and ${chars[i]}, which appears in your loop, indexes into it.
  • Brace expansion. In Bash, and also in many other shells, {a..z} is expanded to a b c d e f g h i j k l m n o p q r s t u v w x y z. However, this is not a universal (or standardized) feature of Bourne-style shells, and Dash doesn't support it.
  • The C-style alternate for-loop syntax. Although based on arithmetic expansion, which is not itself specific to Bash (though some very old, non-POSIX-compliant shells don't have it, either), the C-style for loop is a Bash-ism and is not widely portable to other shells.

Bash is widely available, especially on GNU/Linux systems like Ubuntu, and (as you have seen) is also available on macOS and many other systems. Considering how much you're using Bash-specific features, you might just want to use them, and simply make sure you're using Bash (or some other shell that supports the features you're using) when you run your scripts.

However, you can replace them with portable constructs if you like. The array and C-style for loop are easy to replace; generating the range of letters without brace expansion (and without hard-coding them in your script) is the one part that's a little tricky.


First, here's a script that prints all the lower-case Latin letters:

#!/bin/sh

for i in $(seq 97 122); do
    printf "\\$(printf %o $i)\n"
done
  • The seq command generates numeric sequences. $( ) performs command substitution, so $(seq 97 122) is replaced with the output of seq 97 122. These are the character codes for a through z.
  • The powerful printf command can turn character codes into letters (e.g., printf '\141' prints a, followed by a newline), but the codes must be in octal, while seq outputs only in decimal. So I've used printf twice: the inner printf %o $i converts decimal numbers (provided by seq) to octal, and is substituted into the outer printf command. (Although it's also possible to use hexadecimal, it's no simpler and seems to be less portable.)
  • printf interprets \ followed by an octal number as the character with that code, and \n as a newline. But the shell also uses \ as an escape character. A \ in front of $ will prevent $ from causing an expansion to occur (in this case, command substitution), but I don't want to prevent that, so I have escaped it with another \; that's the reason for \\. The second \ before n doesn't need to be escaped because, unlike \$, \n doesn't have special meaning to the shell in a double-quoted string.
  • For more information on how double quotes and the backslash are used in shell programming, see the section on quoting in the international standard. See also 3.1.2 Quoting in the Bash Reference Manual, especially 3.1.2.1 Escape Character and 3.1.2.3 Double Quotes. (Here's the whole section, in context.) Note that single quotes (') are also an important part of shell quoting syntax, I just don't happen to have used them in that script.

This is portable to most Unix-like systems and doesn't depend which Bourne-style shell you use. However, a few Unix-like systems don't have seq installed by default (they tend to use jot instead, which is not installed by default most GNU/Linux systems). You can use a loop with expr or arithmetic substitution to increase portability further, if you need to:

#!/bin/sh

i=97
while [ $i -le 122 ]; do
    printf "\\$(printf %o $i)\n"
    i=$((i + 1))
done

That uses a while-loop with the [ command to continue looping only when $i is in range.


Rather than printing the whole alphabet, your script defines a variable n and prints the first $n lower-case letters. Here's a version of your script that relies on no Bash-specific features and works on Dash, but requires seq:

#!/bin/sh

n=3 start=97
for i in $(seq $start $((start + n - 1))); do
    printf "\\$(printf %o $i)\n"
done

Adjusting the value of n changes how many letters are printed, as in your script.

Here's a version that doesn't require seq:

#!/bin/sh

n=3 i=97 stop=$((i + n))
while [ $i -lt $stop ]; do
    printf "\\$(printf %o $i)\n"
    i=$((i + 1))
done

There, $stop is one higher than the character code of the last letter that ought to be printed, so I use -lt (less than) rather than -le (less than or equal) with the [ command. (It would also have worked to make stop=$((i + n - 1)) and use [ $i -le $stop ]).