文本文件处理示例

在文件开头输出内容

while {[getline line]} {
   when {$FNR==1} {
      puts "我在文件开头"
   }
}

在文件末尾添加内容

while {[getline line]} {
   when {$FNR==1} {
      puts "我在文件开头"
   }
}
puts "我在文件末尾"

上面这种方式把文件末尾的处理放到了循环体外面。和文件开头的判断形式上不对称。

可以通过引入“行号小于 0 时表示到了文件末尾”这个概念来写成下面这样。

while {[getline line]} {
   when {$FNR==1} {
      puts "我在文件开头"
   }
   when {$FNR<0} {
      puts "我在文件末尾"
   }
}

匹配提取文本行

# 准备一个盒子
set lines_buffer []

while {[getline line]} {
   when [string match "header-to-sort: *" $line] {
      # 收集到盒子里
      lappend lines_buffer $line
   } [regexp {^header-to-uniq: *} $line] {
      # 收集到盒子里
      lappend lines_buffer $line
   } {string eq $line ""} {
      # 输出之前收集的内容
      puts $lines_buffer
   }
}
  • string match - 通配符匹配
  • regexp - 正则表达式匹配
  • string equal - 判断相等
    • eq - 用操作符判断

神奇盒子:排序

puts [lsort $lines_buffer]

神奇盒子:排序 + 去除重复

puts [lsort -uniq $lines_buffer]

神奇盒子:去除重复 + 维持原序

只保留重复行的第一行

proc uniq-keep-first {lines_list} 
  set lines_result []
  set lines_lut    {}
  foreach line $lines_list {
    if [dict exist $lines_lut $line] continue 
    dict set lines_lut $line
    lappend lines_result $line
  }
  return $lines_result
}

puts [uniq-keep-first $lines_buffer]

只保留重复行的最后一行

proc uniq-keep-last {lines_list} 
  set lines_lut    {}
  foreach line $lines_list {
    dict set lines_lut $line
  }
  return [dict keys $lines_lut]
}

puts [uniq-keep-first $lines_buffer]

匹配提取一个区间

# 准备一个盒子
set lines_buffer []

while {[getline line]} {
   when {$line eq "LIST BEGIN"} {
      # 区间开始,做个标记
      set do_collect 1
   }

   when {$line eq "LIST END"} {
      # 区间结束,取消标记
      set do_collect 0

      # 拿盒子里的东西干点什么
      puts $lines_buffer
   }

   if {$do_collect} {
      # 收集到盒子里
      lappend lines_buffer $line
   }
}

Tcl 程序语言可以把复杂的逻辑进行抽象和封装,从而让表达更简洁。

while {[getline line]} {
   collect-text-block $line "LIST BEGIN" "LIST END" {
      # 区间结束,完成收集,做些什么
      puts $lines_buffer
   }
}

其中 collect-text-block 的实现留待后续。

输出到外部文件

while {[getline line]} {
   collect-text-block $line "LIST BEGIN" "LIST END" {
      # 区间结束,完成收集,做些什么
      wirte-file output.txt $lines_buffer
   }
}

多个匹配区间的输出

set output-file-seq 0

while {[getline line]} {
   collect-text-block $line "LIST BEGIN" "LIST END" {
      set outfile [format "output-%d.txt" [incr output-file-seq]]
      wirte-file $outfile $lines_buffer
   }
}

匹配模式

Tcl 里常用的匹配模式有三种。这点可以参考 lsearch 命令的说明

  • -exact : 完全一样
  • -glob : 通配符匹配
  • -regexp : 正则表达式匹配

因此,比如我们要提取一个由数字开头、以空行结尾的区间,比如先这样

0002 ...  # 区块开始
...
...
          ;# 区块结束
0003 ...  ;# 新的区块开始
...
...
          ;# 区块结束

只要稍微改动一下 collect-block-text 的调用如下即可。

while {[getline line]} {
   collect-text-block $line -regexp {^00\d+} {^\s*$} {
      # 区间结束,完成收集,做些什么
      wirte-file output.txt $lines_buffer
   }
}

修改区块内容

如果需要区块里的内容做一定修改,可以借助 list 相关的命令。

while {[getline line]} {
   collect-text-block $line -regexp {^00\d+} {^\s*$} {
      # 区间结束,完成收集,做些什么
      set lines_buffer [linsert  $lines_buffer 1   "new line"]
      set lines_buffer [lreplace $lines_buffer end "new line"]
      wirte-file output.txt $lines_buffer
   }
}