タグ: シェルスクリプト Page 1 of 6

Google Geminiによるシェルスクリプト。(SSLの整合性確認)

Google Geminiに

「発行/別サーバからインポートしたSSL証明書の整合性確認を行うシェルスクリプト」

を書いてほしいと尋ねました。

条件は

  • コマンド [SSL証明書の入ったディレクトリ]形式
  • 証明書は
    • ドメイン名.crt.yyyymm
    • Let's Encryptのように証明書と中間証明書が結合された形式
  • 秘密鍵は
    • ドメイン名.key.yyyymm

の2つが入ったディレクトリを想定。

できあがったスクリプト

#!/bin/bash

# SSL証明書と秘密鍵の情報を表示するスクリプト
# サーバー証明書の有効期限、ドメイン名、発行局を表示します。
# また、対応する秘密鍵の存在と一致確認も行います。

# 使用法を表示する関数
usage() {
  echo "使用法: $0 <証明書と鍵が格納されているディレクトリ>"
  echo "ディレクトリには '[ドメイン名].crt.yyyymm' のような名前の証明書ファイルを配置してください。"
  echo "また、対応する秘密鍵ファイルは '[ドメイン名].key.yyyymm' の形式です。"
  echo "証明書ファイルは、サーバー証明書と中間証明書が結合された形式を想定しています。"
  exit 1
}

# 引数が指定されているか確認
if [ -z "$1" ]; then
  usage
fi

TARGET_DIR="$1"

# ディレクトリが存在するか確認
if [ ! -d "$TARGET_DIR" ]; then
  echo "エラー: ディレクトリ '$TARGET_DIR' が見つかりません。"
  exit 1
fi

# openssl コマンドが利用可能か確認
if ! command -v openssl &> /dev/null; then
    echo "エラー: openssl コマンドが見つかりません。OpenSSLをインストールしてください。"
    exit 1
fi

echo "ディレクトリ '$TARGET_DIR' 内のSSL証明書情報:"
echo "=================================================="

CERT_FILES_FOUND=0 # 処理した証明書ファイルのカウンタを初期化

# プロセス置換を使用して、CERT_FILES_FOUND が現在のシェルで更新されるようにする
while IFS= read -r -d $'\0' CERT_FILE; do
  CERT_FILES_FOUND=$((CERT_FILES_FOUND + 1)) # カウンタを増やす
  FILENAME=$(basename "$CERT_FILE")
  echo "処理中の証明書: $FILENAME"

  # --- 有効期限 ---
  EXPIRY_DATE_FULL=$(openssl x509 -noout -enddate -in "$CERT_FILE" 2>/dev/null)
  if [ $? -ne 0 ] || [ -z "$EXPIRY_DATE_FULL" ]; then
      EXPIRY_DATE_DISPLAY="有効期限の抽出エラー"
  else
      EXPIRY_DATE_DISPLAY=$(echo "$EXPIRY_DATE_FULL" | sed 's/notAfter=//')
  fi
  echo "  有効期限: $EXPIRY_DATE_DISPLAY"

  # --- ドメイン名 (サブジェクトのコモンネーム(CN) および サブジェクト代替名(SANs)) ---
  DOMAIN_NAME_DISPLAY="N/A"
  SUBJECT_CN_RAW=$(openssl x509 -noout -subject -nameopt multiline -in "$CERT_FILE" 2>/dev/null | grep 'commonName')
  SUBJECT_CN=""
  if [ $? -eq 0 ] && [ -n "$SUBJECT_CN_RAW" ]; then # grep で commonName が見つかった場合
    SUBJECT_CN=$(echo "$SUBJECT_CN_RAW" | sed -e 's/^\s*commonName\s*=\s*//' -e 's/\s*(Default)\s*$//' | head -n 1)
  fi
  
  # SANs を取得 (もしあれば最初の SAN 行のみ)
  SANS_TEXT=$(openssl x509 -noout -text -in "$CERT_FILE" 2>/dev/null | awk '/X509v3 Subject Alternative Name:/ {found=1; getline; print; exit} END{if(!found)print ""}')
  SANS_TEXT_CLEANED=$(echo "$SANS_TEXT" | sed 's/^\s*//') # 先頭の空白を削除
  SANS_LIST=""
  PRIMARY_SAN_DOMAIN=""

  if [[ "$SANS_TEXT_CLEANED" == "DNS:"* ]]; then
      # SANsリストを整形: DNS:プレフィックスを除去、空白除去、カンマ区切り、重複除去
      SANS_LIST=$(echo "$SANS_TEXT_CLEANED" | sed 's/DNS://g' | tr -d ' ' | tr ',' '\n' | awk '!seen[$0]++' | tr '\n' ',' | sed 's/,$//')
      if [ -n "$SANS_LIST" ]; then
        PRIMARY_SAN_DOMAIN=$(echo "$SANS_LIST" | cut -d',' -f1) # 最初のSANを主要ドメインとする
      fi
  fi

  if [ -n "$PRIMARY_SAN_DOMAIN" ]; then # 主要SANがある場合
      DOMAIN_NAME_DISPLAY="$PRIMARY_SAN_DOMAIN"
      # 主要SAN以外のSANがあれば表示
      OTHER_SANS=$(echo "$SANS_LIST," | sed "s/$PRIMARY_SAN_DOMAIN,//" | sed 's/,$//') 
      if [ -n "$OTHER_SANS" ]; then
          DOMAIN_NAME_DISPLAY="$PRIMARY_SAN_DOMAIN (他のSAN: $OTHER_SANS)"
      fi
      # CNが存在し、かつ主要SANと異なる場合にCNも表示
      if [ -n "$SUBJECT_CN" ] && [ "$SUBJECT_CN" != "$PRIMARY_SAN_DOMAIN" ]; then
          DOMAIN_NAME_DISPLAY="$DOMAIN_NAME_DISPLAY [CNも存在: $SUBJECT_CN]"
      fi
  elif [ -n "$SUBJECT_CN" ]; then # 主要SANが無く、CNがある場合
      DOMAIN_NAME_DISPLAY="$SUBJECT_CN"
      if [ -n "$SANS_LIST" ]; then # SANリスト自体は存在する (CNが主要となるケース)
          IS_CN_IN_SANS=0
          # SANS_LISTを配列に変換して確認しやすくする
          IFS=',' read -r -a SAN_ARRAY <<< "$SANS_LIST"
          for san_entry in "${SAN_ARRAY[@]}"; do
            if [ "$san_entry" = "$SUBJECT_CN" ]; then IS_CN_IN_SANS=1; break; fi
          done
          
          if [ $IS_CN_IN_SANS -eq 0 ]; then # CNがSANリストに含まれていない場合
             # SANリストがCNと異なる場合にのみSANリストを表示 (CNが唯一のSANである場合を除く)
             if [ "$SANS_LIST" != "$SUBJECT_CN" ]; then
                DOMAIN_NAME_DISPLAY="$SUBJECT_CN (SAN: $SANS_LIST)" # SAN -> SANs に統一すべきか検討
             fi
          else # CNがSANリストに含まれている場合、CN以外のSANがあれば表示
             OTHER_SANS_THAN_CN=""
             TEMP_SANS_LIST=""
             for san_entry in "${SAN_ARRAY[@]}"; do
                if [ "$san_entry" != "$SUBJECT_CN" ]; then
                    TEMP_SANS_LIST="${TEMP_SANS_LIST}${san_entry},"
                fi
             done
             OTHER_SANS_THAN_CN=$(echo "$TEMP_SANS_LIST" | sed 's/,$//')

             if [ -n "$OTHER_SANS_THAN_CN" ]; then
                DOMAIN_NAME_DISPLAY="$SUBJECT_CN (他のSAN: $OTHER_SANS_THAN_CN)"
             fi
          fi
      fi
  else # CNもSANも取得できなかった場合
      DOMAIN_NAME_DISPLAY="エラー: コモンネームまたはサブジェクト代替名を抽出できませんでした。"
  fi
  echo "  ドメイン名: $DOMAIN_NAME_DISPLAY"

  # --- 発行局 (サーバー証明書を発行した中間認証局) ---
  # 証明書ファイル内の最初の証明書のIssuerを取得
  ISSUER_CN_RAW=$(openssl x509 -noout -issuer -nameopt multiline -in "$CERT_FILE" 2>/dev/null | grep 'commonName')
  ISSUER_CN="N/A"
  if [ $? -eq 0 ] && [ -n "$ISSUER_CN_RAW" ]; then # grepでcommonNameが見つかったか確認
      ISSUER_CN=$(echo "$ISSUER_CN_RAW" | sed -e 's/^\s*commonName\s*=\s*//' -e 's/\s*(Default)\s*$//' | head -n 1)
  fi
  
  ISSUER_FULL_RAW=$(openssl x509 -noout -issuer -nameopt oneline,-space_eq,-show_type -in "$CERT_FILE" 2>/dev/null)
  ISSUER_FULL="N/A"
  if [ $? -eq 0 ] && [ -n "$ISSUER_FULL_RAW" ]; then
      ISSUER_FULL=$(echo "$ISSUER_FULL_RAW" | sed 's/issuer=//')
  fi

  if [ "$ISSUER_CN" != "N/A" ]; then
    echo "  発行局 (CN): $ISSUER_CN"
    echo "  発行局 (詳細): $ISSUER_FULL"
  else
    echo "  発行局 (詳細): $ISSUER_FULL (CNが見つからないか、CNの抽出エラーです)"
  fi

  # --- 秘密鍵の確認 ---
  # 証明書ファイル名から対応する秘密鍵ファイル名を推測 (例: domain.crt.yyyymm -> domain.key.yyyymm)
  KEY_FILE_CANDIDATE_NAME=$(basename "$CERT_FILE" | sed -E 's/\.crt\.([0-9]{6})$/.key.\1/')
  POTENTIAL_KEY_FILE="$TARGET_DIR/$KEY_FILE_CANDIDATE_NAME"

  if [ -f "$POTENTIAL_KEY_FILE" ]; then
    echo "  秘密鍵ファイル: $(basename "$POTENTIAL_KEY_FILE")"
    KEY_MATCH_STATUS="未確定 (エラーまたは非対応の鍵タイプ)"
    KEY_TYPE="不明"

    CERT_PUBKEY=$(openssl x509 -noout -pubkey -in "$CERT_FILE" 2>/dev/null) # 証明書から公開鍵を抽出
    KEY_IS_ENCRYPTED_OR_BAD_FORMAT=0
    PRIV_KEY_PUBKEY="" # 初期化

    # 鍵の種類を特定し、その公開鍵コンポーネントを取得 ('openssl pkey' は汎用的な鍵読み取り試行)
    if openssl pkey -in "$POTENTIAL_KEY_FILE" -noout >/dev/null 2>&1; then # まず鍵として読めるか
        if openssl rsa -in "$POTENTIAL_KEY_FILE" -noout -check >/dev/null 2>&1; then # RSA鍵か
            KEY_TYPE="RSA"
            PRIV_KEY_PUBKEY=$(openssl rsa -in "$POTENTIAL_KEY_FILE" -pubout 2>/dev/null) # RSA秘密鍵から公開鍵を抽出
        elif openssl ec -in "$POTENTIAL_KEY_FILE" -noout -check >/dev/null 2>&1; then # EC鍵か
            KEY_TYPE="EC"
            PRIV_KEY_PUBKEY=$(openssl ec -in "$POTENTIAL_KEY_FILE" -pubout 2>/dev/null) # EC秘密鍵から公開鍵を抽出
        else # その他の鍵タイプ (DSAなど) か、上記チェックをパスしないがpkeyでは読める鍵
            KEY_TYPE="その他 ('openssl pkey'で解析可能)"
            PRIV_KEY_PUBKEY=$(openssl pkey -in "$POTENTIAL_KEY_FILE" -pubout 2>/dev/null) # 汎用的に公開鍵抽出を試行
        fi
        if [ $? -ne 0 ] || [ -z "$PRIV_KEY_PUBKEY" ]; then # 公開鍵の抽出に失敗した場合
             KEY_IS_ENCRYPTED_OR_BAD_FORMAT=1 # 暗号化されているか、形式に問題がある可能性
        fi
    else # 'openssl pkey' で解析不可の場合
        KEY_IS_ENCRYPTED_OR_BAD_FORMAT=1 # 破損または暗号化の可能性が高い
        KEY_TYPE="読み取り不可または暗号化"
    fi

    if [ "$KEY_IS_ENCRYPTED_OR_BAD_FORMAT" -eq 1 ]; then
        KEY_MATCH_STATUS="鍵の読み取りエラー (暗号化、パスワード保護、または不正な形式の可能性あり)。検出された鍵タイプ: $KEY_TYPE"
    elif [ -n "$CERT_PUBKEY" ] && [ -n "$PRIV_KEY_PUBKEY" ]; then # 証明書の公開鍵と秘密鍵由来の公開鍵が両方取得できた場合
      if [ "$CERT_PUBKEY" = "$PRIV_KEY_PUBKEY" ]; then # 公開鍵同士を比較
        KEY_MATCH_STATUS="証明書と一致 ($KEY_TYPE)"
      else
        KEY_MATCH_STATUS="証明書と不一致 ($KEY_TYPE)"
      fi
    elif [ -z "$CERT_PUBKEY" ]; then # 証明書から公開鍵が取得できなかった場合
      KEY_MATCH_STATUS="証明書からの公開鍵生成エラー"
    else # 秘密鍵ファイルから公開鍵が取得できなかった場合 (鍵は読めたが公開鍵抽出に失敗)
      KEY_MATCH_STATUS="秘密鍵ファイルからの公開鍵生成エラー ($KEY_TYPE)"
    fi
    echo "    鍵の状態: $KEY_MATCH_STATUS"
  else
    echo "  秘密鍵ファイル: $(basename "$KEY_FILE_CANDIDATE_NAME") 見つかりません"
  fi
  echo "--------------------------------------------------"
done < <(find "$TARGET_DIR" -type f -name "*.crt.*[0-9][0-9][0-9][0-9][0-9][0-9]" -print0) # プロセス置換の終了

# このチェックで、ファイルが処理されたかどうかが正しく反映される
if [ "$CERT_FILES_FOUND" -eq 0 ]; then
    echo "ディレクトリ '$TARGET_DIR' 内に *.crt.*[数字6桁] のパターンに一致する証明書ファイルは見つかりませんでした。"
    echo "(例: domain.com.crt.202312)"
fi

これを `ssl-check.sh` として保存し、実行権限を付けます。

使用例

./ssl-check.sh /ディレクトリのフルパス

で実行した結果、

ディレクトリ '/hoge/' 内のSSL証明書情報:
==================================================
処理中の証明書: hoge.example.com.crt.202505
  有効期限: Aug 28 23:17:58 2025 GMT
  ドメイン名: *.hoge.example.com
  発行局 (CN): E5
  発行局 (詳細): C=US, O=Let's Encrypt, CN=E5
  秘密鍵ファイル: hoge.example.com.202505
    鍵の状態: 証明書と一致 (EC)
--------------------------------------------------

と、

  • 有効期限
  • ドメイン名
  • 自己証明でないか(発行局から発行されているか)

が明示的に分かるようになりました。

Rubyスクリプト改修。(SSL証明書の有効期限確認)

SSL証明書の有効期限を確認するスクリプト、改修しました。

スクリプト内容

require 'openssl'
require 'socket'
require 'date'
require 'uri'
require 'timeout'

# ユーザーからURLを対話的に受け取る
def get_user_input
  print "チェックしたいサイトのドメインを入力してください(例: example.com): "
  domain = gets.chomp

  # 入力がhttp://またはhttps://で始まらない場合は、https://を追加
  domain = "https://#{domain}" unless domain.start_with?('http://', 'https://')
  
  domain
end

# 変数で指定したURLに接続して証明書の有効期限を取得するメソッド
def get_certificate_expiry_date(url)
  uri = URI.parse(url)
  hostname = uri.host
  ssl_socket = nil
  tcp_client = nil

  begin
    # タイムアウトを5秒に設定してSSL接続を確立
    Timeout.timeout(5) do
      tcp_client = TCPSocket.new(hostname, 443)
      ssl_context = OpenSSL::SSL::SSLContext.new
      ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_client, ssl_context)
      ssl_socket.hostname = hostname
      ssl_socket.connect

      # 証明書の有効期限を取得
      cert = ssl_socket.peer_cert
      expiration_date = DateTime.parse(cert.not_after.to_s)
      days_remaining = (expiration_date - DateTime.now).to_i

      return expiration_date, days_remaining
    end
  rescue Timeout::Error
    return nil, "サーバーへの接続がタイムアウトしました。"
  rescue => e
    return nil, e.to_s
  ensure
    ssl_socket&.close
    tcp_client&.close
  end
end

# メイン処理
def main
  # コマンドライン引数を確認
  url = if ARGV[0]
          # 引数でドメインが指定されている場合
          domain = ARGV[0]
          domain = "https://#{domain}" unless domain.start_with?('http://', 'https://')
          domain
        else
          # 対話的に入力を受け付ける
          get_user_input
        end

  expiration_date, days_remaining = get_certificate_expiry_date(url)

  if expiration_date
    formatted_date = expiration_date.strftime("%Y/%m/%d")
    puts "サイト #{url} の有効期限は #{formatted_date} です。残り #{days_remaining} 日です。"
  else
    puts "証明書の取得に失敗しました: #{days_remaining}"
  end
end

# メイン処理を呼び出し
main

改修内容

引数化したことです。

ruby ssl_checker.rb google.co.jp
サイト https://google.co.jp の有効期限は 2025/03/31 です。残り 61 日です。

と、スクリプトの後にドメイン指定で残り日数を示します。

ruby ssl_checker.rb
チェックしたいサイトのドメインを入力してください(例: example.com): google.co.jp
サイト https://google.co.jp の有効期限は 2025/03/31 です。残り 61 日です。

引数なしだと対話式に切り替わります。

これで、変数をハードコートする必要がなくなり、他のスクリプトにも組み込みやすくなりました。

シェルスクリプト天気予報ツールの改良:機能追加とエラー処理の強化

以前作成したコマンドラインでの天気予報ツールについて改良を行いました。

修正前のスクリプト

まず、修正前のスクリプトはこちらです。

#!/bin/bash

# 都市名をコマンドライン引数から取得するか、ユーザーに尋ねる
city=$1
if [[ -z "$city" ]]; then
  echo "都市名を入力してください:"
  read city
  if [[ -z "$city" ]]; then
    echo "都市名が入力されませんでした。"
    exit 1
  fi
fi

# ansiweatherコマンドを実行して天気情報を表示
echo "ansiweatherの情報:"
if ! ansiweather -l "$city"; then
  echo "ansiweatherから情報を取得できませんでした。"
fi

# curlコマンドを使用してwttr.inから天気情報を表示
echo "wttr.inの情報:"
if ! curl -s "wttr.in/${city}?lang=ja"; then
  echo "wttr.inから情報を取得できませんでした。"
fi

このスクリプトは、コマンドライン引数で都市名を受け取るか、引数がなければユーザーに都市名の入力を求め、ansiweathercurlを使って天気情報を取得し表示します。

問題点

  • エラー処理が不十分: 都市名が入力されない場合のエラー処理はありますが、ansiweathercurlコマンドが失敗した場合の処理は警告メッセージを表示するだけです。
  • 入力検証がない: 空白のみの入力など、不正な入力に対する検証が行われていません。
  • 複数都市に対応していない: コマンドライン引数で複数の都市を指定することができません。
  • コードの再利用性がない: 処理がまとまっていないため、コードの再利用が難しいです。

修正後のスクリプト

そこで、以下のように修正です。

#!/bin/bash

# 都市名を取得する関数
get_city() {
  if [[ -z "$1" ]]; then
    read -p "都市名を入力してください: " city
    if [[ -z "$city" ]]; then
      echo "エラー: 都市名が入力されていません。" >&2
      return 1
    fi
  else
    city="$1"
  fi

  # 入力値の検証
  if [[ "$city" =~ ^[[:space:]]+$ ]]; then
    echo "エラー: 都市名に空白のみが入力されています。" >&2
    return 1
  fi
  return 0
}

# 天気情報を表示する関数
show_weather() {
  local city="$1"

  echo "--------------------"
  echo "ansiweatherの情報 (${city}):"
  if ! ansiweather -l "$city"; then
    echo "警告: ansiweatherから情報を取得できませんでした。" >&2
  fi

  echo "--------------------"
  echo "wttr.inの情報 (${city}):"
  if ! curl -fs --connect-timeout 5 "wttr.in/${city}?lang=ja"; then
    echo "警告: wttr.inから情報を取得できませんでした。" >&2
  fi
}

# メイン処理
if [[ $# -eq 0 ]]; then
    if ! get_city; then
        exit 1
    fi
    show_weather "$city"
elif [[ $# -gt 0 ]]; then
    for city in "$@"; do
        if ! get_city "$city"; then
            echo "$city の処理をスキップします。" >&2
            continue
        fi
        show_weather "$city"
    done
fi

exit 0

修正の意図と変更点

  • 関数化: get_city()関数とshow_weather()関数に処理を分割し、コードの可読性と再利用性を向上させました。
  • エラー処理の改善:
  • get_city()関数内で都市名が入力されない場合や空白のみが入力された場合にエラーメッセージを出力し、終了ステータスを返しています。エラーメッセージは標準エラー出力(>&2)に出力することで、通常の出力と区別しています。
  • show_weather()関数内でansiweathercurlコマンドが失敗した場合に警告メッセージを標準エラー出力に出力するように変更しました。
  • 入力検証の追加: get_city()関数内で空白のみの入力に対する検証を追加しました。
  • 複数都市への対応: コマンドライン引数で複数の都市を指定できるように変更しました。forループを使って、それぞれの都市に対して天気情報を取得し表示します。
    ./script Narita Londonとすることで、出発地と目的地の天気を同時に示すことができます。
  • curlコマンドのオプション変更: curl -sに加えて-f(エラー時にHTTPステータスコードを返す)と--connect-timeout 5(接続タイムアウトを5秒に設定)を追加し、より堅牢な処理を実現しました。

修正後に加わった挙動。

  • コマンドライン引数で複数の都市を指定して実行できるようになりました。例:./script.sh Osaka Kyoto
  • 都市名が入力されない場合や空白のみが入力された場合、エラーメッセージが表示されるようになりました。
  • ansiweathercurlコマンドが失敗した場合、警告メッセージが標準エラー出力に出力されるようになりました。
  • curlコマンドのタイムアウトが設定されたため、ネットワークの問題などで応答がない場合に処理が止まるのを防ぐことができます。

BBC Newsの見出しを取得するBashスクリプト、更に改良

これを更に改良です。

スクリプト

  • bbc_headlin.sh
#!/bin/bash

# デフォルト値の設定
default_section="world"
default_count=3

# メインセクションのリスト
main_sections=("world" "uk" "business" "politics" "health" "education" "science_and_environment" "technology" "entertainment_and_arts")

# グローバルセクションのリスト
global_sections=("africa" "asia" "europe" "latin_america" "middle_east" "us_and_canada")

# 全セクションのリストを統合
all_sections=("${main_sections[@]}" "${global_sections[@]}")

# 引数の処理
if [[ "$1" =~ ^[0-9]+$ ]]; then
section=$default_section
count=$1
else
section=${1:-$default_section} # 引数1が指定されていない場合はデフォルト値を使用
count=${2:-$default_count}     # 引数2が指定されていない場合はデフォルト値を使用
fi

# 引数の短縮形を対応する正式名に変換
case "$section" in
"usa" | "n-usa") section="us_and_canada" ;;
"me") section="middle_east" ;;
"latam" | "la") section="latin_america" ;;
"eu") section="europe" ;;
"science") section="science_and_environment" ;;
"entertainment") section="entertainment_and_arts" ;;
*) section=$section ;;  # その他はそのまま
esac

# セクションの検証
if [[ ! " ${all_sections[@]} " =~ " ${section} " ]]; then
echo "Error: Invalid section '${section}'. Valid sections are: ${all_sections[*]}"
exit 1
fi

# URLの構築
if [[ " ${main_sections[@]} " =~ " ${section} " ]]; then
url="https://feeds.bbci.co.uk/news/${section}/rss.xml"
else
url="https://feeds.bbci.co.uk/news/world/${section}/rss.xml"
fi

# BBC NewsのRSSフィードから見出しを取得
headlines=$(curl -s "$url" | xmllint --format - | grep -oP '(?<=<title>).*?(?=</title>)' | sed -n '3,'"$((count+2))"'p' | sed 's/<!\[CDATA\[//g' | sed 's/\]\]>//g')

# 見出しの表示
if [ -z "$headlines" ]; then
echo "No headlines found for section '${section}'. Please check the section name or try again later."
else
echo "BBC News - ${section} section (${count} headlines)"
echo "$headlines"
fi

改良点

  • BBC 英国版とワールド版の両方のセクションを参照できるようにしました。
  • us_and_canada, latain_americaなどはusa(n-usa)、latam(la)など、省略形を引数にできます。

使用例

  • ヨーロッパのニュースを4件表示
./bbc_headline.sh eu 4
BBC News - europe section (4 headlines)
Six killed in strike on Russia's Kursk after deadly missile attack on Kyiv
Child, 7, dies in stabbing at Croatian primary school
Italy's deputy PM Salvini cleared in kidnap trial of migrants blocked at sea
Eight migrants drown after boat tries to evade Greek ship
  • サイエンス分野のニュースを表示
./bbc_headline.sh science
BBC News - science_and_environment section (3 headlines)
Ancient landmarks closed off to walkers, campaigners say
Trouble in Arctic town as polar bears and people face warming world
Nasa astronauts Butch and Suni's homecoming delayed again

後はupdate-motdなどに仕込むことで、ターミナルでログインすると同時にニュースの見出しを見ることができます。

Apache設定ファイル反映を効率化するスクリプトをコマンド化。

先日ご紹介したApache環境のWebサービス再起動を効率的に行うスクリプトをコマンドとして登録します。

コマンドとして登録

  • 実行権限付与
sudo chmod +x apache_check_restart.sh
  • シンボリックリンク付与
sudo ln -s /path/to/script/apache_check_restart.sh /usr/local/bin/apache_check_restart

スクリプトが配置されたディレクトリをフルパスで書きます。

  • シンボリックリンク付与確認
 which apache_check
/usr/local/bin/apache_check_restart

実行例

sudo apache_check_restart
==== 有効なサイト設定ファイル ====
設定ファイル: atelier.conf
ServerName atelier.reisalin.com
servername atelier.reisalin.com
設定ファイル: bookstack.conf
ServerName barrel.reisalin.com
servername barrel.reisalin.com
構文チェック中...
Syntax OK
構文チェック完了: 問題ありません。
Apacheを再起動しますか? (y/n): 

Apacheが正常に再起動されました。

となり、yならapacheサービスを再起動後にステータスを表示。nならそのままスクリプトを抜けます。

Apacheのサービス再起動と確認スクリプト。

Ubuntu 24.04/Apache環境で

  • 再起動前に稼働しているサイトを確認
  • 構文チェック
  • 再起動
  • 再起動後のサービス状況

を一括で行うスクリプトです。

スクリプト

  • apache_check_restart.sh
#!/bin/bash


# サイト設定ディレクトリ
SITES_DIR="/etc/apache2/sites-enabled"

# スクリプトを root ユーザーで実行しているかチェック
if [ "$EUID" -ne 0 ]; then
    echo "このスクリプトは root 権限で実行する必要があります。"
    exit 1
fi

# 1. /etc/apache2/sites-enabled 配下のファイルとURL表示
echo "==== 有効なサイト設定ファイル ===="
if [ -z "$(ls -A $SITES_DIR)" ]; then
    echo "サイト設定が存在しません。"
else
    for site in "$SITES_DIR"/*; do
        echo "設定ファイル: $(basename "$site")"
        # URL部分を含むServerNameやServerAliasをgrepして表示 (重複行を削除)
        grep -Ei "ServerName|ServerAlias" "$site" | sed 's/^[ \t]*//' | sort | uniq
    done
fi

echo "=================================="

# 2. Apache構文チェック
echo "構文チェック中..."
apachectl configtest
if [ $? -ne 0 ]; then
    echo "構文エラーが検出されました。Apacheを再起動できません。"
    exit 1
fi
echo "構文チェック完了: 問題ありません。"

# 3. Apache再起動の確認
read -p "Apacheを再起動しますか? (y/n): " CONFIRM
if [[ "$CONFIRM" =~ ^[Yy]$ ]]; then
    echo "Apacheを再起動します..."
    systemctl restart apache2
    if [ $? -ne 0 ]; then
        echo "Apacheの再起動に失敗しました。"
        exit 1
    fi
    echo "Apacheが正常に再起動されました。"

    # 4. Apacheステータス確認
    echo "==== Apacheステータス ===="
    systemctl status apache2 --no-pager
else
    echo "Apacheの再起動はキャンセルされました。"
fi

root権限で作成し、

sudo chmod +x apache_check_restart.sh

で実行権限を付与します。

動作例

sudo bash apache_check_restart.sh
==== 有効なサイト設定ファイル ====
設定ファイル: atelier.conf
ServerName atelier.reisalin.com
servername atelier.reisalin.com
設定ファイル: bookstack.conf
ServerName barrel.reisalin.com
servername barrel.reisalin.com
構文チェック中...
Syntax OK
構文チェック完了: 問題ありません。
Apacheを再起動しますか? (y/n): 

Apacheが正常に再起動されました。
==== Apacheステータス ====
● apache2.service - The Apache HTTP Server
     Loaded: loaded (/usr/lib/systemd/system/apache2.service; enabled; preset: enabled)
     Active: active (running) since Wed 2024-12-18 08:23:29 JST; 12ms ago
       Docs: https://httpd.apache.org/docs/2.4/
    Process: 335542 ExecStart=/usr/sbin/apachectl start (code=exited, status=0/SUCCESS)
      Tasks: 11 (limit: 4690)
     Memory: 535.1M (peak: 2.2G swap: 0B swap peak: 67.9M)
        CPU: 510ms
     CGroup: /system.slice/apache2.service
             ├─335551 "Passenger core"
             ├─335577 "PassengerWatchdog (cleaning up...)"
             └─335580 /usr/sbin/apache2 -k start

と表示されます。上記、一連の流れを一括で行うので便利です。

CSSの要素を表示するシェルスクリプト。

こういうさらっとしたツールを作る際にGTPは役立ちます。

興味でCSSをいじることがあり、どのCSSにどの要素が含まれているかを解析するためのコードを生成してもらいました。

スクリプト内容

  • css-inspector.sh
#!/bin/bash

# CSSファイルを引数として受け取る
if [ "$#" -ne 1 ]; then
  echo "Usage: $0 <css_file>"
  exit 1
fi

CSS_FILE="$1"

# ファイルが存在するか確認
if [ ! -f "$CSS_FILE" ]; then
  echo "Error: File '$CSS_FILE' does not exist."
  exit 1
fi

# CSS要素を抽出して解説する
awk '
BEGIN {
  print "CSS要素を抽出し、それぞれを解説します:\n"
}
{
  if ($0 ~ /\{/ || $0 ~ /\}/) {
    current_selector = $0
    gsub(/\{|\}/, "", current_selector)
    gsub(/^\s+|\s+$/, "", current_selector)
  }

  if ($0 ~ /font-size/) {
    print "font-sizeが見つかりました: テキストのサイズを指定します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /background-color/) {
    print "background-colorが見つかりました: 要素の背景色を定義します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /color:/) {
    print "colorが見つかりました: テキストの色を設定します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /line-height/) {
    print "line-heightが見つかりました: テキスト行間のスペースを制御します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /margin/) {
    print "marginが見つかりました: 要素の周囲の余白を定義します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /padding/) {
    print "paddingが見つかりました: コンテンツとボーダー間の余白を定義します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /border/) {
    print "borderが見つかりました: 要素のボーダーのスタイル、幅、色を指定します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /width/) {
    print "widthが見つかりました: 要素の幅を設定します。セレクタ -> " current_selector ". 値 -> " $0
  }

  if ($0 ~ /height/) {
    print "heightが見つかりました: 要素の高さを設定します。セレクタ -> " current_selector ". 値 -> " $0
  }
}
END {
  print "\nCSS要素の抽出と解説が完了しました。"
}' "$CSS_FILE"

echo "\n完了しました!"

後は

スクリプト実行結果

chmod +x css-inspector.sh

として実行権を付与し、

./css-inspector.sh css_file

で解析します。

CSS要素を抽出し、それぞれを解説します:

font-sizeが見つかりました: テキストのサイズを指定します。セレクタ -> :root. 値 ->   --font-size: 1.1em; /* 1.1emで10%大きく */
font-sizeが見つかりました: テキストのサイズを指定します。セレクタ -> .issue, .description, .journal, .wiki. 値 ->   font-size: var(--font-size); /* 定義したサイズを適用 */
font-sizeが見つかりました: テキストのサイズを指定します。セレクタ -> code, pre. 値 ->   font-size: var(--font-size); /* ターミナル風のフォントでもサイズを変更 */

CSS要素の抽出と解説が完了しました。
\n完了しました!

さっくりとしたものですが、それでも、ターミナル上での視認性が良くなりました。

BBC Newsの見出しを取得するBashスクリプト・改良

概要

以前に書いたBBC Newsの特定のセクションから最新の見出しを取得するBashスクリプトを改良しました。Ubuntu 24.04で動作を確認しています。

必要なライブラリのインストール

このスクリプトを実行するには、xmllintが必要です。

sudo aptitude update
sudo aptitude install libxml2-utils

コード

bbc_headlin.sh

#!/bin/bash

# デフォルト値の設定
default_section="world"
default_count=3

# メインセクションのリスト
main_sections=("world" "uk" "business" "politics" "health" "education" "science_and_environment" "technology" "entertainment_and_arts")

# 引数の処理
if [[ "$1" =~ ^[0-9]+$ ]]; then
section=$default_section
count=$1
else
section=${1:-$default_section} # 引数1が指定されていない場合はデフォルト値を使用
count=${2:-$default_count}     # 引数2が指定されていない場合はデフォルト値を使用
fi

# メインセクションのURLの構築
if [[ ! " ${main_sections[@]} " =~ " ${section} " ]]; then
echo "Error: Invalid section '${section}'. Valid sections are: ${main_sections[*]}"
exit 1
fi

url="https://feeds.bbci.co.uk/news/${section}/rss.xml"

# BBC NewsのRSSフィードから見出しを取得
headlines=$(curl -s "$url" | xmllint --format - | grep -oP '(?<=<title>).*?(?=</title>)' | sed -n '3,'"$((count+2))"'p' | sed 's/<!\[CDATA\[//g' | sed 's/\]\]>//g')

# 見出しの表示
if [ -z "$headlines" ]; then
echo "No headlines found for section '${section}'. Please check the section name or try again later."
else
echo "BBC News - ${section} section (${count} headlines)"
echo "$headlines"
fi

作成後、

chmod +x bbc_headline.sh

としてスクリプトに実行権を与えます。

動作例

このスクリプトの動作例です:

デフォルトのセクション(world)から3つの見出しを取得する場合:

./bbc_headline.sh
  • 出力例:
BBC News - world section (3 headlines)
I hope Assad pays the price, says mother whose son's death inflamed 2011 Syrian revolution
Israel confirms attack on Syrian naval fleet
Another headline example

technologyセクションから5つの見出しを取得する場合:

./bbc_headline.sh technology 5
  • 出力例:
BBC News - technology section (5 headlines)
Tech company announces new product
Breakthrough in AI technology
Another tech headline
More tech news
Latest in technology

見出し数のみを指定する場合(デフォルトのセクションworldを使用):

./bbc_headline.sh 5
  • 出力例:
BBC News - world section (5 headlines)
I hope Assad pays the price, says mother whose son's death inflamed 2011 Syrian revolution
Israel confirms attack on Syrian naval fleet
Another headline example
More world news
Latest in world news

利用可能なセクション

利用可能なセクション
スクリプトで指定できるセクションは、以下の通りです:

  • world: 世界のニュース
  • uk: イギリス国内のニュース
  • business: ビジネス関連のニュース
  • politics: 政治関連のニュース
  • health: 健康関連のニュース
  • education: 教育関連のニュース
  • science_and_environment: 科学・環境関連のニュース
  • technology: テクノロジー関連のニュース
  • entertainment_and_arts: エンターテインメント・アート関連のニュース

メインセクション及び見出し数を引数として利用できるのが改善点です。

Redmineのリマインダースクリプトをもう少し改良。

概要

Redmine標準で備わっているリマインダー。

締め切りが近いチケットを担当者に送付できる機能をcronで登録しています。これを更に改良です。

スクリプト

#!/bin/bash

# 引数でルートディレクトリを指定(デフォルトは/home/www-data/redmine)
REDMINE_ROOT=${1:-"/home/www-data/redmine"}

# 引数で日数を指定(デフォルトは3日)
DAYS=${2:-3}

# Redmineのルートディレクトリに移動
cd $REDMINE_ROOT

# リマインダーを送信
bundle exec rake redmine:send_reminders days=$DAYS RAILS_ENV=production

前は締め切り日数を

./redmine_reminder.sh 5

のように、引数で指定していました。新しいスクリプトでは

./redmine_reminder.sh /home/wwww-data/redmine2 5

と、同一サーバ上にある別Redmineでもルートディレクトリを指定できるようになっています。

後はこれを

20 8 * * * /home/hoge/scripts/redmine_reminder.sh /home/www-data/redmine2 31

など、crontabでの自動化を行いやすくしました。

MySQLのデータベースを暗号化してバックアップするスクリプト。

概要

以前も作成していた、MySQLのデータベースのバックアップを自動的に取得するスクリプトを少々リファインさせました。

変数指定により、複数のDBを任意にバックアップできます。

スクリプトの動き

  • サーバ内にあるDBのバックアップを取得し、暗号化して指定ディレクトリに保存します。
  • 複合化のパスワードはスクリプトが自動生成し、暗号化と同時に別ディレクトリに保存します。
  • cronの自動実行を前提としているため、古い暗号化ファイルと複合化のパスワードは一定期間後に削除を行います。

動作を確認したサーバ

  • Ubuntu 24.04
  • MySQL 8.0.39

前準備

アカウントファイル

任意のディレクトリに、db_account.txtといった、DBにユーザー名とパスワードを記したファイルを作成しておきます。

[client]
user = db_user
password = "password"
  • パーミッションは400にします。(chmod 400 db_account.txt)
  • 取り扱いは慎重に行ってください。

バックアップDBの格納先

  1. 冗長性があり
  2. 機密性が保たれる

場所を指定してください。筆者はクラウドストレージ(wasabi)をマウントしています。

パスワードの格納先

/home/hoge/dbrestore_password のように、複合化のパスワードを格納するディレクトリを作っておきます。
こちらも運用に合わせ、適切に保管ができる場所を指定します。

それぞれ、スクリプト実行アカウントがアクセス/実行できるものにします。

スクリプト

スクリプト内容

  • スクリプトファイル名(例)
    • mysql_db_backup.sh

変数などは間違いの無いように指定ください。

#!/bin/bash

## 変数ここから ##
# $HOMEの変数を指定します。
HOME_DIR="/home/hoge"
# SQLをバックアップするディレクトリ(保管先)を指定します。運用に合わせて指定ください。
backup_dir="/path/to/backup/directory"
# 保持するバックアップの世代を日数で指定します。
keep_days=7
# ファイルに付与する日付/作業ディレクトリ名/バックアップファイル名を指定します。
current_date=$(date +%Y%m%d)
backup_name="backup_mysql_${current_date}"
zip_file="backup_mysql.${current_date}.zip"
# アカウントファイルを指定します。運用に合わせて指定ください。
credentials_file="${HOME_DIR}/script/config/db_account/db_account.txt"
# パスワードを記録するファイル名を指定します。運用に併せてして指定ください。
password_dir="${HOME_DIR}/dbrestore_password"
password_file="${password_dir}/db-restore.${current_date}.txt"
# redmineのデータベース名を指定します。
database_name=database
# バックアップ時に指定するオプションを指定します。
options="--defaults-extra-file=$credentials_file --no-tablespaces --single-transaction"
# バックアップファイルのパターンを指定します。
backup_file_pattern="backup_mysql.*.zip"
# パスワードファイルのパターンを指定します。
password_file_pattern="*restore*.txt"
## 変数ここまで ##

## 処理ここから ##

# 1.アカウントファイルのパーミッションが400かどうかチェックします。
# 400以外は処理そのものを終了します。
permissions=$(stat -c "%a" "$credentials_file")
if [ "$permissions" != "400" ]; then
echo "アカウントファイルのパーミッションは400である必要があります。"
exit 1
fi

# 2.一時的なバックアップディレクトリを作成します。
mkdir "${backup_dir}/${backup_name}"

# 3. mysqldumpを実行してデータベースのバックアップを取ります。
mysqldump $options -h localhost $database_name > "${backup_dir}/${backup_name}/${backup_name}.sql"

# 4. パスワードによる暗号化を実施します。
password=$(openssl rand -base64 12)
cd "${backup_dir}/${backup_name}"
zip -r "${backup_dir}/${zip_file}" -P "$password" .
cd -

# 5. 一時的なバックアップディレクトリを削除します。
rm -rf "${backup_dir}/${backup_name}"

# 6. 解凍パスワードを指定ディレクトリに保存します。
echo $password > $password_file

# 7.パスワードの読み取り権限を600に変更します。
chmod 600 $password_file

# 8. 保持期間より古いバックアップファイルを削除します。
find "$backup_dir" -name "$backup_file_pattern" ! -type f -newermt "${keep_days} days ago" -delete
find "$password_dir" -name "$password_file_pattern" ! -type f -newermt "${keep_days} days ago" -delete

## 処理ここまで ##

作成後、

chmod +x mysql_db_backup.sh

で実行権を付与します。

スクリプトの動かし方

スクリプトの動き

./mysql_db_backup.sh

として実行すると、

  1. 変数で指定したアカウントファイルを読み込み、mysqldumpでバックアップを取ります。
  2. バックアップ作成後、パスワード付きzipファイルに圧縮します。
  3. 圧縮されたバックアップと複合化のためのテキストファイルを変数で指定したディレクトリに格納します。
  4. 変数で指定した期日が過ぎたバックアップファイルと複合化のためのテキストファイルは自動的に削除されます。

バックアップDBの解凍

unzip backup_mysql.yyyymmdd.zip

とすると、パスワードを尋ねられます。

db-restore.yyyymmdd.txtに表示された文字列を入力します。

または、

unzip -P $(cat /path/to/password_file.txt) /path/to/zip_file.zip -d /path/to/output_directory

でファイルを直接引数にして解凍することもできます。

cronの指定

動作を確認したら、

crontab -e -u hoge

でcron編集画面を出し、

0 2 * * * /home/hoge/script/directory/mysql_db_backup.sh

などとして指定すれば、日次のDBバックアップを取得可能です。

Page 1 of 6

Powered by WordPress & Theme by Anders Norén