#!/bin/bash
# Skrypt verifycron sprawdzający poprawność pliku programu cron.
# Wymagany format to min godz dz_mies mies dz_tyg polecenie, gdzie
# wartość min jest z zakresu 0-59, godz 0-23, dz_mies 1-31, mies 1-12
# (lub jest nazwą miesiąca), dz_tyg 0-6 (lub jest nazwą dnia tygodnia).
# Wartości można podawać w postaci zakresów (1-3), list z przecinkami
# (1, 2, 3) lub zastępować gwiazdką. Pamiętaj, że notacja Vixie
# (np. 2-6/2) nie jest obsługiwana przez ten skrypt.

validNum()
{
  # Funkcja zwraca wartość 0, jeżeli argument jest liczbą całkowitą,
  # lub 1 w przeciwnym wypadku. Drugim argumentem jest
  # maksymalna dopuszczalna wartość pierwszego argumentu. 
  num=$1   max=$2

  # Dla uproszczenia gwiazdka jest oznaczona znakiem X,
  # zatem wartość X jest traktowana jako poprawna.

  if [ "$num" = "X" ] ; then
    return 0
  elif [ ! -z $(echo $num | sed 's/[[:digit:]]//g') ] ; then
    # Coś zostało po usunięciu wszystkich cyfr? To niedobrze.
    return 1
  elif [ $num -gt $max ] ; then
    # Podana wartość jest większa niż maksymalna.
    return 1
  else
    return 0
  fi
}

validDay()
{
  # Funkcja zwracająca wartość 0, jeżeli argument jest poprawną
  # nazwą dnia tygodnia. W przeciwnym wypadku funkcja zwraca
  # wartość 1.

  case $(echo $1 | tr '[:upper:]' '[:lower:]') in
    sun*|mon*|tue*|wed*|thu*|fri*|sat*) return 0 ;;
    X) return 0 ;;         # Przypadek szczególny - X zamiast gwiazdki.
    *) return 1
  esac
}

validMon()
{
  # Funkcja zwracająca wartość 0, jeżeli argument jest poprawną
  # nazwą miesiąca. W przeciwnym wypadku funkcja zwraca
  # wartość 1.

   case $(echo $1 | tr '[:upper:]' '[:lower:]') in 
     jan*|feb*|mar*|apr*|may|jun*|jul*|aug*) return 0           ;;
     sep*|oct*|nov*|dec*)                    return 0           ;;
     X) return 0 ;; # Przypadek szczególny - X zamiast gwiazdki.
     *) return 1        ;;
   esac
}

fixvars()
{
  # Zamiana wszystkich znaków '*' na 'X', aby nie myliły się
  # z symbolem stosowanym w poleceniach powłoki. Oryginalny
  # wiersz jest umieszczany w zmiennej sourceline na potrzeby
  # wyświetlenia w komunikacie o błędzie.

  sourceline="$min $hour $dom $mon $dow $command"
   min=$(echo "$min" | tr '*' 'X')      # minuta
  hour=$(echo "$hour" | tr '*' 'X')     # godzina
   dom=$(echo "$dom" | tr '*' 'X')      # dzień miesiąca
   mon=$(echo "$mon" | tr '*' 'X')      # miesiąc
   dow=$(echo "$dow" | tr '*' 'X')      # dzień tygodnia
}

if [ $# -ne 1 ] || [ ! -r $1 ] ; then
  # Plik programu cron nie istnieje lub nie można go odczytać. Błąd.
  echo "Użycie: $0 nazwa_pliku_cron" >&2; exit 1
fi

lines=0  entries=0  totalerrors=0

# Przeglądanie pliku programu cron i sprawdzanie go wiersz po wierszu.

while read min hour dom mon dow command
do
  lines="$(( $lines + 1 ))" 
  errors=0
  
  if [ -z "$min" -o "${min%${min#?}}" = "#" ] ; then
    # Jeżeli wiersz jest pusty lub pierwszym znakiem jest "#", pomijamy go.
    continue    # Wiersza nie trzeba sprawdzać.
  fi

  ((entries++))

  fixvars

  # W tym miejscu bieżący wiersz jest dzielony na części, a poszczególne wartości
  # zapisywane w zmiennych, przy czym gwiazdka jest zamieniana na "X".
  # Zaczyna się sprawdzanie poprawności wartości.

  # Sprawdzenie oznaczenia minut.

  for minslice in $(echo "$min" | sed 's/[,-]/ /g') ; do
    if ! validNum $minslice 60 ; then
      echo "Wiersz ${lines}: błędne oznaczenie minut \"$minslice\"."
      errors=1
    fi
  done

  # Sprawdzenie oznaczenia godziny.
  
  for hrslice in $(echo "$hour" | sed 's/[,-]/ /g') ; do
    if ! validNum $hrslice 24 ; then
      echo "Wiersz ${lines}: błędne oznaczenie godziny \"$hrslice\"." 
      errors=1
    fi
  done

  # Sprawdzenie dnia miesiąca.

  for domslice in $(echo $dom | sed 's/[,-]/ /g') ; do
    if ! validNum $domslice 31 ; then
      echo "Wiersz ${lines}: błędne oznaczenie dnia miesiąca \"$domslice\"."
      errors=1
    fi
  done

  # W przypadku oznaczenia miesiąca trzeba sprawdzić wartość liczbową,
  # jak również nazwę. Pamiętaj, że polecenie warunkowe "if ! warunek"
  # sprawdza, czy warunek ma wartość FAŁSZ, a nie prawda.

  for monslice in $(echo "$mon" | sed 's/[,-]/ /g') ; do
    if ! validNum $monslice 12 ; then
      if ! validMon "$monslice" ; then
        echo "Wiersz ${lines}: błędne oznaczenie miesiąca \"$monslice\"."
        errors=1
      fi
    fi
  done

  # Sprawdzenie dnia tygodnia: jak poprzednio, dopuszczalna jest wartość lub nazwa.

  for dowslice in $(echo "$dow" | sed 's/[,-]/ /g') ; do
    if ! validNum $dowslice 7 ; then
      if ! validDay $dowslice ; then
        echo "Wiersz ${lines}: błędne oznaczenie dnia tygodnia \"$dowslice\"."
        errors=1
      fi
    fi
  done

  if [ $errors -gt 0 ] ; then
    echo ">>>> ${lines}: $sourceline"
    echo ""
    totalerrors="$(( $totalerrors + 1 ))"
  fi
done < $1 # Odczytanie pliku programu cron podanego w argumencie skryptu.

# Zwróć uwagę, że dopiero tutaj, na samym końcu pętli while,
# umieszczone jest przekierowanie zawartości pliku podanego
# w argumencie, dzięki czemu można przeanalizować jego
# zawartość!

echo "Koniec. Znalezionych błędów: $totalerrors w $entries wpisach pliku programu cron."

exit 0
