在 bash 脚本中不考虑空格并用换行符替换字符串的各种方法

Various ways of splitting strings not respecting spaces and replacing with newlines in a bash script

免责声明:总 bash 菜鸟很抱歉在这个问题上有任何明显愚蠢的地方。我真的开始喜欢它了,但是,它让我想起了我 7 或 8 岁时使用 C 的头几个月:)

简介: 这是脚本的一部分,将用于在名称指定的机器上执行某些自动化 recovery/restoration 任务(即 --device mba ) 如果他们需要全新安装。它不仅可以节省大量时间——尽管我花时间来写作——而且可以作为一种学习语言及其特性、能力和局限性的方式。

我设置我的 devices 键值对数组如下(附带问题:devices=()必要的吗?。我不再偷懒了,自己试了一下,不,没有必要。)

# Devices
devices=()
devices[0]="mba=Lee's Macbook Air"
devices[1]="mms=Lee's Mac Mini Server"
# devices[2]="mml=Lee's Mac Mini (Living Room)"
# devices[3]="mmb=Lee's Mac Mini (Bedroom)"
# devices[4]="mps=Lee's Mac Pro Server"

我最有意义的第一次尝试是只迭代数组并输出 device 参数,但它只适用于这种方法。在脚本的其他部分,我需要拆分字符串以获得短名称。

for device in ${devices[@]}; do

    echo $device

done

没有意义!为什么按空格拆分?

第二次尝试(我喜欢这个,因为稍后我需要获取密钥,以便我可以基于它执行其他操作,当 --devices 标志时调用这里简单的方法部分在调用脚本时设置然后存在):

for device in ${devices[@]//=/}; do

    echo ${device[0]} ${device[1]} 

done

或者:

for device in ${devices[@]}; do

    deviceKeyValuePair=(${device//=/})

    echo ${deviceKeyValuePair[0]} ${deviceKeyValuePair[1]} 

done

生成以下内容:

mbaLee 的 Mac本书 空气 mmsLee's Mac 小型的 服务器

第三次尝试:

for device in ${devices[@]}; do

    deviceKeyValuePair=(`echo $device | tr "[:alnum:]" "[:alnum:]"`)

    echo ${deviceKeyValuePair[0]} ${deviceKeyValuePair[1]} 

done

产生:

mba=李氏 Mac本书 空气 mms=李氏 Mac 小型的 服务器

我也试过TR方法:

deviceKeyValuePair=(`echo $device | tr "[:alnum:]" "[:alnum:]"`)

输出:

mba=李氏 Mac本书 空气 mms=李氏 Mac 小型的 服务器

我可以使用关联数组,但它们仅在 BASH 4 中可用,甚至认为本周所有机器都已更新到 El Capitan BASH 仍然是版本 3.2.57(1)所以这是不行的,否则下面的方法很可能会奏效。虽然我不是这种方法的粉丝……只是厌恶迭代和数组,然后根据键进行查找——这对我来说有时很奇怪!我一直使用 .NET 词典和列表来做这件事。以下最有可能起作用:

declare -A devices

devices["mba"]="Lee's Macbook Air"
devices["mms"]="Lee's Mac Mini Server"
devices["mml"]="Lee's Mac Mini (Living Room)"
devices["mmb"]="Lee's Mac Mini (Bedroom)"
devices["mps"]="Lee's Mac Pro Server"

for device in ${!devices[@]}; do

    echo ${device} ${devices[${device}]}

done

在 OS X El Capitan 上升级 bash 安全吗?会不会摔坏什么东西?其实我还是很想知道...

想出来了:IFS!!!!

function listDevices() {

    oldIFS=IFS

    IFS=''

    for device in ${devices[@]}; do

        deviceKeyValuePair=(${device//=/})

        printf "${deviceKeyValuePair[0]}=${deviceKeyValuePair[1]}\n"

    done    

    IFS=$oldIFS

    exit 0
}

以上是我的首选方式,但会产生:

mbaLee 的 Macbook Air= mmsLee 的 Mac 迷你服务器= mmlLee 的 Mac 迷你(客厅)= mmbLee 的 Mac 迷你(卧室)= mpsLee 的 Mac Pro 服务器=

我想我对(${device//=/})中的//=/的理解不正确?我认为这是 bash 自己内置的根据指定的分隔符拆分字符串的方法。似乎更像是 bash 删除字符的方式所以另一个问题是我在 bash 字符串操作页面中找不到对它的引用!

无论如何,我现在已经确定了以下内容:

function listDevices() {

    # Pointless since this method exits once complete
    # oldIFS=IFS

    IFS=''

    for device in ${devices[@]}; do

        deviceKey=${device%%=*}
        deviceName=${device##*=} 

        echo "$deviceKey: ${deviceName}"

    done    

    # Pointless since this method exits once complete
    # IFS=$oldIFS

    exit 0
}

输出:

mba:Lee 的 Macbook Air mms: Lee's Mac 迷你服务器 mml:Lee's Mac Mini(客厅) mmb: Lee's Mac Mini (卧室) mps:Lee's Mac 专业服务器

我花了很长时间写这篇文章 post 但仍有问题需要回答(隐含的和明确的)所以这是不废弃它的两个原因加上它可以用来教育其他不这样做的菜鸟我没有完全阅读手册,我有一个严重的 ADHD 形式的原因,但没有多动症,所以 ADD 但不是每个人都知道这意味着什么,我从不阅读设备手册,也没有看过 Lego® 说明书,除了 Lego@ Technics .还有人可能会向我展示一个不涉及 IFS 的更好的方法...

谢谢你们!!!

即将发表评论:我要精通 bash 和 shell 脚本编写还有很长的路要走! 另外我正在学习 Groovy 和 SmartThings 变体以及 Lua 都在同一时间。这是给你的。

编辑: 希望这能解决 Cyrus 的顾虑:

#!/bin/bash    

# Devices
devices[0]="mba=Lee's Macbook Air"
devices[1]="mms=Lee's Mac Mini Server"
devices[2]="mml=Lee's Mac Mini (Living Room)"
devices[3]="mmb=Lee's Mac Mini (Bedroom)"
devices[4]="mps=Lee's Mac Pro Server"

function listDevices() {

    # Pointless since this method exits once complete
    # oldIFS=IFS

    IFS=''

    for device in ${devices[@]}; do

        deviceKey=${device%%=*}
        deviceName=${device##*=} 

        echo "$deviceKey: ${deviceName}"

    done    
    # Pointless since this method exits once complete
    # IFS=$oldIFS

    exit 0
}

listDevices

其实不是。以后我会更加努力,尽量不那么冗长,并确保其他用户可以将代码复制并粘贴到文件中并执行。

IFS 导致了问题。或者更准确地说,我对它的目的缺乏理解。

$IFS (Internal Field Separator)

This variable determines how Bash recognizes fields, or word boundaries, >when it interprets character strings.

$IFS defaults to whitespace (space, tab, and newline), but may be changed.

来源:高级Bash-脚本指南

所以设置IFS=''解决了问题。还假设这个函数在完成时没有退出, IFS 的值应该在更改之前存储并在完成后恢复,例如:

function listDevices() {

    previousIFS=IFS

    IFS=''

    for device in ${devices[@]}; do

        deviceKey=${device%%=*}
        deviceName=${device##*=} 

        echo "$deviceKey: ${deviceName}"

    done    

    IFS=$previousIFS
}

shell 脚本中的一个标准陷阱是(几乎)任何不在引号中的内容都将被拆分为单词(基于 IFS 中的字符)并且通配符扩展为匹配文件名列表。除非您特别希望这种情况发生(而且您通常不会这样做),否则您应该将诸如变量引用之类的东西放在双引号中。 (将 IFS 设置为 "" 有效地禁用了拆分,但留下了通配符扩展,所以它不是那么好。它还有其他可能令人不快的副作用。)所以不要管 ISF 并编写你的循环命令像这样:

for device in "${devices[@]}"; do

其次,像 ${device//=/} 这样的替换会将每个“=”替换为……什么都没有。本质上,它将删除字符串中的等号。如果您使用 ${device//=/ } 会将它们变成空格,但这也不是您想要的,因为那样它们就无法与右侧的空格区分开来。您选择的方法几乎是正确的,但是 deviceKey=${device%%=*} 从最左边的“=”开始修剪,deviceName=${device##*=} 从最右边的“=”开始修剪。如果字符串中总是恰好有一个“=”,这是可以的,但是如果设备名称可能包含一个“=”,您应该使用deviceName=${device#*=}(注意只有一个"#") 所以它会修剪 最小的 匹配项,即通过最左边的“=”。顺便说一句,这是为数不多的可以安全地将变量引用不加引号的情况之一,但是在 IMO 中,无论如何只使用双引号比试图准确记住何时安全和何时不安全更容易和更安全。因此:

deviceKey="${device%%=*}"
deviceName="${device#*=}"

实际上,有一种基于数组的方法来进行拆分:

IFS="=" read -a deviceKeyValuePair <<<"$devices"

...此处 "$devices" 周围的双引号可防止在将字符串传递给 read 之前进行分词和通配符扩展。 read根据IFS将字符串拆分成一个数组。请注意,IFS 设置用作 read 命令的前缀,因此它仅适用于该命令(即您不必之后重新设置它)。但是就像你的版本一样,如果设备名称包含“=”,这就会有问题,因为它会根据它把它分成额外的数组元素……所以实际上我建议不要使用这个。