2

I'm working with an existing script which was written a bit messily. Setting up a loop with all of the spaghetti code could make a bigger headache than I want to deal with in the near term. Maybe when I have more time I can clean it up but for now, I'm just looking for a simple fix.

The script deals with virtual disks on a xen server. It reads multipath output and asks if particular LUNs should be formatted in any way based on specific criteria. However, rather than taking that disk path and inserting it, already formatted, into a configuration file, it simply presents every line in the format

'phy:/dev/mapper/UUID,xvd?,w',

UUID, of course, is an actual UUID.

The script actually presents each of the found LUNs in this format expecting the user to copy and paste them into the config file replacing each ? with a letter in sequence. This is tedious at best.

There are several ways to increment a number in bash. Among others:

var=$((var+1))
((var+=1))
((var++))

Is there a way to do the same with characters which doesn't involve looping over the entire alphabet such that I could easily "increment" the disk assignment from xvda to xvdb, etc?

theillien
  • 1,189
  • 3
  • 19
  • 33

5 Answers5

4

To do an "increment" on a letter, define the function:

incr() {   LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'$1")+1))")"; }

Now, observe:

$ echo $(incr a)
b
$ echo $(incr b)
c
$ echo $(incr c)
d

Because, this increments up through ASCII, incr z becomes {.

How it works

The first step is to convert a letter to its ASCII numeric value. For example, a is 97:

$ printf '%d' "'a"
97

The next step is to increment that:

$ echo "$((97+1))"
98

Or:

$ echo "$(($(printf '%d' "'a")+1))"
98

The last step is convert the new incremented number back to a letter:

$ LC_CTYPE=C printf "\\$(printf '%03o' "98")"
b

Or:

$ LC_CTYPE=C printf "\\$(printf '%03o' "$(($(printf '%d' "'a")+1))")"
b

Alternative

With bash, we can define an associative array to hold the next character:

$ declare -A Incr; last=a; for next in {b..z}; do Incr[$last]=$next; last=$next; done; Incr[z]=a

Or, if you prefer code spread out over multiple lines:

declare -A Incr
last=a
for next in {b..z}
do
    Incr[$last]=$next
    last=$next
done
Incr[z]=a

With this array, characters can be incremented via:

$ echo "${Incr[a]}"
b
$ echo "${Incr[b]}"
c
$ echo "${Incr[c]}"
d

In this version, the increment of z loops back to a:

$ echo "${Incr[z]}"
a
John1024
  • 109,961
  • 14
  • 137
  • 171
2

How about an array with entries A-Z assigned to indexes 1-26?

IFS=':' read -r -a alpharray <<< ":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"

This has 1=A, 2=B, etc. If you want 0=A, 1=B, and so on, remove the first colon.

IFS=':' read -r -a alpharray <<< "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"

Then later, where you actually need the letter;

var=$((var+1))
'phy:/dev/mapper/UUID,xvd${alpharray[$var]},w',

The only problem is that if you end up running past 26 letters, you'll start getting blanks returned from the array.

Guest
  • 124
  • 7
  • I was thinking something along these lines might be the simplest solution. I doubt we'll ever have to worry about >26 disks so that shouldn't be an issue. – theillien Nov 01 '16 at 08:26
1

Use a Bash 4 Range

You can use a Bash 4 feature that lets you specify a range within a sequence expression. For example:

for letter in {a..z}; do
    echo "phy:/dev/mapper/UUID,xvd${letter},w"
done

See also Ranges in the Bash Wiki.

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199
  • The loop is specifically what I'm trying to avoid since it would require wrapping a mess of code. In the long run, I can worry about fixing the whole script to be more elegant making a loop a better option. For now I need to be able to say `var=a, var++` so to speak. – theillien Nov 01 '16 at 07:18
  • This is definitely available in earlier versions. – 123 Nov 01 '16 at 08:56
1

Here's a function that will return the next letter in the range a-z. An input of 'z' returns 'a'.

nextl(){
  ((num=(36#$(printf '%c' $1)-9) % 26+97));
  printf '%b\n' '\x'$(printf "%x" $num);
}

It treats the first letter of the input as a base 36 integer, subtracts 9, and returns the character whose ordinal number is 'a' plus that value mod 26.

Eric
  • 1,431
  • 13
  • 14
0

Use Jot

While the Bash range option uses built-ins, you can also use a utility like the BSD jot utility. This is available on macOS by default, but your mileage may vary on Linux systems. For example, you'll need to install athena-jot on Debian.

More Loops

One trick here is to pre-populate a Bash array and then use an index variable to grab your desired output from the array. For example:

letters=( "" $(jot -w %c 26 a) )
for idx in 1 26; do
    echo ${letters[$idx]}
done

A Loop-Free Alternative

Note that you don't have to increment the counter in a loop. You can do it other ways, too. Consider the following, which will increment any letter passed to the function without having to prepopulate an array:

increment_var () {
    local new_var=$(jot -nw %c 2 "$1" | tail -1)
    if [[ "$new_var" == "{" ]]; then
        echo "Error: You can't increment past 'z'" >&2
        exit 1
    fi
    echo -n "$new_var"
}

var="c"
var=$(increment_var "$var")
echo "$var"

This is probably closer to what the OP wants, but it certainly seems more complex and less elegant than the original loop recommended elsewhere. However, your mileage may vary, and it's good to have options!

Community
  • 1
  • 1
Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199