From 0a6c5fdc5ee1d4936bd8cc57aad575997125b103 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Mon, 2 Sep 2024 02:16:10 +0300 Subject: [PATCH] refactor: extract parser/descriptor --- consts.go | 2 +- describe.go | 135 +++++++++++++++++++++++++++++++++ parse.go | 90 ++++++++++++++++++++++ treelike.go | 209 +--------------------------------------------------- 4 files changed, 230 insertions(+), 206 deletions(-) create mode 100644 describe.go create mode 100644 parse.go diff --git a/consts.go b/consts.go index 9753f61..f872b96 100644 --- a/consts.go +++ b/consts.go @@ -1,7 +1,7 @@ package main const ( - LE string = "\n" + // LE string = "\n" LE_UNIX string = "\n" LE_WIN string = "\r\n" ) diff --git a/describe.go b/describe.go new file mode 100644 index 0000000..97d0d42 --- /dev/null +++ b/describe.go @@ -0,0 +1,135 @@ +package main + +import ( + "os" + "strings" +) + +func helpText() strings.Builder { + LE := getLE() + var builder strings.Builder + builder.WriteString("Usage: treelike [OPTIONS] [PATH]" + LE) + builder.WriteString("Prints a tree-like representation of the input." + LE) + builder.WriteString("" + LE) + builder.WriteString("Options:" + LE) + builder.WriteString(" -h, --help Show this help message and exit" + LE) + builder.WriteString(" -f, --file FILE Read from FILE" + LE) + builder.WriteString(" -, --stdin Read from stdin" + LE) + builder.WriteString(" -c, --charset CHARSET Use CHARSET to display characters (utf-8, ascii)" + LE) + builder.WriteString(" -s, --trailing-slash Display trailing slash on directory" + LE) + builder.WriteString(" -p, --full-path Display full path" + LE) + builder.WriteString(" -D, --no-root-dot Do not display a root element" + LE) + return builder +} + +func describeTree(node *Node, opts *Options) string { + lines := []string{getTreeLine(node, opts)} + LE := getLE() + + for _, child := range node.children { + next := describeTree(child, opts) + for _, line := range strings.Split(next, LE) { + if strings.TrimSpace(line) != "" { + lines = append(lines, line) + } + } + } + + var nonBlankLines []string + for _, line := range lines { + if strings.TrimSpace(line) != "" { + nonBlankLines = append(nonBlankLines, line) + } + } + + return strings.Join(nonBlankLines, LE) +} + +func getPrefixes(opts *Options) (string, string, string, string) { + if opts.charset == "ascii" { + return ASCII_CHILD, ASCII_LAST_CHILD, ASCII_DIRECTORY, ASCII_EMPTY + } + return UTF8_CHILD, UTF8_LAST_CHILD, UTF8_DIRECTORY, UTF8_EMPTY +} + +func getLE() string { + if os.IsPathSeparator('\\') { + return LE_WIN + } + return LE_UNIX +} + +func getTreeLine(node *Node, opts *Options) string { + if node.parent == nil { + if opts.rootDot { + return node.name + } else { + return "" + } + } + + CHILD, LAST_CHILD, DIRECTORY, EMPTY := getPrefixes(opts) + + var chunks strings.Builder + + if isLastChild(node) { + chunks.WriteString(LAST_CHILD) + } else { + chunks.WriteString(CHILD) + } + + chunks.WriteString(getName(node, opts)) + str := chunks.String() + + current := node.parent + for current != nil && current.parent != nil { + if isLastChild(current) { + str = EMPTY + str + } else { + str = DIRECTORY + str + } + current = current.parent + } + + if opts.rootDot { + return str + } + return removePrefix(str, opts) +} + +func getName(node *Node, opts *Options) string { + var chunks strings.Builder + + chunks.WriteString(node.name) + + if opts.trailingSlash && len(node.children) > 0 && node.name[len(node.name)-1] != '/' { + chunks.WriteString("/") + } + + str := chunks.String() + + if opts.fullPath && node.parent != nil { + newOpts := Options{false, "", strings.Builder{}, opts.charset, true, opts.fullPath, opts.rootDot} + str = getName(node.parent, &newOpts) + str + } + + return str +} + +func isLastChild(node *Node) bool { + return node.parent != nil && node.parent.children[len(node.parent.children)-1] == node +} + +func removePrefix(str string, opts *Options) string { + CHILD, LAST_CHILD, DIRECTORY, EMPTY := getPrefixes(opts) + + prefixes := []string{CHILD, LAST_CHILD, DIRECTORY, EMPTY} + + for _, prefix := range prefixes { + if strings.HasPrefix(str, prefix) { + return strings.Replace(str, prefix, "", 1) + } + } + + return str +} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..0211e34 --- /dev/null +++ b/parse.go @@ -0,0 +1,90 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func parseDepth(line string, indentSize int) int { + depth := 0 + for j := 0; j < len(line); j++ { + if line[j] != ' ' && line[j] != '\t' { + break + } + depth++ + } + if indentSize > 0 { + depth /= indentSize + } + return depth +} + +func parseInput(input string) *Node { + input = strings.Replace(input, "\r", "", -1) + root := &Node{".", 0, []*Node{}, nil} + current := root + indentSize := 0 + LE := LE_UNIX + + for _, line := range strings.Split(input, LE) { + if line == "" { + continue + } + depth := parseDepth(line, indentSize) + if depth > 0 && indentSize == 0 { + indentSize = depth + depth /= indentSize + } + if depth < 0 { + depth = 0 + } + name := line[depth*indentSize:] + if depth <= current.depth && current.parent != nil { + for current != nil && current.depth >= depth { + current = current.parent + } + } + if current == nil { + current = root + } + current.children = append(current.children, &Node{name, depth, []*Node{}, current}) + current = current.children[len(current.children)-1] + } + return root +} + +func parseRawInput(opts *Options) (strings.Builder, error, int) { + var input strings.Builder + + if opts.fromStdin { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + input.WriteString(scanner.Text() + "\n") + } + if err := scanner.Err(); err != nil { + return input, fmt.Errorf("error reading from stdin: %w", err), 1 + } + } else if opts.fromFile != "" { + file, err := os.Open(opts.fromFile) + if err != nil { + return input, fmt.Errorf("error opening file %s: %w", opts.fromFile, err), 1 + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + input.WriteString(scanner.Text() + "\n") + } + if err := scanner.Err(); err != nil { + return input, fmt.Errorf("error reading from file: %w", err), 1 + } + } else if opts.extra.Len() > 0 { + input.WriteString(opts.extra.String()) + } else { + help := helpText() + return input, fmt.Errorf(help.String()), 2 + } + + return input, nil, 0 +} diff --git a/treelike.go b/treelike.go index 1d93b6b..4de78c3 100644 --- a/treelike.go +++ b/treelike.go @@ -1,218 +1,17 @@ package main import ( - "bufio" "fmt" "os" - "strings" ) -func helpText() { - fmt.Println("Usage: treelike [OPTIONS] [PATH]") - fmt.Println("Prints a tree-like representation of the input.") - fmt.Println("") - fmt.Println("Options:") - fmt.Println(" -h, --help Show this help message and exit") - fmt.Println(" -f, --file FILE Read from FILE") - fmt.Println(" -, --stdin Read from stdin") - fmt.Println(" -c, --charset CHARSET Use CHARSET to display characters (utf-8, ascii)") - fmt.Println(" -s, --trailing-slash Display trailing slash on directory") - fmt.Println(" -p, --full-path Display full path") - fmt.Println(" -D, --no-root-dot Do not display a root element") -} - -func parseDepth(line string, indentSize int) int { - depth := 0 - for j := 0; j < len(line); j++ { - if line[j] != ' ' && line[j] != '\t' { - break - } - depth++ - } - if indentSize > 0 { - depth /= indentSize - } - return depth -} - -func parseInput(input string) *Node { - input = strings.Replace(input, "\r", "", -1) - root := &Node{".", 0, []*Node{}, nil} - current := root - indentSize := 0 - - for _, line := range strings.Split(input, LE) { - if line == "" { - continue - } - depth := parseDepth(line, indentSize) - if depth > 0 && indentSize == 0 { - indentSize = depth - depth /= indentSize - } - if depth < 0 { - depth = 0 - } - name := line[depth*indentSize:] - if depth <= current.depth && current.parent != nil { - for current != nil && current.depth >= depth { - current = current.parent - } - } - if current == nil { - current = root - } - current.children = append(current.children, &Node{name, depth, []*Node{}, current}) - current = current.children[len(current.children)-1] - } - return root -} - -func describeTree(node *Node, opts *Options) string { - lines := []string{getTreeLine(node, opts)} - - for _, child := range node.children { - next := describeTree(child, opts) - for _, line := range strings.Split(next, LE) { - if strings.TrimSpace(line) != "" { - lines = append(lines, line) - } - } - } - - var nonBlankLines []string - for _, line := range lines { - if strings.TrimSpace(line) != "" { - nonBlankLines = append(nonBlankLines, line) - } - } - - return strings.Join(nonBlankLines, LE) -} - -func getPrefixes(opts *Options) (string, string, string, string) { - if opts.charset == "ascii" { - return ASCII_CHILD, ASCII_LAST_CHILD, ASCII_DIRECTORY, ASCII_EMPTY - } - return UTF8_CHILD, UTF8_LAST_CHILD, UTF8_DIRECTORY, UTF8_EMPTY -} - -// func getLE(input string) string { -// if strings.Contains(input, LE_WIN) || os.IsPathSeparator('\\') { -// return LE_WIN -// } -// return LE_UNIX -// } - -func getTreeLine(node *Node, opts *Options) string { - if node.parent == nil { - if opts.rootDot { - return node.name - } else { - return "" - } - } - - CHILD, LAST_CHILD, DIRECTORY, EMPTY := getPrefixes(opts) - - var chunks strings.Builder - - if isLastChild(node) { - chunks.WriteString(LAST_CHILD) - } else { - chunks.WriteString(CHILD) - } - - chunks.WriteString(getName(node, opts)) - str := chunks.String() - - current := node.parent - for current != nil && current.parent != nil { - if isLastChild(current) { - str = EMPTY + str - } else { - str = DIRECTORY + str - } - current = current.parent - } - - if opts.rootDot { - return str - } - return removePrefix(str, opts) -} - -func getName(node *Node, opts *Options) string { - var chunks strings.Builder - - chunks.WriteString(node.name) - - if opts.trailingSlash && len(node.children) > 0 && node.name[len(node.name)-1] != '/' { - chunks.WriteString("/") - } - - str := chunks.String() - - if opts.fullPath && node.parent != nil { - newOpts := Options{false, "", strings.Builder{}, opts.charset, true, opts.fullPath, opts.rootDot} - str = getName(node.parent, &newOpts) + str - } - - return str -} - -func isLastChild(node *Node) bool { - return node.parent != nil && node.parent.children[len(node.parent.children)-1] == node -} - -func removePrefix(str string, opts *Options) string { - CHILD, LAST_CHILD, DIRECTORY, EMPTY := getPrefixes(opts) - - prefixes := []string{CHILD, LAST_CHILD, DIRECTORY, EMPTY} - - for _, prefix := range prefixes { - if strings.HasPrefix(str, prefix) { - return strings.Replace(str, prefix, "", 1) - } - } - - return str -} - func main() { - var input strings.Builder - opts := getOpts() - if opts.fromStdin { - scanner := bufio.NewScanner(os.Stdin) - for scanner.Scan() { - input.WriteString(scanner.Text() + "\n") - } - if err := scanner.Err(); err != nil { - fmt.Fprintln(os.Stderr, "Error reading from stdin:", err) - os.Exit(1) - } - } else if opts.fromFile != "" { - file, err := os.Open(opts.fromFile) - if err != nil { - fmt.Fprintf(os.Stderr, "Error opening file %s: %v\n", opts.fromFile, err) - os.Exit(1) - } - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - input.WriteString(scanner.Text() + "\n") - } - if err := scanner.Err(); err != nil { - fmt.Fprintln(os.Stderr, "Error reading from file:", err) - os.Exit(1) - } - } else if opts.extra.Len() > 0 { - input.WriteString(opts.extra.String()) - } else { - helpText() - os.Exit(1) + input, err, code := parseRawInput(&opts) + if err != nil { + fmt.Println(err) + os.Exit(code) } node := parseInput(input.String()) fmt.Println(describeTree(node, &opts))