mirror of
https://github.com/zellyn/diskii.git
synced 2024-11-21 08:32:21 +00:00
Initial commit
Includes "applesoft decode" command to convert Applesoft bytes to listings.
This commit is contained in:
commit
7aa075b594
4
.travis.yml
Normal file
4
.travis.yml
Normal file
@ -0,0 +1,4 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.7
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright © 2016 Zellyn Hunter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
9
README.md
Normal file
9
README.md
Normal file
@ -0,0 +1,9 @@
|
||||
diskii
|
||||
======
|
||||
diskii is a commandline tool for working with Apple II disk images.
|
||||
|
||||
Eventually, it aims to be a comprehensive disk image manipulation tool.
|
||||
|
||||
It is pronounced so as to rhyme with "whiskey".
|
||||
|
||||
[![Build Status](https://travis-ci.org/zellyn/diskii.svg?branch=master)](https://travis-ci.org/zellyn/diskii)
|
27
cmd/applesoft.go
Normal file
27
cmd/applesoft.go
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
// applesoftCmd represents the applesoft command
|
||||
var applesoftCmd = &cobra.Command{
|
||||
Use: "applesoft",
|
||||
Short: "work with applesoft programs",
|
||||
Long: `diskii applesoft contains the subcommands useful for working
|
||||
with Applesoft programs.`,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(applesoftCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// applesoftCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
// Cobra supports local flags which will only run when this command
|
||||
// is called directly, e.g.:
|
||||
// applesoftCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
67
cmd/decode.go
Normal file
67
cmd/decode.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/zellyn/diskii/lib/applesoft"
|
||||
"github.com/zellyn/diskii/lib/helpers"
|
||||
)
|
||||
|
||||
var location uint16 // flag for starting location in memory
|
||||
var rawControlCodes bool // flag for whether to skip escaping control codes
|
||||
|
||||
// decodeCmd represents the decode command
|
||||
var decodeCmd = &cobra.Command{
|
||||
Use: "decode filename",
|
||||
Short: "convert a binary applesoft program to a LISTing",
|
||||
Long: `
|
||||
decode converts a binary Applesoft program to a text LISTing.
|
||||
|
||||
Examples:
|
||||
decode filename # read filename
|
||||
decode - # read stdin`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runDecode(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(-1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// runDecode performs the actual decode logic.
|
||||
func runDecode(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("decode expects one argument: the filename (or - for stdin)")
|
||||
}
|
||||
contents, err := helpers.FileContentsOrStdIn(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listing, err := applesoft.Decode(contents, location)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rawControlCodes {
|
||||
os.Stdout.WriteString(listing.String())
|
||||
} else {
|
||||
os.Stdout.WriteString(applesoft.ChevronControlCodes(listing.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
applesoftCmd.AddCommand(decodeCmd)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
|
||||
// Cobra supports Persistent Flags which will work for this command
|
||||
// and all subcommands, e.g.:
|
||||
// decodeCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||
|
||||
decodeCmd.Flags().Uint16VarP(&location, "location", "l", 0x801, "Starting program location in memory")
|
||||
decodeCmd.Flags().BoolVarP(&rawControlCodes, "raw", "r", false, "Print raw control codes (no escaping)")
|
||||
}
|
61
cmd/root.go
Normal file
61
cmd/root.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var cfgFile string
|
||||
|
||||
// RootCmd represents the base command when called without any subcommands
|
||||
var RootCmd = &cobra.Command{
|
||||
Use: "diskii",
|
||||
Short: "Operate on Apple II disk images and their contents",
|
||||
Long: `diskii is a commandline tool for working with Apple II disk
|
||||
images.
|
||||
|
||||
Eventually, it aims to be a comprehensive disk image manipulation tool.`,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
if err := RootCmd.Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports Persistent Flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.diskii.yaml)")
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
// RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
|
||||
// initConfig reads in config file and ENV variables if set.
|
||||
func initConfig() {
|
||||
if cfgFile != "" { // enable ability to specify config file via flag
|
||||
viper.SetConfigFile(cfgFile)
|
||||
}
|
||||
|
||||
viper.SetConfigName(".diskii") // name of config file (without extension)
|
||||
viper.AddConfigPath("$HOME") // adding home directory as first search path
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
// If a config file is found, read it in.
|
||||
if err := viper.ReadInConfig(); err == nil {
|
||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
||||
}
|
||||
}
|
247
lib/applesoft/applesoft.go
Normal file
247
lib/applesoft/applesoft.go
Normal file
@ -0,0 +1,247 @@
|
||||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// Package applesoft provides routines for working with Applesoft
|
||||
// files.
|
||||
package applesoft
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// TokensByCode is a map from byte value to token text.
|
||||
var TokensByCode = map[byte]string{
|
||||
0x80: "END",
|
||||
0x81: "FOR",
|
||||
0x82: "NEXT",
|
||||
0x83: "DATA",
|
||||
0x84: "INPUT",
|
||||
0x85: "DEL",
|
||||
0x86: "DIM",
|
||||
0x87: "READ",
|
||||
0x88: "GR",
|
||||
0x89: "TEXT",
|
||||
0x8A: "PR #",
|
||||
0x8B: "IN #",
|
||||
0x8C: "CALL",
|
||||
0x8D: "PLOT",
|
||||
0x8E: "HLIN",
|
||||
0x8F: "VLIN",
|
||||
0x90: "HGR2",
|
||||
0x91: "HGR",
|
||||
0x92: "HCOLOR=",
|
||||
0x93: "HPLOT",
|
||||
0x94: "DRAW",
|
||||
0x95: "XDRAW",
|
||||
0x96: "HTAB",
|
||||
0x97: "HOME",
|
||||
0x98: "ROT=",
|
||||
0x99: "SCALE=",
|
||||
0x9A: "SHLOAD",
|
||||
0x9B: "TRACE",
|
||||
0x9C: "NOTRACE",
|
||||
0x9D: "NORMAL",
|
||||
0x9E: "INVERSE",
|
||||
0x9F: "FLASH",
|
||||
0xA0: "COLOR=",
|
||||
0xA1: "POP",
|
||||
0xA2: "VTAB",
|
||||
0xA3: "HIMEM:",
|
||||
0xA4: "LOMEM:",
|
||||
0xA5: "ONERR",
|
||||
0xA6: "RESUME",
|
||||
0xA7: "RECALL",
|
||||
0xA8: "STORE",
|
||||
0xA9: "SPEED=",
|
||||
0xAA: "LET",
|
||||
0xAB: "GOTO",
|
||||
0xAC: "RUN",
|
||||
0xAD: "IF",
|
||||
0xAE: "RESTORE",
|
||||
0xAF: "&",
|
||||
0xB0: "GOSUB",
|
||||
0xB1: "RETURN",
|
||||
0xB2: "REM",
|
||||
0xB3: "STOP",
|
||||
0xB4: "ON",
|
||||
0xB5: "WAIT",
|
||||
0xB6: "LOAD",
|
||||
0xB7: "SAVE",
|
||||
0xB8: "DEF FN",
|
||||
0xB9: "POKE",
|
||||
0xBA: "PRINT",
|
||||
0xBB: "CONT",
|
||||
0xBC: "LIST",
|
||||
0xBD: "CLEAR",
|
||||
0xBE: "GET",
|
||||
0xBF: "NEW",
|
||||
0xC0: "TAB",
|
||||
0xC1: "TO",
|
||||
0xC2: "FN",
|
||||
0xC3: "SPC(",
|
||||
0xC4: "THEN",
|
||||
0xC5: "AT",
|
||||
0xC6: "NOT",
|
||||
0xC7: "STEP",
|
||||
0xC8: "+",
|
||||
0xC9: "-",
|
||||
0xCA: "*",
|
||||
0xCB: "/",
|
||||
0xCC: ";",
|
||||
0xCD: "AND",
|
||||
0xCE: "OR",
|
||||
0xCF: ">",
|
||||
0xD0: "=",
|
||||
0xD1: "<",
|
||||
0xD2: "SGN",
|
||||
0xD3: "INT",
|
||||
0xD4: "ABS",
|
||||
0xD5: "USR",
|
||||
0xD6: "FRE",
|
||||
0xD7: "SCRN (",
|
||||
0xD8: "PDL",
|
||||
0xD9: "POS",
|
||||
0xDA: "SQR",
|
||||
0xDB: "RND",
|
||||
0xDC: "LOG",
|
||||
0xDD: "EXP",
|
||||
0xDE: "COS",
|
||||
0xDF: "SIN",
|
||||
0xE0: "TAN",
|
||||
0xE1: "ATN",
|
||||
0xE2: "PEEK",
|
||||
0xE3: "LEN",
|
||||
0xE4: "STR$",
|
||||
0xE5: "VAL",
|
||||
0xE6: "ASC",
|
||||
0xE7: "CHR$",
|
||||
0xE8: "LEFT$",
|
||||
0xE9: "RIGHT$",
|
||||
0xEA: "MID$",
|
||||
}
|
||||
|
||||
// Listing holds a listing of an entire BASIC program.
|
||||
type Listing []Line
|
||||
|
||||
// Line holds a single BASIC line, with line number and text.
|
||||
type Line struct {
|
||||
Num int
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// Decode turns a raw binary file into a basic program. Location
|
||||
// specifies the program's location in RAM (0x801 for in-ROM Applesoft, 0x3001 for tape-loaded Applesoft).
|
||||
func Decode(raw []byte, location uint16) (Listing, error) {
|
||||
// First two bytes of Applesoft files on disk are length. Let's be
|
||||
// tolerant to getting either format.
|
||||
if len(raw) >= 2 {
|
||||
size := int(raw[0]) + (256 * int(raw[1]))
|
||||
if size == len(raw)-2 || size == len(raw)-3 {
|
||||
raw = raw[2:]
|
||||
}
|
||||
}
|
||||
|
||||
bounds := fmt.Sprintf("$%X to $%X", location, int(location)+len(raw))
|
||||
|
||||
calcOffset := func(address int) int {
|
||||
return address - int(location)
|
||||
}
|
||||
listing := []Line{}
|
||||
last := 0 // last line number
|
||||
next := int(location)
|
||||
for next != 0 {
|
||||
ofs := calcOffset(next)
|
||||
if ofs < -1 || ofs+1 >= len(raw) {
|
||||
return nil, fmt.Errorf("line %d has next line at $%X, which is outside the input range of %s", last, next, bounds)
|
||||
}
|
||||
next = int(raw[ofs]) + 256*int(raw[ofs+1])
|
||||
ofs += 2
|
||||
if next == 0 {
|
||||
break
|
||||
}
|
||||
if ofs+1 >= len(raw) {
|
||||
if len(listing) == 0 {
|
||||
return nil, fmt.Errorf("ran out of input trying to read the first line number")
|
||||
}
|
||||
return nil, fmt.Errorf("ran out of input trying to read line number of line after %d", last)
|
||||
}
|
||||
line := Line{Num: int(raw[ofs]) + 256*int(raw[ofs+1])}
|
||||
ofs += 2
|
||||
for {
|
||||
if ofs >= len(raw) {
|
||||
return nil, fmt.Errorf("Ran out of input at location $%X in line %d", ofs+int(location), line.Num)
|
||||
}
|
||||
char := raw[ofs]
|
||||
if char == 0 {
|
||||
break
|
||||
}
|
||||
if char < 0x80 {
|
||||
line.Bytes = append(line.Bytes, char)
|
||||
} else {
|
||||
token := TokensByCode[char]
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("unknown token $%X in line %d", char, line.Num)
|
||||
}
|
||||
line.Bytes = append(line.Bytes, char)
|
||||
}
|
||||
ofs++
|
||||
}
|
||||
listing = append(listing, line)
|
||||
}
|
||||
|
||||
return listing, nil
|
||||
}
|
||||
|
||||
func (l Line) String() string {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "%d ", l.Num)
|
||||
for _, char := range l.Bytes {
|
||||
if char < 0x80 {
|
||||
buf.WriteByte(char)
|
||||
} else {
|
||||
token := TokensByCode[char]
|
||||
buf.WriteString(" " + token + " ")
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (l Listing) String() string {
|
||||
var buf bytes.Buffer
|
||||
for _, line := range l {
|
||||
buf.WriteString(line.String())
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
var controlCharRegexp = regexp.MustCompile(`[\x00-\x1F]`)
|
||||
|
||||
// ChevronControlCodes converts ASCII control characters like chr(4)
|
||||
// to chevron-surrounded codes like «ctrl-D».
|
||||
func ChevronControlCodes(s string) string {
|
||||
return controlCharRegexp.ReplaceAllStringFunc(s, func(s string) string {
|
||||
if s == "\n" || s == "\t" {
|
||||
return s
|
||||
}
|
||||
if s >= "\x01" && s <= "\x1a" {
|
||||
return "«ctrl-" + string('A'-1+s[0]) + "»"
|
||||
}
|
||||
code := "?"
|
||||
switch s[0] {
|
||||
case '\x00':
|
||||
code = "NUL"
|
||||
case '\x1C':
|
||||
code = "FS"
|
||||
case '\x1D':
|
||||
code = "GS"
|
||||
case '\x1E':
|
||||
code = "RS"
|
||||
case '\x1F':
|
||||
code = "US"
|
||||
}
|
||||
|
||||
return "«" + code + "»"
|
||||
})
|
||||
}
|
53
lib/applesoft/hello_test.go
Normal file
53
lib/applesoft/hello_test.go
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
package applesoft
|
||||
|
||||
import "testing"
|
||||
|
||||
// helloBinary is a simple basic program used for testing. Listing
|
||||
// below.
|
||||
var helloBinary = []byte{
|
||||
0x11, 0x08, 0x0A, 0x00, 0xBA, 0x22, 0x04, 0x43, 0x41, 0x54, 0x41, 0x4C, 0x4F, 0x47, 0x22, 0x00,
|
||||
0x17, 0x08, 0x14, 0x00, 0xBA, 0x00, 0x25, 0x08, 0x1E, 0x00, 0xBA, 0x22, 0x48, 0x45, 0x4C, 0x4C,
|
||||
0x4F, 0x22, 0x3B, 0x00, 0x34, 0x08, 0x28, 0x00, 0xBA, 0x22, 0x2C, 0x20, 0x57, 0x4F, 0x52, 0x4C,
|
||||
0x44, 0x22, 0x00, 0x49, 0x08, 0x32, 0x00, 0x81, 0x49, 0xD0, 0x31, 0xC1, 0x34, 0x30, 0x3A, 0xBA,
|
||||
0x22, 0x2A, 0x22, 0x3B, 0x3A, 0x82, 0x49, 0x00, 0x6B, 0x08, 0x3C, 0x00, 0x81, 0x49, 0xD0, 0x31,
|
||||
0xC1, 0x31, 0x30, 0x3A, 0x81, 0x4A, 0xD0, 0x31, 0xC1, 0x49, 0x3A, 0xBA, 0x22, 0x20, 0x22, 0x3B,
|
||||
0x3A, 0x82, 0x3A, 0xBA, 0x22, 0x2A, 0x22, 0x3A, 0x82, 0x00, 0x98, 0x08, 0x46, 0x00, 0xB2, 0x22,
|
||||
0x54, 0x48, 0x49, 0x53, 0x20, 0x49, 0x53, 0x20, 0x41, 0x20, 0x54, 0x45, 0x53, 0x54, 0x20, 0x4F,
|
||||
0x46, 0x20, 0x41, 0x20, 0x4C, 0x4F, 0x4E, 0x47, 0x20, 0x4C, 0x49, 0x4E, 0x45, 0x20, 0x4F, 0x46,
|
||||
0x20, 0x54, 0x45, 0x58, 0x54, 0x22, 0x00, 0xEC, 0x08, 0x50, 0x00, 0xBA, 0x22, 0x54, 0x48, 0x49,
|
||||
0x53, 0x20, 0x49, 0x53, 0x20, 0x41, 0x20, 0x54, 0x45, 0x53, 0x54, 0x20, 0x4F, 0x46, 0x20, 0x41,
|
||||
0x4E, 0x20, 0x45, 0x56, 0x45, 0x4E, 0x20, 0x4C, 0x4F, 0x4E, 0x47, 0x45, 0x52, 0x20, 0x4C, 0x49,
|
||||
0x4E, 0x45, 0x20, 0x4F, 0x46, 0x20, 0x54, 0x45, 0x58, 0x54, 0x20, 0x54, 0x48, 0x41, 0x54, 0x20,
|
||||
0x49, 0x53, 0x20, 0x45, 0x56, 0x45, 0x4E, 0x20, 0x4D, 0x4F, 0x52, 0x45, 0x20, 0x54, 0x48, 0x41,
|
||||
0x4E, 0x20, 0x38, 0x30, 0x20, 0x43, 0x4F, 0x4C, 0x53, 0x22, 0x00, 0x04, 0x09, 0x5A, 0x00, 0xBA,
|
||||
0x22, 0x41, 0x4C, 0x4C, 0x20, 0x44, 0x4F, 0x4E, 0x45, 0x20, 0x54, 0x45, 0x53, 0x54, 0x49, 0x4E,
|
||||
0x47, 0x22, 0x00, 0x00, 0x00, 0x0A,
|
||||
}
|
||||
|
||||
// helloListing is the text version of the basic program above. Note
|
||||
// that there are trailing newlines on lines 20 and 60.
|
||||
var helloListing = `10 PRINT "«ctrl-D»CATALOG"
|
||||
20 PRINT
|
||||
30 PRINT "HELLO";
|
||||
40 PRINT ", WORLD"
|
||||
50 FOR I = 1 TO 40: PRINT "*";: NEXT I
|
||||
60 FOR I = 1 TO 10: FOR J = 1 TO I: PRINT " ";: NEXT : PRINT "*": NEXT
|
||||
70 REM "THIS IS A TEST OF A LONG LINE OF TEXT"
|
||||
80 PRINT "THIS IS A TEST OF AN EVEN LONGER LINE OF TEXT THAT IS EVEN MORE THAN 80 COLS"
|
||||
90 PRINT "ALL DONE TESTING"
|
||||
`
|
||||
|
||||
// TestParse tests the full parsing and output of a basic program from
|
||||
// bytes.
|
||||
func TestParse(t *testing.T) {
|
||||
listing, err := Decode(helloBinary, 0x801)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
text := ChevronControlCodes(listing.String())
|
||||
if text != helloListing {
|
||||
t.Fatalf("Wrong listing; want:\n%s\ngot:\n%s", helloListing, text)
|
||||
}
|
||||
}
|
10
lib/applesoft/notes.org
Normal file
10
lib/applesoft/notes.org
Normal file
@ -0,0 +1,10 @@
|
||||
** Program location
|
||||
Defaults to $801 for ROM-based Applesoft. Locations $67/$68 point to
|
||||
the start of the program, and the byte preceding it must be #0.
|
||||
|
||||
Details:
|
||||
- http://www.atarimagazines.com/compute/issue11/36_1_THE_APPLE_GAZETTE_RESOLVING_APPLESOFT_AND_HIRES_GRAPHICS_MEMORY_CONFLICTS.php
|
||||
- http://retrocomputing.stackexchange.com/questions/1604
|
||||
|
||||
** Format
|
||||
DOS stores an additional byte on the end, which should be ignored.
|
17
lib/helpers/helpers.go
Normal file
17
lib/helpers/helpers.go
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright © 2016 Zellyn Hunter <zellyn@gmail.com>
|
||||
|
||||
// Package helpers contains various routines used to help cobra
|
||||
// commands stay succinct.
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func FileContentsOrStdIn(s string) ([]byte, error) {
|
||||
if s == "-" {
|
||||
return ioutil.ReadAll(os.Stdin)
|
||||
}
|
||||
return ioutil.ReadFile(s)
|
||||
}
|
Loading…
Reference in New Issue
Block a user