使用 xmlstarlet 以特定顺序解析列表

using xmlstarlet to parse a list in a specific order

我正在尝试使用 xmlstarlet sel 来列出我需要从 xml 文件创建的磁盘分区,该文件将它们列在磁盘上的升序块位置(示例:https://github.com/finley/SystemImager/blob/initrd-from-imageserver-and-dont-package-initrd/doc/examples/disk-layout-complex.xml) 该文件是通过转储必须复制的已安装系统生成的。然后用户可以将他想要适配到新磁盘的分区的大小替换为“*”。

现在我正在做以下事情:

    local IFS=;
    DISK_DEV=sda
    DISKS_LAYOUT_FILE=/tmp/disk-layout-complex.xml
    cd /tmp
    wget https://raw.githubusercontent.com/finley/SystemImager/initrd-from-imageserver-and-dont-package-initrd/doc/examples/disk-layout-complex.xml
    xmlstarlet sel -t -m "config/disk[@dev=\"${DISK_DEV}\"]/part" -v "concat(@num,';',@size,';',@p_type,';',@id,';',@p_name,';',@flags,';',@lvm_group,';',@raid_dev)" -n ${DISKS_LAYOUT_FILE} | sed '/^\s*$/d' |\
    while read P_NUM P_SIZE P_TYPE P_ID P_NAME P_FLAGS P_LVM_GROUP P_RAID_DEV
    do
        # process partitions creation.
        echo "Creating partition $P_NUM of size $P_SIZE tpye $P_TYPE"
    done

上面的 xmlstarlet 将产生以下输出,然后由 while read 循环处理:

    1;500;primary;;;boot;;
    3;4096;primary;;;;;;
    4;*;extended;;;;;;
    7;4096;logical;;;;;;
    5;*;logical;;;;;;
    6;2048;logical;;;;;;
    2;1024;primary;;;swap;;

处理第 3 行(分区 #4)后,磁盘上没有 space 剩余,循环将处理第 4 行(分区 #7)并将失败,没有 space 剩余在磁盘上。

问题是可变大小分区(使用 100%(文件中的“*”))。如果一个在其他剩余的之前列出(在上述情况下的第 4 部分),那么,它被创建时有完整的剩余 space,没有在磁盘上留下 space 来处理最后一个。因此,例如,不可能将主交换分区放在具有可变大小的 / 分区的磁盘的末尾。

问:有没有巧妙的方法使用xmlstarlet sel按以下顺序列出分区:

按照 xml 文件中写入的相同顺序列出所有主分区和扩展分区,直到看到大小为“*”的分区;

对于所有分区,添加一个字段说明它是按顺序列出还是倒序排列,这样我就可以知道我是否必须相对于空闲 space 开始或相对于空闲结束 space。 (可变分区将被标记为正常顺序,因为它们将在空闲 space 开始时创建)

对于列出的示例 (disk-layout-complex.xml),这将按以下顺序列出要创建的分区: (下面,xmlstarlet 的输出将由类似于上述代码的 while read 循环处理,但前面还有一个读取参数 OFFSET_CREATE 将读取 normal/reverse 值)

    normal;1;500;primary;;;boot;;
    normal;3;4096;primary;;;;;;
    reverse;2;1024;primary;;;swap;;
    normal;4;*;extended;;;;;;
    normal;7;4096;logical;;;;;;
    reverse;6;2048;logical;;;;;;
    normal;5;*;logical;;;;;;

处理上面的 xmlstarlet 输出永远不会触发有一些分区要创建而磁盘没有 space 剩余的情况,因为创建的分区有 100% 剩余 space.

我在特制的 initrd 中处理这个,所以我只能访问最常见的实用程序,如 sed/grep/bash2/xmlstarlet/awk。没有 perl,没有 python,没有一般需要库的语言。

我非常相信有一个解决方案可以完成大部分工作(如果不是全部的话),但我的技术还不够熟练,甚至无法评估是否可以通过这种方式完成。我想我可以在纯粹的 bash 中实现这一点,但这将远没有那么优雅。

经过广泛的谷歌搜索和学习 xslt 的基础知识后,这似乎就是我要找的东西。

(要处理的 xml 文件的完整来源可在此处获得:https://github.com/finley/SystemImager/blob/initrd-from-imageserver-and-dont-package-initrd/doc/examples/disk-layout-complex.xml

<!— Sample cut, relevant for this question —>
<config>
      <disk dev="/dev/sda" label_type="msdos" unit_of_measurement="MiB">
              <part num="1" size="500" p_type="primary" flags="boot" />
              <part num="3" size="4096" p_type="primary" />
              <part num="4" size="*" p_type="extended" />
              <part num="7" size="4096" p_type="logical" />
              <part num="5" size="*" p_type="logical" />
              <part num="6" size="2048" p_type="logical" />
              <part num="2" size="1024" p_type="primary" flags="swap" />
      </disk>
</config>

xslt 没有变量,因此我需要一个递归算法来实现智能排序,从而产生正确的分区创建顺序。

目的是让 xmlstarlet tr layout.xslt disk-layout.xml 产生以下输出:

normal;1;500;primary;;;boot;;
normal;3;4096;primary;;;;;;
reverse;2;1024;primary;;;swap;;
normal;4;*;extended;;;;;;
normal;7;4096;logical;;;;;;
reverse;6;2048;logical;;;;;;
normal;5;*;logical;;;;;;

递归算法如下所示:

do_partition_template(index, type)
  if partition(index) is same type and if variable_partition is not yet seen:
    print "normal;line"
    do_partition_template(index+1,type)
  if partition(index) is same type and if variable partition has been seen:
    do_partition_template(index+1, type)
    print "reverse;line"
  if(partition(index) is same type and if  size(partition)="*":
    do_partition_template(index+1, type)
    print "normal;line"
  if partition is not the same type:
    do_partition_template(index+1, type)
  fi
call template 1st partition type='primary|extended'
call template 1st partition type='logical'

我不确定算法的递归形式,但需要按顺序创建的是: - 从磁盘开始创建的 primary/extended 分区列表 - 从磁盘末尾创建的 primary/extended 分区列表 - 大小为“”的 primary/extended 分区 - 从扩展分区开始创建的逻辑分区列表 - 从扩展分区末尾创建的逻辑分区列表 - 大小为“

的逻辑分区

作为 xsl 的新手,解决这个问题对我来说非常复杂。 我使用 xmlstarlet sel -C -t -m "config/disk[@dev=\"/dev/sda\"]/part" -v "concat(@num,';',@size,';',@p_type,';',@id,';',@p_name,';',@flags,';',@lvm_group,';',@raid_dev)" -n ./disk-layout-complex.xml > do_part.xslt 从我的问题开始,但是我很难将一个模板形式化,该模板将在 2 次扫描中命中所有分区(一次用于 primary/extended 分区,一次用于逻辑分区)...

没有变量的工作在某种程度上并不容易。

最终答案如下

<!— Sample cut, relevant for this question —>
<config>
      <disk dev="/dev/sda" label_type="msdos" unit_of_measurement="MiB">
              <part num="1" size="500" p_type="primary" flags="boot" />
              <part num="3" size="4096" p_type="primary" />
              <part num="4" size="*" p_type="extended" />
              <part num="7" size="4096" p_type="logical" />
              <part num="5" size="*" p_type="logical" />
              <part num="6" size="2048" p_type="logical" />
              <part num="2" size="1024" p_type="primary" flags="swap" />
      </disk>
</config>

我们需要以下输出以便按顺序列出分区,例如我们可以按磁盘上的外观顺序创建分区。大小为'*'的分区将占据所有剩余space。所以最后一定要创建。

因此我们需要依次创建如下分区。 (开始和结束是参考,告诉我们是否需要创建分区相对于可用的开始 space 或相对于可用的结束 space)

/dev/sda;end;2;1024;MiB;primary;;;swap;;
/dev/sda;beginning;1;500;MiB;primary;;;boot;;
/dev/sda;beginning;3;4096;MiB;primary;;;;;
/dev/sda;beginning;4;*;MiB;extended;;;;;
/dev/sda;end;6;2048;MiB;logical;;;;;
/dev/sda;beginning;7;4096;MiB;logical;;;;;
/dev/sda;beginning;5;*;MiB;logical;;;;;

通过运行 sxl 转换文件获得:

xmlstarlet tr do_part.xsl ./disk-layout-complex.xml

现在代码:

<?xml version="1.0" encoding="UTF-8"?>
<!-- Call me with:
     xmlstarlet tr do_part.xsl disk-layout.xml

     Output: List of partitions to create in order.
        Each line list the following values separated by semicolons:
        - disk device
        - creation reference
        - partition number
        - partition size
        - partition size unit
        - partition type
        - partition id
        - partition name
        - partition flags
        - lvm group it belongs to
        - raid device it belongs to

      Author: Olivier LAHAYE (c) 2019
      Licence: GPLv2
-->
<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 method="text" omit-xml-declaration="yes" indent="no"/>
  <xsl:strip-space elements="*"/>
  <xsl:template match="/config/disk"> <!-- We are loking for disk informations only -->
    <!-- For each disk block -->
    <xsl:call-template name="PrintPartition"> <!-- Compute primary or extended partitions to create -->
      <xsl:with-param name="index"><xsl:value-of select="count(part)"/></xsl:with-param>
      <xsl:with-param name="reference">end</xsl:with-param>
      <xsl:with-param name="type">primary|extended</xsl:with-param>
    </xsl:call-template>
    <xsl:call-template name="PrintPartition"> <!-- then, compute logical partitions to create -->
      <xsl:with-param name="index"><xsl:value-of select="count(part)"/></xsl:with-param>
      <xsl:with-param name="reference">end</xsl:with-param>
      <xsl:with-param name="type">logical</xsl:with-param>
    </xsl:call-template>
  </xsl:template> <!-- We're done -->

  <!-- Main recursive template that will dump partitions to create for the matched disk -->
  <xsl:template name="PrintPartition">
    <xsl:param name="index"/> <!-- partition node number within disk item-->
    <xsl:param name="reference"/> <!-- beginning or end: should we create partition relative from beginning or from end of free space -->
    <xsl:param name="type"/> <!-- type of partitions -->
    <xsl:choose>
      <xsl:when test="$index=1">
        <xsl:if test="contains($type,part[position()=$index]/@p_type)">
          <xsl:value-of select="concat(@dev,';',$reference,';',part[position()=$index]/@num,';',part[position()=$index]/@size,';',@unit_of_measurement,';',part[position()=$index]/@p_type,';',part[position()=$index]/@id,';',part[position()=$index]/@p_name,';',part[position()=$index]/@flags,';',part[position()=$index]/@lvm_group,';',part[position()=$index]/@raid_dev,'&#10;')"/> <!-- write partition information -->
        </xsl:if>
      </xsl:when>
      <xsl:when test="contains($type,part[position()=$index]/@p_type) and part[position()=$index]/@size!='*'">
        <xsl:if test="$reference='end'">
          <xsl:value-of select="concat(@dev,';',$reference,';',part[position()=$index]/@num,';',part[position()=$index]/@size,';',@unit_of_measurement,';',part[position()=$index]/@p_type,';',part[position()=$index]/@id,';',part[position()=$index]/@p_name,';',part[position()=$index]/@flags,';',part[position()=$index]/@lvm_group,';',part[position()=$index]/@raid_dev,'&#10;')"/> <!-- write partition information -->
        </xsl:if>
        <xsl:call-template name="PrintPartition">
          <xsl:with-param name="index"><xsl:value-of select="number($index)-1"/></xsl:with-param>
          <xsl:with-param name="reference"><xsl:value-of select="$reference"/></xsl:with-param>
          <xsl:with-param name="type"><xsl:value-of select="$type"/></xsl:with-param>
        </xsl:call-template>
        <xsl:if test="$reference='beginning'">
          <xsl:value-of select="concat(@dev,';',$reference,';',part[position()=$index]/@num,';',part[position()=$index]/@size,';',@unit_of_measurement,';',part[position()=$index]/@p_type,';',part[position()=$index]/@id,';',part[position()=$index]/@p_name,';',part[position()=$index]/@flags,';',part[position()=$index]/@lvm_group,';',part[position()=$index]/@raid_dev,'&#10;')"/> <!-- write partition information -->
        </xsl:if>
      </xsl:when>
      <xsl:when test="contains($type,part[position()=$index]/@p_type) and part[position()=$index]/@size='*'">
        <xsl:call-template name="PrintPartition">
          <xsl:with-param name="index"><xsl:value-of select="number($index)-1"/></xsl:with-param>
          <xsl:with-param name="reference">beginning</xsl:with-param>
          <xsl:with-param name="type"><xsl:value-of select="$type"/></xsl:with-param>
        </xsl:call-template>
        <xsl:value-of select="concat(@dev,';','beginning;',part[position()=$index]/@num,';',part[position()=$index]/@size,';',@unit_of_measurement,';',part[position()=$index]/@p_type,';',part[position()=$index]/@id,';',part[position()=$index]/@p_name,';',part[position()=$index]/@flags,';',part[position()=$index]/@lvm_group,';',part[position()=$index]/@raid_dev,'&#10;')"/> <!-- write partition information -->
      </xsl:when>
      <xsl:otherwise>
        <xsl:call-template name="PrintPartition">
          <xsl:with-param name="index"><xsl:value-of select="number($index)-1"/></xsl:with-param>
          <xsl:with-param name="reference"><xsl:value-of select="$reference"/></xsl:with-param>
          <xsl:with-param name="type"><xsl:value-of select="$type"/></xsl:with-param>
        </xsl:call-template>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

瞧瞧:效果很好。

肯定有更好或更优雅的解决方案。 (欢迎评论)

  • concat 输出很难看,但我不知道如何让它看起来更性感
  • contains($type,part[position()=$index]/@p_type)用于测试$type(分区类型)是否匹配(test="@p_type= $type" with $type contains 'primary|extended' or 'logical')
  • 我不得不使用 position()=$index 以相反的顺序导航(从最后一个元素到第一个元素)(我发现以相反顺序执行某种 foreach 的最佳方法)
  • 我需要修复一些错误:(如果在磁盘部分中没有声明分区,则会无限循环,相对于磁盘末尾创建唯一的可变大小分区。虽然这可行,但不是最佳的)

(此处提供代码(几乎没有更改):https://github.com/finley/SystemImager/blob/initrd-from-imageserver-and-dont-package-initrd/lib/dracut/modules.d/51systemimager/do_partitions.xsl

如果我对你的理解正确,你想要的排序顺序可以按如下方式实现(为清楚起见缩短了代码):

DISK_DEV=/dev/sda
DISKS_LAYOUT_FILE=./disk-layout-complex.xml
xmlstarlet sel --text -t \
    -m "config/disk[@dev=\"${DISK_DEV}\"]" \
    -m "part[@p_type='primary']"  -s A:N:L @p_type \
    -v "concat(@num,';',@size,';',@p_type,';',@flags)" -n -b \
    -m "part[@p_type='extended']" -s D:N:L @p_type \
    -v "concat(@num,';',@size,';',@p_type,';',@flags)" -n -b \
    -m "part[@p_type='logical']"  -s D:N:L @size \
    -v "concat(@num,';',@size,';',@p_type,';',@flags)" -n -b \
    ${DISKS_LAYOUT_FILE}

输出:

1;500;primary;boot
3;4096;primary;
2;1024;primary;swap
4;*;extended;
7;4096;logical;
6;2048;logical;
5;*;logical;

请记住,您可以在 XSLT 中使用多个排序键,例如... -m "part[@p_type='primary']" -s A:N:L @p_type -s A:T:L @flags

我又看了一遍。您指定的列表分为 2乘3组:

  1. primary/extended 个分区之前的可变大小分区(如果有的话), 按文档顺序
  2. primary/extended 个分区跟随一个可变大小的分区(如果有的话), 反向文档顺序
  3. primary/extended 可变大小分区(如果有),在文档中 顺序
  4. 为 1。但对于逻辑分区
  5. 为 2。但对于逻辑分区
  6. 为 3。但对于逻辑分区

这是一个复杂的 xmlstarlet 命令,包含 6 个 XPath 节点测试和 插入一些 shell 变量(小心引用)以减少重复。一个额外的谓词 - [boolean(...)] - 在情况 2 和 5 中防止在不存在可变大小分区的情况下出现双重输出。如果没有声明分区,则不会生成任何输出。

#!/bin/sh
disksLayoutFile="${DISKS_LAYOUT_FILE:-./disk-layout-complex.xml}"
diskDev="${DISK_DEV:-/dev/sda}"
#
cfgDisk="config/disk[@dev='${diskDev}']"
type_PE="(@p_type='primary' or @p_type='extended')"
type__L="(@p_type='logical')"
preSibVar_PE="preceding-sibling::part[${type_PE}]/@size='*'"
folSibVar_PE="following-sibling::part[${type_PE}]/@size='*'"
preSibVar__L="preceding-sibling::part[${type__L}]/@size='*'"
folSibVar__L="following-sibling::part[${type__L}]/@size='*'"
valueList="';',@num,';',@size,';',@p_type,';',@flags,';'"
#
xmlstarlet sel --text --template \
    -m "${cfgDisk}/part[${type_PE}][@size!='*'][not(${preSibVar_PE})]" \
    -v "concat('beg',${valueList})" -n -b \
    -m "${cfgDisk}/part[${type_PE}][@size!='*'][not(${folSibVar_PE})][boolean(${preSibVar_PE})]" \
    -s 'D:N:L' 'count(preceding-sibling::*)' \
    -v "concat('end',${valueList})" -n -b \
    -m "${cfgDisk}/part[${type_PE}][@size='*']" \
    -v "concat('beg',${valueList})" -n -b \
    -m "${cfgDisk}/part[${type__L}][@size!='*'][not(${preSibVar__L})]" \
    -v "concat('beg',${valueList})" -n -b \
    -m "${cfgDisk}/part[${type__L}][@size!='*'][not(${folSibVar__L})][boolean(${preSibVar__L})]" \
    -s 'D:N:L' 'count(preceding-sibling::*)' \
    -v "concat('end',${valueList})" -n -b \
    -m "${cfgDisk}/part[${type__L}][@size='*']" \
    -v "concat('beg',${valueList})" -n -b \
    "${disksLayoutFile}"

输出:

beg;1;500;primary;boot;
beg;3;4096;primary;;
end;2;1024;primary;swap;
beg;4;*;extended;;
beg;7;4096;logical;;
end;6;2048;logical;;
beg;5;*;logical;;

虽然可以为此命令增加更多的复杂性 (例如 disk/partition 布局问题)我认为它会更好 在适当的 XSLT 脚本中;突破 shell+xmlstarlet 用法的极限 已经.