DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 17: file operations and command-line tool

In previous lessons, we learned about library and package management, mastering core skills for code organization and dependency management. Today we'll explore Dart's capabilities in file operations and command-line tool development. These skills are extremely useful for data processing, automation scripts, and tool development, significantly boosting your development efficiency.

I. File Operations Basics: Using the dart:io Library

Dart's standard dart:io library provides comprehensive APIs for file and directory operations, supporting reading, writing, deleting files, and directory management.

1. Importing the dart:io Library

Before working with files, import the dart:io library:

import 'dart:io';
Enter fullscreen mode Exit fullscreen mode

2. Reading File Content

There are multiple ways to read files, choose the appropriate method based on file size and requirements:

(1) Reading Small Files at Once

Create a new example.txt in the project root directory, For small text files, you can read the entire content at once with readAsString():

import 'dart:io';

void main() async {
  // Define file path
  final file = File('example.txt');

  try {
    // Read file content (asynchronous operation)
    String content = await file.readAsString();
    print('File content:');
    print(content);
  } catch (e) {
    print('Failed to read file: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode
(2) Reading Files Line by Line

Create a new large_file.txt in the project root directory, For large files, reading line by line is more efficient and avoids excessive memory usage:

import 'dart:io';
import 'dart:convert'; // Provides utf8 encoding

void main() async {
  final file = File('large_file.txt');

  try {
    // Read line by line (returns Stream<String>)
    Stream<String> lines = file
        .openRead()
        .transform(utf8.decoder)
        .transform(LineSplitter());

    await for (String line in lines) {
      print('Line content: $line');
      // Add processing logic here, like searching for specific content
    }
  } catch (e) {
    print('Failed to read file: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Writing File Content

There are also multiple ways to write files, supporting overwriting or appending content:

(1) Overwriting a File
import 'dart:io';

void main() async {
  final file = File('output.txt');

  try {
    // Write string (overwrites existing content)
    await file.writeAsString('Hello, Dart file operations!\nThis is the second line');
    print('File written successfully');
  } catch (e) {
    print('Failed to write file: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode
(2) Appending to a File
import 'dart:io';

void main() async {
  final file = File('output.txt');

  try {
    // Append content (mode: FileMode.append)
    await file.writeAsString(
      '\nThis is appended content',
      mode: FileMode.append,
    );
    print('Content appended successfully');
  } catch (e) {
    print('Failed to append: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode
(3) Writing Byte Data

Put a original_image.png picture in the root directory,For binary files (like images, audio), use writeAsBytes():

import 'dart:io';

void main() async {
  final imageFile = File('image_copy.png');
  final sourceFile = File('original_image.png');

  try {
    // Read byte data
    List<int> bytes = await sourceFile.readAsBytes();
    // Write byte data
    await imageFile.writeAsBytes(bytes);
    print('Binary file copied successfully');
  } catch (e) {
    print('File copy failed: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Directory Operations

In addition to files, dart:io supports directory creation, deletion, and traversal:

import 'dart:io';

void main() async {
  // Define directory path
  final dir = Directory('my_files');

  // Check if directory exists
  if (!await dir.exists()) {
    // Create directory (recursive: true creates multi-level directories)
    await dir.create(recursive: true);
    print('Directory created successfully');
  }

  // Traverse files and subdirectories in the directory
  await for (FileSystemEntity entity in dir.list()) {
    if (entity is File) {
      print('File: ${entity.path}');
    } else if (entity is Directory) {
      print('Directory: ${entity.path}');
    }
  }

  // Delete directory (recursive: true deletes directory and all contents)
  // await dir.delete(recursive: true);
  // print('Directory deleted successfully');
}
Enter fullscreen mode Exit fullscreen mode

II. Command-Line Argument Parsing: Using the args Package

When developing command-line tools, you typically need to parse user input arguments (like --help, -o output.txt). The args package is the preferred tool for handling command-line arguments in the Dart ecosystem.

1. Adding the args Dependency

Add the dependency to pubspec.yaml:

dependencies:
  args: ^2.7.0
Enter fullscreen mode Exit fullscreen mode

Run dart pub get to install the dependency.

2. Basic Argument Parsing

import 'package:args/args.dart';

void main(List<String> arguments) {
  // Create argument parser
  final parser = ArgParser()
    ..addFlag(
      'help',
      abbr: 'h',
      help: 'Show help information',
      negatable: false,
    )
    ..addOption('output', abbr: 'o', help: 'Specify output file path')
    ..addOption(
      'mode',
      help: 'Run mode',
      allowed: ['debug', 'release'],
      defaultsTo: 'debug',
    );

  try {
    // Parse arguments
    final results = parser.parse(arguments);

    // Handle --help argument
    if (results['help'] as bool) {
      print('Usage: dart script.dart [options]');
      print(parser.usage);
      return;
    }

    // Get other argument values
    print('Output file: ${results['output'] ?? 'not specified'}');
    print('Run mode: ${results['mode']}');
    print(
      'Positional arguments: ${results.rest}',
    ); // Unparsed positional arguments
  } on FormatException catch (e) {
    print('Argument error: $e');
    print('Use --help for assistance');
  }
}
Enter fullscreen mode Exit fullscreen mode

Argument type explanations:

  • addFlag: Boolean arguments (like --help), abbr specifies short option (like -h)
  • addOption: Options with values (like --output file.txt)
  • results.rest: Gets unparsed positional arguments

Example usage:

dart script.dart -o result.txt --mode release input1.txt input2.txt
Enter fullscreen mode Exit fullscreen mode

Output:

Output file: result.txt
Run mode: release
Positional arguments: [input1.txt, input2.txt]
Enter fullscreen mode Exit fullscreen mode

III. Practical Project: Batch File Renaming Script

Combining file operations and command-line argument parsing, we'll develop a useful batch renaming tool. This tool supports:

  • Specifying target directory
  • Adding prefixes/suffixes
  • Renaming with sequence numbers
  • Previewing renaming effect (without actual modification)

Complete Code Implementation

import 'dart:io';
import 'package:args/args.dart';
import 'package:path/path.dart' as path;

void main(List<String> arguments) async {
  // Create argument parser
  final parser = ArgParser()
    ..addFlag(
      'help',
      abbr: 'h',
      help: 'Show help information',
      negatable: false,
    )
    ..addFlag(
      'preview',
      abbr: 'p',
      help: 'Preview changes without modifying',
      negatable: false,
    )
    ..addOption(
      'dir',
      abbr: 'd',
      help: 'Target directory path',
      defaultsTo: '.',
    )
    ..addOption('prefix', abbr: 'x', help: 'Filename prefix')
    ..addOption('suffix', abbr: 's', help: 'Filename suffix')
    ..addFlag(
      'number',
      abbr: 'n',
      help: 'Add sequence numbers',
      negatable: false,
    )
    ..addOption(
      'start',
      abbr: 't',
      help: 'Starting number for sequence',
      defaultsTo: '1',
    );

  try {
    final results = parser.parse(arguments);

    // Show help information
    if (results['help'] as bool) {
      print('Batch Rename Tool');
      print('Usage: dart rename.dart [options]');
      print(parser.usage);
      return;
    }

    // Parse arguments
    final previewMode = results['preview'] as bool;
    final targetDir = Directory(results['dir'] as String);
    final prefix = results['prefix'] as String? ?? '';
    final suffix = results['suffix'] as String? ?? '';
    final addNumber = results['number'] as bool;
    final startNumber = int.tryParse(results['start'] as String) ?? 1;

    // Check if directory exists
    if (!await targetDir.exists()) {
      print('Error: Directory does not exist - ${targetDir.path}');
      return;
    }

    // Get files in directory (exclude subdirectories)
    final files = await targetDir
        .list()
        .where((entity) => entity is File)
        .toList();
    files.sort((a, b) => a.path.compareTo(b.path)); // Sort by path

    if (files.isEmpty) {
      print('No files in directory: ${targetDir.path}');
      return;
    }

    // Batch rename process
    print(
      '${previewMode ? 'Previewing' : 'Performing'} rename (${files.length} files total):',
    );
    int currentNumber = startNumber;

    for (final file in files.cast<File>()) {
      // Get filename and extension
      final fileName = path.basename(file.path);
      final extension = path.extension(fileName);
      final baseName = path.basenameWithoutExtension(fileName);

      // Build new filename
      String newName = '$prefix$baseName$suffix';
      if (addNumber) {
        newName = '${newName}_$currentNumber';
        currentNumber++;
      }
      newName += extension;

      // Build new path
      final newPath = path.join(targetDir.path, newName);

      // Show or perform rename
      print('${file.path}$newPath');
      if (!previewMode && file.path != newPath) {
        await file.rename(newPath);
      }
    }

    print('${previewMode ? 'Preview' : 'Rename'} completed');
  } on FormatException catch (e) {
    print('Argument error: $e');
    print('Use --help for assistance');
  } catch (e) {
    print('Operation failed: $e');
  }
}
Enter fullscreen mode Exit fullscreen mode

Code Explanation

  1. Argument Definition: Various parameters are defined through the args package to meet different renaming needs.
  2. Directory Check: Verifies if the target directory exists to avoid errors.
  3. File Processing:
    • Filters files in the directory (excludes subdirectories)
    • Sorts by path to ensure consistent renaming order
  4. Filename Construction:
    • Separates filename and extension
    • Generates new filenames based on prefix, suffix, sequence number and other parameters
  5. Rename Execution: Supports preview mode (shows effect only) and actual execution mode.

Usage Examples

  1. Preview adding prefix "photo_" and sequence numbers to files in current directory:
dart rename.dart -p -x photo_ -n
Enter fullscreen mode Exit fullscreen mode

2 . Add suffix "_edited" to files in the images directory:

dart rename.dart -d images -s _edited
Enter fullscreen mode Exit fullscreen mode

IV. Other Useful File Operation Techniques

1. Getting File Information

import 'dart:io';

void printFileInfo(File file) async {
  final stat = await file.stat();
  print('Path: ${file.path}');
  print('Size: ${stat.size} bytes');
  print('Modified: ${stat.modified}');
  print('Is file: ${stat.type == FileSystemEntityType.file}');
}
Enter fullscreen mode Exit fullscreen mode

2. Copying Files

import 'dart:io';

Future<void> copyFile(String sourcePath, String targetPath) async {
  final source = File(sourcePath);
  final target = File(targetPath);

  if (await source.exists()) {
    await source.copy(targetPath);
    print('Copied successfully: $targetPath');
  } else {
    print('Source file does not exist: $sourcePath');
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Recursively Traversing Directories

import 'dart:io';
import 'package:path/path.dart' as path;

Future<void> listAllFiles(Directory dir, {int indent = 0}) async {
  final spaces = '  ' * indent;

  await for (final entity in dir.list()) {
    if (entity is Directory) {
      print('${spaces}Directory: ${entity.path}');
      // Recursively traverse subdirectories
      await listAllFiles(entity, indent: indent + 1);
    } else if (entity is File) {
      print('${spaces}File: ${path.basename(entity.path)}');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)