Skip to content

Instantly share code, notes, and snippets.

@Nezz7
Last active November 11, 2020 19:31
Show Gist options
  • Save Nezz7/0e3d35b2f051d652d5acfda01d742d0e to your computer and use it in GitHub Desktop.
Save Nezz7/0e3d35b2f051d652d5acfda01d742d0e to your computer and use it in GitHub Desktop.
Unix ls command go implementation
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
// Apostrophe used for names with spaces
const Apostrophe = "'"
// NumberOfNamesPerLine when outputing without list option
const NumberOfNamesPerLine = 4
// DirectorySymbol used at the end of each directory name
const DirectorySymbol = "\\"
var listPtr = flag.Bool("l", false, "Use a long listing format")
var sortPtr = flag.Bool("S", false, "Sort by file size, largest first")
var reversePtr = flag.Bool("r", false, "Reverse order while sorting")
var recursivePtr = flag.Bool("R", false, "List subdirectories recursively")
var allPtr = flag.Bool("a", false, "Do not ignore entries starting with .")
func parseTime(t time.Time) string {
return fmt.Sprintf("%02d/%02d/%d %02d:%02d", t.Month(), t.Day(), t.Year(), t.Hour(), t.Minute())
}
func parseName(f os.FileInfo) string {
var name string
if strings.Contains(f.Name(), " ") {
name += Apostrophe + f.Name() + Apostrophe
} else {
name += f.Name()
}
if f.IsDir() {
name += DirectorySymbol
}
return name
}
func isHidden(filename string) bool {
if len(filename) == 0 {
log.Println("empty file name")
}
if filename[0:1] == "." && len(filename) > 1 {
return true
}
return false
}
func getDirPath() string {
path, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
return path
}
func getRecursiveFiles(dirPath string) []os.FileInfo {
files := make([]os.FileInfo, 0)
err := filepath.Walk(".",
func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Fatal(err)
return err
}
if isHidden(info.Name()) && info.IsDir() && !*allPtr {
return filepath.SkipDir
}
if !isHidden(info.Name()) || *allPtr {
files = append(files, info)
}
return nil
})
if err != nil {
log.Fatal(err)
}
return files
}
func getNonRecursiveFiles(dirPath string) []os.FileInfo {
files, err := ioutil.ReadDir(dirPath)
if err != nil {
log.Fatal(err)
}
return files
}
func getFiles(dirPath string) []os.FileInfo {
if *recursivePtr {
return getRecursiveFiles(dirPath)
}
return getNonRecursiveFiles(dirPath)
}
func listFiles(files []os.FileInfo, dirPath string) {
fmt.Printf("\n\tDirectory: %s\n\n\n", dirPath)
fmt.Printf("Mode \t\t LastWriteTime \t\t Length Name\n")
fmt.Printf("---- \t\t ------------- \t\t ------ ----\n")
for _, f := range files {
fmt.Printf("%s \t%s ", f.Mode(), parseTime(f.ModTime()))
if f.IsDir() {
fmt.Printf("\t\t")
} else {
fmt.Printf("%13d ", f.Size())
}
fmt.Printf("%s\n", parseName(f))
}
}
func printFiles(files []os.FileInfo) {
for i, f := range files {
if i%NumberOfNamesPerLine == 0 {
fmt.Println()
}
fmt.Printf("%40s", parseName(f))
}
}
func reverse(files []os.FileInfo) []os.FileInfo {
for left, right := 0, len(files)-1; left < right; left, right = left+1, right-1 {
files[left], files[right] = files[right], files[left]
}
return files
}
func main() {
flag.Parse()
dirPath := getDirPath()
files := getFiles(dirPath)
if *sortPtr {
sort.SliceStable(files, func(i, j int) bool {
return files[i].Size() > files[j].Size()
})
if *reversePtr {
files = reverse(files)
}
}
if *listPtr {
listFiles(files, dirPath)
} else {
printFiles(files)
}
fmt.Println()
fmt.Println()
}
@Dainerx
Copy link

Dainerx commented Nov 5, 2020

  • A lot of magic numbers and strings. For instance if i%4 == 0 what does 4 stand for?. Moreover "'" and "\" should defined as constants.
  • I believe there is a better way to write reverse.
  • isHidden should be renamed hidden, why preceding with an "is"? If hidden do something, else do something else.
  • isHidden should return an error if the file name is empty.
  • name := "" while this is acceptable, but go string default values are empty string. var name string is the better option here.
  • If you care about performance stop using string+=anotherString this will make tons of copies that will halt your performance; use bytes.Buffer instead.

Error handling in go is quite different than other languages, but you should be familiar with the three known strategies:

  1. Progress is impossible and the caller should be the one to handle it. Error propagation (generally to the caller).
  2. Progress is possible, return nil in the result or just log it inside the function.
  3. Ignore the error.

Now let's look at this code snippet inside the getNonRecursiveFiles

	files, err := ioutil.ReadDir(dirPath)
	if err != nil {
		log.Fatal(err)
	}

Do you believe that a failure upon reading a directory can assure the non failure of your app? No! You should propagate the error to the caller (main) and the caller would fail cause an IO error is no joke.

@Nezz7
Copy link
Author

Nezz7 commented Nov 11, 2020

  • I added constants for magic numbers and strings.
  • isHidden name helps understand that the function returns boolean
  • For errors handling I used log.Fatal which ensure exit(1)

@Dainerx
Copy link

Dainerx commented Nov 11, 2020

It is not a logging level problem. The fact that you are crashing from inside a function is my concern.

Why not make functions just return errors? Here is a snippet of your code written in the idiomatic Go way:

func hidden(filename string) (bool,error) {
	if len(filename) == 0 {
		return false,fmt.Errorf("Empty file name")
	}
	if filename[0:1] == "." && len(filename) > 1 {
		return true,nil
	}
	return false,nil
}

func main() {
        hidden, err := isHidden("file.txt")
        if err != nil {
           log.Fatal(err)
        }
        // carry on
        // hidden is safe to use
}

isHidden stutters. Which one is easier to read if is hidden or if hidden?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment