diff --git a/coreutils.go b/coreutils.go
index b774e7c..7c623e3 100644
--- a/coreutils.go
+++ b/coreutils.go
@@ -1,6 +1,7 @@
package coreutils
import (
+ "context"
"errors"
"io"
"sync"
@@ -18,9 +19,10 @@ func Register(name string, r Runnable) {
cmds[name] = r
}
-type Runnable func(ctx Ctx, args ...string) error
+type Runnable func(ctx Context, args ...string) error
-type Ctx struct {
+type Context struct {
+ context.Context
Dir string
GetEnv func(string) string
Stdin io.Reader
@@ -28,7 +30,7 @@ type Ctx struct {
Stderr io.Writer
}
-func Run(ctx Ctx, name string, args ...string) error {
+func Run(ctx Context, name string, args ...string) error {
cmdsMu.Lock()
fn := cmds[name]
cmdsMu.Unlock()
diff --git a/rm/cmd.go b/rm/cmd.go
new file mode 100644
index 0000000..ac28a8a
--- /dev/null
+++ b/rm/cmd.go
@@ -0,0 +1,189 @@
+package rm
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ coreutils "github.com/ericlagergren/go-coreutils"
+ flag "github.com/spf13/pflag"
+)
+
+func init() {
+ coreutils.Register("rm", run)
+}
+
+// Sentinal flags for default values or flags with single-character options and
+// without multi-character options. (e.g., if we want -i but not --i.)
+const (
+ uniNonChar = 0xFDD0
+ interDefault = string(uniNonChar + 1)
+ bad1 = string(uniNonChar + 2)
+ bad2 = string(uniNonChar + 3)
+ bad3 = string(uniNonChar + 4)
+)
+
+func newCommand() *cmd {
+ var c cmd
+ c.f.BoolVarP(&c.force, "force", "f", false, "ignore non-existent files and arguments; never prompt prior to removal")
+ c.f.BoolVarP(&c.moreInter, bad1, "i", false, "prompt before each removal")
+ c.f.BoolVarP(&c.lessInter, bad2, "I", false, "prompt (once) prior to removing more than three files or when removing recursively")
+ c.f.StringVar(&c.interactive, "interactive", interDefault, "prompt: 'never', 'once' (-i), 'always' (-I)")
+ c.f.BoolVar(&c.oneFileSystem, "one-file-system", false, "when recursing, skip directories that are on a different filesystem")
+ c.f.BoolVar(&c.preserveRoot, "preserve-root", true, "do not remove '/'")
+ c.f.BoolVar(&c.noPreserveRoot, "no-preserve-root", false, "do not special-case '/'")
+ c.f.BoolVarP(&c.recursive, "recursive", "r", false, "remove directories and their contents recursively")
+ c.f.BoolVarP(&c.recursive, bad3, "R", false, "remove directories and their contents recursively")
+ c.f.BoolVarP(&c.rmdir, "dir", "d", false, "remove empty directories")
+ c.f.BoolVarP(&c.verbose, "verbose", "v", false, "explain what's occurring")
+ c.f.BoolVar(&c.version, "version", false, "print version information and exit")
+ return &c
+}
+
+type cmd struct {
+ f flag.FlagSet
+ force bool
+ moreInter, lessInter bool
+ interactive string
+ preserveRoot bool
+ noPreserveRoot bool
+ oneFileSystem bool
+ recursive bool
+ rmdir bool
+ verbose bool
+ version bool
+}
+
+func run(ctx coreutils.Context, args ...string) error {
+ c := newCommand()
+ if err := c.f.Parse(args); err != nil {
+ return err
+ }
+
+ if c.version {
+ fmt.Fprintf(ctx.Stdout, "rm (go-coreutils) 1.0")
+ return nil
+ }
+
+ var opts RemoveOption
+ if c.noPreserveRoot && !c.preserveRoot {
+ opts |= NoPreserveRoot
+ }
+ if c.force {
+ opts |= Force
+ }
+ if c.recursive {
+ opts |= Recursive
+ }
+ if c.rmdir {
+ opts |= RemoveEmpty
+ }
+ if c.oneFileSystem {
+ opts |= OneFileSystem
+ }
+ if c.verbose {
+ opts |= Verbose
+ }
+ switch c.interactive {
+ case interDefault:
+ if c.moreInter {
+ opts |= PromptAlways
+ c.lessInter = false
+ }
+ case "never", "no", "none":
+ opts &= PromptAlways
+ case "once":
+ c.lessInter = true
+ opts &= IgnoreMissing
+ case "always", "yes", "":
+ opts |= PromptAlways
+ opts &= IgnoreMissing
+ default:
+ return errors.New("unknown interactive option: " + c.interactive)
+ }
+
+ if c.lessInter && (opts&Recursive != 0 || c.f.NArg() >= 3) {
+ n := c.f.NArg()
+ arg := "arguments"
+ adj := ""
+ if opts&Recursive != 0 {
+ adj = " recursively "
+ if n == 1 {
+ arg = "argument"
+ }
+ }
+ fmt.Fprintf(ctx.Stderr, "rm: remove %d %s%s? ", n, arg, adj)
+ switch yes, err := getYesNo(ctx.Stdin); {
+ case err != nil:
+ return err
+ case !yes:
+ return nil
+ }
+ }
+
+ r := NewRemover(opts)
+
+ if r.opts&PromptAlways != 0 {
+ r.Prompt = func(name string, opts PromptOption) bool {
+
+ wp := " "
+ if opts&WriteProtected != 0 {
+ wp = " write-protected "
+ }
+
+ msg := "rm: remove%s%s %q? "
+ typ := "file"
+ if opts&(Descend|Directory) != 0 {
+ typ = "directory"
+ if opts&Descend != 0 {
+ msg = "rm: descend into%s%s %q? "
+ }
+ }
+
+ fmt.Fprintf(ctx.Stderr, msg, wp, typ, name)
+ yes, err := getYesNo(ctx.Stdin)
+ return yes && err == nil
+ }
+ }
+
+ if r.Log != nil {
+ defer close(r.Log)
+ go func() {
+ for msg := range r.Log {
+ fmt.Fprintln(ctx.Stdout, msg)
+ }
+ }()
+ }
+
+ var nerrs int
+ for _, name := range c.f.Args() {
+ switch err := r.Remove(name); err.(type) {
+ case nil:
+ // OK
+ case rmError:
+ fmt.Fprintf(ctx.Stderr, "rm: %v\n", err)
+ default:
+ fmt.Fprintf(ctx.Stderr, "rm: %v\n", err)
+ return err
+ }
+ }
+ if nerrs > 0 {
+ return errNonFatal
+ }
+ return nil
+}
+
+var errNonFatal = errors.New("at least one non-fatal error occurred")
+
+func getYesNo(r io.Reader) (yes bool, err error) {
+ var resp string
+ fmt.Fscanln(r, &resp)
+ switch resp {
+ case "yes", "Y", "y":
+ return true, nil
+ case "no", "N", "n":
+ return false, nil
+ default:
+ return false, errors.New("unknown response (must be 'yes' or 'no')")
+ }
+}
diff --git a/rm/internal/sys/sys_unix.go b/rm/internal/sys/sys_unix.go
new file mode 100644
index 0000000..c78dcf2
--- /dev/null
+++ b/rm/internal/sys/sys_unix.go
@@ -0,0 +1,25 @@
+// +build !windows
+
+package sys
+
+import (
+ "os"
+ "syscall"
+)
+
+var root *syscall.Stat_t
+
+func init() {
+ if info, err := os.Lstat("/"); err == nil {
+ root = info.Sys().(*syscall.Stat_t)
+ }
+}
+
+func IsRoot(info os.FileInfo) bool {
+ stat := info.Sys().(*syscall.Stat_t)
+ return root.Ino == stat.Ino && root.Dev == stat.Dev
+}
+
+func DiffFS(orig, test os.FileInfo) bool {
+ return orig.Sys().(*syscall.Stat_t).Dev != test.Sys().(*syscall.Stat_t).Dev
+}
diff --git a/rm/internal/sys/sys_windows.go b/rm/internal/sys/sys_windows.go
new file mode 100644
index 0000000..d97cab9
--- /dev/null
+++ b/rm/internal/sys/sys_windows.go
@@ -0,0 +1,8 @@
+// +build windows
+
+package sys
+
+import "os"
+
+func IsRoot(_ os.FileInfo) bool { return false }
+func DiffFS(_, _ os.FileInfo) bool { return false }
diff --git a/rm/rm.go b/rm/rm.go
index 29c198b..4787c12 100644
--- a/rm/rm.go
+++ b/rm/rm.go
@@ -1,94 +1,235 @@
-/*
- Go rm - prints the current working directory.
- Copyright (C) 2015 Robert Deusser
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-*/
-
-/*
- Written by Robert Deusser
-*/
-
-package main
+package rm
import (
+ "errors"
"fmt"
"os"
+ "path/filepath"
+ "runtime"
- flag "github.com/ogier/pflag"
+ "github.com/ericlagergren/go-coreutils/rm/internal/sys"
+ "golang.org/x/sys/unix"
)
+type RemoveOption uint8
+
const (
- Help = `
-Usage: rm [OPTION]... FILE...
-Remove (unlink) the FILE(s).
-
- -f, --force ignore nonexistent files and arguments, never prompt
- -i prompt before every removal
- -I prompt once before removing more than three files, or
- when removing recursively; less intrusive than -i,
- while still giving protection against most mistakes
- --interactive[=WHEN] prompt according to WHEN: never, once (-I), or
- always (-i); without WHEN, prompt always
- --one-file-system when removing a hierarchy recursively, skip any
- directory that is on a file system different from
- that of the corresponding command line argument
- --no-preserve-root do not treat '/' specially
- --preserve-root do not remove '/' (default)
- -r, -R, --recursive remove directories and their contents recursively
- -d, --dir remove empty directories
- -v, --verbose explain what is being done
- --help display this help and exit
- --version output version information and exit
-
-By default, rm does not remove directories. Use the --recursive (-r or -R)
-option to remove each listed directory, too, along with all of its contents.
-
-To remove a file whose name starts with a '-', for example '-foo',
-use one of these commands:
- rm -- -foo
-
- rm ./-foo
-
-Note that if you use rm to remove a file, it might be possible to recover
-some of its contents, given sufficient expertise and/or time. For greater
-assurance that the contents are truly unrecoverable, consider using shred.
-
-`
- Version = `
-rm (Go coreutils) 0.1
-Copyright (C) 2015 Robert Deusser
-License GPLv3+: GNU GPL version 3 or later .
-This is free software: you are free to change and redistribute it.
-There is NO WARRANTY, to the extent permitted by law.
-
-`
+ NoPreserveRoot = 1 << iota
+ Force
+ Recursive
+ RemoveEmpty
+ IgnoreMissing
+ OneFileSystem
+ Verbose
+ PromptAlways
)
-var version = flag.BoolP("version", "v", false, "")
+type PromptOption uint8
+
+const (
+ // WriteProtected signals that the object to be removed or descended upon
+ // has write protection.
+ WriteProtected PromptOption = 1 << iota
+ // Directory indicates the action is on a directory.
+ Directory
+ // Descend indicates the action is to descend into an object.
+ Descend
+ // Remove indicates the action is to remove an object.
+ Remove
+)
-func main() {
- flag.Usage = func() {
- fmt.Fprintf(os.Stderr, "%s", Help)
- os.Exit(1)
+func NewRemover(opts RemoveOption) *Remover {
+ r := Remover{opts: opts}
+ if opts&Verbose != 0 {
+ r.Log = make(chan string)
}
- flag.Parse()
+ return &r
+}
+
+type Remover struct {
+ opts RemoveOption
+ root os.FileInfo
+
+ // Prompt, if non-nil, will be called depending on the Remover's configured
+ // options. If it returns true, the action continues, otherwise it stops.
+ Prompt func(name string, opts PromptOption) bool
- switch {
- case *version:
- fmt.Fprintf(os.Stdout, "%s", Version)
- os.Exit(0)
+ Log chan string
+
+ stack []node
+}
+
+type node struct {
+ path string
+ info os.FileInfo
+ kids int
+}
+
+func (r *Remover) Remove(path string) (err error) {
+ r.root, err = os.Lstat(path)
+ if err != nil {
+ return err
+ }
+
+ if r.opts&Recursive == 0 || !r.root.Mode().IsDir() {
+ if err := r.remove(path, r.root); err != nil && err != errRefused {
+ return err
+ }
+ return nil
+ }
+
+ // GNU rm uses a DFS that, once it reaches a leaf node (doesn't contain any
+ // further directories), clears out all files and "walks back" to the most
+ // recently seen non-leaf node. This is typicall DFS behavior, but the
+ // walking back is important: it allows the prompt for interactive usage to
+ // look like this:
+ //
+ // $ mkdir a/b/c
+ // $ touch a/b/c/d.txt
+ // $ rm a/
+ // rm: descend into 'a'?
+ // rm: descend into 'a/b'?
+ // rm: descend into 'a/b/c'?
+ // rm: remove file 'a/b/c/d.txt'?
+ // rm: remove directory 'a/b/c'?
+ // rm: remove directory 'a/b'?
+ // rm: remove directory 'a'?
+ //
+ // Unfortunately, filepath.Walk doesn't allow us to walk back, so we're
+ // forced to do a little state management ourselves. We push each directory
+ // we encounter onto a stack. Once we hit a leaf node, we manually work our
+ // way back by popping every consecutive leaf node off the stack, removing
+ // it as we go. Since filepath.Walk doesn't work backwards, this works.
+ //
+ // A major downside is the requirement of determining how many objects are
+ // in a directory. This means Stat will be called twice for each directory:
+ // once for filepath.Walk, once for us. Same goes for Readdirnames.
+ return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ if !r.prompt(path, Descend) {
+ return filepath.SkipDir
+ }
+ dir, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ files, err := dir.Readdirnames(-1)
+ if err != nil {
+ return err
+ }
+ r.stack = append(r.stack, node{path: path, info: info, kids: len(files)})
+ return dir.Close()
+ }
+
+ err = r.remove(path, info)
+
+ // Work our way down the r.stack.
+ for i := len(r.stack) - 1; i >= 0; i-- {
+ s := &r.stack[i]
+ s.kids--
+ if s.kids != 0 {
+ r.stack = r.stack[:i+1]
+ break
+ }
+ if err := r.remove(s.path, s.info); err != nil && err != errRefused {
+ return err
+ }
+ }
+
+ if err != nil && err != errRefused {
+ return err
+ }
+ return nil
+ })
+}
+
+var errRefused = errors.New("user refused prompt")
+
+type rmError struct{ msg string }
+
+func (r rmError) Error() string { return r.msg }
+
+func (r *Remover) prompt(name string, opts PromptOption) bool {
+ if r.opts&PromptAlways != 0 && r.Prompt != nil {
+ return r.Prompt(name, opts)
+ }
+ return true
+}
+
+func (r *Remover) rm(name string, dir bool) (err error) {
+ opts := Remove
+ if dir {
+ opts |= Directory
+ }
+ if !r.prompt(name, opts) {
+ return errRefused
+ }
+
+ switch runtime.GOOS {
+ case "windows", "plan9":
+ err = os.Remove(name)
default:
- for _, v := range flag.Args() {
- os.Remove(v)
+ // For unix systems, os.Remove is a call to Unlink followed by a call to
+ // Rmdir. Since os.Remove doesn't know whether the object is a file or
+ // directory, this provides better performance in the common case. But,
+ // since we know the type of the object ahead of time, we can simply call
+ // the proper syscall.
+ if !dir {
+ err = unix.Unlink(name)
+ } else {
+ err = unix.Rmdir(name)
+ }
+ if err != nil {
+ err = &os.PathError{Op: "remove", Path: name, Err: err}
}
}
+ if err != nil && (r.opts&IgnoreMissing == 0 || !os.IsNotExist(err)) {
+ return err
+ }
+ if r.opts&Verbose != 0 {
+ if dir {
+ r.Log <- fmt.Sprintf("removed directory %s")
+ } else {
+ r.Log <- fmt.Sprintf("removed %s")
+ }
+ }
+ return nil
+}
+
+func (r *Remover) remove(path string, info os.FileInfo) error {
+ if info.Mode()&os.ModeDir != 0 {
+ switch info.Name() {
+ // POSIX doesn't let us do anything with . or ..
+ case ".", "..":
+ return rmError{msg: "cannot remove '.' or '..'"}
+ case "/":
+ return rmError{msg: "cannot remove root directory"}
+ default:
+ if r.opts&NoPreserveRoot == 0 && sys.IsRoot(info) {
+ return rmError{msg: "cannot remove root directory"}
+ }
+ }
+ if r.opts&Recursive == 0 && (r.opts&RemoveEmpty == 0 || isEmpty(path)) {
+ return rmError{msg: fmt.Sprintf("cannot remove directory: %q", path)}
+ }
+ return r.rm(path, true)
+ }
+ if r.opts&OneFileSystem != 0 && sys.DiffFS(r.root, info) {
+ return rmError{msg: "cannot recurse into a different filesystem"}
+ }
+ return r.rm(path, false)
+}
+
+func isEmpty(path string) bool {
+ file, err := os.Open(path)
+ if err != nil {
+ return false
+ }
+ defer file.Close()
+ names, err := file.Readdirnames(1)
+ return len(names) != 0 && err == nil
}
diff --git a/wc/cmd.go b/wc/cmd.go
index 9857d76..aac4bbc 100644
--- a/wc/cmd.go
+++ b/wc/cmd.go
@@ -31,6 +31,7 @@ func newCommand() *cmd {
If F is - then read names from standard input`)
c.f.Int64VarP(&c.tabWidth, "tab", "t", 8, "change the tab width")
c.f.BoolVarP(&c.unicode, "unicode-version", "u", false, "display unicode version and exit")
+ c.f.BoolVar(&c.version, "version", false, "display version information and exit")
return &c
}
@@ -40,19 +41,23 @@ type cmd struct {
filesFrom string
tabWidth int64
unicode bool
+ version bool
}
var errMixedArgs = errors.New("file operands cannot be combined with --files0-from")
-func run(ctx coreutils.Ctx, args ...string) error {
+func run(ctx coreutils.Context, args ...string) error {
c := newCommand()
- // TODO(eric): usage
-
if err := c.f.Parse(args); err != nil {
return err
}
+ if c.version {
+ fmt.Fprintf(ctx.Stdout, "wc (go-coreutils) 1.0")
+ return nil
+ }
+
if c.unicode {
fmt.Fprintf(ctx.Stdout, "Unicode version: %s\n", unicode.Version)
return nil