Kirjoittaja: qalle
Kirjoitettu: 23.08.2015 – 01.10.2017
Tagit: hyvää koodia, projektin esittely, vinkki, komentorivi
Apuohjelma, joka kopioi halutun osan (tietyn määrän tavuja tietystä osoitteesta alkaen) binääritiedostosta toiseen binääritiedostoon. Ohjelma on tarkoitettu täydentämään heksaeditoreja ja demonstroi seuraavia Pythonin ominaisuuksia:
""" FileSlice v. 1.5 - kopioi palan tiedostosta uudeksi tiedostoksi. Testattu Python 3:lla Windows 10:llä. Funktiohierarkia: main() parse_arguments() parse_integer_argument() copy_slice() validate_and_adjust_integer_arguments() to_printable() read_file_in_chunks() """ import sys import os.path import getopt # kopiointipuskurin oletuskoko tavuina FILE_COPY_BUFFER_SIZE = 2 ** 20 # lukuparametreissa sallitut loppuliitteet NUMBER_SUFFIXES = { "K" : 2 ** 10, # kilo "M" : 2 ** 20, # mega "G" : 2 ** 30, # giga "T" : 2 ** 40, # tera } # pitkiä tulostettavia tekstejä (virheilmoituksia ja ohje) ARGUMENT_ERROR = """\ Väärä määrä pakollisia komentoriviparametreja. Näet ohjeen ajamalla ohjelman ilman parametreja.\ """ OPTION_ERROR = """\ Virhe valinnaisissa komentoriviparametreissa. Näet ohjeen ajamalla ohjelman ilman parametreja.\ """ INTEGER_ERROR = """\ Epäkelpo kokonaislukuparametri. Näet ohjeen ajamalla ohjelman ilman parametreja.\ """ HELP_TEXT = """\ FileSlice v. 1.5 - kopioi palan tiedostosta uudeksi tiedostoksi Valinnaiset komentoriviparametrit (missä järjestyksessä tahansa mutta ennen pakollisia parametreja; kirjainkoolla ei ole väliä): -fN --from-address=N Ensimmäisen kopioitavan tavun osoite on N. Oletusarvo: 0 -tN --to-address=N Viimeisen kopioitavan tavun osoite on N. Oletusarvo: -1 Tätä parametria ei saa antaa yhdessä --length:in kanssa. -lN --length=N Kopioitavan palan pituus on N tavua (1 tai suurempi). Tätä parametria ei saa antaa yhdessä --to-address:in kanssa. Pakolliset komentoriviparametrit (valinnaisten parametrien jälkeen, tässä järjestyksessä): Lähdetiedosto Tiedosto, josta kopioidaan. Kohdetiedosto Tiedosto, johon kopioidaan. Tämä tiedosto ylikirjoitetaan, jos se on jo olemassa. Huomautuksia: * Parametreissa --from-address ja --to-address ei-negatiiviset arvot tarkoittavat etäisyyttä tiedoston alusta: 0 = ensimmäinen tavu, 1 = toinen, jne. Negatiiviset arvot tarkoittavat etäisyyttä tiedoston lopusta: -1 = viimeinen tavu, -2 = toiseksi viimeinen, jne. * Oletuksena lähdetiedosto kopioidaan kokonaan kohdetiedostoon. * Numeeriset parametrit (--from-address, --to-address ja --length) voidaan antaa myös 16-kantaisina: esim. 0xff = 255 ja -0xff = -255. * Numeeristen parametrien lopussa saa olla jokin seuraavista liitteistä: K: kilo (2 ** 10) M: mega (2 ** 20) G: giga (2 ** 30) T: tera (2 ** 40)\ """ def parse_integer_argument(options, shortName, longName): """Tulkitse kokonaislukumuotoinen valinnainen komentoriviparametri, esim. "-0x10M". Palauta None, jos parametria ei ole annettu.""" # lue parametri merkkijonona string = options.get(shortName, options.get(longName)) if string is None: return None # muunna parametrin kirjaimet isoiksi, jotta jälkiliite voidaan tunnistaa string = string.upper() # jos syötteessä on jälkiliite, poista se ja muista kerroin if len(string) > 0 and string[-1] in NUMBER_SUFFIXES: multiplier = NUMBER_SUFFIXES[string[-1]] string = string[:-1] else: multiplier = 1 # tulkitse jäljelle jäänyt parametri (esim. "-0x10") try: value = int(string, 0) except ValueError: exit(INTEGER_ERROR) return value * multiplier def parse_arguments(): """Tulkitse komentoriviparametrit getopt-moduulin avulla ja palauta ne dict:issä. (Asetuksia ei vielä juurikaan tarkisteta, koska luettavan tiedoston kokoa ei tiedetä.)""" # options = valinnaiset (viiva- ja kaksoisviiva-alkuiset), arguments = muut try: (options, arguments) = getopt.getopt( sys.argv[1:], "f:t:l:", ["from-address=", "to-address=", "length="] ) except getopt.GetoptError: exit(OPTION_ERROR) # tarkista pakollisten parametrien määrä if len(arguments) != 2: exit(ARGUMENT_ERROR) # muunna valinnaiset parametrit dict-tyyppiseksi käsittelyn helpottamiseksi options = dict(options) return { # kopioinnin aloitusosoite (int/None) "fromAddress": parse_integer_argument(options, "-f", "--from-address"), # kopioinnin lopetusosoite (int/None) "toAddress": parse_integer_argument(options, "-t", "--to-address"), # kopioitavan osan pituus (int/None) "length": parse_integer_argument(options, "-l", "--length"), # lähdetiedosto (str) "sourceFile": arguments[0], # kohdetiedosto (str) "targetFile": arguments[1], } def validate_and_adjust_integer_arguments(settings, sourceSize): """Tarkista ja muunna kokonaislukumuotoiset asetukset nyt, kun lähdetiedoston koko tiedetään.""" # kopioitavan palan alkuosoite (aseta oletusarvoon tai muunna tiedoston # alusta lasketuksi) if settings["fromAddress"] is None: settings["fromAddress"] = 0 else: if settings["fromAddress"] < 0: settings["fromAddress"] += sourceSize if not 0 <= settings["fromAddress"] <= sourceSize - 1: exit("Kopioitavan palan alkuosoite on liian pieni tai suuri.") # kopioitavan palan loppuosoite ja pituus if settings["toAddress"] is None and settings["length"] is None: # kumpaakaan ei annettu -> pituus oletusarvoon settings["length"] = sourceSize - settings["fromAddress"] elif settings["toAddress"] is None: # vain pituus annettu -> tarkista maxLength = sourceSize - settings["fromAddress"] if not 1 <= settings["length"] <= maxLength: exit("Kopioitavan palan pituus on liian pieni tai suuri.") elif settings["length"] is None: # vain loppuosoite annettu -> muunna tiedoston alusta lasketuksi, # tarkista ja muunna pituudeksi if settings["toAddress"] < 0: settings["toAddress"] += sourceSize minToAddress = settings["fromAddress"] maxToAddress = sourceSize - 1 if not minToAddress <= settings["toAddress"] <= maxToAddress: exit("Kopioitavan palan loppuosoite on liian pieni tai suuri.") settings["length"] = settings["toAddress"] - settings["fromAddress"] + 1 else: # molemmat annettu exit(OPTION_ERROR) # poista pituudeksi muunnettu loppuosoite tarpeettomana del settings["toAddress"] return settings def to_printable(string): """Korvaa merkkijonon muut kuin 7-bittiset ASCII-merkit kenoviivakoodeilla.""" byteString = string.encode("ascii", errors = "backslashreplace") return byteString.decode("ascii") def read_file_in_chunks(sourceHnd, fromAddress, bytesLeft): """Generaattori, joka palauttaa tiedostosta halutut tavut. sourceHnd: lähdetiedosto fromAddress: ensimmäisen luettavan tavun osoite bytesLeft: luettavien tavujen määrä generoi: 1...FILE_COPY_BUFFER_SIZE luettua tavua/kutsu""" sourceHnd.seek(fromAddress) while bytesLeft > 0: # laske lohkon koko (pienempi jäljellä olevien tavujen määrästä ja # lohkon maksimikoosta) chunkSize = min(bytesLeft, FILE_COPY_BUFFER_SIZE) # lue ja palauta lohko lähdetiedostosta (koska tämä funktio on # generaattori, seuraavan kutsun yhteydessä jatka yield-käskyn jäljestä # muistaen funktion sisäinen tila) yield sourceHnd.read(chunkSize) # pienennä jäljellä olevien tavujen määrää bytesLeft -= chunkSize def copy_slice(sourceHnd, targetHnd, settings): """Kopioi pala lähdetiedostosta kohdetiedostoksi.""" # lue lähdetiedoston koko sourceSize = sourceHnd.seek(0, 2) if sourceSize == 0: exit("Virhe: lähdetiedosto on tyhjä.") # tarkista ja muunna kokonaislukumuotoiset asetukset settings = validate_and_adjust_integer_arguments(settings, sourceSize) toAddress = settings["fromAddress"] + settings["length"] - 1 print('Lähdetiedosto : "{:s}"'.format(to_printable(settings["sourceFile"]))) print('Kohdetiedosto : "{:s}"'.format(to_printable(settings["targetFile"]))) print("Lähdetiedoston koko: {n:d} (0x{n:x})".format(n = sourceSize)) print("Palan alkuosoite : {n:d} (0x{n:x})".format(n = settings["fromAddress"])) print("Palan loppuosoite : {n:d} (0x{n:x})".format(n = toAddress)) print("Palan pituus : {n:d} (0x{n:x})".format(n = settings["length"])) print("Kopioidaan...") targetHnd.seek(0) for chunk in read_file_in_chunks(sourceHnd, settings["fromAddress"], settings["length"]): targetHnd.write(chunk) # tarkista, että oikea määrä tavuja kopioitui targetSize = targetHnd.tell() if targetSize != settings["length"]: exit("Kopioinnissa tapahtui virhe.") def main(): # näytä ohjeteksti, jos ajetaan ilman komentoriviparametreja if len(sys.argv) == 1: exit(HELP_TEXT) # tulkitse komentoriviparametrit settings = parse_arguments() # lähde- ja kohdetiedosto eivät saa olla samat if os.path.abspath(settings["sourceFile"]) == os.path.abspath(settings["targetFile"]): exit("Lähde- ja kohdetiedosto eivät saa olla samat.") # kopioi pala lähdetiedostosta kohdetiedostoksi try: with open(settings["sourceFile"], "rb") as sourceHnd, \ open(settings["targetFile"], "wb") as targetHnd: bytesWritten = copy_slice(sourceHnd, targetHnd, settings) except OSError: exit("Virhe luettaessa lähdetiedostoa tai kirjoitettaessa kohdetiedostoa.") print("Kopiointi valmis.") if __name__ == "__main__": main()
FileSlice v. 1.5 - kopioi palan tiedostosta uudeksi tiedostoksi Valinnaiset komentoriviparametrit (missä järjestyksessä tahansa mutta ennen pakollisia parametreja; kirjainkoolla ei ole väliä): -fN --from-address=N Ensimmäisen kopioitavan tavun osoite on N. Oletusarvo: 0 -tN --to-address=N Viimeisen kopioitavan tavun osoite on N. Oletusarvo: -1 Tätä parametria ei saa antaa yhdessä --length:in kanssa. -lN --length=N Kopioitavan palan pituus on N tavua (1 tai suurempi). Tätä parametria ei saa antaa yhdessä --to-address:in kanssa. Pakolliset komentoriviparametrit (valinnaisten parametrien jälkeen, tässä järjestyksessä): Lähdetiedosto Tiedosto, josta kopioidaan. Kohdetiedosto Tiedosto, johon kopioidaan. Tämä tiedosto ylikirjoitetaan, jos se on jo olemassa. Huomautuksia: * Parametreissa --from-address ja --to-address ei-negatiiviset arvot tarkoittavat etäisyyttä tiedoston alusta: 0 = ensimmäinen tavu, 1 = toinen, jne. Negatiiviset arvot tarkoittavat etäisyyttä tiedoston lopusta: -1 = viimeinen tavu, -2 = toiseksi viimeinen, jne. * Oletuksena lähdetiedosto kopioidaan kokonaan kohdetiedostoon. * Numeeriset parametrit (--from-address, --to-address ja --length) voidaan antaa myös 16-kantaisina: esim. 0xff = 255 ja -0xff = -255. * Numeeristen parametrien lopussa saa olla jokin seuraavista liitteistä: K: kilo (2 ** 10) M: mega (2 ** 20) G: giga (2 ** 30) T: tera (2 ** 40)
C:\>python fileslice.py --from-address=-0x10k -t-100 doomwad slice Lähdetiedosto : "doomwad" Kohdetiedosto : "slice" Lähdetiedoston koko: 4196020 (0x4006b4) Palan alkuosoite : 4179636 (0x3fc6b4) Palan loppuosoite : 4195920 (0x400650) Palan pituus : 16285 (0x3f9d) Kopioidaan... Kopiointi valmis.
Muutama parannusehdotus:
Kiitos ehdotuksista, toteutin kolme ensimmäistä. Ohjelmaan saattoi jäädä bugeja, koska se on nyt monimutkaisempi.
Tein uuden version, jossa on suomenkieliset tekstit ja pari uutta ominaisuutta.
Tyylillisen yhdenmukaisuuden takia laittaisin aina rivinvaihdon kaksoispisteen jälkeen eli esim. if-ehto ja ehdon takana oleva koodi eri riveille. Luettavuus paranee niin, vaikka koodin pituus hieman kasvaa.
Muuten koodi näyttää oikein selkeältä. Toimivuutta en testannut.
Kun minulla ei ole mitään muuta olennaista sanottavaa, viilaan pilkkua ja sanon että tämän rivin:
lengthInsteadOfEndPos = (endPosOrLength[0] == "+")
laittaisin näin:
lengthInsteadOfEndPos = endPosOrLength.startswith('+')
Kiitos, korjasin nuo ja pari muuta juttua.
Uusi versio 1.4: selkeytetty käyttäjän syöttämien lukujen tulkintaa (ConvertNumber()-funktio).
Uusi versio 1.5:
with
-käskyä tiedostojen käsittelyyn