File Processing Recipes
Practical recipes for working with files and directories.
Every recipe below creates its own sample input in a temp file, so you can paste any block straight into a .si file and run it.
Reading Line by Line
stream::read_all() and stream::read_lines() are eager — they pull the whole file into memory. To process a large file one line at a time, loop on read_line(), which returns nil at end of file.
Recipe
import std:io
import std:os
import std:println
# Sample input
path = `mktemp`
f = io:open(path, true, true)
f::write("""INFO service started
ERROR database connection refused
INFO retrying
ERROR database connection refused
""")
f::close()
process_large_file = |filename| {
line_count = 0
error_count = 0
file = io:open(filename)
loop {
line = file::read_line()
line == nil && break
line_count++
match { line ~ /ERROR/ => { error_count++ } }
}
file::close()
{ "lines": line_count, "errors": error_count }
}
stats = process_large_file(path)
println("Processed ${stats:lines} lines, found ${stats:errors} errors")
os:rm(path)
Output:
Processed 4 lines, found 2 errors
Use Cases
- Processing log files
- Analyzing large text files
- Streaming data transformation
- Memory-efficient file parsing
CSV Processing
Parse and transform CSV data with std:csv. csv:parse returns a list of rows, and every cell is a string — convert with ::to_number() when you need arithmetic.
Recipe
import std:csv
import std:io
import std:os
import std:println
# Sample input
path = `mktemp`
f = io:open(path, true, true)
f::write("""customer,amount,date,category
Alice,1200,2024-01-15,retail
Bob,300,2024-01-16,retail
Carol,4500,2024-01-17,wholesale
""")
f::close()
file = io:open(path)
content = file::read_all()
file::close()
rows = csv:parse(content)
# Skip the header row, keep the large orders
large = rows[1;]
::filter(|row| row[1]::to_number() > 1000)
::map(|row| {
"customer": row[0],
"amount": row[1]::to_number(),
"date": row[2],
"category": row[3],
})
total = large::fold(0, |acc, row| acc + row:amount)
println("Large orders: ${large::length()}")
println("Total: ${total}")
# Write the filtered rows back out. csv:generate requires string cells,
# so convert numbers with ::to_string().
body = large::map(|row| [row:customer, row:amount::to_string()])
out_rows = [["customer", "amount"]] + body
out_path = `mktemp`
out_file = io:open(out_path, true, true) # create=true, truncate=true
out_file::write(csv:generate(out_rows))
out_file::close()
println(`cat ${out_path}`)
os:rm(path)
os:rm(out_path)
Output:
Large orders: 2
Total: 5700
customer,amount
Alice,1200
Carol,4500
Variations
Convert CSV to JSON
import std:csv
import std:io
import std:json
import std:os
import std:println
path = `mktemp`
f = io:open(path, true, true)
f::write("id,name\n1,Alice\n2,Bob\n")
f::close()
file = io:open(path)
rows = csv:parse(file::read_all())
file::close()
headers = rows[0]
records = rows[1;]::map(|row| {
record = {}
loop through 0..headers::length() with i {
record[headers[i]] = row[i]
}
record
})
println(json:generate(records))
os:rm(path)
[{"id":"1","name":"Alice"},{"id":"2","name":"Bob"}]
Log Analysis
Extract and analyze information from log files. Suji’s regex support is match-only (no capture groups), so pull fields out with index_of and slicing.
Recipe
import std:io
import std:os
import std:println
# Sample input
path = `mktemp`
f = io:open(path, true, true)
f::write("""[2024-01-15 10:30:00] ERROR: Database connection failed
[2024-01-15 10:30:05] WARN: Retry scheduled
[2024-01-15 10:30:06] INFO: HTTP GET /health 200
[2024-01-15 10:31:00] ERROR: Database connection failed
""")
f::close()
parse_entry = |line| {
close = line::index_of("]")
timestamp = match { close >= 0 => line[1;close], _ => "unknown", }
rest = match { close >= 0 => line[(close + 2);], _ => line, }
sep = rest::index_of(": ")
level = match { sep >= 0 => rest[0;sep], _ => "UNKNOWN", }
message = match { sep >= 0 => rest[(sep + 2);], _ => rest, }
{ "timestamp": timestamp, "level": level, "message": message }
}
analyze_logs = |log_file| {
errors = []
warnings = []
requests = 0
file = io:open(log_file)
lines = file::read_lines()
file::close()
loop through lines with line {
entry = parse_entry(line)
match entry:level {
"ERROR" => { errors::push(entry) }
"WARN" => { warnings::push(entry) }
}
match { line ~ /HTTP/ => { requests++ } }
}
{
"error_count": errors::length(),
"warning_count": warnings::length(),
"request_count": requests,
"first_error": errors::first(nil),
}
}
report = analyze_logs(path)
println("Errors: ${report:error_count}")
println("Warnings: ${report:warning_count}")
println("Requests: ${report:request_count}")
println("First error: ${report:first_error:message}")
os:rm(path)
Output:
Errors: 2
Warnings: 1
Requests: 1
First error: Database connection failed
Batch Operations
The standard library has no directory listing function, so use the shell for discovery and Suji for the logic. ls -1 returns one name per line; split it and drop the empty trailing entry.
Recipe: Batch Rename
import std:os
import std:path
import std:println
# Sample directory with three files
dir = `mktemp -d`
loop through ["notes.txt", "todo.txt", "image.png"] with name {
`touch ${dir}/${name}`
}
list_files = |directory| `ls -1 ${directory}`
::split("\n")
::filter(|n| n::length() > 0)
batch_rename = |directory, pattern, old_ext, new_ext| {
renamed = 0
loop through list_files(directory) with file {
file !~ pattern && continue
new_name = file::replace(old_ext, new_ext)
old_path = path:join([directory, file])
new_path = path:join([directory, new_name])
`mv ${old_path} ${new_path}`
renamed++
println("Renamed: ${file} -> ${new_name}")
}
println("Renamed ${renamed} files")
}
batch_rename(dir, /\.txt$/, ".txt", ".md")
`rm -rf ${dir}`
Output:
Renamed: notes.txt -> notes.md
Renamed: todo.txt -> todo.md
Renamed 2 files
Recipe: Batch Backup
import std:os
import std:path
import std:println
import std:time
dir = `mktemp -d`
loop through ["a.json", "b.json", "c.yaml"] with name {
`touch ${dir}/${name}`
}
backup_files = |directory, pattern| {
stamp = time:now():epoch_ms
backup_dir = "${directory}_backup_${stamp}"
os:mkdir(backup_dir)
files = `ls -1 ${directory}`
::split("\n")
::filter(|n| n::length() > 0)
matching = files::filter(|f| f ~ pattern)
loop through matching with file {
source = path:join([directory, file])
dest = path:join([backup_dir, file])
`cp ${source} ${dest}`
println("Backed up: ${file}")
}
println("Backed up ${matching::length()} files")
backup_dir
}
backup_dir = backup_files(dir, /\.json$/)
`rm -rf ${dir} ${backup_dir}`
Output:
Backed up: a.json
Backed up: b.json
Backed up 2 files
Directory Traversal
There is no recursive walker in the stdlib either — find does the walking and Suji does the work.
Recipe
import std:println
dir = `mktemp -d`
`mkdir -p ${dir}/src/nested`
`touch ${dir}/src/main.si ${dir}/src/nested/util.si ${dir}/src/notes.txt`
walk_files = |root| `find ${root} -type f`
::split("\n")
::filter(|p| p::length() > 0)
all_files = walk_files(dir)
si_files = all_files::filter(|p| p ~ /\.si$/)
println("Found ${all_files::length()} files")
println("Suji sources: ${si_files::length()}")
`rm -rf ${dir}`
Output:
Found 3 files
Suji sources: 2
Use Case: Count Lines of Code
import std:io
import std:println
dir = `mktemp -d`
f1 = io:open("${dir}/a.si", true, true)
f1::write("import std:println\nprintln(1)\n")
f1::close()
f2 = io:open("${dir}/b.si", true, true)
f2::write("x = 1\n")
f2::close()
count_loc = |directory, pattern| {
total_lines = 0
file_count = 0
paths = `find ${directory} -type f`
::split("\n")
::filter(|p| p::length() > 0)
loop through paths::filter(|p| p ~ pattern) with filepath {
file = io:open(filepath)
total_lines += file::read_lines()::length()
file::close()
file_count++
}
{
"files": file_count,
"lines": total_lines,
"average": match {
file_count > 0 => total_lines / file_count,
_ => 0,
},
}
}
stats = count_loc(dir, /\.si$/)
println("Files: ${stats:files}")
println("Total lines: ${stats:lines}")
println("Average: ${stats:average::round()}")
`rm -rf ${dir}`
Output:
Files: 2
Total lines: 3
Average: 2
Checking Whether a File Exists
os:stat raises a fatal error when the path is missing, and there is no way to catch it — so test first with the shell, which can absorb the failure:
import std:println
exists = |p| `test -e "${p}" && echo yes || echo no` == "yes"
println(exists("/etc/hosts")) # true
println(exists("/no/such/file")) # false
Once you know the path exists, os:stat(path) gives you size, is_directory, mtime and friends.
Best Practices
DO:
- Use
read_line()in a loop for large files;read_all()andread_lines()load everything - Check that a file exists (
test -evia the shell) beforeos:statorio:open - Close streams with
::close()when you are done - Build paths with
path:join([a, b])instead of string concatenation - Quote interpolated paths in shell commands:
`ls -1 "${dir}"`
DON’T:
- Assume
os:statreturnsnilfor a missing path — it terminates the script - Let a shell command that may fail run unguarded; add
|| trueor&& echo yes || echo no - Feed numbers to
csv:generate— every cell must be a string - Hardcode file paths that only exist on your machine
- Forget that
csv:parsekeeps the header row as row0