在没有 XML 解析器的情况下过滤 svn ls --xml 最近修改的文件

Filtering svn ls --xml for recently modified files without an XML parser

我对 Linux 和 Bash 脚本编写非常陌生,我在开始使用它时遇到了一些困难。我在 XML 中有一个元素列表,我只想 select 其中的几个。基于具有最新年份和最新月份(最后更改)的元素,我只想按名称 select 那些最近 4 个月更改的元素。基本上我想要一份过去 4 个月中使用的元素名称的列表。我正在使用 svn ls --xml 在 xml 中吐出数据,我正在尝试将其传输到 grep 执行上述操作。我不能使用 xml 解析器,因为这需要我在脚本将 运行 开启的每个系统中安装它。这里有两个这样的 xml 条目:

<entry
   kind="directory">
<name>foo</name>
<commit
   revision="69">
<author>myself</author>
<date>2016-05-13T00:21:59.396753Z</date>
</commit>
</entry>
<entry
   kind="directory">
<name>bar</name>
<commit
   revision="666">
<author>myself</author>
<date>2013-04-04T01:56:54.484359Z</date>
</commit>
</entry>
</list>
</lists>

你要求的答案很糟糕,不好,非常糟糕

假设(这是一个假设绝对不能保证在未来的版本中成立)此输出的格式在未来将保持不变(以超出格式良好的方式) XML 规范提供的保证),并且您的文件名永远不会包含需要在 XML:

中转义的字符
date_re='^<date>(.*)</date>$'
name_re='^<name>(.*)</name>$'
end_re='^</entry>$'

limit=$(date -d 'now - 4 months' '+%Y-%m-%dT%H:%M:%S') || exit

date=; name=
while read -r line; do
  [[ $line =~ $date_re ]] && date=${BASH_REMATCH[1]}
  [[ $line =~ $name_re ]] && name=${BASH_REMATCH[1]}
  [[ $line =~ $end_re && $date && $name ]] && [[ $date > $limit ]] && {
    printf '%s\t%q\n' "$date" "$name"
    date=; name=
  }
done < <(svn ls --xml) | sort -r

它的输出将是一个类似于(对于您的输入)的流:

2016-05-13T00:21:59.396753Z foo

请注意,如果您的文件名非常有趣,此 表现得很糟糕。期望 &gt;&amp; 和类似的输出,而实际文件名包含 >& 等。如果 SVN 的未来版本向这些 XML 标签添加属性,它也将停止工作,这是完全允许的。不要这样做。


正确的事情

...获取四个最新文件:

xmlstarlet sel -t -m '//entry' -v './commit/date' -o $'\t' -v './name' -n \
  | sort -r \
  | head -n 4

...现在,只有当我们假设 Subversion 不能用文字换行符存储文件名时,这才是明确的。幸运的是,这是它在实践中强制执行的规则;因此,此输出流中第一个制表符之后的所有内容都可以安全地解释为文件系统组件。


正确的东西,便携

上面的xmlstarlet命令正好等同于使用xsltproc应用下面的模板:

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exslt="http://exslt.org/common" version="1.0" extension-element-prefixes="exslt">
  <xsl:output omit-xml-declaration="yes" indent="no"/>
  <xsl:template match="/">
    <xsl:for-each select="//entry">
      <xsl:call-template name="value-of-template">
        <xsl:with-param name="select" select="./commit/date"/>
      </xsl:call-template>
      <xsl:text>        </xsl:text>
      <xsl:call-template name="value-of-template">
        <xsl:with-param name="select" select="./name"/>
      </xsl:call-template>
      <xsl:value-of select="'&#10;'"/>
    </xsl:for-each>
  </xsl:template>
  <xsl:template name="value-of-template">
    <xsl:param name="select"/>
    <xsl:value-of select="$select"/>
    <xsl:for-each select="exslt:node-set($select)[position()&gt;1]">
      <xsl:value-of select="'&#10;'"/>
      <xsl:value-of select="."/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

如果保存为names-and-dates.xslt,则:

xsltproc names-and-dates.xslt - < <(svn ls --xml) | sort -r | head

...将相应地应用它。


脚注:应用截止日期

如果您想强制执行日期截止而不是采用 head 的最后 N 方法,请将上面的 head 替换为 awk -v min_date=$(date -d 'now - 4 months' '+%Y-%m-%dT%H:%M:%S') '( < min_date) { exit } { print }'

如果您想相对于第一个条目花费四个月,而不是相对于当前日期,您可以通过以下方式管道化结果:

{
   read -r date name
   min_date=$(date -d "$date - 4 months" '+%Y-%m-%dT%H:%M:%S')
   printf '%s\t%s\n' "$date" "$name"
   while read -r date name; do
     [[ $date > $min_date ]] || break
     printf '%s\t%s\n' "$date" "$name"
   done
}

请注意,这假定了 GNU 日期;为非 GNU 平台的可移植性进行调整留作 reader.

的练习。