#!/bin/sh - # file: rechnung.sh # date: 02.05.2021 # user: bernd@nr18.space # desc: Liest verschiedene Kunden und Dienstleistungen aus der Datei # rechnung.ini und erstellt daraus Rechnungen mit Hilfe eines # Latex-Templates. set -u set -e # ein paar Sicherheitsvorkehrungen # -------------------------------- OLDIFS=$IFS IFS=' ' unset -f command unset -f unalias \unalias -a OLDPATH=$PATH PATH=/bin:/usr/bin export PATH # Benötigte Programme # ------------------- TR_BIN=$(which tr 2>/dev/null || echo "tr") BC_BIN=$(which bc 2>/dev/null || echo "bc") SED_BIN=$(which sed 2>/dev/null || echo "sed") CUT_BIN=$(which cut 2>/dev/null || echo "cut") DATE_BIN=$(which date 2>/dev/null || echo "date") TPUT_BIN=$(which tput 2>/dev/null || echo "tput") PDFLATEX_BIN=$(which pdflatex 2>/dev/null || echo "pdflatex") KPSEWHICH_BIN=$(which kpsewhich 2>/dev/null || echo "kpsewhich") GIT_BIN=$(which git 2>/dev/null || echo "git") LATEX_BIN=$(which latex 2>/dev/null || echo "latex") RUBBER_BIN=$(which rubber 2>/dev/null || echo "rubber") # Nicht auf rubber prüfen; Programm ist optional # Nicht auf git prüfen; wird nur benötigt, wenn rechnung.sty nicht existiert. # Nicht auf latex prüfen; wird nur benötigt, wenn rechnung.sty nicht texistiert. EXECUTABLES_TO_CHECK="$BC_BIN $TR_BIN $SED_BIN $CUT_BIN $TPUT_BIN $DATE_BIN $PDFLATEX_BIN $KPSEWHICH_BIN" GENPDF="" if [ -x "$RUBBER_BIN" ]; then GENPDF="$RUBBER_BIN -d " else GENPDF="$PDFLATEX_BIN -halt-on-error " fi # Variablen definieren # -------------------- PROGRAMM_NAME=$(basename "$0") PROGRAMM_VERSION="0.0.1" VERBOSITY=1 STY_HOME="$HOME/texmf/tex/latex" STY_RECHNUNG="rechnung.sty" RECHNUNGSFILE="rechnung.ini" TEMPLATEFILE="template.ltx" LCOFILE="absender.lco" LCO=$(basename $LCOFILE .lco) INSTALL_LCO_FILE=0 SEDFILE="sed.txt" LINE_BUFFER="" KEY_BUFFER="" VALUE_BUFFER="" KUNDE="" ADRESSE="" ANREDE="" ANZAHL="" POSITION="" EINLEITUNG="" SHORT="" BETRAG="" LFDNR="" # Farben definieren # ----------------- NUMBER_OF_COLORS=$($TPUT_BIN colors) COLORED_OUTPUT=0 RED="$(tput bold)$(tput setaf 1)" GREEN="$(tput bold)$(tput setaf 2)" YELLOW="$(tput bold)$(tput setaf 3)" BLUE="$(tput bold)$(tput setaf 4)" NORMAL="$(tput bold)$(tput sgr0)" # Ausgabefunktionen für Terminal # ------------------------------ print_start() { msg=$* if [ "$VERBOSITY" -eq 0 ]; then return else if [ "$COLORED_OUTPUT" -eq 1 ]; then printf "%s[+]%s %s" "$GREEN" "$NORMAL" "$msg" else printf " Failed" fi fi } print_failed() { msg=$* if [ "$VERBOSITY" -eq 0 ]; then return else if [ "$COLORED_OUTPUT" -eq 1 ]; then printf "%s Failed%s\n" "$RED" "$NORMAL" else printf " Failed\n" fi if [ -n "$msg" ]; then print_error "$msg" fi fi } print_ok() { if [ "$VERBOSITY" -eq 0 ]; then return else if [ "$COLORED_OUTPUT" -eq 1 ]; then printf "%s OK%s\n" "$GREEN" "$NORMAL" else printf " OK\n" fi fi } print_deko() { msg=$* if [ "$VERBOSITY" -eq 0 ]; then return else if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${BLUE}${msg}${NORMAL}" else echo "$msg" fi fi } print_info() { msg=$* if [ "$VERBOSITY" -eq 0 ]; then return else if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${GREEN}[+]$NORMAL $msg" else echo "[-] $msg" fi fi } print_warn() { msg=$* if [ "$VERBOSITY" -eq 0 ]; then return else if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${YELLOW}[-]$NORMAL $msg" else echo "[-] $msg" fi fi } print_error() { msg=$* if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${RED}[-]$NORMAL $msg" else echo "[-] $msg" fi } failed_and_exit() { print_failed exit "$*" } # Diverse Checks # -------------- check_readable() { # testet, ob die übergebene datei kein symlink ist, existiert und lesbar # ist. schlägt einer der tests fehl, wird das programm beendet. print_start "Datei $1 ist lesbar ..." if test -h "$1"; then target=$(get_link_target "$1") print_failed "$1 ist ein symbolischer Link auf $target" exit 1 fi if test -f "$1" && test -r "$1"; then print_ok else failed_and_exit 1 fi } check_sty() ( # sucht in einer subshell nach dem paket rechnung.sty. gibt true oder # false zurück. print_start "Suche nach $STY_RECHNUNG ..." if $KPSEWHICH_BIN "$STY_RECHNUNG" > /dev/null 2>&1 ; then print_ok return 0 else print_failed return 1 fi ) check_binaries() ( # die funktion prüft in einer subshell, ob alle benötigten binaries # installiert sind. sollte das binary ein symlink auf eine ander datei # sein, wird geprüft, ob sich diese im PATH befindet und nicht ebenfalls # ein symlink ist. schlägt eine der prüfungen fehl, wird das programm # beendet. program= binaries=$* for program in $binaries; do print_start "Suche nach $program ..." if test ! -x "$program"; then print_failed "Das Programm $program wird benötigt, aber nicht gefunden." quit 3 else if [ -h "$program" ]; then program=$(get_link_target "$program") fi if ! which "$program" > /dev/null 2>&1 ; then print_failed "$program ist nicht im Pfad" fi print_ok fi done ) get_link_target() ( # die funktion bekommt einen symbolischen link und folgt ihm bis zum # ende. mit dem echo-befehl wird das ziel an die aufrufende funktion # zurück gegeben. program=$1 while [ -h "$program" ]; do program=$(readlink "$program") done echo "$program" ) is_option() ( # die funktion überprüft in einer subshell, ob die übergebene zeile ein # = als trennzeichen enthält. gibt ja oder falsch zurück. line="$*" if [ "${line#*=}" != "$line" ]; then return 0 else return 1 fi ) check_rechnunsdaten() ( # die funktion überprüft in einer subshell, ob alle variablen belegt # sind. ist eine variable leer, wird der name der variablen ausgegeben # und ein fehler (1) zurück gegeben. print_start "Prüfe Rechnungsdaten ..." if test -z "$KUNDE"; then print_failed "Das Feld Kunde ist leer" return 1 elif test -z "$ADRESSE"; then print_failed "Das Feld Adresse ist leer" return 1 elif test -z "$ANREDE"; then print_failed "Das Feld Anrede ist leer" return 1 elif test -z "$ANZAHL"; then print_failed "Das Feld Anzahl ist leer" return 1 elif test -z "$POSITION"; then print_failed "Das Feld Position ist leer" return 1 elif test -z "$EINLEITUNG"; then print_failed "Das Feld Einleitung ist leer" return 1 elif test -z "$SHORT"; then print_failed "Das Feld Short ist leer" return 1 elif test -z "$BETRAG"; then print_failed "Das Feld Betrag ist leer" return 1 elif test -z "$LFDNR"; then print_failed "Das Feld LfdNr ist leer" return 1 else print_ok return 0 fi ) # Whitespaces entfernen # in Buffer speichern # --------------------- trim_line() { set -f set -- "$*" LINE_BUFFER=$(printf '%s' "$*") set +f } trim_key() { set -f set -- "$*" KEY_BUFFER=$(printf '%s' "$*") set +f } trim_value() { set -f set -- "$*" VALUE_BUFFER=$(printf '%s' "$*") set +f } # LaTeX-Paket rechnung.sty installieren # ------------------------------------- install_sty() ( old_pwd=$(pwd) check_binaries "$GIT_BIN $LATEX_BIN" print_start "Hole Repo von $STY_URL ..." if $GIT_BIN clone "$STY_URL" >/dev/null 2>&1; then print_ok cd rechnung mkdir -p "$STY_HOME/rechnung/" print_start "Kompiliere $STY_RECHNUNG ..." if $LATEX_BIN rechnung.ins >/dev/null 2>&1; then print_ok print_start "Kopiere $STY_RECHNUNG nach $STY_HOME/rechnung ..." if cp rechnung.sty "$STY_HOME/rechnung/"; then print_ok else failed_and_exit 3 fi cd "$old_pwd" rm -rf rechnung/ print_info "Paket $STY_RECHNUNG erfolgreich installiert" else print_error "Installation fehlgeschlagen" exit 4 fi else print_error "Klonen des Repos fehlgeschlagen" exit 5 fi ) # wenn ein neuer Kunde beginnt, alle Variablen neu initialisieren # --------------------------------------------------------------- reset_values() { print_info "Beginne neue Rechnung" LINE_BUFFER="" KEY_BUFFER="" VALUE_BUFFER="" KUNDE="" ADRESSE="" ANREDE="" ANZAHL="" POSITION="" EINLEITUNG="" SHORT="" BETRAG="" LFDNR="" } set_value() { key=$(echo "$*" | $CUT_BIN -d "=" -f1) value=$(echo "$*" | $CUT_BIN -d "=" -f2) trim_key "$key" trim_value "$value" case $KEY_BUFFER in kunde) KUNDE="$VALUE_BUFFER";; adresse) ADRESSE="$VALUE_BUFFER";; anrede) ANREDE="$VALUE_BUFFER";; anzahl) ANZAHL="$VALUE_BUFFER";; position) POSITION="$VALUE_BUFFER";; einleitung) EINLEITUNG="$VALUE_BUFFER";; betrag) BETRAG="$VALUE_BUFFER";; lfdnr) LFDNR="$VALUE_BUFFER";; short) SHORT="$VALUE_BUFFER";; *) print_warn "Unbekanntes Feld: $KEY_BUFFER";; esac } make_invoice() ( # die funktion erstellt in einer subshell die rechnung. schlägt die # prüfung der rechnungsdaten fehl, kehrt sie zur aufrufenden shell # zurück. ansonst wird eine kopie des latex-templates erstellt, die # darin befindlichen platzhalter durch die entsprechenden variablen zu # ersetzen und das ganze zu kompilieren. if ! check_rechnunsdaten ; then return 1 fi dead_days="14" if [ "$(date +%u)" -gt 5 ]; then dead_days="16" fi # Bei der Ersetzung unten wird jedes ';' durch genau ein '\' ersetzt. Daher ist die Anzahl der ';' wichtig. month_ascii=$(date '+%B') inv_date=$(date '+%Y%m%d') inv_dead=$(date -d "+${dead_days}days" '+%d.;,%m.;,%Y') rechnungsnummer="$inv_date--$LFDNR" subject="Rechnung zu $SHORT" filename="$(date '+%Y%m%d')_Rechnung_${month_ascii}_$(echo "$KUNDE" | $TR_BIN " " "_").ltx" buffer="$KUNDE;; $ADRESSE" #anschrift=$(echo "$buffer" | sed 's/;/\\\\/g') print_info "Erstelle Rechnung: $filename" { printf "s!#LCOFILE#!%s!\n" "$LCO" printf "s!#SUBJECT#!%s!\n" "$subject" printf "s!#ANREDE#!%s!\n" "$ANREDE" printf "s!#ANSCHRIFT#!%s!\n" "$(echo "$buffer" | sed 's/;/\\\\/g')" printf "s!#RECHNUNGSNUMMER#!%s!\n" "$rechnungsnummer" printf "s!#ANZAHL#!%s!\n" "$ANZAHL" printf "s!#POSITION#!%s!\n" "$POSITION" printf "s!#EINLEITUNG#!%s!\n" "$EINLEITUNG" printf "s!#BETRAG#!%s!\n" "$BETRAG" printf "s!#DEADLINE#!%s!\n" "$(echo "$inv_dead" | sed 's/;/\\\\/g')" } >$SEDFILE cp "$TEMPLATEFILE" "$filename" $SED_BIN -i -f "$SEDFILE" "$filename" if ! $GENPDF "$filename" 1>&2 >/dev/null; then print_error "Kompilierung von $filename fehlgeschlagen" logfile=$(echo "$filename" | sed -e s/ltx/log/) print_error "$(grep "Error" "$logfile")" else print_info "Rechnung $filename erfolgreich erstellt" fi if [ -x "$RUBBER_BIN" ]; then "$RUBBER_BIN" --clean "$filename" fi ) proced_ini_line() { line=$* first=$(echo "$line" | cut -c1-1) # leerzeilen ignorieren if [ "$line" = "" ]; then return # kommentarzeilen ignorieren elif [ "$first" = "#" ]; then return # start einer neuen rechnung elif [ "$line" = "[start]" ]; then reset_values # ende einer rechnung elif [ "$line" = "[ende]" ]; then make_invoice # alle anderen zeilen else if is_option "$line"; then set_value "$line" else print_warn "Invalid syntax: $line" fi fi } display_help() ( echo "Usage: $PROGRAMM_NAME [-f rechnungsfile] [-h] [-l lco-file]" ) display_version() ( echo "$PROGRAMM_NAME version $PROGRAMM_VERSION" ) quit() { printf "\n" printf "%s" "$BLUE" printf "[-] Exit%s\n" "$NORMAL" exit 1 } # --- Programmstart --- # # --------------------- # # CLI Optionen auswerten while getopts f:hi:l:qv opt do case $opt in f) RECHNUNGSFILE=$OPTARG;; h) display_help; exit 0;; i) INSTALL_LCO_FILE=1;; l) LCOFILE=$OPTARG;; q) VERBOSITY=0;; v) display_version; exit 0;; *) display_help; exit 1;; esac done # Farbe aktivieren wenn vorhanden if [ -n "$NUMBER_OF_COLORS" ] && [ "$NUMBER_OF_COLORS" -ge 8 ]; then COLORED_OUTPUT=1 fi print_deko " ========================" print_deko " = $PROGRAMM_NAME v$PROGRAMM_VERSION =" print_deko " ========================" # Verschiedene Basic Checks if [ "$(id -u)" -eq 0 ]; then print_error "$0: Das Skript darf nicht als Root ausgeführt werden." exit 1 fi check_binaries "$EXECUTABLES_TO_CHECK" check_readable "$RECHNUNGSFILE" check_readable "$TEMPLATEFILE" check_readable "$LCOFILE" if ! check_sty; then print_warn "Verwenden sie -i um $STY_RECHNUNG zu installieren" if [ $INSTALL_LCO_FILE -eq 0 ]; then exit 7 else install_sty fi fi # Kundendatei einlesen und rechnungen erstellen while IFS= read -r rawline do trim_line "$rawline" proced_ini_line "$LINE_BUFFER" done < "$RECHNUNGSFILE"