【问题标题】:Is there a way to implement a counter in bash but for letters instead of numbers?有没有办法在 bash 中实现一个计数器,但要使用字母而不是数字?
【发布时间】:2016-11-01 06:51:17
【问题描述】:

我正在处理一个写得有点乱的现有脚本。使用所有意大利面条代码设置循环可能会比我近期想要处理的更令人头疼。也许当我有更多时间时,我可以清理它,但现在,我只是在寻找一个简单的修复方法。

该脚本处理 xen 服务器上的虚拟磁盘。它读取multipath 输出并询问是否应根据特定标准以任何方式格式化特定LUN。但是,它不是采用该磁盘路径并将其插入已格式化的配置文件中,而是简单地以格式显示每一行

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

当然,UUID 是一个实际的 UUID。

脚本实际上以这种格式呈现每个找到的 LUN,希望用户将它们复制并粘贴到配置文件中,依次用一个字母替换每个 ?。这充其量是乏味的。

有几种方法可以在 bash 中增加数字。其中:

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

有没有办法对不涉及循环整个字母表的字符做同样的事情,这样我就可以轻松地将磁盘分配从xvda“增加”到xvdb等?

【问题讨论】:

标签: bash


【解决方案1】:

要对字母进行“增量”,请定义函数:

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

现在,观察:

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

因为,它通过 ASCII 递增,incr z 变为 {

工作原理

第一步是将字母转换为其 ASCII 数值。例如,a 是 97:

$ printf '%d' "'a"
97

下一步是增加它:

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

或者:

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

最后一步是将新的递增数字转换回字母:

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

或者:

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

另类

使用 bash,我们可以定义一个关联数组来保存下一个字符:

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

或者,如果您更喜欢分散在多行的代码:

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

使用这个数组,字符可以通过以下方式递增:

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

在这个版本中,z 的增量循环回a

$ echo "${Incr[z]}"
a

【讨论】:

  • 我喜欢那个关联数组方法
【解决方案2】:

将条目 A-Z 分配给索引 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"

这有 1=A、2=B 等。如果您想要 0=A、1=B 等,请删除第一个冒号。

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"

然后,你真正需要这封信的地方;

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

唯一的问题是,如果您最终运行超过 26 个字母,您将开始从数组中返回空白。

【讨论】:

  • 我在想这些方面的东西可能是最简单的解决方案。我怀疑我们永远不必担心超过 26 个磁盘,所以这应该不是问题。
【解决方案3】:

使用 Bash 4 范围

您可以使用 Bash 4 功能来指定 sequence expression 内的范围。例如:

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

另请参阅 Bash Wiki 中的 Ranges

【讨论】:

  • 这个循环是我特别想避免的,因为它需要包装一堆乱七八糟的代码。从长远来看,我可以担心将整个脚本修复为更优雅,从而使循环成为更好的选择。现在我需要能够说var=a, var++
  • 这在早期版本中肯定是可用的。
【解决方案4】:

这是一个函数,它将返回 a-z 范围内的下一个字母。 'z' 的输入返回 'a'。

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

它将输入的第一个字母视为基数为 36 的整数,减去 9,然后返回序数为 'a' 的字符加上该值 mod 26。

【讨论】:

    【解决方案5】:

    使用记事本

    虽然 Bash 范围选项使用内置函数,但您也可以使用像 BSD jot 实用程序这样的实用程序。默认情况下,这在 macOS 上可用,但您的里程在 Linux 系统上可能会有所不同。例如,您需要在 Debian 上安装 athena-jot

    更多循环

    这里的一个技巧是预填充 Bash 数组,然后使用索引变量从数组中获取所需的输出。例如:

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

    无循环替代方案

    请注意,您没有 在循环中递增计数器。您也可以通过其他方式进行操作。考虑以下内容,它将递增传递给函数的任何字母,而无需预先填充数组:

    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"
    

    这可能更接近 OP 想要的,但它肯定看起来比原始循环 recommended elsewhere 更复杂,更不优雅。但是,您的里程可能会有所不同,最好有选择!

    【讨论】:

      猜你喜欢
      • 2016-03-04
      • 2021-10-15
      • 1970-01-01
      • 2022-11-02
      • 2022-06-12
      • 1970-01-01
      • 2019-05-15
      • 1970-01-01
      • 2014-09-26
      相关资源
      最近更新 更多