Skip to content

Go 实现 ls

Source code

The ls implementation is available at https://github.com/Chang-LeHung/blog-code/blob/master/2025/ls/ls.go

go
package main

import (
	"fmt"
	"os"
	"os/user"
	"sort"
	"strconv"
	"strings"
	"syscall"
)

const (
	EXIT_SUCCESS = 0
	EXIT_FAILURE = -1
)

func Fatal(msg string) {
	_, _ = fmt.Fprintf(os.Stderr, "%s", msg)
	os.Exit(EXIT_FAILURE)
}

type CommandLine struct {
	Command   string
	Params    []string
	Remainder []string
	AllInfo   bool
	List      bool
	Sorted    bool
	Reverse   bool
	Human     bool
}

type CmdVec interface {
	GetCommand() string
	ContaineArgs(string) bool
}

func (c *CommandLine) ContaineArgs(s string) bool {
	for _, v := range c.Params {
		if v == s {
			return true
		}
	}
	return false
}

func (c *CommandLine) GetCommand() string {
	return c.Command
}

func (c *CommandLine) Parse() {
	for idx, val := range c.Params {
		if strings.HasPrefix(val, "-") {
			rest := c.Params[idx][1:]
			for _, ch := range rest {
				switch ch {
				case 'a':
					c.AllInfo = true
				case 'l':
					c.List = true
				case 's':
					c.Sorted = true
				case 'S':
					c.Reverse = true
					c.Sorted = true
				case 'h':
					c.Human = true
				default:
					Fatal(fmt.Sprintf("unknown option: %c, in %s\n", ch, rest))
				}
			}
			continue
		}
		c.Remainder = c.Params[idx:]
		break
	}
}

type CmdFile struct {
	Cmd    *CommandLine
	Files  []os.FileInfo
	Lines  []string
	Parsed bool
}

func (c *CmdFile) DoListFiles() {
	files := make([]string, 0)
	if len(c.Cmd.Remainder) == 0 {
		files = append(files, ".")
	} else {
		files = c.Cmd.Remainder
	}
	for idx := 0; idx < len(files); idx++ {
		file := files[idx]
		fi, err := os.Stat(file)
		if err != nil {
			Fatal(fmt.Sprintf("ls: %s: %s\n", file, err))
		}
		if fi.IsDir() {
			c.Files = append(c.Files, GetFileInfos(file)...)
		} else {
			c.Files = append(c.Files, fi)
		}
	}
}

func (c *CmdFile) String() string {
	if c.Parsed {
		return strings.Join(c.Lines, "\n")
	}
	if c.Cmd.Sorted {
		if c.Cmd.Reverse {
			sort.Slice(c.Files, func(i, j int) bool {
				return c.Files[i].Size() > c.Files[j].Size()
			})
		} else {
			sort.Slice(c.Files, func(i, j int) bool {
				return c.Files[i].Size() < c.Files[j].Size()
			})
		}
	}
	for _, f := range c.Files {
		if !c.Cmd.AllInfo && f.Name() == "." && f.Name() == ".." {
			// ignore . and ..
			continue
		}
		c.Lines = append(c.Lines, c.ParseFile(f))
	}
	c.Parsed = true
	return c.String()
}

func (c *CmdFile) ParseFile(f os.FileInfo) string {
	if !c.Cmd.List {
		return f.Name()
	}
	var res string
	// parse file type
	switch {
	case f.Mode().IsRegular():
		res += "-"
	case f.Mode().IsDir():
		res += "d"
	default:
		m := f.Mode()
		if m&os.ModeSymlink != 0 {
			res += "l"
		} else if m&os.ModeNamedPipe != 0 {
			res += "p"
		} else if m&os.ModeSocket != 0 {
			res += "s"
		} else if m&os.ModeDevice != 0 {
			res += "b"
		} else if m&os.ModeCharDevice != 0 {
			res += "c"
		} else {
			res += "?"
		}
	}
	perm := f.Mode().Perm()
	perms := "rwx"
	for i := 8; i >= 0; i-- {
		if perm&(1<<i) != 0 {
			res += string(perms[i%3])
		} else {
			res += "-"
		}
	}
	res += "\t"
	// get file hard link count
	res += fmt.Sprintf("%d\t", f.Sys().(*syscall.Stat_t).Nlink)
	// get user and group
	uid := f.Sys().(*syscall.Stat_t).Uid
	gid := f.Sys().(*syscall.Stat_t).Gid
	res += fmt.Sprintf("%s\t%s\t", GetUserName(int(uid)), GetGroupName(int(gid)))
	res += fmt.Sprintf("%s\t%s\t", GetFileSize(int(f.Size()), c.Cmd.Human),
		f.ModTime().Format("2006-01-02 15:04:05"))
	res += f.Name()
	if f.Mode()&os.ModeSymlink != 0 {
		link, err := os.Readlink(f.Name())
		if err != nil {
			Fatal(fmt.Sprintf("ls: %s: %s\n", f.Name(), err))
		}
		res += fmt.Sprintf(" -> %s", link)
	}
	return res
}

func GetFileSize(val int, h bool) string {
	if !h {
		return strconv.Itoa(val)
	}
	var unit string
	var size float32
	if val < 1024 {
		unit = "B"
		size = float32(val)
	} else if val < 1024*1024 {
		unit = "K"
		size = float32(val / 1024)
	} else if val < 1024*1024*1024 {
		unit = "M"
		size = float32(val / 1024 / 1024)
	} else {
		unit = "G"
		size = float32(val / 1024 / 1024 / 1024)
	}
	return fmt.Sprintf("%.1f%s", size, unit)
}

func GetUserName(uid int) string {
	u, err := user.LookupId(strconv.Itoa(uid))
	if err != nil {
		Fatal(err.Error())
	}
	return u.Username
}

func GetGroupName(gid int) string {
	group, err := user.LookupGroupId(strconv.Itoa(gid))
	if err != nil {
		Fatal(err.Error())
	}
	return group.Name
}

func GetFileInfos(dir string) []os.FileInfo {
	files, err := os.ReadDir(dir)
	if err != nil {
		Fatal(fmt.Sprintf("ls: %s: %s\n", dir, err))
	}
	var res []os.FileInfo
	for _, file := range files {
		fi, err := file.Info()
		if err != nil {
			Fatal(fmt.Sprintf("ls: %s: %s\n", file, err))
		}
		res = append(res, fi)
	}
	return res
}

func main() {
	args := os.Args
	c := &CommandLine{
		Command: args[0],
		Params:  args[1:],
	}
	c.Parse()
	if c.ContaineArgs("-h") || c.ContaineArgs("--help") {
		fmt.Println("Usage: ls [OPTION]... [FILE]...")
		os.Exit(0)
	}
	cmd := &CmdFile{
		Cmd: c,
	}
	cmd.DoListFiles()
	fmt.Println(cmd.String())
}

湘ICP备2025100831号-1