1 // "ainsl" sub-command: "append if no such line" (inspired by FAI's ainsl),
2 // append lines to files (if not present) without replacing the file
5 // FAI: https://fai-project.org
7 // Copyright (C) 2021 Simon Ruderich
9 // This program is free software: you can redistribute it and/or modify
10 // it under the terms of the GNU General Public License as published by
11 // the Free Software Foundation, either version 3 of the License, or
12 // (at your option) any later version.
14 // This program is distributed in the hope that it will be useful,
15 // but WITHOUT ANY WARRANTY; without even the implied warranty of
16 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 // GNU General Public License for more details.
19 // You should have received a copy of the GNU General Public License
20 // along with this program. If not, see <http://www.gnu.org/licenses/>.
35 "ruderich.org/simon/safcm/cmd/safcm-remote/sync"
38 func Main(args []string) error {
40 fmt.Fprintf(os.Stderr,
41 "usage: %s ainsl [<options>] <path> <line>\n",
46 optionCreate := flag.Bool("create", false,
47 "create the path if it does not exist")
49 flag.CommandLine.Parse(args[2:])
56 changes, err := handle(flag.Args()[0], flag.Args()[1], *optionCreate)
58 return fmt.Errorf("ainsl: %v", err)
60 for _, x := range changes {
61 fmt.Fprintln(os.Stderr, x)
66 func handle(path string, line string, create bool) ([]string, error) {
67 // See safcm-remote/sync/files.go for details on the implementation
70 return nil, fmt.Errorf("empty line")
72 if strings.Contains(line, "\n") {
73 return nil, fmt.Errorf("line must not contain newlines: %q",
81 data, stat, err := readFileNoFollow(path)
83 if !os.IsNotExist(err) {
87 return nil, fmt.Errorf(
88 "%q: file does not exist, use -create",
92 uid, gid = os.Getuid(), os.Getgid()
93 // Read current umask. Don't do this in programs where
94 // multiple goroutines create files because this is inherently
95 // racy! No goroutines here, so it's fine.
96 umask := syscall.Umask(0)
98 // Apply umask to created file
99 mode = 0666 & ^fs.FileMode(umask)
101 changes = append(changes,
102 fmt.Sprintf("%q: created file (%d/%d %s)",
103 path, uid, gid, mode))
106 // Preserve user/group and mode of existing file
107 x, ok := stat.Sys().(*syscall.Stat_t)
109 return nil, fmt.Errorf("unsupported Stat().Sys()")
115 stat = nil // prevent accidental use
117 // Check if the expected line is present
119 for _, x := range bytes.Split(data, []byte("\n")) {
120 if string(x) == line {
125 // Make sure the file has a trailing newline. This enforces symmetry
126 // with our changes. Whenever we add a line we also append a trailing
127 // newline. When we conclude that no changes are necessary the file
128 // should be in the same state as we would leave it if there were
130 if len(data) != 0 && data[len(data)-1] != '\n' {
131 data = append(data, '\n')
132 changes = append(changes,
133 fmt.Sprintf("%q: added missing trailing newline",
137 // Line present, nothing to do
138 if found && len(changes) == 0 {
144 data = append(data, []byte(line+"\n")...)
145 changes = append(changes,
146 fmt.Sprintf("%q: added line %q", path, line))
149 // Write via temporary file and rename
150 dir := filepath.Dir(path)
151 base := filepath.Base(path)
152 tmpPath, err := sync.WriteTemp(dir, "."+base, data, uid, gid, mode)
156 err = os.Rename(tmpPath, path)
161 err = sync.SyncPath(dir)
169 func readFileNoFollow(path string) ([]byte, fs.FileInfo, error) {
170 fh, err := sync.OpenFileNoFollow(path)
176 stat, err := fh.Stat()
180 if stat.Mode().Type() != 0 /* regular file */ {
181 return nil, nil, fmt.Errorf("%q is not a regular file but %s",
182 path, stat.Mode().Type())
185 x, err := io.ReadAll(fh)
187 return nil, nil, fmt.Errorf("%q: %v", path, err)