// Command ollie-9p is a 9P client for interacting with olliesrv. // Auto-discovers the server via $NAMESPACE and identity via $OLLIE_UNAME. // // Usage: // // ollie-9p [-a addr] cmd args... // // Commands: // // read path read file contents to stdout // write path write stdin to file // rdwr path write stdin then read response (request-response files) // stat path print file metadata // ls [-l] [-d] path list directory contents // create path create a file // rm path remove a file // mkdir path create a directory // mv old new rename/move a file or directory (Twstat) package main import ( "flag" "fmt" "io" "os" "strings" "text/tabwriter" "time" "9fans.net/go/plan9" p9client "9fans.net/go/plan9/client" ) var ( addr = flag.String("a", "", "server address (default: unix!$NAMESPACE/ollie)") ) func usage() { fmt.Fprintf(os.Stderr, "usage: ollie-9p [-a addr] cmd args...\n") fmt.Fprintf(os.Stderr, "commands: read write rdwr rdwrs stat ls create rm mkdir mv\n") os.Exit(1) } func fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "ollie-9p: "+format+"\n", args...) os.Exit(1) } func main() { flag.Usage = usage flag.Parse() if flag.NArg() < 1 { usage() } cmd := flag.Arg(0) args := flag.Args()[1:] switch cmd { case "read": openMarker := false if len(args) == 2 && args[0] == "--open-marker" { openMarker = true args = args[1:] } if len(args) != 1 { fatalf("usage: ollie-9p read [--open-marker] path") } cmdRead(args[0], openMarker) case "write": if len(args) != 1 { fatalf("usage: ollie-9p write path") } cmdWrite(args[0]) case "rdwr": if len(args) != 1 { fatalf("usage: ollie-9p rdwr path") } cmdRdwr(args[0]) case "rdwrs": if len(args) != 1 { fatalf("usage: ollie-9p rdwrs path") } cmdRdwrs(args[0]) case "stat": if len(args) != 1 { fatalf("usage: ollie-9p stat path") } cmdStat(args[0]) case "ls": cmdLs(args) case "create": if len(args) != 1 { fatalf("usage: ollie-9p create path") } cmdCreate(args[0]) case "rm": if len(args) != 1 { fatalf("usage: ollie-9p rm path") } cmdRemove(args[0]) case "mkdir": if len(args) != 1 { fatalf("usage: ollie-9p mkdir path") } cmdMkdir(args[0]) case "mv": if len(args) != 2 { fatalf("usage: ollie-9p mv old new") } cmdMv(args[0], args[1]) default: fatalf("unknown command: %s", cmd) } } func dial() *p9client.Fsys { address := *addr if address == "" { address = p9client.Namespace() + "/ollie" } if strings.HasPrefix(address, "unix!") { address = strings.TrimPrefix(address, "unix!") } network := "unix" dialAddr := address if strings.HasPrefix(address, "tcp!") { network = "tcp" dialAddr = strings.TrimPrefix(address, "tcp!") } else if strings.Contains(address, ":") && !strings.HasPrefix(address, "/") { network = "tcp" } user := os.Getenv("USER") if user == "" { user = "none" } aname := os.Getenv("OLLIE_UNAME") if aname == "" { aname = user } conn, err := p9client.Dial(network, dialAddr) if err != nil { fatalf("dial %s: %v", address, err) } fsys, err := conn.Attach(nil, user, aname) if err != nil { conn.Close() fatalf("attach %s: %v", address, err) } return fsys } func cmdRead(path string, openMarker bool) { fsys := dial() defer fsys.Close() fid, err := fsys.Open(path, plan9.OREAD) if err != nil { fatalf("open %s: %v", path, err) } rc := fid defer rc.Close() if openMarker { fmt.Fprintln(os.Stderr, "OLLIE_9P_OPEN") } buf := make([]byte, 64*1024) for { n, err := rc.Read(buf) if n > 0 { os.Stdout.Write(buf[:n]) } if err != nil { if err != io.EOF { fatalf("read %s: %v", path, err) } return } } } func cmdWrite(path string) { fsys := dial() defer fsys.Close() data, err := io.ReadAll(os.Stdin) if err != nil { fatalf("read stdin: %v", err) } if err := writeFile(fsys, path, data); err != nil { fatalf("write %s: %v", path, err) } } func cmdRdwr(path string) { fsys := dial() defer fsys.Close() data, err := io.ReadAll(os.Stdin) if err != nil { fatalf("read stdin: %v", err) } fid, err := fsys.Open(path, plan9.ORDWR) if err != nil { fatalf("open %s: %v", path, err) } if _, err := fid.Write(data); err != nil { fid.Close() fatalf("write %s: %v", path, err) } if _, err := fid.Seek(0, 0); err != nil { fid.Close() fatalf("seek %s: %v", path, err) } resp, err := io.ReadAll(fid) fid.Close() if err != nil { fatalf("rdwr %s: %v", path, err) } if _, err := os.Stdout.Write(resp); err != nil { fatalf("write response: %v", err) } } func cmdRdwrs(path string) { fsys := dial() defer fsys.Close() data, err := io.ReadAll(os.Stdin) if err != nil { fatalf("read stdin: %v", err) } fid, err := fsys.Open(path, plan9.ORDWR) if err != nil { fatalf("open %s: %v", path, err) } defer fid.Close() if _, err := fid.Write(data); err != nil { fatalf("write %s: %v", path, err) } if _, err := fid.Seek(0, 0); err != nil { fatalf("seek %s: %v", path, err) } // Stream reads until EOF or error buf := make([]byte, 8192) for { n, err := fid.Read(buf) if n > 0 { os.Stdout.Write(buf[:n]) } if err != nil { if err != io.EOF { fatalf("read %s: %v", path, err) } break } } } func cmdStat(path string) { fsys := dial() defer fsys.Close() dir, err := fsys.Stat(path) if err != nil { fatalf("stat %s: %v", path, err) } printDir(dir) } func cmdLs(args []string) { fs := flag.NewFlagSet("ls", flag.ExitOnError) long := fs.Bool("l", false, "long format") dirOnly := fs.Bool("d", false, "list directory itself, not contents") fs.Parse(args) paths := fs.Args() if len(paths) == 0 { paths = []string{"/"} } fsys := dial() defer fsys.Close() for _, path := range paths { if *dirOnly { dir, err := fsys.Stat(path) if err != nil { fatalf("stat %s: %v", path, err) } if *long { printDir(dir) } else { fmt.Println(dir.Name) } continue } dir, err := fsys.Stat(path) if err != nil { fatalf("stat %s: %v", path, err) } if dir.Mode&plan9.DMDIR == 0 { // It's a file if *long { printDir(dir) } else { fmt.Println(dir.Name) } continue } // It's a directory — list contents fid, err := fsys.Open(path, plan9.OREAD) if err != nil { fatalf("open %s: %v", path, err) } dirs, err := fid.Dirreadall() fid.Close() if err != nil { fatalf("dirread %s: %v", path, err) } names := make([]string, len(dirs)) for i, d := range dirs { names[i] = d.Name } if *long { // Need full dir entries for long format; stat each name parent, err := fsys.Stat(path) if err != nil { fatalf("stat %s: %v", path, err) } // We don't have individual stats from Ls, so just print names in long-ish mode for _, name := range names { fmt.Printf("%s\t%s\n", modeString(parent.Mode), name) } } else { for _, name := range names { fmt.Println(name) } } } } func cmdCreate(path string) { fsys := dial() defer fsys.Close() dir, _ := splitPath(path) // Check if parent exists _, err := fsys.Stat(dir) if err != nil { fatalf("open %s: %v", dir, err) } // Check if file already exists _, err = fsys.Stat(path) if err == nil { fatalf("%s: already exists", path) } fid, err := fsys.Create(path, plan9.OWRITE, 0666) if err != nil { fatalf("create %s: %v", path, err) } if err := fid.Close(); err != nil { fatalf("create %s: %v", path, err) } } func cmdRemove(path string) { fsys := dial() defer fsys.Close() if err := fsys.Remove(path); err != nil { fatalf("rm %s: %v", path, err) } } func cmdMkdir(path string) { fsys := dial() defer fsys.Close() fid, err := fsys.Create(path, plan9.OREAD, plan9.DMDIR|0755) if err != nil { fatalf("mkdir %s: %v", path, err) } if err := fid.Close(); err != nil { fatalf("mkdir %s: %v", path, err) } } func printDir(d *plan9.Dir) { w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0) modeStr := modeString(d.Mode) t := time.Unix(int64(d.Mtime), 0).Format("Jan _2 15:04") fmt.Fprintf(w, "%s\t%s\t%s\t%d\t%s\t%s\n", modeStr, d.Uid, d.Gid, d.Length, t, d.Name) w.Flush() } func cmdMv(oldPath, newPath string) { fsys := dial() defer fsys.Close() // Twstat takes a destination basename. Both paths must refer to the // same directory; normalize redundant trailing slashes before comparing. oldPath = strings.TrimRight(oldPath, "/") newPath = strings.TrimRight(newPath, "/") oldParent, _ := splitPath(oldPath) newParent, newLeaf := splitPath(newPath) if oldParent != newParent { fatalf("mv: old and new paths must be in the same directory (%s != %s)", oldParent, newParent) } if newLeaf == "" || newLeaf == "." || newLeaf == ".." { fatalf("mv: invalid destination %q", newPath) } if err := fsys.Wstat(oldPath, &plan9.Dir{Name: newLeaf}); err != nil { fatalf("mv %s %s: %v", oldPath, newPath, err) } } func writeFile(fsys *p9client.Fsys, path string, data []byte) error { fid, err := fsys.Open(path, plan9.OWRITE) if err != nil { return err } if _, err := fid.Write(data); err != nil { fid.Close() return err } return fid.Close() } func modeString(mode plan9.Perm) string { var buf [10]byte if mode&plan9.DMDIR != 0 { buf[0] = 'd' } else { buf[0] = '-' } const rwx = "rwx" for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { if mode&(1<