mirror of
https://github.com/MickMake/GoSungrow.git
synced 2025-03-23 08:01:43 +01:00
651 lines
17 KiB
Go
651 lines
17 KiB
Go
package lsgo
|
|
|
|
import (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/acarl005/textcol"
|
|
colorable "github.com/mattn/go-colorable"
|
|
"github.com/willf/pad"
|
|
kingpin "gopkg.in/alecthomas/kingpin.v2"
|
|
)
|
|
|
|
// DisplayItem wraps the file stat info and string to be printed
|
|
type DisplayItem struct {
|
|
display string
|
|
info os.FileInfo
|
|
basename string
|
|
ext string
|
|
link *LinkInfo
|
|
}
|
|
|
|
// LinkInfo wraps link stat info and whether the link points to valid file
|
|
type LinkInfo struct {
|
|
path string
|
|
info os.FileInfo
|
|
broken bool
|
|
}
|
|
|
|
var (
|
|
// True is a helper varable to help make pointers to `true`
|
|
True = true
|
|
sizeUnits = []string{"B", "K", "M", "G", "T"}
|
|
dateFormat = "02.Jan'06" // uses the "reference time" https://golang.org/pkg/time/#Time.Format
|
|
timeFormat = "15:04"
|
|
start int64 // keep track of execution time
|
|
stdout = colorable.NewColorableStdout() // write to this to allow ANSI color codes to be compatible on Windows
|
|
)
|
|
|
|
// func LsGo(cmd *cobra.Command, args []string) {
|
|
func LsGo() error {
|
|
var err error
|
|
|
|
textcol.Output = stdout
|
|
|
|
start = time.Now().UnixNano()
|
|
// auto-generate help text for the command with -h
|
|
kingpin.CommandLine.HelpFlag.Short('h')
|
|
|
|
// parse the arguments and populate the struct
|
|
kingpin.Parse()
|
|
argsPostParse()
|
|
|
|
// separate the directories from the regular files
|
|
dirs := []string{}
|
|
files := []os.FileInfo{}
|
|
for _, pathStr := range *args.paths {
|
|
var fileStat os.FileInfo
|
|
fileStat, err = os.Stat(pathStr)
|
|
if err != nil && strings.Contains(err.Error(), "no such file or directory") {
|
|
printErrorHeader(err, prettifyPath(pathStr))
|
|
continue
|
|
} else {
|
|
check(err)
|
|
}
|
|
if fileStat.IsDir() {
|
|
dirs = append(dirs, pathStr)
|
|
} else {
|
|
files = append(files, fileStat)
|
|
}
|
|
}
|
|
|
|
// list files first
|
|
if len(files) > 0 {
|
|
pwd := os.Getenv("PWD")
|
|
listFiles(pwd, &files, true)
|
|
}
|
|
|
|
// then list the contents of each directory
|
|
for i, dir := range dirs {
|
|
// print a blank line between directories, but not before the first one
|
|
if i > 0 {
|
|
fmt.Fprintln(stdout, "")
|
|
}
|
|
listDir(dir)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func listDir(pathStr string) {
|
|
items, err := ioutil.ReadDir(pathStr)
|
|
// if we couldn't read the folder, print a "header" with error message and use error-looking colors
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "no such file or directory") || strings.Contains(err.Error(), "permission denied") {
|
|
printErrorHeader(err, prettifyPath(pathStr))
|
|
return
|
|
}
|
|
check(err)
|
|
}
|
|
|
|
// filter by the regexp if one was passed
|
|
if len(*args.find) > 0 {
|
|
filteredItems := []os.FileInfo{}
|
|
for _, fileInfo := range items {
|
|
re, err := regexp.Compile(*args.find)
|
|
check(err)
|
|
if re.MatchString(fileInfo.Name()) {
|
|
filteredItems = append(filteredItems, fileInfo)
|
|
}
|
|
}
|
|
items = filteredItems
|
|
}
|
|
|
|
if !(len(*args.find) > 0 && len(items) == 0) &&
|
|
!(len(*args.paths) == 1 && (*args.paths)[0] == "." && !*args.recurse) {
|
|
printFolderHeader(pathStr)
|
|
}
|
|
|
|
if len(items) > 0 {
|
|
listFiles(pathStr, &items, false)
|
|
}
|
|
|
|
if *args.recurse {
|
|
for _, item := range items {
|
|
if item.IsDir() && (item.Name()[0] != '.' || *args.all) {
|
|
fmt.Fprintln(stdout, "") // put a blank line between directories
|
|
listDir(path.Join(pathStr, item.Name()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func listFiles(parentDir string, items *[]os.FileInfo, forceDotfiles bool) {
|
|
absPath, err := filepath.Abs(parentDir)
|
|
check(err)
|
|
|
|
// collect all the contents here
|
|
files := []*DisplayItem{}
|
|
dirs := []*DisplayItem{}
|
|
|
|
// to help with formatting, we need to know the length of the longest name to add appropriate padding
|
|
longestOwnerName := 0
|
|
longestGroupName := 0
|
|
if *args.owner {
|
|
for _, fileInfo := range *items {
|
|
owner, group := getOwnerAndGroup(&fileInfo)
|
|
longestOwnerName = max(longestOwnerName, len(owner))
|
|
longestGroupName = max(longestGroupName, len(group))
|
|
}
|
|
}
|
|
|
|
for _, fileInfo := range *items {
|
|
// if this is a dotfile (hidden file)
|
|
if fileInfo.Name()[0] == '.' {
|
|
// we can skip everything with this file if we aren't using the `all` option
|
|
if !*args.all && !forceDotfiles {
|
|
continue
|
|
}
|
|
}
|
|
|
|
basename, ext := splitExt(fileInfo.Name())
|
|
|
|
displayItem := DisplayItem{
|
|
info: fileInfo,
|
|
ext: ext,
|
|
basename: basename,
|
|
}
|
|
|
|
// read some info about linked file if this item is a symlink
|
|
if fileInfo.Mode()&os.ModeSymlink != 0 {
|
|
getLinkInfo(&displayItem, absPath)
|
|
}
|
|
|
|
if fileInfo.IsDir() || (fileInfo.Mode()&os.ModeSymlink != 0 &&
|
|
displayItem.link.info != nil &&
|
|
displayItem.link.info.IsDir()) {
|
|
if *args.files {
|
|
continue
|
|
} else {
|
|
dirs = append(dirs, &displayItem)
|
|
}
|
|
} else {
|
|
if *args.dirs {
|
|
continue
|
|
} else {
|
|
files = append(files, &displayItem)
|
|
}
|
|
}
|
|
|
|
owner, group := getOwnerAndGroup(&fileInfo)
|
|
ownerColor, groupColor := getOwnerAndGroupColors(owner, group)
|
|
|
|
if *args.perms {
|
|
displayItem.display += permString(fileInfo, ownerColor, groupColor)
|
|
}
|
|
|
|
if *args.owner {
|
|
paddedOwner := pad.Right(owner, longestOwnerName, " ")
|
|
ownerInfo := []string{Reset + ownerColor + paddedOwner}
|
|
if !*args.nogroup {
|
|
paddedGroup := pad.Right(group, longestGroupName, " ")
|
|
ownerInfo = append(ownerInfo, groupColor+paddedGroup)
|
|
}
|
|
ownerInfo = append(ownerInfo, Reset)
|
|
displayItem.display += strings.Join(ownerInfo, " ")
|
|
}
|
|
|
|
if *args.bytes {
|
|
if fileInfo.Mode()&os.ModeDevice != 0 {
|
|
displayItem.display += deviceNumbers(path.Join(absPath, fileInfo.Name()))
|
|
} else {
|
|
displayItem.display += sizeString(fileInfo.Size())
|
|
}
|
|
}
|
|
|
|
if *args.mdate {
|
|
displayItem.display += timeString(fileInfo.ModTime())
|
|
}
|
|
|
|
displayItem.display += nameString(&displayItem)
|
|
|
|
if *args.links && fileInfo.Mode()&os.ModeSymlink != 0 {
|
|
displayItem.display += linkString(&displayItem, absPath)
|
|
}
|
|
}
|
|
|
|
if *args.sortTime {
|
|
sort.Sort(ByTime(dirs))
|
|
sort.Sort(ByTime(files))
|
|
if *args.backwards {
|
|
reverse(dirs)
|
|
reverse(files)
|
|
}
|
|
}
|
|
|
|
if *args.sortSize {
|
|
sort.Sort(BySize(files))
|
|
if *args.backwards {
|
|
reverse(files)
|
|
}
|
|
}
|
|
|
|
if *args.sortKind {
|
|
sort.Sort(ByKind(files))
|
|
if *args.backwards {
|
|
reverse(files)
|
|
}
|
|
}
|
|
|
|
// combine the items together again after sorting
|
|
allItems := append(dirs, files...)
|
|
|
|
// if using "long" display, just print one item per line
|
|
if *args.bytes || *args.mdate || *args.owner || *args.perms || *args.long {
|
|
for _, item := range allItems {
|
|
fmt.Fprintln(stdout, item.display)
|
|
}
|
|
} else {
|
|
// but if not, try to format in columns, link `ls` would
|
|
strs := []string{}
|
|
for _, item := range allItems {
|
|
strs = append(strs, item.display)
|
|
}
|
|
textcol.PrintColumns(&strs, 2)
|
|
}
|
|
|
|
if *args.stats {
|
|
printStats(len(files), len(dirs))
|
|
}
|
|
}
|
|
|
|
func getLinkInfo(item *DisplayItem, absPath string) {
|
|
fullPath := path.Join(absPath, item.info.Name())
|
|
linkPath, err1 := os.Readlink(fullPath)
|
|
check(err1)
|
|
|
|
linkFullPath := linkPath
|
|
if linkPath[0] != '/' {
|
|
linkFullPath = path.Join(absPath, linkPath)
|
|
}
|
|
|
|
linkInfo, err2 := os.Stat(linkFullPath)
|
|
if *args.linkRel {
|
|
linkRel, _ := filepath.Rel(absPath, linkPath)
|
|
if linkRel != "" && len(linkRel) <= len(linkPath) {
|
|
// i prefer the look of these relative paths prepended with ./
|
|
if linkRel[0] != '.' {
|
|
linkPath = "./" + linkRel
|
|
} else {
|
|
linkPath = linkRel
|
|
}
|
|
}
|
|
}
|
|
|
|
link := LinkInfo{
|
|
path: linkPath,
|
|
}
|
|
item.link = &link
|
|
if linkInfo != nil {
|
|
link.info = linkInfo
|
|
} else if strings.Contains(err2.Error(), "no such file or directory") {
|
|
link.broken = true
|
|
} else if !strings.Contains(err2.Error(), "permission denied") {
|
|
check(err2)
|
|
}
|
|
}
|
|
|
|
func nameString(item *DisplayItem) string {
|
|
mode := item.info.Mode()
|
|
name := item.info.Name()
|
|
if mode&os.ModeDir != 0 {
|
|
return dirString(item)
|
|
} else if mode&os.ModeSymlink != 0 {
|
|
if !item.link.broken && item.link.info.IsDir() {
|
|
color := ConfigColor["link"]["nameDir"]
|
|
if *args.nerdfont {
|
|
var linkIcon string
|
|
if item.link.broken {
|
|
linkIcon = otherIcons["brokenLink"]
|
|
} else {
|
|
linkIcon = otherIcons["linkDir"]
|
|
}
|
|
return color + linkIcon + " " + name + " " + Reset
|
|
} else if *args.icons {
|
|
return color + "🔗 " + name + " " + Reset
|
|
} else {
|
|
return color + " " + name + " " + Reset
|
|
}
|
|
} else {
|
|
color := ConfigColor["link"]["name"]
|
|
if *args.nerdfont {
|
|
var linkIcon string
|
|
if item.link.broken {
|
|
linkIcon = otherIcons["brokenLink"]
|
|
} else {
|
|
linkIcon = otherIcons["link"]
|
|
}
|
|
return color + linkIcon + " " + name + " " + Reset
|
|
} else if *args.icons {
|
|
return color + "🔗 " + name + " " + Reset
|
|
} else {
|
|
return color + name + " " + Reset
|
|
}
|
|
}
|
|
} else if mode&os.ModeDevice != 0 {
|
|
color := ConfigColor["device"]["name"]
|
|
if *args.nerdfont {
|
|
return color + otherIcons["device"] + " " + name + " " + Reset
|
|
} else if *args.icons {
|
|
return color + "💽 " + name + " " + Reset
|
|
} else {
|
|
return color + " " + name + " " + Reset
|
|
}
|
|
} else if mode&os.ModeNamedPipe != 0 {
|
|
return ConfigColor["pipe"]["name"] + " " + name + " " + Reset
|
|
} else if mode&os.ModeSocket != 0 {
|
|
return ConfigColor["socket"]["name"] + " " + name + " " + Reset
|
|
}
|
|
return fileString(item)
|
|
}
|
|
|
|
func linkString(item *DisplayItem, absPath string) string {
|
|
colors := ConfigColor["link"]
|
|
displayStrings := []string{colors["arrow"] + "►"}
|
|
if item.link.info == nil && item.link.broken {
|
|
displayStrings = append(displayStrings, colors["broken"]+item.link.path+Reset)
|
|
} else if item.link.info != nil {
|
|
linkname, linkext := splitExt(item.link.path)
|
|
displayItem := DisplayItem{
|
|
info: item.link.info,
|
|
basename: linkname,
|
|
ext: linkext,
|
|
}
|
|
displayStrings = append(displayStrings, nameString(&displayItem))
|
|
} else {
|
|
displayStrings = append(displayStrings, item.link.path)
|
|
}
|
|
return strings.Join(displayStrings, " ")
|
|
}
|
|
|
|
func fileString(item *DisplayItem) string {
|
|
key := strings.ToLower(item.ext)
|
|
// figure out which color to choose
|
|
colors := FileColor["_default"]
|
|
alias, hasAlias := FileAliases[key]
|
|
if hasAlias {
|
|
key = alias
|
|
}
|
|
betterColor, hasBetterColor := FileColor[key]
|
|
if hasBetterColor {
|
|
colors = betterColor
|
|
}
|
|
|
|
ext := item.ext
|
|
if ext != "" {
|
|
ext = "." + ext
|
|
}
|
|
|
|
// in some cases files have icons if front
|
|
// if nerd font enabled, then it'll be a file-specific icon, or if its an executable script, a little shell icon
|
|
// if the regular --icons flag is used instead, then it will show a ">_" only if the file is executable
|
|
icon := ""
|
|
executable := isExecutableScript(item)
|
|
if *args.nerdfont {
|
|
if executable {
|
|
icon = colors[0] + getIconForFile("", "shell") + " "
|
|
} else {
|
|
icon = colors[0] + getIconForFile(item.basename, item.ext) + " "
|
|
}
|
|
} else if *args.icons {
|
|
if executable {
|
|
icon = BgGray(1) + FgRGB(0, 5, 0) + ">_" + Reset + " "
|
|
}
|
|
}
|
|
displayStrings := []string{icon, colors[0], item.basename, colors[1], ext, Reset}
|
|
return strings.Join(displayStrings, "")
|
|
}
|
|
|
|
// check for executable permissions
|
|
func isExecutableScript(item *DisplayItem) bool {
|
|
if item.info.Mode()&0111 != 0 && item.info.Mode().IsRegular() {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func dirString(item *DisplayItem) string {
|
|
colors := ConfigColor["dir"]
|
|
if item.basename == "" {
|
|
colors = ConfigColor[".dir"]
|
|
}
|
|
displayStrings := []string{colors["name"]}
|
|
icon := ""
|
|
if *args.icons {
|
|
displayStrings = append(displayStrings, "📂 ")
|
|
} else if *args.nerdfont {
|
|
icon = getIconForFolder(item.info.Name()) + " "
|
|
displayStrings = append(displayStrings, icon)
|
|
} else {
|
|
displayStrings = append(displayStrings, " ")
|
|
}
|
|
ext := item.ext
|
|
if ext != "" {
|
|
ext = "." + ext
|
|
}
|
|
displayStrings = append(displayStrings, item.basename, colors["ext"], ext, " ", Reset)
|
|
return strings.Join(displayStrings, "")
|
|
}
|
|
|
|
func rwxString(mode os.FileMode, i uint, color string) string {
|
|
bits := mode >> (i * 3)
|
|
coloredStrings := []string{color}
|
|
if bits&4 != 0 {
|
|
coloredStrings = append(coloredStrings, "r")
|
|
} else {
|
|
coloredStrings = append(coloredStrings, "-")
|
|
}
|
|
if bits&2 != 0 {
|
|
coloredStrings = append(coloredStrings, "w")
|
|
} else {
|
|
coloredStrings = append(coloredStrings, "-")
|
|
}
|
|
if i == 0 && mode&os.ModeSticky != 0 {
|
|
if bits&1 != 0 {
|
|
coloredStrings = append(coloredStrings, "t")
|
|
} else {
|
|
coloredStrings = append(coloredStrings, "T")
|
|
}
|
|
} else {
|
|
if bits&1 != 0 {
|
|
coloredStrings = append(coloredStrings, "x")
|
|
} else {
|
|
coloredStrings = append(coloredStrings, "-")
|
|
}
|
|
}
|
|
return strings.Join(coloredStrings, "")
|
|
}
|
|
|
|
// generates the permissions string, ya know like "drwxr-xr-x" and stuff like that
|
|
func permString(info os.FileInfo, ownerColor string, groupColor string) string {
|
|
defaultColor := PermsColor["other"]["_default"]
|
|
|
|
// info.Mode().String() does not produce the same output as `ls`, so we must build that string manually
|
|
mode := info.Mode()
|
|
// this "type" is not the file extension, but type as far as the OS is concerned
|
|
filetype := "-"
|
|
if mode&os.ModeDir != 0 {
|
|
filetype = "d"
|
|
} else if mode&os.ModeSymlink != 0 {
|
|
filetype = "l"
|
|
} else if mode&os.ModeDevice != 0 {
|
|
if mode&os.ModeCharDevice == 0 {
|
|
filetype = "b" // block device
|
|
} else {
|
|
filetype = "c" // character device
|
|
}
|
|
} else if mode&os.ModeNamedPipe != 0 {
|
|
filetype = "p"
|
|
} else if mode&os.ModeSocket != 0 {
|
|
filetype = "s"
|
|
}
|
|
coloredStrings := []string{defaultColor, filetype}
|
|
coloredStrings = append(coloredStrings, rwxString(mode, 2, ownerColor))
|
|
coloredStrings = append(coloredStrings, rwxString(mode, 1, groupColor))
|
|
coloredStrings = append(coloredStrings, rwxString(mode, 0, defaultColor), Reset, Reset)
|
|
return strings.Join(coloredStrings, " ")
|
|
}
|
|
|
|
func sizeString(size int64) string {
|
|
sizeFloat := float64(size)
|
|
for i, unit := range sizeUnits {
|
|
base := math.Pow(1024, float64(i))
|
|
if sizeFloat < base*1024 {
|
|
var sizeStr string
|
|
if i == 0 {
|
|
sizeStr = strconv.FormatInt(size, 10)
|
|
} else {
|
|
sizeStr = fmt.Sprintf("%.2f", sizeFloat/base)
|
|
}
|
|
return SizeColor[unit] + pad.Left(sizeStr, 6, " ") + unit + " " + Reset
|
|
}
|
|
}
|
|
return strconv.Itoa(int(size))
|
|
}
|
|
|
|
func timeString(modtime time.Time) string {
|
|
dateStr := modtime.Format(dateFormat)
|
|
timeStr := modtime.Format(timeFormat)
|
|
hour, err := strconv.Atoi(timeStr[0:2])
|
|
check(err)
|
|
// generate a color based on the hour of the day. darkest around midnight and whitest around noon
|
|
timeColor := 14 - int(8*math.Cos(math.Pi*float64(hour)/12))
|
|
colored := []string{FgGray(22) + dateStr, FgGray(timeColor) + timeStr, Reset}
|
|
return strings.Join(colored, " ")
|
|
}
|
|
|
|
// when we list out any subdirectories, print those paths conspicuously above the contents
|
|
// this helps with visual separation
|
|
func printFolderHeader(pathStr string) {
|
|
colors := ConfigColor["folderHeader"]
|
|
headerString := colors["arrow"] + "►" + colors["main"] + " "
|
|
prettyPath := prettifyPath(pathStr)
|
|
|
|
if prettyPath == "/" {
|
|
headerString += "/"
|
|
} else {
|
|
folders := strings.Split(prettyPath, "/")
|
|
coloredFolders := make([]string, 0, len(folders))
|
|
for i, folder := range folders {
|
|
if i == len(folders)-1 { // different color for the last folder in the path
|
|
coloredFolders = append(coloredFolders, colors["lastFolder"]+folder)
|
|
} else {
|
|
coloredFolders = append(coloredFolders, colors["main"]+folder)
|
|
}
|
|
}
|
|
headerString += strings.Join(coloredFolders, colors["slash"]+"/")
|
|
}
|
|
|
|
fmt.Fprintln(stdout, headerString+" "+Reset)
|
|
}
|
|
|
|
func printErrorHeader(err error, pathStr string) {
|
|
fmt.Fprintln(stdout, ConfigColor["folderHeader"]["error"]+"► "+pathStr+Reset)
|
|
fmt.Fprintln(stdout, err.Error())
|
|
}
|
|
|
|
func prettifyPath(pathStr string) string {
|
|
prettyPath, err := filepath.Abs(pathStr)
|
|
check(err)
|
|
pwd := os.Getenv("PWD")
|
|
home := os.Getenv("HOME")
|
|
|
|
if strings.HasPrefix(prettyPath, pwd) {
|
|
prettyPath = "." + prettyPath[len(pwd):]
|
|
} else if strings.HasPrefix(prettyPath, home) {
|
|
prettyPath = "~" + prettyPath[len(home):]
|
|
}
|
|
return prettyPath
|
|
}
|
|
|
|
func getOwnerAndGroupColors(owner string, group string) (string, string) {
|
|
if owner == os.Getenv("USER") {
|
|
owner = "_self"
|
|
}
|
|
ownerColor := PermsColor["user"][owner]
|
|
if ownerColor == "" {
|
|
ownerColor = PermsColor["user"]["_default"]
|
|
}
|
|
groupColor := PermsColor["group"][group]
|
|
if groupColor == "" {
|
|
groupColor = PermsColor["group"]["_default"]
|
|
}
|
|
return ownerColor, groupColor
|
|
}
|
|
|
|
func printStats(numFiles, numDirs int) {
|
|
colors := ConfigColor["stats"]
|
|
end := time.Now().UnixNano()
|
|
microSeconds := (end - start) / int64(time.Microsecond)
|
|
milliSeconds := float64(microSeconds) / 1000
|
|
statStrings := []string{
|
|
colors["text"],
|
|
colors["number"] + strconv.Itoa(numDirs),
|
|
colors["text"] + "dirs",
|
|
colors["number"] + strconv.Itoa(numFiles),
|
|
colors["text"] + "files",
|
|
colors["ms"] + fmt.Sprintf("%.2f", milliSeconds),
|
|
colors["text"] + "ms",
|
|
Reset,
|
|
}
|
|
fmt.Fprintln(stdout, strings.Join(statStrings, " "))
|
|
}
|
|
|
|
func splitExt(filename string) (basepath, ext string) {
|
|
basename := filepath.Base(filename)
|
|
if basename[0] == '.' {
|
|
ext = basename[1:]
|
|
basepath = filename[:len(filename)-len(ext)-1]
|
|
} else {
|
|
ext = filepath.Ext(filename)
|
|
basepath = filename[:len(filename)-len(ext)]
|
|
if ext != "" {
|
|
ext = ext[1:]
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Go doesn't provide a `Max` function for ints like it does for floats (wtf?)
|
|
func max(a int, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func check(err error) {
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|