Category talk:Wren-ioutil

From Rosetta Code

Source code

/* Module "ioutil.wren" */

import "io" for File, FileFlags, Stdin, Stdout
import "os" for Platform

/*
   FileUtil supplements the File class with various other operations on files.
   All methods automatically close any files they have opened before returning.
*/
class FileUtil {
    // Returns the string representing a line break for the current platform.
    static lineBreak { Platform.isWindows ? "\r\n" : "\n" }

    // Returns the default buffer size.
    static bufferSize { 4096 }

    // Returns whether or not two paths refer to the same file. The file(s) must exist.
    static areSameFile(path1, path2) { File.realPath(path1) == File.realPath(path2) }

    // Returns whether or not the contents of the two paths are exactly the same.
    // The files must exist and not be the same file.
    // Use only if the entire contents of both files can be held temporarily in memory.
    static areDuplicates(path1, path2) { !areSameFile(path1, path2) && read(path1) == read(path2) }

    // Private worker method for copying files.
    static copy_(fromPath, toPath, overwrite, checked) {
        if (!checked) {
            if (overwrite.type != Bool) Fiber.abort("Overwrite must be true or false.")
            if (!File.exists(fromPath)) Fiber.abort("'%(fromPath)' does not exist.")
            if (!overwrite && File.exists(toPath)) Fiber.abort("'%(toPath)' already exists.")
            if (fromPath == toPath) return
        }
        var contents = File.read(fromPath)
        File.create(toPath) { |file| file.writeBytes(contents) }
    }

    // Copies a file from 'fromPath' to 'toPath'. If 'toPath' already exists it fails unless
    // 'overwrite' is set to true in which case it is truncated first.
    // Use only if the entire contents of 'fromPath' can be held temporarily in memory.
    static copy(fromPath, toPath, overwrite) { copy_(fromPath, toPath, overwrite, false) }

    // Convenience version of the 'copy' method in which 'overwrite' is always set to false.
    static copy(fromPath, toPath) { copy_(fromPath, toPath, false, false) }

    // Moves a file from 'fromPath' to 'toPath'. If 'toPath' already exists it fails unless.
    // 'overwrite' is set to true in which case it is truncated first.
    // Use only if the entire contents of 'fromPath' can be held temporarily in memory.
    static move(fromPath, toPath, overwrite) {
        if (overwrite.type != Bool) Fiber.abort("Overwrite must be true or false.")
        if (!File.exists(fromPath)) Fiber.abort("'%(fromPath)' does not exist.")
        if (!overwrite && File.exists(toPath)) Fiber.abort("'%(toPath)' already exists.")
        if (fromPath == toPath) return
        copy_(fromPath, toPath, overwrite, true)
        File.delete(fromPath)
    }

    // Convenience version of the 'move' method in which 'overwrite' is always set to false.
    static move(fromPath, toPath) { move(fromPath, toPath, false) }

    // Private worker method for copying large files.
    static copyLarge_(fromPath, toPath, overwrite, checked) {
        if (!checked) {
            if (overwrite.type != Bool) Fiber.abort("Overwrite must be true or false.")
            if (!File.exists(fromPath)) Fiber.abort("'%(fromPath)' does not exist.")
            if (!overwrite && File.exists(toPath)) Fiber.abort("'%(toPath)' already exists.")
            if (fromPath == toPath) return
        }
        var size = File.size(fromPath) 
        var chunks = (size/bufferSize).floor
        var rem = size % bufferSize
        if (rem > 0) chunks = chunks + 1
        var offset = 0
        var fout = File.create(toPath)
        File.open(fromPath) { |fin|
            for (i in 1..chunks) {
                var bytes = fin.readBytes(bufferSize, offset)
                fout.writeBytes(bytes)
                var offset = offset + bufferSize
            }
        }
        fout.close()
    }

    // Copies a file from 'fromPath' to 'toPath'. If 'toPath' already exists it fails unless
    // 'overwrite' is set to true in which case it is truncated first.
    // Use if the entire contents of 'fromPath' are too large to be held temporarily in memory.
    static copyLarge(fromPath, toPath, overwrite) { copyLarge_(fromPath, toPath, overwrite, false) }

    // Convenience version of the 'copyLarge' method in which 'overwrite' is always set to false.
    static copyLarge(fromPath, toPath) { copyLarge_(fromPath, toPath, false, false) }

    // Moves a file from 'fromPath' to 'toPath'. If 'toPath' already exists it fails unless
    // 'overwrite' is set to true in which case it is truncated first.
    // Use if the entire contents of 'fromPath' are too large to be held temporarily in memory.
    static moveLarge(fromPath, toPath, overwrite) {
        if (overwrite.type != Bool) Fiber.abort("Overwrite must be true or false.")
        if (!File.exists(fromPath)) Fiber.abort("'%(fromPath)' does not exist.")
        if (!overwrite && File.exists(toPath)) Fiber.abort("'%(toPath)' already exists.")
        if (fromPath == toPath) return    
        copyLarge_(fromPath, toPath, overwrite, true)
        File.delete(fromPath)
    }

    // Convenience version of the 'moveLarge' method in which 'overwrite' is always set to false.
    static moveLarge(fromPath, toPath) { moveLarge(fromPath, toPath, false) }

    // Reads the entire contents of the file at 'path' and returns it as a string.
    // Use only if the entire contents of 'path' can be held in memory.
    // Works the same as File.read(path) but gives a clearer error message if 'path' does not exist.
    static read(path) {
        if (!File.exists(path)) Fiber.abort("'%(path)' does not exist.")
        return File.read(path)
    }

    // Reads and returns the entire contents of the file at 'path' split into lines.
    // Use only if the entire contents of 'path' can be held in memory.
    static readLines(path) {
        if (!File.exists(path)) Fiber.abort("'%(path)' does not exist.")
        return File.read(path).split(lineBreak)
    }

    // Returns a function which reads a file chunk by chunk and 'yields' each chunk to the calling fiber.
    // Use if the entire contents of 'path' are too large to be held in memory.
    static readLarge(path) {
        if (!File.exists(path)) Fiber.abort("'%(path)' does not exist.")
        return Fn.new {
            var size = File.size(path)
            var chunks = (size/bufferSize).floor
            var rem = size % bufferSize
            if (rem > 0) chunks = chunks + 1
            var offset = 0
            var file = File.open(path)
            for (i in 1..chunks) {
                var bytes = file.readBytes(bufferSize, offset)
                Fiber.yield(bytes)
                var offset = offset + bufferSize
            }
            file.close()
        }
    }

    // Returns a function which reads a file line by line and 'yields' each line to the calling fiber.
    // Use if the entire contents of 'path' are too large to be held in memory.
    static readEachLine(path) {
        if (!File.exists(path)) Fiber.abort("'%(path)' does not exist.")
        return Fn.new {
            var file = File.open(path)
            var offset = 0
            var line = ""
            while(true) {
                var b = file.readBytes(1, offset)
                offset = offset + 1
                if (b == "\n") {
                    Fiber.yield(line)
                    line = "" // reset line variable
                } else if (b == "\r") { // Windows
                    // wait for following "\n"
                } else if (b == "") { // end of stream
                    break
                } else {
                    line = line + b
                }
            }
            file.close()
        }
    }

    // Creates a new file, or truncates an existng one, and writes 'bytes' to it.
    static write(path, bytes) {
        if (bytes.type != String) Fiber.abort("'Bytes' must be a string.")
        File.create(path) { |file| file.writeBytes(bytes) }
    }

    // Creates a new file, or truncates an existng one, and writes 'lines' to it.
    static writeLines(path, lines) {
        if (!(lines is Sequence)) Fiber.abort("'Lines' must be a sequence.")
        File.create(path) { |file|
           for (line in lines) {
                file.writeBytes((line is String) ? line : line.toString)
                file.writeBytes(lineBreak)
           }
        }
    }

    // Appends 'bytes' to the end of the file at 'path'. If 'path' doesn't exist, a new file is created.
    static append(path, bytes) {
        if (bytes.type != String) Fiber.abort("'Bytes' must be a string.")
        var exists = File.exists(path)
        if (exists && bytes == "") return
        var flags = FileFlags.writeOnly
        if (!exists) flags = FileFlags.create | flags
        File.openWithFlags(path, flags) { |file| file.writeBytes(bytes) }
    }

    // Appends 'lines' to the end of the file at 'path'. If 'path' doesn't exist, a new file is created.
    static appendLines(path, lines) {
        if (!(lines is Sequence)) Fiber.abort("'Lines' must be a sequence.")
        var exists = File.exists(path)
        if (exists && lines.isEmpty) return
        var flags = FileFlags.writeOnly
        if (!exists) flags = FileFlags.create | flags
        File.openWithFlags(path, flags) { |file|
            for (line in lines) {
                file.writeBytes((line is String) ? line : line.toString)
                file.writeBytes(lineBreak)
            }
        }
    }

    // Removes up to 'numBytes' bytes from the end of the file at 'path'.
    // Use only if the entire contents of 'path' can be held temporarily in memory.
    static remove(path, numBytes) {
        if (!File.exists(path)) Fiber.abort("'%(path)' does not exist.")
        if (numBytes.type != Num || !numBytes.isInteger || numBytes < 1) {
            Fiber.abort("Number of bytes to be removed must be a positive integer.")
        }
        var size = File.size(path)
        if (size <= number) {
            File.create(path)
            return
        }
        var contents = File.read(path)[0...-number]
        File.create(path) { |file| file.writeBytes(contents) }
    }

    // Removes up to 'numLines' lines from the end of the file at 'path'.
    // Use only if the entire contents of 'path' can be held temporarily in memory. 
    static removeLines(path, numLines) {
        if (numLines.type != Num || !numLines.isInteger || numLines < 1) {
            Fiber.abort("Number of lines to be removed must be a positive integer.")
        }
        var lines = readLines(path)
        var count = lines.count
        if (count <= numLines) {
            File.create(path)
            return
        }
        File.create(path) { |file|
            for (line in lines.take(count - numLines)) {
                file.writeBytes(line)
                file.writeBytes(lineBreak)
            }
        }
    }

    // Truncates the file at 'path' to the specified size in bytes.
    // If the old size is not greater than the new size, the file is left unchanged.
    // Use only if the entire contents of 'path' can be held temporarily in memory. 
    static truncate(path, newSize) {
        if (!File.exists(path)) Fiber.abort("'%(path)' does not exist.")
        if (newSize.type != Num || !newSize.isInteger || newSize < 0) {
            Fiber.abort("New size must be a non-negative integer.")
        }
        var oldSize = File.size(path)
        if (oldSize <= newSize) return
        var contents = File.read(path)[0...newSize]
        File.create(path) { |file| file.writeBytes(contents) }
    }

    // Truncates the file at 'path' to the specified number of lines.
    // If the old length is not greater than the new length, the file is left unchanged.
    // Use only if the entire contents of 'path' can be held temporarily in memory. 
    static truncateLines(path, newLen) {
        if (newLen.type != Num || !newLen.isInteger || newLen < 0) {
            Fiber.abort("New length must be a non-negative integer.")
        }
        var lines = readLines(path)
        if (lines.count <= newLen) return
        File.create(path) { |file|
            for (line in lines.take(newLen)) {
                file.writeBytes(line)
                file.writeBytes(lineBreak)
            }
        }
    }
}

/* Input supplements Stdin with some additional methods for reading user input. */
class Input {
    // Gets or sets the string to be used to quit input - null by default.
    static quit    { __quit }
    static quit=(q) { __quit = (q is String) ? q : q.toString }

    // Prompts the user to enter some text and returns it.
    static text(prompt) {
        Output.fwrite(prompt)
        return Stdin.readLine()
    }

    // Prompts the user to enter some text of a minimum length and returns it.
    static text(prompt, minLen) {
        if (minLen.type != Num || !minLen.isInteger || minLen < 0) {
            Fiber.abort("Minimum length must be a non-negative integer.")
        }
        if (minLen == 0) return text(prompt)
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            if (text.count < minLen) {
                System.print("Must have a minimum length of %(minLen) characters, try again.")
            } else return text
        }
    }

    // Prompts the user to enter some text with a minimum/maximum length and returns it.
    static text(prompt, minLen, maxLen) {
        if (minLen.type != Num || !minLen.isInteger || minLen < 0) {
            Fiber.abort("Minimum length must be a non-negative integer.")
        }
        if (maxLen.type != Num || !maxLen.isInteger || maxLen < minLen) {
            Fiber.abort("Maximum length must not be less than minimum length.")
        }
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            if (text.count < minLen || text.count > maxLen) {
                if (maxLen > minLen) {
                    System.print("Must have a length between %(minLen) and %(maxLen) characters, try again.")
                } else {
                    System.print("Must have a length of exactly %(minLen) characters, try again.")
                }
            } else return text
        }
    }

    // Prompts the user to enter a number and returns it.
    static number(prompt) {
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            var number = Num.fromString(text.trim())
            if (!number) {
                System.print("Must be a number, try again.")
            } else return number
        }
    }

    // Prompts the user to enter a number with a minimum value and returns it.
    static number(prompt, min) {
        if (min.type != Num) Fiber.abort("Minimum value must be a number.")
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            var number = Num.fromString(text.trim())
            if (!number || number < min) {
                System.print("Must be a number no less than %(min), try again.")
            } else return number
        }
    }

    // Prompts the user to enter a number with a minimum/maximum value and returns it.
    static number(prompt, min, max) {
        if (min.type != Num) Fiber.abort("Minimum value must be a number.")
        if (max.type != Num || max <= min) {
            Fiber.abort("Maximum value must be a number greater than minimum value.")
        }
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            var number = Num.fromString(text.trim())
            if (!number || number < min || number > max) {
                System.print("Must be a number between %(min) and %(max), try again.")
            } else return number
        }
    }
 
    // Prompts the user to enter an integer and returns it.
    static integer(prompt) {
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            var integer = Num.fromString(text.trim())
            if (!integer || !integer.isInteger) {
                System.print("Must be an integer, try again.")
            } else return integer
        }
    }

    // Prompts the user to enter an integer with a minimum value and returns it.
    static integer(prompt, min) {
        if (min.type != Num || !min.isInteger) Fiber.abort("Minimum value must be an integer.")
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            var integer = Num.fromString(text.trim())
            if (!integer || !integer.isInteger || integer < min) {
                System.print("Must be an integer no less than %(min), try again.")
            } else return integer
        }
    }

    // Prompts the user to enter an integer with a minimum/maximum value and returns it.
    static integer(prompt, min, max) {
        if (min.type != Num || !min.isInteger) Fiber.abort("Minimum value must be an integer.")
        if (max.type != Num || !max.isInteger || max <= min) {
            Fiber.abort("Maximum value must be an integer greater than minimum value.")
        }
        while (true) {
            Output.fwrite(prompt)
            var text = Stdin.readLine()
            if (text == __quit) return text
            var integer = Num.fromString(text.trim())
            if (!integer || !integer.isInteger || integer < min || integer > max) {
                System.print("Must be an integer between %(min) and %(max), try again.")
            } else return integer
        }
    }

    // Prompts the user to enter a single character option from a string of options and returns it.
    // Only the first character entered is significant, any others are ignored.
    // Where both lower and upper case characters are permitted both must be specified.
    static option(prompt, options) {
        if (options.type != String || options.count < 2) {
            Fiber.abort("Options must be a string of minimum length 2.")
        }
        while (true) {
            Output.fwrite(prompt)
            var option = Stdin.readLine()
            if (option == __quit) return option
            if (option.count == 0) {
                System.print("Option must have (at least) one character, try again")
            } else {
                option = option[0]
                if (!options.contains(option)) {
                    System.print("Must be one of '%(options)', try again.")
                } else return option
            }
        }
    }

    // Prompts the user to enter an option from a list of textual options and returns it.
    // The text entered, after any whitespace is trimmed off, must match an option exactly.
    static txtOpt(prompt, options) {
        if (options.type != List || options.count < 2 || options[0].type != String) {
            Fiber.abort("Options must be a list of strings of minimum length 2.")
        }
        while (true) {
            Output.fwrite(prompt)
            var option = Stdin.readLine()
            if (option == __quit) return option
            option = option.trim()
            if (!options.contains(option)) {
                System.print("Must be one of %(options), try again.")
            } else return option
        }
    }

    // Prompts the user to enter an option from a list of numeric options and returns it.
    // The number entered must match an option exactly.
    static numOpt(prompt, options) {
        if (options.type != List || options.count < 2 || options[0].type != Num) {
            Fiber.abort("Options must be a list of numbers of minimum length 2.")
        }
        while (true) {
            Output.fwrite(prompt)
            var option = Stdin.readLine()
            if (option == __quit) return option
            option = Num.fromString(option.trim())
            if (!option || !options.contains(option)) {
                System.print("Must be one of %(options), try again.")
            } else return option
        }
    }

    // Prompts the user to enter an option from a list of integral options and returns it.
    // The integer entered must match an option exactly.
    static intOpt(prompt, options) {
        if (options.type != List || options.count < 2 || options[0].type != Num || !options[0].isInteger) {
            Fiber.abort("Options must be a list of integers of minimum length 2.")
        }
        while (true) {
            Output.fwrite(prompt)
            var option = Stdin.readLine()
            if (option == __quit) return option
            option = Num.fromString(option.trim())
            if (!option || !option.isInteger || !options.contains(option)) {
                System.print("Must be one of %(options), try again.")
            } else return option
        }
    }

    // Prompts the user to enter a password with a minimum/maximum length and returns it.
    // Only printable ASCII characters other than space are valid or if 'allowSymbols' is 
    // false, only ASCII alphanumeric characters. Other characters, except
    // ENTER, BACKSPACE, CTRL-C and CTRL-D which behave normally, are ignored.
    // Valid characters are replaced with bullets as they are typed though any attempt
    // to type characters beyond the maximum length is ignored.
    static password(prompt, minLen, maxLen, allowSymbols) {
        if (minLen.type != Num || !minLen.isInteger || minLen < 0) {
            Fiber.abort("Minimum length must be a non-negative integer.")
        }
        if (maxLen.type != Num || !maxLen.isInteger || maxLen < minLen) {
            Fiber.abort("Maximum length must not be less than minimum length.")
        }
        Stdin.isRaw = true
        var alphanum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        var pwd = ""
        Output.fwrite(prompt)
        while (true) {
            var byte = Stdin.readByte()
            if (byte > 32 && byte < 127) {
                if (pwd.count < maxLen) {
                    var char = String.fromByte(byte)
                    if (allowSymbols || alphanum.indexOf(char) != -1) {
                        Output.fwrite("•")
                        pwd = pwd + char
                    }
                }
            } else if (byte == 8 || byte == 127) {
                if (pwd.count > 0) {
                    Output.fwrite("\b \b")
                    pwd = pwd[0...-1]
                }
            } else if (byte == 10 || byte == 13) {
                if (pwd != __quit && pwd.count < minLen) {
                    System.print("\nMinimum length is %(minLen) characters, try again.")
                    pwd = ""
                    Output.fwrite(prompt)
                } else {
                    Stdin.isRaw = false
                    System.print()
                    return pwd
                }
            } else if (byte == 3 || byte == 4) {
                Stdin.isRaw = false
                if (byte == 3) {
                    System.print("^C")
                } else {
                    System.print("Stdin was closed.")
                }
                Fiber.abort("")
            }
        }
    }

    // Convenience version of the above method which always allows symbols.
    static password(prompt, minLen, maxLen) { password(prompt, minLen, maxLen, true) }

    // Reads 1, 2, 3 or 4 hex bytes (2 digits per byte, upper/lower case a-f, big endian)
    // entered at the terminal and returns the decimal value of the byte group entered.
    // Returns -1 if the return key is pressed after a byte group to signify end of input.
    // Returns -2 if the space or tab key is pressed afer a byte group which enables
    // calling code to skip intervening whitespace.
    // Pressing these keys in the middle of byte groups or entering other non-hex
    // characters is an error.
    static readHexBytes(nBytes) {
        if (!(1..4).contains(nBytes)) {
            Fiber.abort("Invalid value for nBytes, must be 1, 2, 3 or 4.")
        }
        var nDigits = nBytes * 2
        var hl = List.filled(nDigits, 0)
        for (i in 0...nDigits) {
            var h = Stdin.readByte()
            if (h == 10) {
                if (i == 0) return -1
                Fiber.abort("Invalid hex digit entered.")
            }
            if (h == 32 || h == 9) {
                if (i == 0) return -2
                Fiber.abort("Invalid hex digit entered.")
            }
            if (h >= 48 && h <= 57) {
                h = h - 48
            } else if (h >= 97 && h <= 102) {
                h = h - 87
            } else if (h >= 65 && h <= 70) {
                h = h - 55
            } else {
                Fiber.abort("Invalid hex digit entered.")
            }
            hl[i] = h
        }
        var sum = hl[0]
        for (i in 1...hl.count) sum = sum * 16 + hl[i]
        return sum
    }

    // Convenience version of readHexBytes method which reads a single hex byte.
    static readHexByte() { readHexBytes(1) }
}

/* Output supplements Stdout with some additional methods to flush output automatically. */
class Output {
    // Prints a single value to the console, but does not print a newline character afterwards.
    // Converts the value to a string by calling toString on it and flushes output immediately.
    static fwrite(object) {
        System.write(object)
        Stdout.flush()
    }

    // Iterates over sequence and prints each element, but does not print a single newline at the end.
    // Each element is converted to a string by calling toString on it and output is flushed immediately.
    static fwriteAll(sequence) {
        System.writeAll(sequence)
        Stdout.flush()
    }
}