UNIX에서 Bash 스크립트를 사용하여 XML 파일을 정렬하시나요?

UNIX에서 Bash 스크립트를 사용하여 XML 파일을 정렬하시나요?

아래에 표시된 XML 파일을 알파벳순으로 정렬하려고 합니다. 이는 더 큰 bash 스크립트의 일부이므로 해당 스크립트 내에서 작동해야 합니다.

<Module>
    <Settings>
        <Dimensions>
            <Volume>13000</Volume>
            <Width>5000</Width>
            <Length>2000</Length>
        </Dimensions>
        <Stats>
            <Mean>1.0</Mean>
            <Max>3000</Max>
            <Median>250</Median>
        </Stats>
    </Settings>
    <Debug>
        <Errors>
            <Strike>0</Strike>
            <Wag>1</Wag>
            <MagicMan>0</MagicMan>
        </Errors>
    </Debug>
</Module>

나는 최종 결과가 다음과 같기를 원합니다. 가장 안쪽 태그만 정렬되기를 원합니다.

<Module>
    <Settings>
        <Dimensions>
            <Length>2000</Length>
            <Volume>13000</Volume>
            <Width>5000</Width>
        </Dimensions>
        <Stats>
            <Max>3000</Max>
            <Mean>1.0</Mean>
            <Median>250</Median>
        </Stats>
    </Settings>
    <Debug>
        <Errors>
            <MagicMan>0</MagicMan>
            <Strike>0</Strike>
            <Wag>1</Wag>
        </Errors>
    </Debug>
</Module>

-t가 > 구분 기호로 정렬한 다음 내부에 있는 네 번째 열로 4를 정렬하는 것과 같은 정렬을 사용하려고 하는데 작동하지 않습니다.

sort -t'>' -k4 file > final.xml

정렬된 내부 레이블을 사용하여 다른 열을 정렬하는 이상한 출력을 얻습니다.

어떤 도움이라도 대단히 감사하겠습니다.

답변1

[아낌없는 도움으로선행은 이루기가 어렵다]

xq다음 래퍼를 사용하여 이 작업을 수행 할 수 있습니다 .yq( YAML/XML 래퍼)은 정렬 기능을 jq활용합니다 .jq

$ xq -x 'getpath([paths(scalars)[0:-1]] | unique | .[])
    |= (to_entries|sort_by(.key)|from_entries)' file.xml
<Module>
  <Settings>
    <Dimensions>
      <Length>2000</Length>
      <Volume>13000</Volume>
      <Width>5000</Width>
    </Dimensions>
    <Stats>
      <Max>3000</Max>
      <Mean>1.0</Mean>
      <Median>250</Median>
    </Stats>
  </Settings>
  <Debug>
    <Errors>
      <MagicMan>0</MagicMan>
      <Strike>0</Strike>
      <Wag>1</Wag>
    </Errors>
  </Debug>
</Module>

설명하다:

  • paths(scalars)루트에서 리프까지의 모든 경로 목록을 생성한 다음 배열 슬라이스를 [0,-1]통해 리프 노드를 제거하면 리프가 아닌 가장 깊은 노드에 대한 경로 목록이 생성됩니다.

    ["Module","Settings","Dimensions"]
    ["Module","Settings","Dimensions"]
    ["Module","Settings","Dimensions"]
    ["Module","Settings","Stats"]
    ["Module","Settings","Stats"]
    ["Module","Settings","Stats"]
    ["Module","Debug","Errors"]
    ["Module","Debug","Errors"]
    ["Module","Debug","Errors"]
    
  • [paths(scalars)[0:-1]] | unique | .[]로 중복 항목을 제거할 수 있도록 목록을 배열에 넣습니다 unique. 반복자는 .[]이를 목록으로 반환합니다.

    ["Module","Debug","Errors"]
    ["Module","Settings","Dimensions"]
    ["Module","Settings","Stats"]
    
  • getpath()|=업데이트 할당 연산자를 사용하여 내용을 정렬하고 업데이트할 수 있는 기본 개체로 중복 제거된 목록을 변환합니다.

-x옵션은 xq결과를 JSON으로 두지 않고 다시 XML로 변환하도록 지시합니다.

여기에서는 전자를 sort대체 하지만 키가 고유하지 않은 경우 값과 키를 기준으로 암시적으로 정렬합니다.sort_by(.key)

답변2

모든 Unix 시스템의 모든 셸에서 any awk, sort, 를 사용 cut하고 입력이 항상 질문에 제공한 예와 동일한 형식이라고 가정합니다. 여기서 정렬하려는 줄에는 항상 시작/끝 마커가 있고 다른 줄에는 그렇지 않습니다. , 그리고 <t가 없는 s는 입력의 다른 곳에 나타납니다.

$ cat tst.sh
#!/usr/bin/env bash

awk '
BEGIN { FS="<"; OFS="\t" }
{
    idx = ( (NF == 3) && (pNF == 3) ? idx : NR )
    print idx, $0
    pNF = NF
}
' "${@:--}" |
sort -k1,1n -k2,2 |
cut -f2-

$ ./tst.sh file
<Module>
    <Settings>
        <Dimensions>
            <Length>2000</Length>
            <Volume>13000</Volume>
            <Width>5000</Width>
        </Dimensions>
        <Stats>
            <Max>3000</Max>
            <Mean>1.0</Mean>
            <Median>250</Median>
        </Stats>
    </Settings>
    <Debug>
        <Errors>
            <MagicMan>0</MagicMan>
            <Strike>0</Strike>
            <Wag>1</Wag>
        </Errors>
    </Debug>
</Module>

위의 내용은 awk를 사용하여 입력을 장식하므로 sort전체 sort파일에 대해 한 번 실행한 다음 cut추가된 숫자를 제거하는 데 사용할 수 있습니다 awk. 무슨 일이 일어나고 있는지 이해할 수 있는 중간 단계는 다음과 같습니다.

awk '
BEGIN { FS="<"; OFS="\t" }
{
    idx = ( (NF == 3) && (pNF == 3) ? idx : NR )
    print idx, $0
    pNF = NF
}
' file
1       <Module>
2           <Settings>
3               <Dimensions>
4                   <Volume>13000</Volume>
4                   <Width>5000</Width>
4                   <Length>2000</Length>
7               </Dimensions>
8               <Stats>
9                   <Mean>1.0</Mean>
9                   <Max>3000</Max>
9                   <Median>250</Median>
12              </Stats>
13          </Settings>
14          <Debug>
15              <Errors>
16                  <Strike>0</Strike>
16                  <Wag>1</Wag>
16                  <MagicMan>0</MagicMan>
19              </Errors>
20          </Debug>
21      </Module>

awk '
BEGIN { FS="<"; OFS="\t" }
{
    idx = ( (NF == 3) && (pNF == 3) ? idx : NR )
    print idx, $0
    pNF = NF
}
' file | sort -k1,1n -k2,2
1       <Module>
2           <Settings>
3               <Dimensions>
4                   <Length>2000</Length>
4                   <Volume>13000</Volume>
4                   <Width>5000</Width>
7               </Dimensions>
8               <Stats>
9                   <Max>3000</Max>
9                   <Mean>1.0</Mean>
9                   <Median>250</Median>
12              </Stats>
13          </Settings>
14          <Debug>
15              <Errors>
16                  <MagicMan>0</MagicMan>
16                  <Strike>0</Strike>
16                  <Wag>1</Wag>
19              </Errors>
20          </Debug>
21      </Module>

awk또는 다음 용도 로 GNU를 사용하세요 sorted_in.

$ cat tst.awk
BEGIN { FS="<" }
NF == 3 {
    rows[$0]
    f = 1
    next
}
f && (NF < 3) {
    PROCINFO["sorted_in"] = "@ind_str_asc"
    for (row in rows) {
        print row
    }
    delete rows
    f = 0
}
{ print }

GNU가 없으면 any 및 any를 사용하여 동일한 접근 방식을 얻을 awk수 있습니다 .awksort

$ cat tst.awk
BEGIN { FS="<" }
NF == 3 {
    rows[$0]
    f = 1
    next
}
f && (NF < 3) {
    cmd = "sort"
    for (row in rows) {
        print row | cmd
    }
    close(cmd)
    delete rows
    f = 0
}
{ print }

sort그러나 중첩된 각 라인 블록을 호출하기 위해 하위 쉘을 생성하므로 위의 처음 두 솔루션보다 훨씬 느립니다 .

답변3

요청한 대로 답변하세요: pure(ish) bash 솔루션(그러나 여전히 sort를 호출합니다). 샘플 입력에서 지정된 출력을 생성합니다. 물론 XML을 행 지향으로 처리하는 모든 솔루션이 그러하듯이 이는 취약합니다.

#!/bin/bash

function FunkySort(){
    local inputfile="$1"
    local -a linestosort=()
    local line ltchars
    while IFS= read -r line; do
        # strip all but less-than characters
        ltchars="${line//[^<]}"
        # if we guess it is "innermost" tag
        if [ ${#ltchars} -gt 1 ]; then
            # append to array
            linestosort+=("${line}")
        else
            # if non-innermost but have accumulated some of them
            if [ ${#linestosort} -gt 0 ]; then
                # then emit accumulated lines in sorted order
                printf "%s\n" "${linestosort[@]}" | sort
                # and reset array
                linestosort=()
            fi
            printf "%s\n" "$line"
        fi
    done < "$inputfile"
}

FunkySort "test.xml" >"test.out"

관련 정보