All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.dakusui.actionunit.actions.cmd.Commander Maven / Gradle / Ivy

package com.github.dakusui.actionunit.actions.cmd;


import com.github.dakusui.actionunit.core.Action;
import com.github.dakusui.actionunit.core.Context;
import com.github.dakusui.actionunit.core.StreamGenerator;
import com.github.dakusui.actionunit.utils.Checks;
import com.github.dakusui.cmd.Cmd;
import com.github.dakusui.cmd.Shell;
import com.github.dakusui.cmd.exceptions.UnexpectedExitValueException;

import java.io.File;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.github.dakusui.actionunit.actions.cmd.CommanderUtils.quoteWithSingleQuotesForShell;
import static com.github.dakusui.actionunit.core.ActionSupport.*;
import static com.github.dakusui.actionunit.utils.Checks.*;
import static java.util.Objects.requireNonNull;

/**
 * COMMAND action buildER class for ActionUnit style.
 *
 * @param  The class you implement by extending this class.
 */
public abstract class Commander> extends Action.Builder implements Cloneable {
  private final int                     summaryLength;
  private       StreamGenerator stdin;

  private static final Consumer DEFAULT_STDOUT_CONSUMER = System.out::println;
  private static final Consumer DEFAULT_STDERR_CONSUMER = System.err::println;
  List> options;
  private       Cmd.Builder                cmdBuilder;
  private       int                        numRetries;
  private       String                     description;
  private       long                       retryIntervalDuration;
  private       TimeUnit                   retryIntervalTimeUnit;
  private       TimeUnit                   timeOutTimeUnit;
  private       long                       timeOutDuration;
  private       Class retryOn = UnexpectedExitValueException.class;
  private       File                       cwd     = null;
  private final Map        env     = new LinkedHashMap<>();


  /**
   * This is a helper method to create a {@code Commander} object for a given
   * {@code command} without defining a custom class.
   *
   * @param command A command for which the returned builder works.
   * @return A new {@code Commander} object.
   */
  @SuppressWarnings("unchecked")
  public static Commander commander(String command) {
    return new Commander() {
      @Override
      protected String program() {
        return command;
      }
    };
  }

  /**
   * Creates an instance of this class.
   */
  public Commander() {
    this.summaryLength = 60;
    this.options = new LinkedList<>();
    this.cmdBuilder = Cmd.builder()
        .with(new Bash())
        .consumeStdout(DEFAULT_STDOUT_CONSUMER)
        .consumeStderr(DEFAULT_STDERR_CONSUMER);
    this.disconnectStdin().disableTimeout();
  }

  @SuppressWarnings("unchecked")
  public B clone() {
    try {
      B ret = ((B) super.clone());
      ret.options = new LinkedList<>(ret.options);
      return ret;
    } catch (CloneNotSupportedException e) {
      throw impossibleLineReached(e.getMessage(), e);
    }
  }

  @SuppressWarnings("unchecked")
  public B describe(String description) {
    this.description = requireNonNull(description);
    return (B) this;
  }

  @SuppressWarnings("unchecked")
  public B stdin(StreamGenerator stdin) {
    this.stdin = requireNonNull(stdin);
    return (B) this;
  }

  public B disconnectStdin() {
    return stdin(c -> Stream.empty());
  }

  /**
   * Exposes a builder of {@code Cmd} object.
   *
   * @return An internal builder for {@code Cmd} object.
   */
  public Cmd.Builder cmdBuilder() {
    return this.cmdBuilder;
  }

  @SuppressWarnings("unchecked")
  public B cmd(Consumer b) {
    requireNonNull(b).accept(this.cmdBuilder);
    return (B) this;
  }


  @SuppressWarnings("unchecked")
  public B retries(int times) {
    this.numRetries = Checks.requireArgument(v -> v >= 0, times);
    return (B) this;
  }

  @SuppressWarnings("unchecked")
  public B retryOn(Class retryOn) {
    this.retryOn = requireNonNull(retryOn);
    return (B) this;
  }

  @SuppressWarnings("unchecked")
  public B interval(long duration, TimeUnit timeUnit) {
    checkArgument(duration > 0);
    requireNonNull(timeUnit);
    this.retryIntervalDuration = duration;
    this.retryIntervalTimeUnit = timeUnit;
    return (B) this;
  }

  @SuppressWarnings("unchecked")
  public B timeoutIn(long duration, TimeUnit timeUnit) {
    checkArgument(duration > 0);
    requireNonNull(timeUnit);
    this.timeOutDuration = duration;
    this.timeOutTimeUnit = timeUnit;
    return (B) this;
  }

  @SuppressWarnings("unchecked")
  public B disableTimeout() {
    this.timeOutDuration = 0;
    return (B) this;
  }

  public Action build() {
    return readFrom(this.stdin);
  }

  /**
   * This method returns a stream for standard output of a command built by
   * this builder.
   * 

* And this does not create any action. This method is meant to reuse a * {@code Commander} object create for creating an action to other purposes * such as a source of data used in a {@code ForEach} action structure. * {@code toStream(Context)} calls {@code composeCmd(Context)} to create a {@code Cmd} * object that creates a stream to be returned. * * @param context A context object from which a {@code cmd} object is created. * @return A stream for standard output of the command. * @see Commander#composeCmd(Context) */ public Stream toStream(Context context) { return composeCmd(context).stream(); } /** * This method returns an iterator for standard output of a command built by * this builder. * * @param context A context object from which {@code cmd} object is created. * @return An iterator for standard output of the command.n * @see Commander#toStream(Context) */ public Iterable toIterable(Context context) { return () -> toStream(context).iterator(); } @Override public String toString() { return Objects.toString(this.description); } private Action readFrom(StreamGenerator in) { return numRetries > 0 ? retry( this.timeOutIfNecessary(composeAction(in)) ).on( this.retryOn ).times( this.numRetries ).withIntervalOf( this.retryIntervalDuration, this.retryIntervalTimeUnit ).build() : timeOutIfNecessary(composeAction(in)); } private String description() { return CommanderUtils.summarize( this.description != null ? this.description : "(commander)", this.summaryLength ); } /** * Add {@code option} quoting with single quotes "'". * * @param option an option to be added with quotes. * @return This object */ public B addq(String option) { requireNonNull(option); return this.addq(new Function() { @Override public String apply(Context context) { return option; } @Override public String toString() { return option; } }); } @SuppressWarnings("unchecked") public B addq(Function option) { requireNonNull(option); options.add(new Function() { @Override public String apply(Context context) { return quoteWithSingleQuotesForShell(option.apply(context)); } @Override public String toString() { return quoteWithSingleQuotesForShell(Objects.toString(option)); } }); return (B) this; } @SuppressWarnings("unchecked") public B add(Function option) { options.add(requireNonNull(option)); return (B) this; } @SuppressWarnings("unchecked") public B add(String option) { requireNonNull(option); options.add(new Function() { @Override public String apply(Context context) { return option; } @Override public String toString() { return option; } }); return (B) this; } public B add(CommanderOption option) { return add(option, false); } @SuppressWarnings("unchecked") public B add(CommanderOption option, boolean longFormat) { return (B) requireNonNull(option).addTo(this, longFormat); } public B add(CommanderOption option, String value) { return add(option, value, false); } @SuppressWarnings("unchecked") public B add(CommanderOption option, String value, boolean longFormat) { return (B) requireNonNull(option).addTo(this, value, longFormat); } /** * Sets current working directory of a command built by this object to a * given value. * * @param cwd Current working directory for a command built by this object. * @return this object */ @SuppressWarnings("unchecked") public B cwd(File cwd) { requireArgument(File::isDirectory, requireNonNull(cwd)); this.cwd = cwd; this.cmdBuilder.cwd(cwd); return (B) this; } /** * Returns a current working directory set to this object * * @return A current working directory set to this object. */ public File cwd() { if (this.cwd == null) return new File(System.getProperty("user.dir")); return this.cwd; } /** * Sets an environment variable used by a command created by this object. * * @param envvar A name of environment variable. * @param value A value for an environment variable {@code envvar}. * @return This object. */ @SuppressWarnings("unchecked") public B env(String envvar, String value) { this.env.put(requireNonNull(envvar), requireNonNull(value)); return (B) this; } /** * Creates a {@code Cmd} object based on properties this object holds. * * @param context A context object from which {@code Cmd} object to be returned * is created. * @return A created {@code Cmd} object. */ public Cmd toCmd(Context context) { return composeCmd(context); } /** * Get full path to a command for which this builder object works. * * @return full path of command */ protected abstract String program(); private Action timeOutIfNecessary(Action action) { return this.timeOutDuration > 0 ? timeout(action).in(this.timeOutDuration, this.timeOutTimeUnit) : action; } private Action composeAction(StreamGenerator in) { return named( description(), leaf( context -> composeCmd(context).readFrom(requireNonNull(in.apply(context))).stream().forEach(s -> { }) ) ); } protected Cmd composeCmd(Context context) { cmdBuilder.env(this.env); cmdBuilder.command(new Supplier() { @Override public String get() { return String.format( "%s %s", Commander.this.program(), Commander.this.formatOptions(option -> option.apply(context)) ); } @Override public String toString() { return String.format( "%s %s", Commander.this.program(), Commander.this.formatOptions(stringSupplier -> { String ret = Objects.toString(stringSupplier); return ret.contains("$$Lambda") ? "(?)" : ret; }) ); } }); return cmdBuilder.build(); } private String formatOptions(Function, String> formatter) { return this.options.stream() .map(formatter) .collect(Collectors.joining(" ")); } /** * A shell with which a command built by {@code Commander} object is executed * by default. */ public static class Bash implements Shell { /** * Returns a path to a bash program. * * @return A path to a bash program. */ @Override public String program() { return "/bin/bash"; } /** * Returns an option used to execute target command. For this class, it * is defined {@code ["-c"]} based on bash's behaviour. * * @return Returns a command line option passed to shell. */ @Override public List options() { return new ArrayList() {{ add("-c"); }}; } @Override public String toString() { return format(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy