#!/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=' ' UMASK=077 umask=$UMASK OLDPATH=$PATH PATH=/bin:/usr/bin export PATH unset -f command unset -f unalias \unalias -a # Variablen # --------- PROGRAMM_NAME=$(basename $0) PROGRAMM_VERSION="0.0.2" VERBOSITY=0 DEBUG=0 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)" RECHNUNG_STY_URL="https://github.com/tomka/rechnung" RECHNUNG_STY_FILE="rechnung.sty" RECHNUNG_STY_HOME="$HOME/texmf/tex/latex/rechnung" BASEDIR="$HOME/.rechnung" KUNDENFILE="rechnung.ini" TEMPLATEFILE="template.ltx" LCOFILE="absender.lco" LCO= INSTALL_LCO_FILE=0 ANZAHL_RECHNUNGEN=0 READABLES_TO_CHECK= STARTDIR="$(realpath -s `pwd`)" TEMPDIR= OUTPUTDIR="$STARTDIR" LINE_BUFFER= KEY_BUFFER= VALUE_BUFFER= KUNDE= ADRESSE= ANREDE= BEGLEITTEXT= ANZAHL= POSITION= SHORT= BETRAG= LFDNR= # 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 --into $OUTPUTDIR " else GENPDF="$PDFLATEX_BIN -halt-on-error -output-directory $OUTPUTDIR " fi # Ausgabefunktionen für Terminal # ------------------------------ print_start() { if [ "$VERBOSITY" -eq 1 ]; then if [ "$COLORED_OUTPUT" -eq 1 ]; then printf "%s[+]%s %s" "$GREEN" "$NORMAL" "$@" else printf " Failed" fi fi } print_middle() { if [ "$VERBOSITY" -eq 1 ]; then printf "%s" "$@" fi } print_failed() { if [ "$VERBOSITY" -eq 1 ]; then if [ "$COLORED_OUTPUT" -eq 1 ]; then printf "%s Failed%s\n" "$RED" "$NORMAL" else printf " Failed\n" fi fi if [ ! -z "$@" ]; then print_error "$@" fi } print_ok() { if [ "$VERBOSITY" -eq 1 ]; then if [ "$COLORED_OUTPUT" -eq 1 ]; then printf "%s OK%s\n" "$GREEN" "$NORMAL" else printf " OK\n" fi fi } print_info() { if [ "$VERBOSITY" -eq 1 ]; then if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${GREEN}[+]$NORMAL $@" else echo "[-] $@" fi fi } print_warn() { if [ "$VERBOSITY" -eq 1 ]; then if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${YELLOW}[-]$NORMAL $@" 1>&2 else echo "[-] $@" 1>&2 fi fi } print_error() { if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${RED}[-]$NORMAL Error: $@" 1>&2 else echo "[-] Error: $@" 1>&2 fi } print_head() { if [ "$VERBOSITY" -eq 1 ]; then if [ "$COLORED_OUTPUT" -eq 1 ]; then echo "${BLUE} ========================${NORMAL}" echo "${BLUE} = $PROGRAMM_NAME v$PROGRAMM_VERSION =${NORMAL}" echo "${BLUE} ========================${NORMAL}" else echo " ========================" echo " = $PROGRAMM_NAME v$PROGRAMM_VERSION =" echo " ========================" fi fi } print_trenner() { echo "------------------------------------" } print_usage() { cat <&1 >/dev/null; then print_failed "$program ist nicht im Pfad" fi fi print_ok fi } check_file() { # testet, ob die übergebene datei kein symlink ist, existiert und lesbar # ist. schlägt einer der tests fehl, wird das programm beendet. file="$*" print_start "Suche nach Datei $file ..." if [ -h "$file" ]; then target=$(realpath "$file") print_failed "'$file' ist ein symbolischer Link auf '$target'" exit 4 fi if test -f "$file" && test -r "$file"; then print_ok else print_failed "Datei '$file' nicht lesbar oder nicht gefunden" exit 5 fi } check_dir() { # testet, ob das übergebene verzeichnis keine link ist, existiert und # hinein gewechselt werden darf. schlägt ein test fehl, wird das # programm beendet. dir="$*" print_start "Suche nach Verzeichnis $dir ..." if [ -h "$dir" ]; then target=$(realpath "$dir") print_failed "'$dir' ist ein symbolischer Link auf '$target'" exit 6 fi if test -d "$dir" && test -x "$dir"; then print_ok else print_failed "Verzeichnis '$dir' nicht gefunden oder keine Berechtigung" exit 7 fi } check_sty() { # sucht nach dem paket rechnung.sty. gibt true oder false zurück. print_start "Suche nach $RECHNUNG_STY_FILE ..." kpsewhich "$RECHNUNG_STY_FILE" 2>&1 >/dev/null if [ $? -eq 0 ]; then print_ok return 0 else print_failed return 1 fi } 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 prüft, ob alle variablen belegt sind. ist eine variable # leer, wird der name der variablen ausgegeben und ein fehler (1) zurück # gegeben. response=1 print_start "Prüfe Rechnungsdaten ..." if test -z "$KUNDE"; then print_failed "Das Feld Kunde ist leer" elif test -z "$ADRESSE"; then print_failed "Das Feld Adresse ist leer" elif test -z "$ANREDE"; then print_failed "Das Feld Anrede ist leer" elif test -z "$ANZAHL"; then print_failed "Das Feld Anzahl ist leer" elif test -z "$POSITION"; then print_failed "Das Feld Position ist leer" elif test -z "$SHORT"; then print_failed "Das Feld Short ist leer" elif test -z "$BETRAG"; then print_failed "Das Feld Betrag ist leer" elif test -z "$LFDNR"; then print_failed "Das Feld LfdNr ist leer" else print_ok response=0 fi return $response } # Funktionen zum setzen von CLI-Variablen # --------------------------------------- set_cli_opt() { # die funktion setzt für einen übergebenen pfad den absoluten pfad ohne # sich darim zu kümmern, ob er existiert. FIS= sorgt dafür, daß der # übergebene pfad nicht getrennt wird. sollte nur als subshell # aufgerufen werden. IFS= echo "$(realpath -m -s "$@")" } set_abs_path() { filename="$*" first=$(echo "$filename" | cut -c1-1) if [ ! "$first" = "/" ]; then filename=$(realpath -m "$BASEDIR/$filename") fi echo "$filename" } # LaTeX-Paket rechnung.sty installieren # ------------------------------------- install_sty() ( # Clont das Repo von rechnung.sty, baut das Latex-Paket und kopiert es # nach $HOME/texmf/tex/latex/rechnung. Schlägt einer der Schritte fehl, # wird clean_and_exit aufgerufen. ## notwendige programm prüfen for program in "$GIT_BIN" "$LATEX_BIN"; do check_binary $program done ## Repo clonen print_start "Hole Repo von $RECHNUNG_STY_URL ..." if $GIT_BIN clone $RECHNUNG_STY_URL >/dev/null 2>&1; then print_ok cd rechnung ## rechnung.sty kompilieren print_start "Kompiliere $RECHNUNG_STY_FILE ..." if $LATEX_BIN rechnung.ins >/dev/null 2>&1; then print_ok ## mach TEXHOME/rechnung/ kopieren print_start "Kopiere $RECHNUNG_STY_FILE nach $RECHNUNG_STY_HOME ..." if [ ! -d "$RECHNUNG_STY_HOME" ]; then mkdir -p "$RECHNUNG_STY_HOME" fi if cp rechnung.sty "$RECHNUNG_STY_HOME"; then print_ok print_info "Paket $RECHNUNG_STY_FILE erfolgreich installiert" else print_failed "Installation fehlgeschlagen" clean_and_exit 12 fi else print_failed "Kompilieren fehlgeschlagen" clean_and_exit 11 fi else print_failed "Klonen des Repos fehlgeschlagen" clean_and_exit 10 fi cd "$TEMPDIR" ) # 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 } reset_values() { print_info "Beginne neue Rechnung" LINE_BUFFER="" KEY_BUFFER="" VALUE_BUFFER="" KUNDE="" ADRESSE="" ANREDE="" ANZAHL="" POSITION="" SHORT="" BETRAG="" LFDNR="" } set_value() { key=$(echo "$*" | $CUT_BIN -d "=" -f1) value=$(echo "$*" | $CUT_BIN -d "=" -f2) trim_key "$key" trim_value "$value" print_info "Setze $key auf $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";; 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 print_error "Check der Rechnungsdaten fehlgeschlagen" print_error "Erstelle keine Rechnung" print_trenner return fi dead_days="14" if [ "$(date +%u)" -gt 5 ]; then dead_days="16" fi 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" sedfile="$(basename -s .ltx $filename).sed" 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" "$anschrift" printf "s!#RECHNUNGSNUMMER#!%s!\n" "$rechnungsnummer" printf "s!#ANZAHL#!%s!\n" "$ANZAHL" printf "s!#POSITION#!%s!\n" "$POSITION" printf "s!#BETRAG#!%s!\n" "$BETRAG" printf "s!#DEADLINE#!%s!\n" "$inv_dead" } >$sedfile cp "$TEMPLATEFILE" "$filename" $SED_BIN -i -f "$sedfile" "$filename" if [ "$DEBUG" -eq 1 ]; then cp "$filename" "$sedfile" "$OUTPUTDIR" fi if ! $GENPDF "$filename" 1>&2 >/dev/null; then print_error "Kompilierung von $filename fehlgeschlagen" logfile=$(echo "$OUTPUTDIR/$filename" | sed -e s/ltx/log/) print_error "$(grep "Error" $logfile)" else print_info "Rechnung $filename erfolgreich erstellt" ANZAHL_RECHNUNGEN=$(expr $ANZAHL_RECHNUNGEN + 1) fi if [ -x "$RUBBER_BIN" ]; then "$RUBBER_BIN" --clean "$filename" fi print_trenner } parse_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 } # --- Programmstart --- # # --------------------- # ## Nicht als Root laufen lassen if [ "$(id -u)" -eq 0 ]; then echo "$PROGRAMM_NAME: Das Skript sollte nicht als Root ausgeführt werden!" exit 1 fi ## Farbe aktivieren wenn vorhanden NUMBER_OF_COLORS=$($TPUT_BIN colors) if [ -n "$NUMBER_OF_COLORS" ] && [ "$NUMBER_OF_COLORS" -ge 8 ]; then COLORED_OUTPUT=1 fi ## CLI Optionen auswerten while getopts b:dhik:l:o:t:vV opt do case $opt in b) BASEDIR=$(set_cli_opt "$OPTARG") ;; d) DEBUG=1 ;; h) usage_and_exit 0 ;; i) INSTALL_LCO_FILE=1 ;; k) KUNDENFILE=$(set_cli_opt "$OPTARG") ;; l) LCOFILE=$(set_cli_opt "$OPTARG") ;; o) OUTPUTDIR=$(set_cli_opt "$OPTARG") ;; t) TEMPLATEFILE=$(set_cli_opt "$OPTARG") ;; v) VERBOSITY=1 ;; V) print_version ;; *) usage_and_exit 2 ;; esac done print_head ## Die nötigen Programme und rechnung.sty testen for program in $EXECUTABLES_TO_CHECK; do check_binary $program done ## relative pfade in den variablen LCOFILE, KUNDENFILE und TEMPLATEFILE wenn ## nötig in absolute pfade umwandeln. -- TODO: das geht sicher besser. LCOFILE=$(set_abs_path "$LCOFILE") KUNDENFILE=$(set_abs_path "$KUNDENFILE") TEMPLATEFILE=$(set_abs_path "$TEMPLATEFILE") ## verzeichnisse testen check_dir "$BASEDIR" check_dir "$OUTPUTDIR" ## die nötigen Dateien testen und die Variable LCO setzen for file in "$LCOFILE" "$KUNDENFILE" "$TEMPLATEFILE"; do check_file "$file" done LCO=$(basename "$LCOFILE" .lco) ## temporäres Verzeichnis erstellen und hineinwechseln TEMPDIR=$(mktemp -d) if [ ! $? -eq 0 ]; then print_error "Erstellen der temp. Verzeichnis fehlgeschlagen" exit 8 else cd "$TEMPDIR" print_info "Wechsle nach $(pwd)" ## prüfen, ob rechnung.sty installiert ist if ! check_sty; then if [ $INSTALL_LCO_FILE -eq 0 ]; then print_error "Verwenden sie -i um $RECHNUNG_STY_FILE zu installieren" clear_and_exit 9 else print_info "Versuche $RECHNUNG_STY_FILE zu installieren" install_sty fi fi # Kundendatei einlesen und die einzelnen Zeilen an den Zeilenparser # weiterreichen. while IFS= read -r rawline do trim_line "$rawline" parse_line "$LINE_BUFFER" done < "$KUNDENFILE" fi # aufräumen clear_and_exit 0