Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
restore-keys: |
${{ runner.os }}-m2
- name: Test with Maven
run: ./mvnw test -B -Dmaven.test.skip=false -pl fesod-common,fesod-sheet
run: ./mvnw test -B -Dmaven.test.skip=false -pl fesod-common,fesod-sheet,fesod-cli
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: (!cancelled())
Expand Down
112 changes: 112 additions & 0 deletions fesod-cli/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--

Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.

-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.apache.fesod</groupId>
<artifactId>fesod-parent</artifactId>
<version>${revision}</version>
</parent>

<artifactId>fesod-cli</artifactId>
<packaging>jar</packaging>
<name>Apache Fesod CLI Tool</name>
<description>Command-line interface for Apache Fesod spreadsheet processing</description>

<properties>
<picocli.version>4.7.5</picocli.version>
<snakeyaml.version>2.2</snakeyaml.version>
<mainClass>org.apache.fesod.cli.FesodCli</mainClass>
</properties>

<dependencies>
<!-- Fesod Core Dependencies -->
<dependency>
<groupId>org.apache.fesod</groupId>
<artifactId>fesod-sheet</artifactId>
<version>${project.version}</version>
</dependency>

<!-- Picocli for CLI -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>

<!-- JSON Processing -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<scope>compile</scope>
</dependency>

<!-- YAML Configuration -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>

<!-- CSV Support -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
</dependency>

<!-- Logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<!-- Maven Compiler Plugin - Picocli annotation processor -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>info.picocli</groupId>
<artifactId>picocli-codegen</artifactId>
<version>${picocli.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
98 changes: 98 additions & 0 deletions fesod-cli/src/main/java/org/apache/fesod/cli/FesodCli.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.fesod.cli;

import org.apache.fesod.cli.commands.ConvertCommand;
import org.apache.fesod.cli.commands.InfoCommand;
import org.apache.fesod.cli.commands.ReadCommand;
import org.apache.fesod.cli.commands.VersionCommand;
import org.apache.fesod.cli.commands.WriteCommand;
import org.apache.fesod.cli.exception.CliException;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;

@Command(
name = "fesod-cli",
mixinStandardHelpOptions = true,
version = {"Apache Fesod CLI", "Java Runtime: ${java.version}", "OS: ${os.name} ${os.arch}"},
description = "Fast and Easy spreadsheet processing from the command line",
subcommands = {
ReadCommand.class,
WriteCommand.class,
ConvertCommand.class,
InfoCommand.class,
VersionCommand.class,
CommandLine.HelpCommand.class
},
usageHelpAutoWidth = true,
footer = {
"",
"Examples:",
" fesod-cli read data.xlsx --format json",
" fesod-cli convert input.xls output.xlsx",
" fesod-cli convert input.xlsx output.csv --sheet 0",
" fesod-cli convert input.xlsx output.xlsx --sheet-name \"Sales\"",
" fesod-cli info data.xlsx",
"",
"Documentation: https://fesod.apache.org/docs/cli",
"Report bugs: https://github.com/apache/fesod/issues"
})
public class FesodCli implements Runnable {

@Spec
CommandSpec spec;

@Option(
names = {"--verbose", "-v"},
description = "Enable verbose logging")
private boolean verbose;

@Override
public void run() {
// Default: show help when no command specified
spec.commandLine().usage(spec.commandLine().getOut());
}

public static void main(String[] args) {
int exitCode = new CommandLine(new FesodCli())
.setExecutionExceptionHandler((ex, cmd, parseResult) -> {
cmd.getErr().println(cmd.getColorScheme().errorText("Error: " + ex.getMessage()));

if (ex instanceof CliException) {
CliException cliEx = (CliException) ex;
if (cliEx.getCause() != null && parseResult.hasMatchedOption("--verbose")) {
cliEx.printStackTrace(cmd.getErr());
}
return cliEx.getExitCode();
} else {
if (parseResult.hasMatchedOption("--verbose")) {
ex.printStackTrace(cmd.getErr());
}
return 1;
}
})
.execute(args);

System.exit(exitCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.fesod.cli.commands;

import java.io.PrintWriter;
import java.nio.file.Paths;
import org.apache.fesod.cli.config.CliConfig;
import org.apache.fesod.cli.config.ConfigLoader;
import org.apache.fesod.cli.core.DocumentProcessor;
import org.apache.fesod.cli.core.ModuleRegistry;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Spec;

/**
* Base class for all CLI commands
*/
public abstract class BaseCommand implements Runnable {

@Spec
protected CommandSpec spec;

@Option(
names = {"--config", "-c"},
description = "Configuration file path (default: ~/.fesod/config.yaml)",
paramLabel = "<file>")
protected String configFile;

@Option(
names = {"--module", "-m"},
description = "Document module: sheet (default: sheet)",
defaultValue = "sheet")
protected String module;

protected CliConfig config;
protected DocumentProcessor processor;

/**
* Get the output print writer for this command
*/
protected PrintWriter getOut() {
return spec.commandLine().getOut();
}

protected void initialize() {
ConfigLoader loader = new ConfigLoader();
if (configFile != null) {
config = loader.loadFromFile(Paths.get(configFile));
} else {
config = loader.loadDefault();
}

processor = ModuleRegistry.getProcessor(module);
}

@Override
public void run() {
initialize();
execute();
}

protected abstract void execute();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.fesod.cli.commands;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;

/**
* Convert command implementation
*/
@Command(
name = "convert",
description = "Convert spreadsheet between different formats",
mixinStandardHelpOptions = true)
public class ConvertCommand extends BaseCommand {

@Parameters(index = "0", description = "Input file path", paramLabel = "<input>")
private String inputFile;

@Parameters(index = "1", description = "Output file path", paramLabel = "<output>")
private String outputFile;

@Option(
names = {"-s", "--sheet"},
description = "Sheet index (0-based) to convert. If not specified, all sheets will be converted.")
private Integer sheetIndex;

@Option(
names = {"-n", "--sheet-name"},
description = "Sheet name to convert")
private String sheetName;

@Option(
names = {"-a", "--all"},
description = "Convert all sheets (default if no sheet is specified)")
private Boolean convertAll;

@Override
protected void execute() {
Path input = Paths.get(inputFile);
Path output = Paths.get(outputFile);

Copy link

Copilot AI Dec 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ConvertCommand does not validate whether the input and output files have different paths. If a user accidentally specifies the same path for both input and output, the original file could be overwritten before it's fully read, leading to data corruption or loss. Consider adding validation to prevent input and output files from being the same.

Suggested change
if (input.toAbsolutePath().normalize().equals(output.toAbsolutePath().normalize())) {
throw new IllegalArgumentException(
"Input and output file paths must be different: " + inputFile);
}

Copilot uses AI. Check for mistakes.
Map<String, Object> options = new HashMap<String, Object>();
options.put("sheetIndex", sheetIndex);
options.put("sheetName", sheetName);
options.put("convertAll", convertAll);

processor.convert(input, output, options);

String sheetInfo = "";
if (sheetIndex != null) {
sheetInfo = " (sheet " + sheetIndex + ")";
} else if (sheetName != null) {
sheetInfo = " (sheet '" + sheetName + "')";
} else {
sheetInfo = " (all sheets)";
}

getOut().println("✓ Conversion completed" + sheetInfo + ": " + inputFile + " → " + outputFile);
}
}
Loading
Loading