001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2023-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.cli.common;
019
020import com.google.common.annotations.VisibleForTesting;
021
022import picocli.CommandLine;
023
024import java.io.PrintWriter;
025import java.io.StringWriter;
026import java.time.ZoneId;
027import java.util.Arrays;
028import java.util.Locale;
029
030public class CLI {
031
032    private String[] args;
033    private final CommandLine commandLine;
034    private StringWriter out;
035    private StringWriter err;
036
037    private Integer exitCode;
038
039    public CLI(String[] args, Application app) {
040        this(
041                args,
042                new CommandLine(app)
043                        .setUsageHelpAutoWidth(true)
044                        .setCaseInsensitiveEnumValuesAllowed(true)
045                        .registerConverter(Locale.class, new LocaleConverter())
046                        .registerConverter(ZoneId.class, new ZoneIdConverter())
047                        .setExecutionStrategy(LoggingMixin::executionStrategy)
048                        .setExitCodeExceptionMapper(new KnownExitCodeExceptionMapper())
049                        .setExecutionExceptionHandler(new QuietExecutionExceptionHandler(app)));
050    }
051
052    public CLI(String[] args, CommandLine commandLine) {
053        this.commandLine = commandLine;
054        this.args = Arrays.copyOf(args, args.length);
055    }
056
057    @VisibleForTesting
058    public void setArgs(String[] args) {
059        this.args = args;
060    }
061
062    public CLI setOut(PrintWriter out) {
063        commandLine.setOut(out);
064        return this;
065    }
066
067    public CLI setErr(PrintWriter err) {
068        commandLine.setErr(err);
069        return this;
070    }
071
072    public void setOutAndErrStrings() {
073        out = new StringWriter();
074        setOut(new PrintWriter(out));
075
076        err = new StringWriter();
077        setErr(new PrintWriter(err));
078    }
079
080    public String getOutString() {
081        commandLine.getOut().flush();
082        out.flush();
083        return out.toString();
084    }
085
086    public String getErrString() {
087        commandLine.getErr().flush();
088        err.flush();
089        return err.toString();
090    }
091
092    public int execute() {
093        this.exitCode = commandLine.execute(args);
094        commandLine.getOut().flush();
095        commandLine.getErr().flush();
096        return exitCode;
097    }
098
099    public int exitCode() {
100        if (exitCode == null) {
101            throw new IllegalStateException("Must execute() first!");
102        }
103        return exitCode;
104    }
105
106    @Override
107    public String toString() {
108        return "CLI{" + "args=" + Arrays.toString(args) + ", exitCode=" + exitCode + '}';
109    }
110}