Bash中if语句中的模式匹配

3
我正在尝试计算目录中所有 .txt 文件中至少有两个元音字母的单词数量。以下是我的代码:

#!/bin/bash

wordcount=0


for i in $HOME/*.txt
do
cat $i |
while read line
do
    for w in $line
    do
    if [[ $w == .*[aeiouAEIOU].*[AEIOUaeiou].* ]]
    then
        wordcount=`expr $wordcount + 1`
        echo $w ':' $wordcount  
    else
        echo "In else"
    fi
    done
done
echo $i ':' $wordcount
wordcount=0
done

这是我从一个txt文件中提取的样本

最后修改时间:2017年8月20日星期日18:18:27 IST
要删除PPA
sudo apt-get install ppa-purge
sudo ppa-purge ppa:

问题是它不符合if语句中文本文件中所有单词的模式。它直接进入else语句。其次,echo $i ':' $wordcount中的单词计数等于0,但应该是某个值。


while ...; do ...; done <<< "$i" 替换 cat $i | while ...; do ...; done - Cyrus
请查看:http://www.shellcheck.net/ - Cyrus
@Wanmi 如果你的问题是一个正则表达式,那么为什么这个问题中的代码超过两行 -- 一行是将一个固定字符串赋值给一个变量,第二行是对该变量内容应用正则表达式?这里肯定有比最小化展示你的问题所需更多的代码。 - Charles Duffy
@CharlesDuffy:好发现。 - Cyrus
3个回答

5

即时问题:Glob vs Regex

[[ $string = $pattern ]]不能执行正则表达式匹配;它是一种glob样式的模式匹配。虽然在正则表达式中,.表示"任何字符",但在glob中,它只匹配它本身。

您有几个选择:

  1. Use =~ instead to perform regular expression matching:

    [[ $w =~ .*[aeiouAEIOU].*[AEIOUaeiou].* ]]
    
  2. Use a glob-style expression instead of a regex:

    [[ $w = *[aeiouAEIOU]*[aeiouAEIOU]* ]]
    

    Note the use of = rather than == here; while either is technically valid, the former avoids building finger memory that would lead to bugs when writing code for a POSIX implementation of test / [, as = is the only valid string comparison operator there.

重要问题:正确逐字阅读

使用 for w in $line 是本质上不安全的。请使用 read -a 将一行读入一个单词数组中:

#!/usr/bin/env bash

wordcount=0
for i in "$HOME"/*.txt; do
  while read -r -a words; do
    for word in "${words[@]}"; do
      if [[ $word = *[aeiouAEIOU]*[aeiouAEIOU]* ]]; then
        (( ++wordcount ))
      fi
    done
  done <"$i"
  printf '%s: %s\n' "$i" "$wordcount"
  wordcount=0
done

谢谢,这是一份全面的答案。在意识到通配符模式后,我对使用Bash感到沮丧。 - dank8

1

尝试:

awk '/[aeiouAEIOU].*[AEIOUaeiou]/{n++} ENDFILE{print FILENAME":"n; n=0}' RS='[[:space:]]' *.txt

样例输出如下:

$ awk '/[aeiouAEIOU].*[AEIOUaeiou]/{n++} ENDFILE{print FILENAME":"n; n=0}' RS='[[:space:]]' *.txt
one.txt:1
sample.txt:9

它是如何工作的:

  • /[aeiouAEIOU].*[AEIOUaeiou]/{n++}

    每次我们发现一个有两个元音字母的单词时,我们会增加变量n

  • ENDFILE{print FILENAME":"n; n=0}

    在每个文件结束时,我们打印文件名和2元音单词计数n。然后我们将n重置为零。

  • RS='[[:space:]]'

    这告诉awk使用任何空格作为单词分隔符。这使得每个单词成为一个记录。Awk逐个记录读取输入。

Shell问题

使用awk避免了许多shell问题。例如,请考虑以下行for w in $line。这不会按照您的希望工作。请考虑具有以下文件的目录:

$ ls
one.txt  sample.txt

现在,让我们取 line='* Item One' 看看会发生什么:
$ line='* Item One'
$ for w in $line; do echo "w=$w"; done
w=one.txt
w=sample.txt
w=Item
w=One

Shell在处理line中的*时会将其作为通配符并将其扩展成文件列表。很可能这不是你想要的结果。awk的解决方案避免了诸如此类的问题。


1
你所提到的“shell问题”是特定于for w in $line。如果遵循BashFAQ#1实践(尤其是指导不要使用for迭代行),并且更一般地避免未引用的扩展,这些问题就不会发生。 - Charles Duffy
这意味着描述常见的awk错误(例如,未将初始化代码放入BEGIN块中)作为不使用该语言的理由是不公平的。我认为对于常见的shell错误也同样适用。 - Charles Duffy
@CharlesDuffy 当然,正如您所知道的那样,可以正确地执行shell。OP的shell代码确实存在多个问题,我只是在说明其中一个问题的后果。 - John1024
@CharlesDuffy,我看到你更新的答案解决了OP的shell代码中的所有其他问题。+1。 - John1024

0
使用grep - 这很容易做到。
#!/bin/bash

wordcount=0
for file in ./*.txt
do
count=`cat $file | xargs -n1 | grep -ie "[aeiou].*[aeiou]" | wc -l`
wordcount=`expr $wordcount + $count`
done

echo $wordcount

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接