├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── fstab.go ├── mount.go └── mount_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, deniswernert 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-fstab 2 | ======== 3 | 4 | [![Build Status](https://travis-ci.org/deniswernert/go-fstab.svg?branch=master)](https://travis-ci.org/deniswernert/go-fstab) 5 | 6 | Simple fstab parser for Go 7 | -------------------------------------------------------------------------------- /fstab.go: -------------------------------------------------------------------------------- 1 | // Package fstab parses and serializes linux filesystem mounts information 2 | package fstab 3 | 4 | import ( 5 | "bufio" 6 | "fmt" 7 | "io" 8 | "os" 9 | ) 10 | 11 | type Mounts []*Mount 12 | 13 | // String serializes a list of mounts to the fstab format 14 | func (mounts Mounts) String() (output string) { 15 | for i, mount := range mounts { 16 | if i > 0 { 17 | output += "\n" 18 | } 19 | output += mount.String() 20 | } 21 | 22 | return 23 | } 24 | 25 | // PaddedString serializes a list of mounts to the fstab format with padding. 26 | func (mounts Mounts) PaddedString(paddings ...int) (output string) { 27 | for i, mount := range mounts { 28 | if i > 0 { 29 | output += "\n" 30 | } 31 | output += mount.PaddedString(paddings...) 32 | } 33 | 34 | return 35 | } 36 | 37 | // ParseSystem parses your system fstab ("/etc/fstab") 38 | func ParseSystem() (mounts Mounts, err error) { 39 | return ParseFile("/etc/fstab") 40 | } 41 | 42 | // ParseProc parses procfs information 43 | func ParseProc() (mounts Mounts, err error) { 44 | return ParseFile("/proc/mounts") 45 | } 46 | 47 | // ParseFile parses the given file 48 | func ParseFile(filename string) (mounts Mounts, err error) { 49 | file, err := os.Open(filename) 50 | if nil != err { 51 | return nil, err 52 | } else { 53 | defer file.Close() 54 | return Parse(file) 55 | } 56 | } 57 | 58 | func Parse(source io.Reader) (mounts Mounts, err error) { 59 | mounts = make([]*Mount, 0, 10) 60 | 61 | scanner := bufio.NewScanner(source) 62 | lineNo := 0 63 | 64 | for scanner.Scan() { 65 | lineNo++ 66 | mount, err := ParseLine(scanner.Text()) 67 | if nil != err { 68 | return nil, fmt.Errorf("Syntax error at line %d: %s", lineNo, err) 69 | } 70 | 71 | if nil != mount { 72 | mounts = append(mounts, mount) 73 | } 74 | } 75 | 76 | if err := scanner.Err(); err != nil { 77 | return nil, err 78 | } 79 | 80 | return mounts, nil 81 | } 82 | -------------------------------------------------------------------------------- /mount.go: -------------------------------------------------------------------------------- 1 | package fstab 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Mount represetnts the filesystem info 11 | type Mount struct { 12 | // The block special device or remote filesystem to be mounted 13 | Spec string 14 | 15 | // The mount point for the filesystem 16 | File string 17 | 18 | // The type of the filesystem 19 | VfsType string 20 | 21 | // Mount options associated with the filesystem 22 | MntOps map[string]string 23 | 24 | // Used by dump to determine which filesystems need to be dumped. 25 | Freq int 26 | 27 | // Used by the fsck(8) program to determine the order in which filesystem checks are done at reboot time 28 | PassNo int 29 | } 30 | 31 | type DeviceIdentifierType int 32 | 33 | const ( 34 | Path DeviceIdentifierType = iota 35 | Label DeviceIdentifierType = iota 36 | UUID DeviceIdentifierType = iota 37 | PartUUID DeviceIdentifierType = iota 38 | PartLabel DeviceIdentifierType = iota 39 | ) 40 | 41 | // parseOptions parses the options field into an array of strings 42 | func parseOptions(optionsString string) (options map[string]string) { 43 | options = make(map[string]string) 44 | for _, option := range strings.Split(optionsString, ",") { 45 | bits := strings.Split(strings.TrimSpace(option), "=") 46 | if len(bits) > 1 { 47 | options[bits[0]] = bits[1] 48 | } else { 49 | options[bits[0]] = "" 50 | } 51 | } 52 | return 53 | } 54 | 55 | // MntOpsString returns the serialized MntOps value 56 | func (mount *Mount) MntOpsString() (opsstring string) { 57 | first := true 58 | for key, value := range mount.MntOps { 59 | if first { 60 | first = false 61 | } else { 62 | opsstring += "," 63 | } 64 | 65 | opsstring += key 66 | 67 | if "" != value { 68 | opsstring += "=" + value 69 | } 70 | } 71 | return 72 | } 73 | 74 | // String serializes the object into fstab format 75 | func (mount *Mount) String() string { 76 | return mount.format("%s %s %s %s %d %d") 77 | } 78 | 79 | // format serializes the object according to the given format 80 | func (mount *Mount) format(format string) string { 81 | return fmt.Sprintf(format, mount.Spec, mount.File, mount.VfsType, mount.MntOpsString(), mount.Freq, mount.PassNo) 82 | } 83 | 84 | // PaddedString serializes the objet into fstab format with configurable column width. 85 | // Each positional argument specifies the width for the column in order. Up to 6 arguments 86 | // are supported, outstanding arguments will be ignored. 87 | func (mount *Mount) PaddedString(paddings ...int) string { 88 | stringPaddings := 4 89 | intPaddings := 2 90 | if len(paddings) < stringPaddings { 91 | stringPaddings = len(paddings) 92 | intPaddings = 0 93 | } else { 94 | intPaddings = len(paddings) - stringPaddings 95 | if intPaddings > 2 { 96 | intPaddings = 2 97 | } 98 | } 99 | 100 | var fields []string = make([]string, 0, 6) 101 | { 102 | for _, padding := range paddings[:stringPaddings] { 103 | fields = append(fields, "%-"+strconv.Itoa(padding)+"s") 104 | } 105 | 106 | for i := len(fields); i < 4; i++ { 107 | fields = append(fields, "%s") 108 | } 109 | } 110 | 111 | if intPaddings > 0 { 112 | for _, padding := range paddings[4:(4 + intPaddings)] { 113 | fields = append(fields, "%"+strconv.Itoa(padding)+"d") 114 | } 115 | } 116 | 117 | for i := len(fields); i < 6; i++ { 118 | fields = append(fields, "%d") 119 | } 120 | 121 | fmt.Printf("%d %d\n%v\n%v\n", stringPaddings, intPaddings, paddings, fields) 122 | return mount.format(strings.Join(fields, " ")) 123 | } 124 | 125 | func (mount *Mount) IsSwap() bool { 126 | return "swap" == mount.VfsType 127 | } 128 | 129 | func (mount *Mount) IsNFS() bool { 130 | return "nfs" == mount.VfsType 131 | } 132 | 133 | // Equals compares 2 Mount objects 134 | func (mount *Mount) Equals(other *Mount) bool { 135 | return reflect.DeepEqual(*mount, *other) 136 | } 137 | 138 | // SpecType returns the device identifier type 139 | func (mount *Mount) SpecType() (spectype DeviceIdentifierType) { 140 | bits := strings.Split(mount.Spec, "=") 141 | switch strings.ToUpper(bits[0]) { 142 | case "UUID": 143 | spectype = UUID 144 | 145 | case "LABEL": 146 | spectype = Label 147 | 148 | case "PARTUUID": 149 | spectype = PartUUID 150 | 151 | case "PARTLABEL": 152 | spectype = PartLabel 153 | 154 | default: 155 | spectype = Path 156 | } 157 | return 158 | } 159 | 160 | // SpecType returns the device identifier value; that is if Spec is 161 | // "UUID=vogons-ate-my-sandwich", it will return "vogons-ate-my-sandwich" 162 | func (mount *Mount) SpecValue() string { 163 | bits := strings.Split(mount.Spec, "=") 164 | if 1 == len(bits) { 165 | return mount.Spec 166 | } else { 167 | return bits[1] 168 | } 169 | } 170 | 171 | // ParseLine parses a single line (of an fstab). 172 | // It will most frequently return a Mount; however, 173 | // If a parsing error occurs, `err` will be non-nil and provide an error message. 174 | // If the line is either empy or a comment line, `mount` will also be nil. 175 | func ParseLine(line string) (mount *Mount, err error) { 176 | line = strings.TrimSpace(line) 177 | 178 | // Lines starting with a pound sign (#) are comments, and are ignored. So are empty lines. 179 | if ("" == line) || (line[0] == '#') { 180 | return nil, nil 181 | } 182 | 183 | fields := strings.Fields(line) 184 | if len(fields) < 4 { 185 | return nil, fmt.Errorf("too few fields (%d), at least 4 are expected", len(fields)) 186 | } else { 187 | mount = new(Mount) 188 | mount.Spec = fields[0] 189 | mount.File = fields[1] 190 | mount.VfsType = fields[2] 191 | mount.MntOps = parseOptions(fields[3]) 192 | 193 | var convErr error 194 | 195 | if len(fields) > 4 { 196 | mount.Freq, convErr = strconv.Atoi(fields[4]) 197 | if nil != convErr { 198 | return nil, fmt.Errorf("%s is not a number", fields[4]) 199 | } 200 | } 201 | 202 | if len(fields) > 5 { 203 | mount.PassNo, convErr = strconv.Atoi(fields[5]) 204 | if nil != convErr { 205 | return nil, fmt.Errorf("%s it not a number", fields[5]) 206 | } 207 | } 208 | } 209 | 210 | return 211 | } 212 | -------------------------------------------------------------------------------- /mount_test.go: -------------------------------------------------------------------------------- 1 | package fstab 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | var successfulParseLineExpectations map[string]Mount = map[string]Mount{ 9 | "/dev/sda / ext4 defaults 1 2": Mount{ 10 | "/dev/sda", 11 | "/", 12 | "ext4", 13 | map[string]string{ 14 | "defaults": "", 15 | }, 16 | 1, 17 | 2, 18 | }, 19 | 20 | "UUID=homer / ext4 rw,uid=0": Mount{ 21 | "UUID=homer", 22 | "/", 23 | "ext4", 24 | map[string]string{ 25 | "uid": "0", 26 | "rw": "", 27 | }, 28 | 0, 29 | 0, 30 | }, 31 | } 32 | 33 | var successfulMountStringExpectations map[string]Mount = map[string]Mount{ 34 | "/dev/sda / ext4 defaults 1 2": Mount{ 35 | "/dev/sda", 36 | "/", 37 | "ext4", 38 | map[string]string{ 39 | "defaults": "", 40 | }, 41 | 1, 42 | 2, 43 | }, 44 | 45 | "UUID=homer / ext4 uid=0 0 0": Mount{ 46 | "UUID=homer", 47 | "/", 48 | "ext4", 49 | map[string]string{ 50 | "uid": "0", 51 | }, 52 | 0, 53 | 0, 54 | }, 55 | } 56 | 57 | func TestParseLine(t *testing.T) { 58 | for line, expectation := range successfulParseLineExpectations { 59 | mount, err := ParseLine(line) 60 | if nil != err { 61 | t.Errorf("Unexpected parse error while parsing '%s': %s", line, err) 62 | continue 63 | } 64 | 65 | if !mount.Equals(&expectation) { 66 | t.Errorf("Expected %+v, got %+v", expectation, mount) 67 | } 68 | 69 | if 0 == strings.Index(mount.Spec, "UUID") && mount.SpecType() != UUID { 70 | t.Errorf("Expected SpecType to be UUID") 71 | } 72 | } 73 | } 74 | 75 | func TestMountString(t *testing.T) { 76 | for expectation, mount := range successfulMountStringExpectations { 77 | str := mount.String() 78 | if str != expectation { 79 | t.Errorf("Expected '%s', got '%s'", expectation, str) 80 | } 81 | } 82 | } 83 | --------------------------------------------------------------------------------