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 // SPDX-License-Identifier: GPL-3.0-or-later
8 // Copyright (C) 2021-2024 Simon Ruderich
22 "golang.org/x/sys/unix"
24 "ruderich.org/simon/safcm/remote/sync"
27 func Main(args []string) error {
29 fmt.Fprintf(os.Stderr,
30 "usage: %s ainsl [<options>] <path> <line>\n",
35 optionCreate := flag.Bool("create", false,
36 "create the path if it does not exist")
38 flag.CommandLine.Parse(args[2:]) //nolint:errcheck
45 changes, err := handle(flag.Args()[0], flag.Args()[1], *optionCreate)
47 return fmt.Errorf("ainsl: %v", err)
49 for _, x := range changes {
50 fmt.Fprintln(os.Stderr, x)
55 func handle(path string, line string, create bool) ([]string, error) {
56 // See safcm-remote/sync/files.go for details on the implementation
59 return nil, fmt.Errorf("empty line")
61 if strings.Contains(line, "\n") {
62 return nil, fmt.Errorf("line must not contain newlines: %q",
66 parentFd, baseName, err := sync.OpenParentDirectoryNoSymlinks(path)
70 defer unix.Close(parentFd)
76 data, stat, err := readFileAtNoFollow(parentFd, baseName)
78 if !os.IsNotExist(err) {
79 return nil, fmt.Errorf("%q: %v", path, err)
82 return nil, fmt.Errorf(
83 "%q: file does not exist, use -create",
87 uid, gid = os.Getuid(), os.Getgid()
88 // Read current umask. Don't do this in programs where
89 // multiple goroutines create files because this is inherently
90 // racy! No goroutines here, so it's fine.
91 umask := syscall.Umask(0)
93 // Apply umask to created file
94 mode = 0666 & ^fs.FileMode(umask)
96 changes = append(changes,
97 fmt.Sprintf("%q: created file (%d/%d %s)",
98 path, uid, gid, mode))
101 // Preserve user/group and mode of existing file
102 x, ok := stat.Sys().(*syscall.Stat_t)
104 return nil, fmt.Errorf("unsupported Stat().Sys()")
110 stat = nil //nolint:wastedassign // prevent accidental use
112 // Check if the expected line is present
114 for _, x := range bytes.Split(data, []byte("\n")) {
115 if string(x) == line {
120 // Make sure the file has a trailing newline. This enforces symmetry
121 // with our changes. Whenever we add a line we also append a trailing
122 // newline. When we conclude that no changes are necessary the file
123 // should be in the same state as we would leave it if there were
125 if len(data) != 0 && data[len(data)-1] != '\n' {
126 data = append(data, '\n')
127 changes = append(changes,
128 fmt.Sprintf("%q: added missing trailing newline",
132 // Line present, nothing to do
133 if found && len(changes) == 0 {
139 data = append(data, []byte(line+"\n")...)
140 changes = append(changes,
141 fmt.Sprintf("%q: added line %q", path, line))
144 // Write via temporary file and rename
145 tmpBase, err := sync.WriteTempAt(parentFd, "."+baseName,
146 data, uid, gid, mode)
150 err = unix.Renameat(parentFd, tmpBase, parentFd, baseName)
152 unix.Unlinkat(parentFd, tmpBase, 0 /* flags */) //nolint:errcheck
155 err = unix.Fsync(parentFd)
163 func readFileAtNoFollow(dirfd int, base string) ([]byte, fs.FileInfo, error) {
164 fh, err := sync.OpenAtNoFollow(dirfd, base)
170 stat, err := fh.Stat()
174 if stat.Mode().Type() != 0 /* regular file */ {
175 return nil, nil, fmt.Errorf("not a regular file but %s",
179 x, err := io.ReadAll(fh)