Bash in Python

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
berell
User
Beiträge: 1
Registriert: Dienstag 12. März 2019, 18:25

Hallo Leute.

Ich habe, da ich mich damit ein wenig auskenne ein kleines Bash-Script erstellt welches ich gerne unter Windows ein paar Usern zur Verfügung stellen würde, aber überhaupt kein Plan habe wie ich das lösen soll.

Es geht um folgendes:

Es muss eine Datei Zeilenweise eingelesen werden,
Der Wert AltitudePoint Distance gefunden werden und je nach dem welchen wert ich dem Script Übergebe um diesen gekürzt werden. Idealerweise gebe ich noch an von welcher Zeile bis welcher Zeile das ganze ausgeführt werden soll.

Also ums zu verdeutlichen. die GPS Datei hat unendlich viele Zeilen und viele die ich in einer bestimmten Range ändern muss:
<AltitudePoint Distance="34914.724489331216" Lat="50.54771812726846" Lng="8.098671068368624" VideoTime="5346.8102428908333" />

der Wert AltitudePoint Distance=34914.72..." muss um 55 gekürzt werden, also auf 34864.71... oder auch bspw. 30 je nach dem was ich dem script übergebe.

Hier das Bash-Scripz was alles macht was gefordert ist:

Code: Alles auswählen

#!/bin/bash

echo $1 $2 $3  #Die übergebenen Variablen: $1=Dateiname, $2=Von Zeile,$3= Bis Zeile, $4=kürzung $5=Output-Datei

typeset -i i

while read entry

do

i=i+1


d=`echo $entry | grep "AltitudePoint" | grep "Distance" | cut -d\" -f2 | cut -d. -f1`


if [ $d ]; then
#echo "Alt:" $entry
a=`expr $d - $4`
    if (( "$d" >= "0" )); then
        if (( "$i" >= "$2" )); then
             if (( "$i" <= "$3" )); then
             echo ""
              echo "ändere Zeile " $i
                echo "Alt:" $entry
                echo "Neu:" $entry | sed s/$d/$a/g
                echo $entry | sed s/$d/$a/g >> $5
             fi   
        
        fi
    fi
else
i=i-1
echo $entry >> $5
fi


echo -n .

done < $1
echo ""

Also im Prinzip recht easy.

Wäre hier jemand bereit mir das in Python umzuschreiben, ohne das ich grad die ganze Sprache lernen muss ? Das wäre echt Super.
Cu Bernd
nezzcarth
User
Beiträge: 1635
Registriert: Samstag 16. April 2011, 12:47

Die Eingabe sieht nach einer XML-Datei aus. Dein Skript würde man so eher nicht nach Python portieren, sondern vmtl. einen XML-Parser verwenden. Solche Quick & Dirty Zeichenersetzungen sind bei Python eher nicht üblich, allerdings ist die "korrekte" Lösung mit Python auch nicht wesentlich schwieriger. Dafür müsste man aber einen repräsentativen Ausschnitt aus der Datei sehen, der valides XML ist und alle wichtigen Elemente und Attribute enthält.
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

Bash-Script ist schon hässlich.
Muss man mal sagen.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@berell: XML als Textdatei zu behandeln ist *falsch*. Das fängt damit an, das Du das zeilenweise verarbeitest, es aber gar nicht garantiert ist, dass alles was Du bearbeiten willst jeweils in einzelnen Zeilen steht. Es können auch mehrere <AltitudePoint>-Elemente in einer Zeile stehen. Andersherum ist auch nicht garantiert das alles was zu einem <AltitudePoint>-Element gehört in einer Zeile steht. Genau so wenig ist die Reihenfolge der Attribute in XML-Elementen garantiert.

Gute Namen machen auch in Shell-Skripten Sinn und auch dort sind Namen die nur aus einem Zeichen bestehen in der Regel keine guten Namen. Die Positionsargumente sollte man beispielsweise an bessere Namen binden statt nur in einem Kommentar zu erwähnen was die bedeuten. Der hilft einem später im Skript nämlich weniger als wenn man überall wo der Name verwendet wird, einfach ablesen kann was er bedeutet.

Es wäre gut am Anfang zu prüfen ob genügend Argumente übergeben wurden, damit das nicht mit (teilweise) leeren Variablen durchlaufen kann.

Die Variablen sind an einigen Stellen nicht gequotet was zu Problemen mit Leer und Sonderzeichen führen kann.

Korrekte Einrückung ist im Gegensatz zu Python bei der Bash für die Sprache irrelevant, aber nicht für menschliche Leser.

``read`` muss man fast immer die `-r`-Option mitgeben. Ich glaube ich persönlich hatte noch nie den Fall wo das weglassen nicht falsch gewesen wäre. ``read`` entfernt Leerzeichen am Anfang und am Ende der gelesenen Zeile wenn man $IFS nicht passend setzt. Und falls die letzte Zeile nicht mit einem Zeilenendezeichen endet, wird sie *nicht* verarbeitet! Es ist echt gruselig das einem Shells es so schwer machen etwas so einfaches und alltägliches wie das zeilenweise Lesen einer Textdatei korrekt zu machen.

Backticks sind nicht gut zu lesen und lassen sich nicht verschachteln. Da verwendet man besser ``$(…)``.

``expr`` benutzt man eigentlich nicht mehr für Rechnungen, dafür hat die Bash Syntax.

Die drei verschachtelten ``if``\s lassen sich sehr leicht zu einem zusammenfassen.

Die neue/angepasste Entfernung muss nur berechnet werden wenn die drei Bedingungen zutreffen.

Der ``sed``-Aufruf steht zweimal gleich im Programm. Das könnte man sich sparen wenn man das Ergebnis an einen Namen binden würde. ``sed`` ist vielleicht auch gar nicht nötig denn so eine einfache Ersetzung kann die Bash schon selbst.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/bin/bash

if (( $# < 5)); then
    echo "Usage: $0 inFile startLine endLine difference outFile"
    exit 1
fi
readonly inFile=$1
readonly startLine=$2
readonly endLine=$3
readonly difference=$4
readonly outFile=$5
# 
# TODO Should an already existing $outFile be emptied at this point in the
#   script, or is it okay that the script appends to it?
# 
while IFS= read -r line || [ -n "$line" ]
do
    i=$(( i + 1 ))
    # 
    # FIXME This breaks *very* easily with real world XML.
    # 
    distance=$(echo "$line" | grep 'AltitudePoint' | grep 'Distance' \
                | cut -d'"' -f2 | cut -d. -f1)

    if [ "$distance" ]; then
        #echo "Alt: $line"
        if (( distance >= 0 && i >= startLine && i <= endLine )); then
            newDistance=$((distance - difference))
            # 
            # FIXME This also breaks *very* easily with real world XML.
            # 
            newLine=${line//$distance/$newDistance}
            echo
            echo "ändere Zeile $i"
            echo "Alt: $line"
            echo "Neu: $newLine"
            echo "$newLine" >> "$outFile"
        fi
    else
        i=$(( i - 1 ))
        echo "$line" >> "$outFile"
    fi
    echo -n .
done < "$inFile"
echo
Allerdings ist das wie gesagt kein sinnvoller Weg mit XML zu arbeiten. Wenn man das von einem Shellskript aus machen will, würde ich einen Blick auf ``xmlstarlet`` empfehlen.

Oder man verwendet eine richtige Programmiersprache und einen XML-Parser um das robust und richtig zu machen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Das Original-Bash-Skript in Python (ungetestet) 😈:

Code: Alles auswählen

#!/usr/bin/env python3
import sys


def to_int(value):
    try:
        return int(value)
    except ValueError:
        return 0


def append_to_file(filename, data):
    with open(filename, 'ab') as out_file:
        out_file.write(data + b'\n')


def main():
    argv = (sys.argv + [''] * 5)[:6]
    print(argv[1], argv[2], argv[3])
    
    with open(argv[1], 'rb') as in_file:
        i = 0
        for entry in in_file:
            if b'\n' in entry:
                entry = entry.strip()
                i += 1
                if b'AltitudePoint' in entry and b'Distance' in entry:
                    try:
                        d = entry.split(b'"', 2)[1].split(b'.', 1)[0]
                    except IndexError:
                        d = b''
                    if d:
                        # print('Alt:', entry)
                        a = str(to_int(d) - to_int(argv[4])).encode()
                        if (
                            to_int(d) >= 0
                            and to_int(argv[2]) <= i <= to_int(argv[3])
                        ):
                            print()
                            print('Alt:', entry)
                            print('Neu:', entry.replace(d, a))
                            append_to_file(argv[5], entry.replace(d, a))
                    else:
                        i -= 1
                        append_to_file(argv[5], entry)
            print('.', end='')
    print()


if __name__ == '__main__':
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17753
Registriert: Sonntag 21. Oktober 2012, 17:20

@__blackjack__: ich sehe da noch einen Einrückfehler. Ist aber wahrscheinlich besser so, denn sonst benutzt das noch jemand.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

:oops:

Code: Alles auswählen

#!/usr/bin/env python3
import sys


def to_int(value):
    try:
        return int(value)
    except ValueError:
        return 0


def append_to_file(filename, data):
    with open(filename, 'ab') as out_file:
        out_file.write(data + b'\n')


def main():
    argv = (sys.argv + [''] * 5)[:6]
    print(argv[1], argv[2], argv[3])
    
    with open(argv[1], 'rb') as in_file:
        i = 0
        for entry in in_file:
            if b'\n' in entry:
                entry = entry.strip()
                i += 1
                d = b''
                if b'AltitudePoint' in entry and b'Distance' in entry:
                    try:
                        d = entry.split(b'"', 2)[1].split(b'.', 1)[0]
                    except IndexError:
                        pass  # Intentionally ignored.
                if d:
                    # print('Alt:', entry)
                    a = str(to_int(d) - to_int(argv[4])).encode()
                    if (
                        to_int(d) >= 0
                        and to_int(argv[2]) <= i <= to_int(argv[3])
                    ):
                        print()
                        print('Alt:', entry)
                        print('Neu:', entry.replace(d, a))
                        append_to_file(argv[5], entry.replace(d, a))
                else:
                    i -= 1
                    append_to_file(argv[5], entry)
            print('.', end='')
    print()


if __name__ == '__main__':
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17753
Registriert: Sonntag 21. Oktober 2012, 17:20

Es ist halt schon verlockend, wenn man die ganzen Probleme, die man mit XML als Text-Datei hat, ignoriert, wie einfach Textmanipulation mit Python ist:

Code: Alles auswählen

import sys
import re

def main():
    filename, start, end, difference, output = sys.argv[1:]
    start, end = int(start), int(end)
    difference = float(difference)
    with open(filename) as input:
        text = input.read()
    parts = text.split("<AltitudePoint")
    parts[start:end + 1] = [
        re.sub('(Distance=")([^"]+?)', lambda m: "{}{}".format(
            m.group(1), float(m.group(2)) + difference), p)
        for p in parts[start:end + 1]
    ]
    with open(output) as output:
        output.write("<AltitudePoint".join(parts))

if __name__ == '__main__':
    main()
nezzcarth
User
Beiträge: 1635
Registriert: Samstag 16. April 2011, 12:47

__blackjack__ hat geschrieben: Mittwoch 13. März 2019, 10:03 Das fängt damit an, das Du das zeilenweise verarbeitest, es aber gar nicht garantiert ist, dass alles Es ist echt gruselig das einem Shells es so schwer machen etwas so einfaches und alltägliches wie das zeilenweise Lesen einer Textdatei korrekt zu machen.
Daher sollte man zeilenweises Einlesen m.M.n. wann immer möglich einem Tool überlassen, das für so etwas gedacht ist. In dem Fall könnte man die Datei z.B. mit 'xml2/2xml' oder 'xmlstarlet pyx/p2x' in ein Shell-freundliches Format verwandeln und die kleine Ersetzung dann mit AWK machen. Umwerfend ist das auch nicht, aber stabiler und kürzer allemal. Und wenn man kein Python + (l)xml verwenden möchte, ist XSLT ja auch immer eine Option... :twisted:
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Oh, offensichtlich habe ich etwas geschrieben, aber vergessen zu senden... Muss wohl zwischen SerialException und ☕ verloren gegangen sein..
Eins vorweg, komplett parsen ist besser als Zeilenweise parsen.

XML händisch zu parsen, ist doof.


Ich versuche es mal ohne zu testen aus dem Kopf:

Code: Alles auswählen

import xml.etree.ElementTree as ET


def get_attr(xml_string):
    element = ET.fromstring(xml_string)
    return element.attribute
So ungefähr. Das ist natürlich nicht die komplette Lösung, sondern nur der Teil, der dir einfach die Attribute aus dem Element holt.
Die Reihenfolge der Attribute bleibt leider nicht erhalten. (hab einen Schrittmotor mit XML REST API, der benötigt die richtige Reihenfolge der Atribute.

Besser wäre es aber das ganze Dokument zu parsen. Dann holt man sich den root-node und iteriert über die Elemente und holt dich dann die Attribute raus.
Alternativ kann man auch find verwenden. Müsste dazu aber selbst jetzt die IPython Shell aufmachen und testen.

Sicherheitsblabla
Was die Sicherheit betrifft, sollte man sich im Klaren sein, dass die volle Implementierung nach XML Standard es auch zulässt Schleifen zu "programmieren". :lol:
In solchen Fällen nutzt man "dümmere" Parser, wenn die Eingabe aus unbekannten Quellen stammt.

F: Wie verhindert man die Nutzung von älteren Python-Versionen?
A: Überall f-strings verwenden
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Antworten