001package net.filebot.cli; 002 003import static java.util.Collections.*; 004import static java.util.stream.Collectors.*; 005import static net.filebot.Execute.*; 006import static net.filebot.Logging.*; 007 008import java.io.File; 009import java.util.ArrayList; 010import java.util.EnumSet; 011import java.util.List; 012import java.util.Objects; 013import java.util.Set; 014import java.util.stream.IntStream; 015import java.util.stream.Stream; 016 017import javax.script.Bindings; 018import javax.script.ScriptException; 019 020import net.filebot.ExecuteException; 021import net.filebot.ExitCode; 022import net.filebot.format.ExpressionBindings; 023import net.filebot.format.ExpressionFormat; 024import net.filebot.format.MediaBindingBean; 025 026public class ExecCommand { 027 028 private List<ExpressionFormat> template; 029 private boolean parallel; 030 private boolean distinct; 031 032 private File directory; 033 034 public ExecCommand(List<ExpressionFormat> template, boolean parallel, boolean distinct, File directory) { 035 this.template = template; 036 this.parallel = parallel; 037 this.distinct = distinct; 038 this.directory = directory; 039 } 040 041 public IntStream execute(Stream<MediaBindingBean> group) { 042 if (parallel) { 043 return executeParallel(group.map(ExpressionBindings::new)); 044 } else { 045 return executeSequence(group.map(ExpressionBindings::new)); 046 } 047 } 048 049 private IntStream executeSequence(Stream<Bindings> group) { 050 Stream<List<String>> sequence = group.map(v -> { 051 return template.stream().map(t -> getArgumentValue(t, v)).filter(Objects::nonNull).collect(toList()); 052 }); 053 054 // deduplicate commands 055 if (distinct) { 056 return sequence.distinct().mapToInt(this::execute); 057 } else { 058 return sequence.mapToInt(this::execute); 059 } 060 } 061 062 private IntStream executeParallel(Stream<Bindings> group) { 063 // collect all bindings and combine them into a single command 064 List<Bindings> bindings = group.collect(toList()); 065 066 if (bindings.isEmpty()) { 067 return IntStream.empty(); 068 } 069 070 // collect single command 071 List<String> command = template.stream().flatMap(t -> { 072 Stream<String> sequence = bindings.stream().map(v -> getArgumentValue(t, v)).filter(Objects::nonNull); 073 // deduplicate constant argument value (e.g. echo echo -n -n) and account for short-hand {} empty expressions (equivalent to {f} by convention) 074 if (t.isConstant() && !t.isEmpty()) { 075 return sequence.limit(1); 076 } 077 // deduplicate dynamic argument values (e.g. echo 23 23 23) 078 if (distinct) { 079 return sequence.distinct(); 080 } 081 return sequence; 082 }).collect(toList()); 083 084 // execute single command 085 return Stream.of(command).mapToInt(this::execute); 086 } 087 088 private int execute(List<String> command) { 089 if (command.isEmpty()) { 090 return ExitCode.SUCCESS; 091 } 092 093 try { 094 // chain internal command 095 ArgumentProcessor filebot = ArgumentProcessor.parseCommand(command); 096 if (filebot != null) { 097 debug.finest(format("Run %s", command)); 098 return filebot.run(); 099 } 100 101 // chain system command 102 system(command.get(0), command.subList(1, command.size()), directory, null); 103 return ExitCode.SUCCESS; 104 } catch (ExecuteException e) { 105 log.warning(e::getMessage); 106 return e.getExitCode(); 107 } catch (Exception e) { 108 log.warning(e::getMessage); 109 return ExitCode.ERROR; 110 } 111 } 112 113 private String getArgumentValue(ExpressionFormat template, Bindings variables) { 114 // make -find -exec echo {} work as if it was {f} to mimic Unix find -exec 115 if (template.isEmpty() && "{}".equals(template.getExpression())) { 116 return variables.get("f").toString(); 117 } 118 119 try { 120 return template.format(variables); 121 } catch (Exception e) { 122 debug.warning(cause(template.getExpression(), e)); 123 } 124 return null; 125 } 126 127 public static ExecCommand parse(List<String> args, File directory) { 128 if (args == null || args.isEmpty()) { 129 throw new IllegalArgumentException("args is empty"); 130 } 131 132 // execute one command per file or one command with many file arguments 133 Set<Flag> mark = Flag.parse(args.get(args.size() - 1)); 134 135 if (!mark.isEmpty()) { 136 args = args.subList(0, args.size() - 1); 137 } 138 139 List<ExpressionFormat> template = new ArrayList<ExpressionFormat>(); 140 for (String argument : args) { 141 try { 142 template.add(new ExpressionFormat(argument)); 143 } catch (ScriptException e) { 144 throw new IllegalArgumentException("Invalid expression: " + argument + ": " + e.getMessage(), e); 145 } 146 } 147 148 return new ExecCommand(template, mark.contains(Flag.Parallel), !mark.contains(Flag.Duplicate), directory); 149 } 150 151 public static enum Flag { 152 153 Parallel, Duplicate; 154 155 public static Set<Flag> parse(String mark) { 156 switch (mark) { 157 case "+": 158 return EnumSet.of(Parallel); 159 case "*": 160 return EnumSet.of(Duplicate); 161 case "+*": 162 return EnumSet.of(Parallel, Duplicate); 163 default: 164 return emptySet(); 165 } 166 } 167 } 168 169}