CSharp Options flag
Let's Add a new command-line flag:
1. Update Program.cs to include the --replace flag:
// Program.cs
using System;
using System.CommandLine;
using System.IO;
using System.Threading.Tasks;
using JsonToMarkdownAppender;
using JsonToMarkdownAppender.Core;
using JsonToMarkdownAppender.Models;
using JsonToMarkdownAppender.Services;
class Program
{
static async Task<int> Main(string[] args)
{
var jsonFileOption = new Option<FileInfo>(
name: "--json-file",
description: "The input JSON file path.",
getDefaultValue: () => new FileInfo("data.json"));
var targetDirOption = new Option<DirectoryInfo>(
name: "--target-directory",
description: "The directory to search for the target Markdown file.",
getDefaultValue: () => new DirectoryInfo("."));
var targetPatternOption = new Option<string>(
name: "--target-pattern",
description: "Regex pattern to find the target Markdown file (e.g., \"^README.*\\.md$\").",
getDefaultValue: () => "^NOTES.*\\.md$");
var archiveSubDirOption = new Option<string?>(
name: "--archive-subdir",
description: "Optional subdirectory within the source/target file's directory to move archived files to.");
var replaceOption = new Option<bool>( // New option
name: "--replace",
description: "If set, the target file's content will be replaced instead of appended.",
getDefaultValue: () => false); // Default to false (append mode)
var rootCommand = new RootCommand("CLI tool to parse JSON, convert to Markdown, and append/replace content in a file.");
rootCommand.AddOption(jsonFileOption);
rootCommand.AddOption(targetDirOption);
rootCommand.AddOption(targetPatternOption);
rootCommand.AddOption(archiveSubDirOption);
rootCommand.AddOption(replaceOption); // Add new option to the command
rootCommand.SetHandler(async (jsonFile, targetDir, targetPattern, archiveSubDir, replace) => // Add 'replace' parameter
{
// Simple Dependency Injection setup (manual for now)
IJsonProcessor<JsonContent> jsonProcessor = new DefaultJsonProcessor();
IMarkdownConverter<JsonContent> markdownConverter = new SimpleMarkdownConverter();
IFileFinder fileFinder = new RegexFileFinder();
IFileArchiver fileArchiver = new TimestampFileArchiver();
// IFileWriter fileWriter = new DefaultFileWriter(); // We'll use File.WriteAllTextAsync directly in AppLogic now
var appLogic = new AppLogic(
jsonProcessor,
markdownConverter,
fileFinder,
fileArchiver
// fileWriter // Removed from AppLogic constructor
);
await appLogic.RunAsync(jsonFile.FullName, targetDir.FullName, targetPattern, archiveSubDir, replace); // Pass 'replace' flag
}, jsonFileOption, targetDirOption, targetPatternOption, archiveSubDirOption, replaceOption); // Pass option to handler
return await rootCommand.InvokeAsync(args);
}
}Changes in Program.cs:
Added a
replaceOptionof typeOption<bool>.Set its
getDefaultValuetofalse, so appending is the default behavior.Added the
replaceOptionto therootCommand.Updated the
SetHandlerlambda to accept thereplaceboolean value.Commented out
IFileWriterinstantiation and passing toAppLogicas we'll handle file writing directly inAppLogicfor more control over append/replace.
2. Update AppLogic.cs to handle the replace flag:
// AppLogic.cs
using System;
using System.IO;
using System.Threading.Tasks;
using JsonToMarkdownAppender.Core;
using JsonToMarkdownAppender.Models;
namespace JsonToMarkdownAppender;
public class AppLogic(
IJsonProcessor<JsonContent> jsonProcessor,
IMarkdownConverter<JsonContent> markdownConverter,
IFileFinder fileFinder,
IFileArchiver fileArchiver) // Removed IFileWriter from constructor
{
public async Task<int> RunAsync(
string jsonFilePath,
string targetDirectory,
string targetFilePattern,
string? archiveSubDir,
bool replaceMode) // New parameter for replace mode
{
Console.WriteLine($"Processing JSON file: {jsonFilePath}");
Console.WriteLine($"Target directory: {targetDirectory}");
Console.WriteLine($"Target file pattern: {targetFilePattern}");
Console.WriteLine($"Operation mode: {(replaceMode ? "Replace" : "Append")}"); // Indicate mode
if (!string.IsNullOrEmpty(archiveSubDir))
{
Console.WriteLine($"Archive subdirectory: {archiveSubDir}");
}
Console.WriteLine("---");
// 1. Process JSON
var jsonData = await jsonProcessor.ProcessAsync(jsonFilePath);
if (jsonData == null)
{
Console.Error.WriteLine("Failed to process JSON data. Aborting.");
return 1;
}
// 2. Convert to Markdown
string newMarkdownContent = markdownConverter.Convert(jsonData);
if (string.IsNullOrEmpty(newMarkdownContent) && !replaceMode) // If replacing, empty content is valid for replacement.
{
Console.WriteLine("Generated Markdown content is empty. No content to append.");
await fileArchiver.ArchiveAsync(jsonFilePath, archiveSubDir);
Console.WriteLine("---");
Console.WriteLine("Operation completed (no content to append/replace).");
return 0;
}
else if (string.IsNullOrEmpty(newMarkdownContent) && replaceMode)
{
Console.WriteLine("Generated Markdown content is empty. Target file will be replaced with empty content (if found and archived).");
}
// 3. Find target Markdown file
string? existingTargetFilePath = fileFinder.FindFile(targetDirectory, targetFilePattern);
string finalTargetFilePath;
string contentToWrite;
if (existingTargetFilePath != null)
{
Console.WriteLine($"Found target file: {existingTargetFilePath}");
finalTargetFilePath = existingTargetFilePath; // We will write back to the original name
string originalTargetContent = string.Empty;
if (!replaceMode) // Only read original content if in append mode
{
try
{
originalTargetContent = await File.ReadAllTextAsync(existingTargetFilePath);
if (!string.IsNullOrEmpty(originalTargetContent) && !originalTargetContent.EndsWith(Environment.NewLine))
{
originalTargetContent += Environment.NewLine;
}
}
catch (FileNotFoundException)
{
Console.WriteLine($"Info: Target file '{existingTargetFilePath}' disappeared before reading. Treating as new file for append.");
originalTargetContent = string.Empty; // Continue as if file was not found for reading
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error reading existing target file for append '{existingTargetFilePath}': {ex.Message}");
// Depending on strictness, you might want to abort here.
// For now, we'll proceed, potentially overwriting if the read failed but file exists.
// Or, treat as if content is empty for append.
originalTargetContent = string.Empty;
}
}
// 4. Archive existing target Markdown file (happens for both append and replace if file exists)
await fileArchiver.ArchiveAsync(existingTargetFilePath, archiveSubDir);
if (replaceMode)
{
contentToWrite = newMarkdownContent;
}
else // Append mode
{
contentToWrite = originalTargetContent + newMarkdownContent;
}
}
else // Target file not found
{
string baseName = targetFilePattern.TrimStart('^')
.Replace(".*", "")
.Replace("\\.md$", ".md", StringComparison.OrdinalIgnoreCase)
.Split('.')[0];
if (string.IsNullOrWhiteSpace(baseName)) baseName = "output";
if (!baseName.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) baseName += ".md";
finalTargetFilePath = Path.Combine(targetDirectory, baseName);
Console.WriteLine($"No target file found matching pattern. Will create: {finalTargetFilePath}");
contentToWrite = newMarkdownContent; // If file doesn't exist, append and replace are effectively the same: write new content.
}
// 5. Write the content (either new, or combined old+new)
try
{
string? directory = Path.GetDirectoryName(finalTargetFilePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
await File.WriteAllTextAsync(finalTargetFilePath, contentToWrite);
Console.WriteLine($"Successfully wrote content to '{finalTargetFilePath}'.");
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error writing content to file '{finalTargetFilePath}': {ex.Message}");
return 1; // Error
}
// 6. Archive source JSON file
await fileArchiver.ArchiveAsync(jsonFilePath, archiveSubDir);
Console.WriteLine("---");
Console.WriteLine("Operation completed successfully.");
return 0; // Success
}
}Changes in AppLogic.cs:
The constructor no longer takes
IFileWriter.RunAsyncnow accepts abool replaceModeparameter.Conditional Content Reading: The content of
existingTargetFilePathis only read if!replaceMode(i.e., we are in append mode).Archiving Target: The
existingTargetFilePathis archived if it exists, regardless ofreplaceMode. This is correct, as in both cases, the original state before this operation is being preserved.Determining
contentToWrite:If
replaceModeis true,contentToWriteis justnewMarkdownContent.If
replaceModeis false (append),contentToWriteisoriginalTargetContent + newMarkdownContent.If the target file wasn't found,
contentToWriteisnewMarkdownContent(as there's nothing to append to or replace from).
File Writing:
File.WriteAllTextAsync(finalTargetFilePath, contentToWrite)is used to write the determined content. This naturally handles both creating a new file and overwriting an existing one with the new (or combined) content.
How to Test:
Ensure
data.jsonandNOTES.mdexist.NOTES.md:# Original Project Notes Some initial content. ---data.json:{ "title": "Feature Update", "paragraphs": ["Added append/replace modes."] }Build:
dotnet buildTest Append Mode (default):
dotnet runNOTES.mdshould be archived asNOTES_<timestamp>.md(containing "Original Project Notes").data.jsonshould be archived.The new
NOTES.mdshould contain:# Original Project Notes Some initial content. --- ## Feature Update Added append/replace modes. ---
Prepare for Replace Test:
Modify
data.jsonagain for different content:{ "title": "Complete Replacement", "author": "Admin", "paragraphs": ["This content fully replaces the old file."] }The
NOTES.mdcurrently has the appended content from the previous step.
Test Replace Mode:
dotnet run --replaceThe current
NOTES.md(with "Feature Update" appended) should be archived asNOTES_<new_timestamp>.md.data.jsonshould be archived.The new
NOTES.mdshould only contain:## Complete Replacement _By: Admin_ This content fully replaces the old file. ---
This structure provides the flexibility you were looking for, defaulting to the safer "append" operation while allowing for a full "replace" when needed.
Last updated