Skip to main content

File Operations

This recipe shows how to create CLI commands for file operations like reading, writing, processing, and managing files.

Basic File Commands

Create commands for basic file operations:

import { Command, CommandHandler } from '@navios/commander'
import { inject, Injectable } from '@navios/di'
import { z } from 'zod'
import * as fs from 'fs/promises'
import * as path from 'path'

@Injectable()
class FileService {
async readFile(filePath: string): Promise<string> {
return fs.readFile(filePath, 'utf-8')
}

async writeFile(filePath: string, content: string): Promise<void> {
await fs.writeFile(filePath, content, 'utf-8')
}

async exists(filePath: string): Promise<boolean> {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}

async getStats(filePath: string) {
return fs.stat(filePath)
}
}

const readSchema = z.object({
file: z.string(),
encoding: z.string().default('utf-8'),
})

@Command({
path: 'file:read',
optionsSchema: readSchema,
})
export class ReadFileCommand implements CommandHandler<
z.infer<typeof readSchema>
> {
private fileService = inject(FileService)

async execute(options) {
if (!(await this.fileService.exists(options.file))) {
throw new Error(`File not found: ${options.file}`)
}

const content = await this.fileService.readFile(options.file)
console.log(content)
}
}

const writeSchema = z.object({
file: z.string(),
content: z.string(),
append: z.boolean().default(false),
})

@Command({
path: 'file:write',
optionsSchema: writeSchema,
})
export class WriteFileCommand implements CommandHandler<
z.infer<typeof writeSchema>
> {
private fileService = inject(FileService)

async execute(options) {
if (options.append) {
const existing = await this.fileService.exists(options.file)
? await this.fileService.readFile(options.file)
: ''
await this.fileService.writeFile(
options.file,
existing + options.content
)
} else {
await this.fileService.writeFile(options.file, options.content)
}
console.log(`File written: ${options.file}`)
}
}

File Processing Commands

Create commands for processing files:

@Injectable()
class FileProcessorService {
async processFile(
input: string,
output: string,
format: 'json' | 'csv' | 'xml'
) {
const content = await fs.readFile(input, 'utf-8')
const processed = this.convertFormat(content, format)
await fs.writeFile(output, processed, 'utf-8')
return { input, output, format, size: processed.length }
}

private convertFormat(content: string, format: string): string {
// Format conversion logic
switch (format) {
case 'json':
return JSON.stringify({ data: content }, null, 2)
case 'csv':
return content.split('\n').map(line => line.split(',').join(',')).join('\n')
case 'xml':
return `<data>${content}</data>`
default:
return content
}
}
}

const processSchema = z.object({
input: z.string(),
output: z.string().optional(),
format: z.enum(['json', 'csv', 'xml']).default('json'),
})

@Command({
path: 'file:process',
optionsSchema: processSchema,
})
export class ProcessFileCommand implements CommandHandler<
z.infer<typeof processSchema>
> {
private processor = inject(FileProcessorService)

async execute(options) {
const output = options.output || options.input.replace(/\.[^.]+$/, `.${options.format}`)
const result = await this.processor.processFile(
options.input,
output,
options.format
)
console.log(`Processed: ${result.input} -> ${result.output}`)
console.log(`Format: ${result.format}, Size: ${result.size} bytes`)
}
}

File Search Commands

Create commands for searching files:

@Injectable()
class FileSearchService {
async search(
directory: string,
pattern: string,
recursive: boolean = true
): Promise<string[]> {
const files: string[] = []
await this.searchDirectory(directory, pattern, recursive, files)
return files
}

private async searchDirectory(
dir: string,
pattern: string,
recursive: boolean,
files: string[]
) {
const entries = await fs.readdir(dir, { withFileTypes: true })

for (const entry of entries) {
const fullPath = path.join(dir, entry.name)

if (entry.isFile() && entry.name.match(new RegExp(pattern))) {
files.push(fullPath)
} else if (entry.isDirectory() && recursive) {
await this.searchDirectory(fullPath, pattern, recursive, files)
}
}
}
}

const searchSchema = z.object({
directory: z.string().default('.'),
pattern: z.string(),
recursive: z.boolean().default(true),
})

@Command({
path: 'file:search',
optionsSchema: searchSchema,
})
export class SearchFileCommand implements CommandHandler<
z.infer<typeof searchSchema>
> {
private searchService = inject(FileSearchService)

async execute(options) {
const files = await this.searchService.search(
options.directory,
options.pattern,
options.recursive
)

if (files.length === 0) {
console.log('No files found')
} else {
console.log(`Found ${files.length} file(s):`)
files.forEach((file) => console.log(` - ${file}`))
}
}
}

File Statistics Commands

Create commands for file statistics:

@Injectable()
class FileStatsService {
async getStats(filePath: string) {
const stats = await fs.stat(filePath)
return {
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
accessed: stats.atime,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
}
}

async getDirectoryStats(directory: string) {
const entries = await fs.readdir(directory, { withFileTypes: true })
let totalSize = 0
let fileCount = 0
let dirCount = 0

for (const entry of entries) {
const fullPath = path.join(directory, entry.name)
const stats = await fs.stat(fullPath)

if (entry.isFile()) {
totalSize += stats.size
fileCount++
} else if (entry.isDirectory()) {
dirCount++
}
}

return { totalSize, fileCount, dirCount }
}
}

const statsSchema = z.object({
file: z.string(),
format: z.enum(['human', 'json']).default('human'),
})

@Command({
path: 'file:stats',
optionsSchema: statsSchema,
})
export class FileStatsCommand implements CommandHandler<
z.infer<typeof statsSchema>
> {
private statsService = inject(FileStatsService)

async execute(options) {
const stats = await this.statsService.getStats(options.file)

if (options.format === 'json') {
console.log(JSON.stringify(stats, null, 2))
} else {
console.log(`File: ${options.file}`)
console.log(`Size: ${this.formatBytes(stats.size)}`)
console.log(`Created: ${stats.created}`)
console.log(`Modified: ${stats.modified}`)
console.log(`Type: ${stats.isFile ? 'File' : 'Directory'}`)
}
}

private formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB']
let size = bytes
let unitIndex = 0

while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}

return `${size.toFixed(2)} ${units[unitIndex]}`
}
}

const dirStatsSchema = z.object({
directory: z.string().default('.'),
})

@Command({
path: 'file:dir-stats',
optionsSchema: dirStatsSchema,
})
export class DirectoryStatsCommand implements CommandHandler<
z.infer<typeof dirStatsSchema>
> {
private statsService = inject(FileStatsService)

async execute(options) {
const stats = await this.statsService.getDirectoryStats(options.directory)
console.log(`Directory: ${options.directory}`)
console.log(`Total size: ${this.formatBytes(stats.totalSize)}`)
console.log(`Files: ${stats.fileCount}`)
console.log(`Directories: ${stats.dirCount}`)
}

private formatBytes(bytes: number): string {
// Same as above
return `${(bytes / 1024).toFixed(2)} KB`
}
}

File Copy and Move Commands

Create commands for copying and moving files:

@Injectable()
class FileOperationService {
async copy(source: string, destination: string) {
await fs.copyFile(source, destination)
}

async move(source: string, destination: string) {
await fs.rename(source, destination)
}

async delete(filePath: string) {
await fs.unlink(filePath)
}
}

const copySchema = z.object({
source: z.string(),
destination: z.string(),
overwrite: z.boolean().default(false),
})

@Command({
path: 'file:copy',
optionsSchema: copySchema,
})
export class CopyFileCommand implements CommandHandler<
z.infer<typeof copySchema>
> {
private fileService = inject(FileOperationService)
private fileService2 = inject(FileService)

async execute(options) {
if (!(await this.fileService2.exists(options.source))) {
throw new Error(`Source file not found: ${options.source}`)
}

if (await this.fileService2.exists(options.destination) && !options.overwrite) {
throw new Error(`Destination file exists. Use --overwrite to replace.`)
}

await this.fileService.copy(options.source, options.destination)
console.log(`Copied: ${options.source} -> ${options.destination}`)
}
}

const moveSchema = z.object({
source: z.string(),
destination: z.string(),
})

@Command({
path: 'file:move',
optionsSchema: moveSchema,
})
export class MoveFileCommand implements CommandHandler<
z.infer<typeof moveSchema>
> {
private fileService = inject(FileOperationService)
private fileService2 = inject(FileService)

async execute(options) {
if (!(await this.fileService2.exists(options.source))) {
throw new Error(`Source file not found: ${options.source}`)
}

await this.fileService.move(options.source, options.destination)
console.log(`Moved: ${options.source} -> ${options.destination}`)
}
}

const deleteSchema = z.object({
file: z.string(),
force: z.boolean().default(false),
})

@Command({
path: 'file:delete',
optionsSchema: deleteSchema,
})
export class DeleteFileCommand implements CommandHandler<
z.infer<typeof deleteSchema>
> {
private fileService = inject(FileOperationService)
private fileService2 = inject(FileService)

async execute(options) {
if (!(await this.fileService2.exists(options.file))) {
if (options.force) {
return // File doesn't exist, nothing to delete
}
throw new Error(`File not found: ${options.file}`)
}

await this.fileService.delete(options.file)
console.log(`Deleted: ${options.file}`)
}
}

Module Organization

Organize file commands into a module:

import { CliModule } from '@navios/commander'
import { ReadFileCommand } from './commands/read-file.command'
import { WriteFileCommand } from './commands/write-file.command'
import { ProcessFileCommand } from './commands/process-file.command'
import { SearchFileCommand } from './commands/search-file.command'
import { FileStatsCommand } from './commands/file-stats.command'
import { DirectoryStatsCommand } from './commands/directory-stats.command'
import { CopyFileCommand } from './commands/copy-file.command'
import { MoveFileCommand } from './commands/move-file.command'
import { DeleteFileCommand } from './commands/delete-file.command'

@CliModule({
commands: [
ReadFileCommand,
WriteFileCommand,
ProcessFileCommand,
SearchFileCommand,
FileStatsCommand,
DirectoryStatsCommand,
CopyFileCommand,
MoveFileCommand,
DeleteFileCommand,
],
})
export class FileModule {}

Usage Examples

# Read file
node cli.js file:read --file data.txt
node cli.js file:read --file data.txt --encoding utf-8

# Write file
node cli.js file:write --file output.txt --content "Hello, World!"
node cli.js file:write --file output.txt --content "Appended" --append

# Process file
node cli.js file:process --input data.txt --output data.json --format json
node cli.js file:process --input data.txt --format csv

# Search files
node cli.js file:search --pattern "\.ts$" --directory src
node cli.js file:search --pattern "test" --recursive false

# File statistics
node cli.js file:stats --file data.txt
node cli.js file:stats --file data.txt --format json
node cli.js file:dir-stats --directory src

# Copy and move
node cli.js file:copy --source file.txt --destination backup.txt
node cli.js file:copy --source file.txt --destination backup.txt --overwrite
node cli.js file:move --source old.txt --destination new.txt
node cli.js file:delete --file temp.txt
node cli.js file:delete --file temp.txt --force