Learn to read a multi-line tree from stdin.
Reads lines from standard input (os.Stdin)
Prints each line prefixed with "Line: "
Use bufio.NewScanner(os.Stdin)
Use a for scanner.Scan() loop
Use scanner.Text() to get each line
project/
cmd/
main.go
Line: project/
Line: cmd/
Line: main.gopackage main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
fmt.Println("Line:", line)
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading input:", err)
}
}Change the program so that each line is printed in uppercase instead of "Line: ...".
use strings.ToUpper(line) (Study the strings package).
Write your version and test it.
Next, we learn how to know which line is a folder/file and its depth.
-
Count leading spaces for each line
-
Divide by 2 → depth level
-
Print both line text and its level
Line: project/ Level: 0
Line: cmd/ Level: 1
Line: main.go Level: 2trimmed := strings.TrimLeft(line, " ")
indent := len(line) - len(trimmed)
level := indent / 2
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimLeft(line, " ")
indent := len(line) - len(trimmed)
level := indent / 2
fmt.Printf("Line: %s Level: %d", strings.ToUpper(line), level)
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading input:", err)
}
}project/
cmd/
main.go
internal/
app.goVerify that each line prints uppercase + correct level
-
What is the level of main.go?
-
What is the level of internal/?
Next step: we use a stack ([]string) to keep track of the path as we go deeper or shallower in the tree.
-
Each folder/file you read has a level
-
If the level is less than the stack length, you pop the stack until it matches the level
-
Then push the current name
-
Full path = filepath.Join(stack...)
Keep a var pathStack []string
For each line:
-
Trim / if it’s a folder
-
Adjust stack based on level
-
Append current name to stack
Full path: project/cmd
Full path: project/cmd/main.go
Full path: project/internal
Full path: project/internal/app.gostrings.TrimSuffix(trimmed, "/") → removes trailing / for dirs
filepath.Join(pathStack...) → build the full path
If level < len(pathStack), slice the stack: pathStack = pathStack[:level]
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
var pathStack []string
for scanner.Scan() {
line := scanner.Text()
trimmed := strings.TrimLeft(line, " ")
indent := len(line) - len(trimmed)
level := indent / 2
name := strings.TrimSuffix(trimmed, "/")
if level < len(pathStack) {
pathStack = pathStack[:level]
}
pathStack = append(pathStack, name)
FullPath := filepath.Join(pathStack...)
fmt.Println("Full path:", FullPath)
}
if err := scanner.Err(); err != nil{
fmt.Println("Error reading input:", err)
}
}project/
cmd/
main.go
internal/
app.goThen answer:
-
What is the stack after reading cmd/?
-
What is the stack after reading main.go?
-
What is the full path printed for app.go?
This will check that you understand stack logic + path building.
Goal:
Turn the full paths we calculated into real folders and files on disk.
Concept:
-
If a line ends with / → it’s a folder
-
Use
os.MkdirAllto create the folder and any missing parents -
Permissions
0755→ owner can read/write/execute, others read/execute
-
Use
strings.HasSuffix(name, "/")to check if it’s a directory -
Use
filepath.Join(stack...)to get the full path -
Handle errors with
if err != nil
You detect directories using:
strings.HasSuffix(trimmed, "/")
Why must you check trimmed and not name?
Given the stack:
pathStack = ["project", "internal"]
name = "handlers"
isDir = true
-
What should the full path be?
-
Write the answer exactly as a string.
Imagine:
isDir == true
fullPath == "project/internal/handlers"
Which function will create the directory?
A) os.Create(fullPath)
B) os.Mkdir(fullPath, 0755)
C) os.MkdirAll(fullPath, 0755)
D) filepath.Join(fullPath)
Choose the correct letter.
When making directories, why must we use MkdirAll instead of Mkdir?
Explain in 1–2 sentences.
Before writing code, we break the logic into tiny parts.
Inside the loop, for every line:
If isDir == true
→ create a folder
If isDir == false
→ create a fileWe already know how to compute:
isDir
fullPath
pathStack
Now you learn how to create things.
os.MkdirAll(fullPath, 0755)
This means:
- "Make this directory and any parents"
0755 = permissions (owner read/write/execute, others read/execute)
This works for:
-
project/internal/handlers
-
even if internal/ didn't exist yet.
f, err := os.Create(fullPath)
This:
-
creates the file and overwrites it if it exists
-
returns a file handle
-
must be closed:
defer f.Close()
Not real Go code yet — just the mental blueprint:
if isDir:
MkdirAll(fullPath)
else:
Create(fullPath)That's it.
You understand this?
No code writing yet — just thinking.
Input:
myapp/
cmd/
main.go
pkg/
utils.goWrite EXACTLY what paths will be created (one per line, in order).
For each, write DIR or FILE:
backend/
backend/api/
backend/api/server.go
README.md
src/config/
Why must the file creation happen after computing pathStack?
(1–2 sentences)
What happens if you try os.Create("src/config")
when "src/config" is supposed to be a directory?
package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
func main() {
fmt.Println("Treemaker v1.0 — starting…")
time.Sleep(3 * time.Second) // creates delay before the next line
fmt.Println()
fmt.Println()
fmt.Println("Please kindly paste/write you're project tree")
fmt.Println()
scanner := bufio.NewScanner(os.Stdin)
var pathStack []string
for scanner.Scan() {
line := scanner.Text()
if strings.TrimSpace(line) == ""{
fmt.Println("\nProcessing tree...\n")
break // stops processing on an empty line
}
trimmed := strings.TrimLeft(line, " ")
indent := len(line) - len(trimmed)
level := indent / 2
isDir := strings.HasSuffix(trimmed, "/")
name := strings.TrimSuffix(trimmed, "/")
if level < len(pathStack) {
pathStack = pathStack[:level]
}
pathStack = append(pathStack, name)
FullPath := filepath.Join(pathStack...)
// fmt.Println("Full path:", FullPath)
if isDir {
err := os.MkdirAll(FullPath, 0755)
if err != nil {
fmt.Println("Error Creating directory: ", err)
return
}
fmt.Println("\nCreated directory: ", FullPath)
}else{
dir := filepath.Dir(FullPath)
err := os.MkdirAll(dir, 0755)
if err != nil {
fmt.Println("Error creating parent directories: ", err)
continue
}
f, err := os.Create(FullPath)
if err != nil {
fmt.Println("Error creating file: ", err)
continue
}
f.Close()
fmt.Println("\nCreated file: ", FullPath)
}
}
fmt.Println("\n Project tree successfully created!")
if err := scanner.Err(); err != nil{
fmt.Println("Error reading input:", err)
}
}Your treemaker currently understands this:
myapp/
cmd/
main.go
But it cannot understand this:
myapp/
├── cmd/
│ └── main.go
└── internal/
└── service.go
This phase teaches you to support both formats using a normalization layer.
Write down (on paper or inside your Guide.md) all the characters that appear in pretty trees:
Example:
├──
└──
│
❓ Question: List at least 4 ASCII tree symbols your treemaker must remove or transform.
Look at these two lines:
cmd/
│ └── main.go
❓ Questions:
1. Which one uses spaces for indentation? 2. Which one uses ASCII tree glyphs to represent indentation? 3. Why must both be converted into the same indentation format before your depth logic works?
Write 2–3 sentences.
Before writing code, outline in plain English:
- What should the normalization step do?
- What must it remove?
- What must it keep?
- Why must this run before indentation detection?
Write 3–5 sentences.
You will now manually transform ASCII tree lines into the simple format your program understands.
Transform each line manually.
Input:
├── src/
│ ├── main.go
│ └── utils.go
└── README.md
Write manually what each line should look like after normalization (spaces only, no ASCII symbols).
Expected pattern:
src/
main.go
utils.go
README.md
You must produce the exact indentation yourself.
For each of the normalized lines you wrote above, count:
- number of left spaces
- calculated level (spaces ÷ 2)
Example answer format:
" main.go" → 2 spaces → level 1
Based on your own transformations, answer:
❓ What are the exact steps to convert an ASCII tree line to your standard format?
Write down the algorithm in bullet points.
Example style (not the solution):
- Remove X
- Convert Y to Z
- Trim W
- Preserve indentation
Write a function signature only:
func normalizeLine(line string) string
Do NOT implement it yet.
Modify normalizeLine so it removes just one symbol:
│
Test input:
│ └── main.go
Expected output (after your manual reasoning — NOT code from me):
└── main.go
Run your program printing normalized lines only.
Now modify normalizeLine to remove:
├──
Test input:
├── src/
Expected output:
src/
Verify your logic.
Make your function handle all ASCII symbols you identified in Exercise 1.
Test with this tree:
app/
├── cmd/
│ ├── main.go
│ └── helper.go
└── README.md
Print ONLY the normalized lines (not paths yet).
Modify your program temporarily to print:
[normalized] -> [spaces] -> [level]
Example:
" main.go" -> 2 spaces -> level 1
Make sure the levels match the logical depth of the ASCII tree.
If not → fix normalization.
Paste a complex pretty tree:
backend/
├── api/
│ ├── server.go
│ └── router.go
├── internal/
│ └── utils.go
└── README.md
Verify the following:
- Normalized output (spaces only)
- Correct indent level detection
- Correct pathStack behavior
- Correct directory/file creation
Fix anything that breaks.
Paste this hybrid tree:
backend/
api/
routes.go
├── pkg/
│ └── helper.go
Your treemaker must:
- support mixed indentation
- still create correct paths
Debug until it works.
Write:
THE 5 RULES OF NORMALIZING ASCII TREES
These should be your own distilled principles after building and debugging the feature.
Now and ONLY now implement your final normalizeLine() function using all the knowledge you gathered.
Run a full project tree through it and verify the output.
Add the following sections:
- What ASCII trees are
- Why normalization is required
- How your algorithm works (in your own words)
- Example conversions
- Exercises
- Your final code (only after completing exercises)