읽을 수 있는 Bash 스크립트

14316 단어 shellprogramming
나는 bash 파이프라인을 작성하는 느낌을 정말 좋아합니다. 명령을 함께 묶고, 출력을 엿보고, 다른 몇 개의 파이프를 추가하여 내 목표에 더 가까운 데이터를 마사지하는 것이 재미있습니다. 나는 그들에 대해 짧은 수업도 했습니다: bgschiller/shell-challenges .

그러나 작성하기 좋은 파이프라인을 유지 관리하기가 어렵다는 것을 쉽게 알 수 있습니다. 한 달 뒤에 읽는 사람이라고 해도 각 조각의 목적이 무엇인지 기억하기 어렵습니다. 이것은 더 읽기 쉬운 스크립트를 위해 파이프라인을 중간 파일로 나누는 방법을 배웠습니다.

작업



내가 작업하고 있는 제품에는 최종 사용자에게 경고해야 하는 조건인 "경보"목록이 있습니다. 우리는 또한 알람 이름을 사람이 읽을 수 있는 이름, 설명 등에 매핑하는 지원되는 각 언어에 대한 현지화 파일을 가지고 있습니다. 이미 번역된 알람을 추적하기 어려울 만큼 많은 알람이 있습니다. 이것은 스크립트를 요구합니다!

알람 파일은 다음과 같습니다.

<!-- Alarms.xml -->
<?xml version="1.0" encoding="utf-8"?>
<config name="AlarmConfig">
   <AlarmList name="IngredientAlarms" version="1.0">
      <Alarm name="MozzarellaTooWarm">
        <!-- some alarm-specific stuff here -->
      </Alarm>
      <Alarm name="PizzaTooCold">
      </Alarm>
      <!-- ... -->


현지화는 다음과 같습니다.


[{ stringId: "alarm_id_MozzarellaTooWarm",
   localString: "MozzarellaTooWarm" },
 { stringId: "alarm_title_MozzarellaTooWarm",
   localString: "Mozzarella Too Warm" },
 { stringId: "alarm_description_MozzarellaTooWarm",
   localString: "The mozzarella has become too warm and must be used within the next five minutes" },
  //...
]


계획을 세우다


  • xpath 를 사용하여 Alarms.xml에서 알람 이름을 추출합니다.
  • 현지화 파일과 일치시키려면 각 경보에 alarm_id_ , alarm_title_ , alarm_description_ , alarm_operator_actions_ 접두사를 붙여야 합니다. 그 방법을 찾아보세요.
  • 현지화 파일의 각 항목에서 "stringId"를 추출합니다. 아, 하지만 현지화해야 하고 전혀 경보가 아닌 항목에 대한 다른 항목이 있습니다. 그것들을 걸러내는 방법을 찾으십시오. 아마도 jq를 사용하십시오.
  • diff를 사용하여 예상 키를 실제 키와 비교합니다.

  • 이것은 bash 파이프라인을 만드는 방법에 대한 게시물이 아니므로 해당 부분에 대해 설명하겠습니다. 제가 생각해낸 것은 다음과 같습니다.

    diff \
      <(join -j 99999 \
        <(echo 'alarm_id_
    alarm_title_
    alarm_description_
    alarm_operator_actions_') \
        <(xpath -q -e config/AlarmList/Alarm/@name Alarms.xml |
          cut -d= -f2 | tr -d '"') \
        | tr -d ' ' | sort) \
      <(jq '.[] | select(.stringId | startswith("alarm_")) |
            .stringId ' i18n/en.json | sort)
    


    복잡하고 그런 면에서 인상적입니다. 그러나 당신이 유지하고 싶은 코드는 아닙니다.

    한 번에 한 단계 씩



    이전 단계는 우리가 필요로 하는 것을 달성했지만 무슨 일이 일어나고 있는지 보는 것은 꽤 어려웠습니다. 내 생각에 그 이유는
  • 이름이 있는 것은 없습니다. 각 프로세스 대체가 부모에게 직접 공급되기 때문에 아무 것도 이름을 받지 않습니다. 중간체의 이름을 지정해야 합니다!
  • 댓글이 없습니다. 모든 줄이 백슬래시로 끝나기 때문에 주석을 추가하기가 어렵습니다. "Enter를 누르지 않은 것처럼 이 줄을 계속하십시오"라는 의미입니다. Bash에서는 "이 줄 계속"이라고 말하고 주석을 추가할 수 없습니다.

  • 중간 파일을 사용하여 각 부분을 놀릴 수 있는지 봅시다.

    cat > alarm_attrs << EOF
    alarm_id_
    alarm_title_
    alarm_description_
    alarm_operator_actions_
    EOF
    
    xpath -q -e config/AlarmList/Alarm/@name Alarms.xml |
      cut -d= -f2 | tr -d '"' > expected_alarm_names
    
    join -j 999 alarm_attrs expected_alarm_names |
      tr -d ' ' | sort > expected_localization_keys
    
    jq -r '.[] | select(.stringId | startswith("alarm_")) |
           .stringId' en.json | sort > actual_localization_keys
    


    이게 낫다! 각 부분은 개별적으로 이해할 수 있으며 단계 간의 종속성은 명명된 파일로 명시적으로 추적됩니다. 남은 것은 주석과 약간의 오류 검사를 추가하고 중간 파일을 정리하는 것뿐입니다.

    우리 자신을 청소하는 것은 약간 까다 롭습니다. 이상적으로는 스크립트가 정상적으로 종료되는지, 충돌이 발생하는지 또는 ctrl-c로 취소되는지 여부에 관계없이 중간 결과 파일을 방치하는 것을 피하고 싶습니다. trap를 사용하여 이를 처리할 수 있습니다. trap "신호"가 발생할 때 실행할 명령을 설정합니다. 모든 중간 파일을 디렉토리에 넣고 trap를 사용하여 스크립트가 종료된다는 신호가 발생할 때 해당 디렉토리를 삭제합니다.

    #!/bin/bash
    
    set -ef -o pipefail
    
    readonly script_name=`basename "$0"`
    usage() {
      cat >&2 << EOF
    Usage: $script_name <path-to-Alarms.xml> <path-to-en.json>
    
      Compare the alarms specified in Alarms.xml against the
      localizations keys provided in a [language-code].json file
      (eg, en.json). Warn if any keys are missing or unexpected.
    EOF
      exit 2
    }
    
    if [[ $# != 2 ]]; then
       echo "error: expected 2 arguments, received $#" 1>&2
       usage
    fi
    
    readonly ALARM_CONFIG_XML=$1
    if [[ ${ALARM_CONFIG_XML: -4} != ".xml" || ! -e $ALARM_CONFIG_XML ]]; then
       echo "error: unable to find XML file at $ALARM_CONFIG_XML" 1>&2
       usage
    fi
    readonly LOCALIZATION_JSON=$2
    if [[ ${LOCALIZATION_JSON: -5} != ".json" || ! -e $LOCALIZATION_JSON ]]; then
       echo "error: unable to find JSON file at $LOCALIZATION_JSON" 1>&2
       usage
    fi
    
    # Make a directory for intermediate results
    tmpdir=$(mktemp -d -p .)
    # ensure it's removed when this script exits
    trap "rm -rf $tmpdir" EXIT HUP INT TERM
    # note: when debugging, turn off that `trap` line to keep
    # intermediate results around
    
    # The plan is ultimately to use diff to compare names between
    # Alarms.xml and en.json. diff will tell us if any names
    # appear in one file but are missing in the other (checks
    # both directions for a mismatch).In pursuit of this, we need
    # to create a couple of temporary files:
    #  1) all the alarm names we expect to find (based on Alarms.xml)
    #  2) all the names actually present in the localization file.
    # A complication: the location file uses a flat format to
    # store the id, title, description, and operator_actions:
    #   { "stringId": "alarm_id_MozzarellaTooWarm", ... },
    #   { "stringId": "alarm_title_MozzarellaTooWarm", ... },
    #   { "stringId": "alarm_description_MozzarellaTooWarm", ... },
    #   { "stringId": "alarm_operator_actions_MozzarellaTooWarm", ... },
    # We want to check that *all* of these keys are present, so
    # we use a cross product of (alarm names) X (those attributes)
    
    cat > $tmpdir/alarm_attrs << EOF
    alarm_id_
    alarm_title_
    alarm_description_
    alarm_operator_actions_
    EOF
    
    xpath -q -e config/AlarmList/Alarm/@name $ALARM_CONFIG_XML |
     cut -d= -f2 | tr -d '"' > $tmpdir/expected_alarm_names
    
    # trick to compute cross product: use `join` with a join
    # field that doesn't exist (999). since both files lack a
    # field at 999, they will compare equal for every key, and
    # each line of the left file will be joined with each line
    # of the right file--a cross product.
    join -j 999 $tmpdir/alarm_attrs $tmpdir/expected_alarm_names |
    tr -d ' ' | sort > $tmpdir/expected_localization_keys
    
    jq -r '.[] | select(.stringId | startswith("alarm_")) |
           .stringId' $LOCALIZATION_JSON |
      sort > $tmpdir/actual_localization_keys
    
    if diff $tmpdir/expected_localization_keys $tmpdir/actual_localization_keys; then
       echo "Success! Found all" $(wc -l $tmpdir/expected_localization_keys) "expected localization keys"  1>&2
    else
       echo "Failure. Found discrepancies between expected alarm names and actual localization keys" 1>&2
       exit 1
    fi
    

    좋은 웹페이지 즐겨찾기