Building a Command-Line File Manager in Kotlin Using Interfaces

Architecture Overview

This implementation uses three main components working together:

  • CommandLineTool: Handles user input parsing
  • LogicController: Routes commands to appropriate handlers
  • FileHandler: Executes file system operations

An interface-based approach provides a clean contract for all file operations.

Command Input Handler

package kfile

import java.util.Scanner

/**
 * Handles command-line input and displays prompts
 */
class CommandLineTool {
    companion object {
        val getInstance: CommandLineTool by lazy {
            CommandLineTool()
        }
    }

    fun run() {
        displayWelcome()
        while (true) {
            processInput()
        }
    }

    private fun displayWelcome() {
        println("Welcome to the File Manager. Enter commands:")
    }

    private fun processInput() {
        print("[$currentPathName]# ")
        val scanner = Scanner(System.`in`)
        val input = scanner.nextLine().trim()
        val tokens = input.split(" ")
        LogicController.getInstance.route(tokens.size, input, tokens)
    }
}

Command Routing Logic

package kfile

import kotlin.system.exitProcess

/**
 * Routes parsed commands to appropriate file operations
 */
class LogicController {
    companion object {
        val getInstance: LogicController by lazy {
            LogicController()
        }
    }

    fun route(argCount: Int, command: String, parts: List<String>) {
        when (argCount) {
            1 -> handleSingleArg(command)
            2 -> handleTwoArgs(parts)
            3 -> handleThreeArgs(parts)
            else -> println("Invalid command format")
        }
    }

    private fun handleSingleArg(cmd: String) {
        when (cmd) {
            "ls" -> FileHandler().listContents(workingDirectory)
            "exit" -> exitProcess(-1)
            "pwd" -> FileHandler().printWorkingDir(workingDirectory)
            else -> println("Unknown command")
        }
    }

    private fun handleTwoArgs(parts: List<String>) {
        val cmd1 = parts[0]
        val cmd2 = parts[1]
        when {
            cmd1 == "ls" && cmd2 == "-l" -> FileHandler().listWithDetails(workingDirectory)
            cmd1 == "mkdirs" -> FileHandler().createNestedDirs(workingDirectory, cmd2)
            cmd1 == "mkdir" -> FileHandler().createDirectory(workingDirectory, cmd2)
            cmd1 == "touch" -> FileHandler().createFile(workingDirectory, cmd2)
            cmd1 == "cd" && cmd2 == ".." -> FileHandler().navigateUp(workingDirectory)
            cmd1 == "cd" -> FileHandler().navigateTo(workingDirectory, cmd2)
            cmd1 == "cat" -> FileHandler().readFile(workingDirectory, cmd2)
            cmd1 == "rm" -> FileHandler().remove(workingDirectory, cmd2)
            else -> println("Unknown command")
        }
    }

    private fun handleThreeArgs(parts: List<String>) {
        val cmd1 = parts[0]
        val cmd2 = parts[1]
        val cmd3 = parts[2]
        when {
            cmd1 == "mv" -> FileHandler().moveFile(workingDirectory, cmd2, cmd3)
            cmd1 == "rn" -> FileHandler().renameItem(workingDirectory, cmd2, cmd3)
            cmd1 == "cp" -> FileHandler().duplicateFile(workingDirectory, cmd2, cmd3)
            else -> println("Unknown command")
        }
    }
}

Global Variables

package kfile

import java.util.ArrayList

// Modify this to match your target directory
var workingDirectory: String = "C:/Users/Username/Desktop"
var currentPathName: String = "desktop"
val pathHistory: ArrayList<String> = arrayListOf()

File Operations Implementation

package kfile

import java.io.File
import java.io.IOException
import java.lang.String.format

class FileHandler : FileOperations {
    override fun listContents(path: String) {
        File(path).list()?.forEach { name ->
            println(name)
        }
    }

    override fun listWithDetails(path: String) {
        File(path).listFiles()?.forEach { file ->
            val sizeMB = file.length().toDouble() / (1024 * 1024)
            println("${file.name}     ${format("%.3f", sizeMB)} MB")
        }
    }

    override fun printWorkingDir(path: String) {
        println(path)
    }

    override fun createNestedDirs(path: String, name: String) {
        val directory = File(path, name)
        if (directory.mkdirs()) {
            println("Directory created")
        } else {
            println("Creation failed")
        }
    }

    override fun createDirectory(path: String, name: String) {
        val directory = File("$path/$name")
        if (directory.mkdir()) {
            println("Directory created")
        } else {
            println("Creation failed")
        }
    }

    override fun createFile(path: String, name: String) {
        val file = File(path, name)
        if (file.createNewFile()) {
            println("File created")
        } else {
            println("Creation failed")
        }
    }

    override fun navigateTo(path: String, name: String) {
        pathHistory.add(name)
        workingDirectory = "$path/$name"
        currentPathName = "$currentPathName/$name"
    }

    override fun navigateUp(path: String) {
        if (pathHistory.isNotEmpty()) {
            val segmentLength = pathHistory[pathHistory.size - 1].length + 1
            workingDirectory = trimEndChars(workingDirectory, segmentLength)
            currentPathName = trimEndChars(currentPathName, segmentLength)
            pathHistory.removeAt(pathHistory.size - 1)
        }
    }

    override fun readFile(path: String, name: String) {
        val file = File(path, name)
        if (file.exists()) {
            println(file.readText())
        } else {
            println("File not found")
        }
    }

    override fun remove(path: String, name: String) {
        val target = File(path, name)
        if (!target.exists()) {
            println("Target does not exist")
            return
        }
        if (recursiveDelete(target)) {
            println("Removed successfully")
        } else {
            println("Removal failed")
        }
    }

    private fun recursiveDelete(item: File): Boolean {
        if (item.exists()) {
            item.listFiles()?.forEach { file ->
                if (file.isDirectory) {
                    recursiveDelete(file)
                } else {
                    file.delete()
                }
            }
        }
        return item.delete()
    }

    override fun moveFile(path: String, source: String, dest: String) {
        val sourceFile = File(path, source)
        val destFolder = File(path, dest)
        val destination = File(destFolder, source)
        try {
            if (sourceFile.renameTo(destination)) {
                println("Moved successfully")
            } else {
                println("Move failed")
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun renameItem(path: String, source: String, dest: String) {
        val sourceFile = File(path, source)
        val destFile = File(path, dest)
        try {
            if (sourceFile.renameTo(destFile)) {
                println("Renamed successfully")
            } else {
                println("Rename failed")
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun duplicateFile(path: String, source: String, dest: String) {
        val sourceFile = File(path, source)
        val destFolder = File(path, dest)
        val resultFile = File(destFolder, source)
        try {
            deepCopy(sourceFile, resultFile)
            println("Copied successfully")
        } catch (e: IOException) {
            e.printStackTrace()
            println("Copy failed: ${e.message}")
        }
    }

    private fun deepCopy(src: File, dest: File) {
        if (src.isDirectory) {
            if (!dest.exists()) {
                dest.mkdirs()
            }
            src.listFiles()?.forEach { file ->
                deepCopy(file, File(dest, file.name))
            }
        } else {
            src.copyTo(dest, overwrite = true)
        }
    }

    private fun trimEndChars(input: String, count: Int): String {
        return if (count >= input.length) "" else input.substring(0, input.length - count)
    }
}

Operation Contract Interface

package kfile

interface FileOperations {
    fun listContents(path: String)
    fun listWithDetails(path: String)
    fun printWorkingDir(path: String)
    fun createNestedDirs(path: String, name: String)
    fun createDirectory(path: String, name: String)
    fun createFile(path: String, name: String)
    fun navigateTo(path: String, name: String)
    fun navigateUp(path: String)
    fun readFile(path: String, name: String)
    fun remove(path: String, name: String)
    fun moveFile(path: String, source: String, dest: String)
    fun renameItem(path: String, source: String, dest: String)
    fun duplicateFile(path: String, source: String, dest: String)
}

Entry Point

package kfile

fun main() {
    CommandLineTool.getInstance.run()
}

Supported Commands

Command Arguments Description
ls 0 List directory contents
ls -l 0 List with file sizes
pwd 0 Print working directory
mkdir 1 Create single directory
mkdirs 1 Create nested directories
touch 1 Create empty file
cd 1 Navigate to directory
cd .. 0 Navigate to parent
cat 1 Display file contents
rm 1 Delete file or folder
mv 2 Move source to destination
rn 2 Raname item
cp 2 Copy source to destination
exit 0 Terminate program

Tags: kotlin file operations Command Line Interface Design Patterns

Posted on Thu, 17 Sep 2026 16:48:44 +0000 by setic