用逗号分隔文本用CSV?

时间:2014-02-18 19:53:02

标签: regex bash csv awk cut

我正在尝试编写一些RHEL安全加固自动化脚本,并且我有一个CSV文件,我正在尝试将信息生成为可读内容。这是我到目前为止所拥有的......

#!/bin/bash

# loop through the file
while read line; do
        # get all of the content
        vulnid=`echo $line | cut -d',' -f1`
        ruleid=`echo $line | cut -d',' -f2`
        stigid=`echo $line | cut -d',' -f3`
        title=`echo $line | cut -d',' -f4`
        discussion=`echo $line | cut -d',' -f5`
        check=`echo $line | cut -d',' -f6`
        fix=`echo $line | cut -d',' -f7`

        # Format the content

        echo "########################################################"
        echo "# Vulnerability ID: $vulnid"
        echo "# Rule ID: $ruleid"
        echo "# STIG ID: $stigid"
        echo "#"
        echo "# Rule: $title"
        echo "#"
        echo "# Discussion:"
        echo "# $discussion"
        echo "# Check:"
        echo "# $check"
        echo "# Fix:"
        echo "# $fix"
        echo "########################################################"
        echo "# Start Check"
        echo
        echo "# Start Remediation"
        echo
        echo "########################################################"

done < STIG.csv

我遇到的问题是我的CSV中的文字包含逗号。这实际上非常好,符合IETF标准(http://tools.ietf.org/html/rfc4180#page-2第2.4节)。但是,正如您可以想象的那样,cut命令不会向前看,以查看逗号后面是否有尾随空格(正如您通常使用的是英文)。这导致我的所有字段搞砸了,我无法弄清楚如何使这一切正常工作。

现在,我觉得我可以使用一些神奇的正则表达式,如',![:blank:]',但如果我知道如何利用它,我会被诅咒。我习惯使用cut只是因为它快速而且脏,但也许有人有更好的建议使用awk或sed。这主要是为了生成我的程序的批量结构,它自身重复并且是评论的TON。

补充说明,这必须在RHEL6的全新安装上运行。我会用Ruby,Python等编写这个。但是,大多数是必须安装的额外包。将部署此脚本的环境是机器没有任何Internet访问或额外包的地方。 Python 2.6默认在CentOS6上,但RHEL6(我认为)。否则,相信我,我会用Ruby编写这一切。

以下是CSV的示例:

V-38447,SV-50247r1_rule,RHEL-06-000519,The system package management tool must verify contents of all files associated with packages.,The hash on important files like system executables should match the information given by the RPM database. Executables with erroneous hashes could be a sign of nefarious activity on the system.,"The following command will list which files on the system have file hashes different from what is expected by the RPM database. # rpm -Va | grep '$1 ~ /..5/ && $2 != 'c''If there is output, this is a finding.","The RPM package management system can check the hashes of installed software packages, including many that are important to system security. Run the following command to list which files on the system have hashes that differ from what is expected by the RPM database: # rpm -Va | grep '^..5'A 'c' in the second column indicates that a file is a configuration file, which may appropriately be expected to change. If the file that has changed was not expected to then refresh from distribution media or online repositories. rpm -Uvh [affected_package]OR yum reinstall [affected_package]"

此外,如果有人好奇,整个项目都是out on GitHub.

5 个答案:

答案 0 :(得分:3)

在Gnu Awk第4版中,您可以尝试:

gawk -f a.awk STIG.csv

其中a.awk是:

BEGIN {
    FPAT = "([^,]*)|(\"[^\"]+\")"
}

{
    for (i=1; i<=NF; i++) 
        print "$"i"=|"$i"|"
    print "# Rule: "$4
}

输出:

$ cat STIG.csv
vulnid,ruleid,stigid,"This is a title, hello","A discussion, ,,",check,fix

$ gawk -f a.awk STIG.csv
$1=|vulnid|
$2=|ruleid|
$3=|stigid|
$4=|"This is a title, hello"|
$5=|"A discussion, ,,"|
$6=|check|
$7=|fix|
# Rule: "This is a title, hello"

答案 1 :(得分:3)

您对问题的所有评论都很好。不支持内置于bash的CSV,所以如果你不想使用像Python,Ruby,Erlang甚至Perl这样的语言,你必须自己动手。

请注意,虽然awk可以使用逗号作为字段分隔符,但它也不能正确支持嵌入引号字段中逗号的CSV。正如Håkon建议的那样,你可以将解决方案与模式结合在一起。

但你不需要在awk中这样做;你也可以单独使用bash并避免调用外部工具。这样的事情怎么样?

#!/bin/bash

nextfield () {
  case "$line" in
    \"*)
      value="${line%%\",*}\""
      line="${line#*\",}"
      ;;
    *)
      value="${line%%,*}"
      line="${line#*,}"
      ;;
  esac
}

# loop through the file
while read line; do

  # get the content
  nextfield; vulnid="$value"
  nextfield; ruleid="$value"
  nextfield; stigid="$value"
  nextfield; title="$value"
  nextfield; discussion="$value"
  nextfield; check="$value"
  nextfield; fix="$value"

  # format the content
  printf "########################################################\n"
  printf "# Vulnerability ID: %s\n" "$vulnid"
  printf "# Rule ID: %s\n# STIG ID: %s\n#\n" "$ruleid" "$stigid"
  printf "# Rule: %s\n" "$title"
  printf "#\n# Discussion:\n"
  fmt -w68 <<<"$discussion" | sed 's/^/#   /'
  printf "# Check:\n"
  fmt -w68 <<<"$check" | sed 's/^/#   /'
  printf "# Fix:\n"
  fmt -w68 <<<"$fix" | sed 's/^/#   /'
  printf "########################################################\n"
  printf "# Start Check\n\n"
  printf "# Start Remediation\n\n"
  printf "########################################################\n"

done < STIG.csv

如果你做了很多这样的话,速度优势将是巨大的。

请注意改进的格式,fmt的礼貌。这种方式会降低避免调用外部程序的速度优势,但它确实使您的输出更容易阅读。 :)

答案 2 :(得分:1)

+1给John Y的评论。这是一个红宝石的例子

ruby -rcsv -e 'CSV.foreach("STIG.csv") do |row|
  (vulnid, ruleid, stigid, title, disc, check, fix) = row
  puts "#" * 40
  puts "# Vulnerability ID: #{vulnid}"
  puts "# Rule ID: #{ruleid}"
  puts "# STID ID: #{stigid}"
  puts "#"
  puts "# Discussion:"
  puts "# #{disc}"
  puts "# Check:"
  puts "# #{check}"
  puts "# Fix:"
  puts "# #{fix}"
  puts "#" * 40
end'

如果你想包裹长行,可以这样做:

  puts fix.gsub(/(.{1,78})(?:\s+|\Z)/) {|s| "# " + s + "\n"}

答案 3 :(得分:0)

您最大的问题是包含换行符的字段的可能性。本着这种精神,使用支持CSV的语言的建议是最好的解决方案。

但是,如果你唯一的问题是逗号(并且你知道你的字段中没有任何换行符),你可以在bash中轻松解决它,暂时用你未使用的字符组合替换引用空格序列选择,并在输出之前将其替换:

#!/bin/bash

while IFS=',' read vulnid ruleid stigid title discussion check fix; do
    echo "# Vulnerability ID: $vulnid"
    ...
    echo "# Discussion:"
    echo "# $discussion"
    ...
done <<<"$(sed 's/, /COMMASPACE/g' <STIG.csv)" | sed 's/COMMASPACE/, /g'

答案 4 :(得分:0)

以下是我在Count number of column in a pipe delimited file的答案的某种改进版本,该版本也适用于此特定问题。一个真正的CSV解析器实现是最好的,但是使用awk的下面的hack只要字段没有分成多行就可以工作,这可以在字段以引号开头并持续到下一个引号不在同一行上时。它还假设它收到的文件已经格式良好。唯一的问题是它会在最后一个字段后输出OFS。在您的特定情况下,这不应成为问题。

只需在上面的while循环之前添加以下内容,并根据需要更改OFS的值,确保更改cut的分隔符以匹配。 OFS默认为|,但如果您希望使用awk允许的-v选项,则可以覆盖它,如下所示:

outfile="$(mktemp 2>/dev/null || printf '%s' "/tmp/STIG.$$")"

outdelim='|'

awk -F',' -vOFS="$outdelim" STIG.csv >"$outfile" <<EOF
#WARNING: outputs OFS after the last field, meaning an empty field is at the end.
BEGIN{ if (OFS=="") OFS='|' }

{
    for (i = 1; i <= NF; i++) {
        if ($i ~ /^".*[^"]$/)
            for (; i <= NF && ($i !~ /.*"$/); i++) {
                printf("%s%s", $i, FS);
            }
        printf("%s%s", $i, OFS);
    }
}
EOF

# loop through the file
while read line; do
    # get all of the content
    vulnid="$(echo $line | cut -d"$outdelim" -f1)"
    .
    .
    .
done < "$outfile"

rm -f "$outfile"