diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c9de045e..7f5c08264 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,7 +86,7 @@ jobs:
restore-keys: |
${{ runner.os }}-m2
- name: Test with Maven
- run: ./mvnw clean package -B -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet
+ run: ./mvnw clean package -B -Dmaven.test.skip=false -pl fesod-common,fesod-shaded,fesod-sheet,fesod-cli
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v2
if: (!cancelled())
diff --git a/fesod-cli/pom.xml b/fesod-cli/pom.xml
new file mode 100644
index 000000000..642997ee3
--- /dev/null
+++ b/fesod-cli/pom.xml
@@ -0,0 +1,175 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.fesod
+ fesod-parent
+ ${revision}
+
+
+ fesod-cli
+ jar
+ Apache Fesod CLI Tool
+ Command-line interface for Apache Fesod spreadsheet processing
+
+
+ 4.7.5
+ 2.2
+ org.apache.fesod.cli.FesodCli
+
+
+
+
+
+ org.apache.fesod
+ fesod-sheet
+ ${project.version}
+
+
+
+
+ info.picocli
+ picocli
+ ${picocli.version}
+
+
+
+
+ com.alibaba.fastjson2
+ fastjson2
+ compile
+
+
+
+
+ org.yaml
+ snakeyaml
+
+
+
+
+ org.apache.commons
+ commons-csv
+
+
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ info.picocli
+ picocli-codegen
+ ${picocli.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+ -Aproject=${project.groupId}/${project.artifactId}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+ false
+
+
+ ${mainClass}
+
+ ${project.name}
+ ${project.version}
+ Apache Software Foundation
+ true
+
+
+
+
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+ 3.6.0
+
+
+ src/assembly/bin.xml
+
+ posix
+
+
+
+ make-assembly
+ package
+
+ single
+
+
+
+
+
+
+
diff --git a/fesod-cli/src/assembly/bin.xml b/fesod-cli/src/assembly/bin.xml
new file mode 100644
index 000000000..6c0d5c73a
--- /dev/null
+++ b/fesod-cli/src/assembly/bin.xml
@@ -0,0 +1,84 @@
+
+
+
+ bin
+
+ tar.gz
+ zip
+
+ true
+ fesod-cli-${project.version}
+
+
+
+
+ ${project.build.directory}
+ lib
+
+ fesod-cli-${project.version}.jar
+
+
+
+
+
+ ${project.basedir}/src/main/scripts
+ bin
+
+ fesod-cli
+ fesod-cli.bat
+
+ 755
+
+
+
+
+ ${project.basedir}/src/main/resources
+ conf
+
+ logback.xml
+ default-config.yaml
+
+
+
+
+
+ ${project.basedir}/..
+ .
+
+ LICENSE
+ NOTICE
+ DISCLAIMER
+
+
+
+
+
+ ${project.basedir}/../dist/licenses
+ licenses
+
+ **/*
+
+
+
+
diff --git a/fesod-cli/src/main/java/org/apache/fesod/cli/FesodCli.java b/fesod-cli/src/main/java/org/apache/fesod/cli/FesodCli.java
new file mode 100644
index 000000000..62d9619ff
--- /dev/null
+++ b/fesod-cli/src/main/java/org/apache/fesod/cli/FesodCli.java
@@ -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);
+ }
+}
diff --git a/fesod-cli/src/main/java/org/apache/fesod/cli/commands/BaseCommand.java b/fesod-cli/src/main/java/org/apache/fesod/cli/commands/BaseCommand.java
new file mode 100644
index 000000000..d4ebc7b8b
--- /dev/null
+++ b/fesod-cli/src/main/java/org/apache/fesod/cli/commands/BaseCommand.java
@@ -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 = "")
+ 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();
+}
diff --git a/fesod-cli/src/main/java/org/apache/fesod/cli/commands/ConvertCommand.java b/fesod-cli/src/main/java/org/apache/fesod/cli/commands/ConvertCommand.java
new file mode 100644
index 000000000..1cdba375a
--- /dev/null
+++ b/fesod-cli/src/main/java/org/apache/fesod/cli/commands/ConvertCommand.java
@@ -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 = "")
+ private String inputFile;
+
+ @Parameters(index = "1", description = "Output file path", paramLabel = "