001package org.unix4j.unix.sed;
002
003import org.unix4j.processor.LineProcessor;
004import org.unix4j.util.StringUtil;
005
006/**
007 * Constants for the sed commands with utility method to derive a constant from
008 * a script (see {@link #fromScript(String)} or from the sed arguments
009 * {@link #fromArgs(SedArguments)}. The constant also provides support for the
010 * instantiation of a new command processor through
011 * {@link #createProcessorFor(SedArguments, LineProcessor)}.
012 */
013public enum Command {
014        print('p') {
015                @Override
016                public boolean matches(SedArguments args) {
017                        return args.isPrint();
018                }
019
020                @Override
021                public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output) {
022                        return new PrintProcessor(this, args, output);
023                }
024
025                @Override
026                public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output) {
027                        return new PrintProcessor(this, script, args, output);
028                }
029        },
030        substitute('s') {
031                @Override
032                public boolean matches(SedArguments args) {
033                        return args.isSubstitute();
034                }
035
036                @Override
037                public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output) {
038                        return new SubstituteProcessor(this, args, output);
039                }
040
041                @Override
042                public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output) {
043                        return new SubstituteProcessor(this, script, args, output);
044                }
045        },
046        append('a') {
047                @Override
048                public boolean matches(SedArguments args) {
049                        return args.isAppend();
050                }
051
052                @Override
053                public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output) {
054                        return new AppendProcessor(this, args, output);
055                }
056
057                @Override
058                public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output) {
059                        return new AppendProcessor(this, script, args, output);
060                }
061        },
062        insert('i') {
063                @Override
064                public boolean matches(SedArguments args) {
065                        return args.isInsert();
066                }
067
068                @Override
069                public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output) {
070                        return new InsertProcessor(this, args, output);
071                }
072
073                @Override
074                public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output) {
075                        return new InsertProcessor(this, script, args, output);
076                }
077        },
078        change('c') {
079                @Override
080                public boolean matches(SedArguments args) {
081                        return args.isChange();
082                }
083
084                @Override
085                public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output) {
086                        return new ChangeProcessor(this, args, output);
087                }
088
089                @Override
090                public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output) {
091                        return new ChangeProcessor(this, script, args, output);
092                }
093        },
094        delete('d') {
095                @Override
096                public boolean matches(SedArguments args) {
097                        return args.isDelete();
098                }
099
100                @Override
101                public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output) {
102                        return new DeleteProcessor(this, args, output);
103                }
104
105                @Override
106                public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output) {
107                        return new DeleteProcessor(this, script, args, output);
108                }
109        },
110        translate('y') {
111                @Override
112                public boolean matches(SedArguments args) {
113                        return args.isTranslate();
114                }
115
116                @Override
117                public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output) {
118                        return new TranslateProcessor(this, args, output);
119                }
120
121                @Override
122                public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output) {
123                        return new TranslateProcessor(this, script, args, output);
124                }
125        };
126        protected final char commandChar;
127
128        private Command(char command) {
129                this.commandChar = command;
130        }
131
132        /**
133         * Returns true if the the command option for this command is set in the
134         * specified sed arguments, and false otherwise.
135         * 
136         * @param args
137         *            the sed arguments
138         * @return true if the command option for this command is set in args
139         */
140        abstract public boolean matches(SedArguments args);
141
142        /**
143         * Returns a new instance of the appropriate sed processor for this command.
144         * Note that this method does not check whether the correct command option
145         * is selected in {@code args}.
146         * 
147         * @param args
148         *            the sed arguments passed to the processor constructor
149         * @param output
150         *            the output object passed to the processor constructor
151         * @return a new sed processor instance for this command
152         */
153        abstract public AbstractSedProcessor createProcessorFor(SedArguments args, LineProcessor output);
154
155        /**
156         * Returns a new instance of the appropriate sed processor for this command.
157         * Note that this method does not check whether the script starts with the
158         * correct command character.
159         * 
160         * @param script
161         *            the sed script passed to the processor constructor
162         * @param args
163         *            the sed arguments passed to the processor constructor
164         * @param output
165         *            the output object passed to the processor constructor
166         * @return a new sed processor instance for this command
167         */
168        abstract public AbstractSedProcessor createProcessorFor(String script, SedArguments args, LineProcessor output);
169
170        /**
171         * Returns the command constant The first non-whitespace character of the
172         * script defines the command for substitute and translate; for all other
173         * commands, it is the first character after a whitespace sequence within
174         * the script (leading whitespace is ignored).
175         * 
176         * @param script
177         *            the script to analyse
178         * @return the matching command or null if not found
179         */
180        public static Command fromScript(String script) {
181                final int len = script.length();
182                final int scriptStart = StringUtil.findStartTrimWhitespace(script);
183                if (scriptStart < len) {
184                        final char firstChar = script.charAt(scriptStart);
185                        if (firstChar == substitute.commandChar) {
186                                return substitute;
187                        } else if (firstChar == translate.commandChar) {
188                                return translate;
189                        } else {
190                                final int scriptEnd = AbstractSedProcessor.indexOfNextDelimiter(script, scriptStart);
191                                if (scriptEnd < 0) {
192                                        throw new IllegalArgumentException("sed regexp pattern is not terminated, expected a second unescaped '" + firstChar + "' character in: " + script);
193                                }
194                                final int whitespaceStart = StringUtil.findWhitespace(script, scriptEnd);
195
196                                // either
197                                // (a) the command is the first char after the whitespace
198                                // following the pattern
199                                // (b) the command is one of the characters in the pattern flags
200                                // after the second delimiter
201
202                                // (a) try to find the command char in the pattern flags
203                                Command command = fromCommandChars(script, scriptEnd + 1, whitespaceStart);
204
205                                // (b) ok after the whitespace then
206                                if (command == null && whitespaceStart < len) {
207                                        final int commandStart = StringUtil.findStartTrimWhitespace(script, whitespaceStart);
208                                        command = fromCommandChars(script, commandStart, commandStart + 1);
209                                }
210
211                                // success?
212                                if (command == null) {
213                                        throw new IllegalArgumentException("command expected in sed script: " + script);
214                                }
215                                return command;
216                        }
217                }
218                return null;
219        }
220
221        /**
222         * Returns the command constant if any of the string characters between
223         * start and end matches one of the command characters. Returns null if no
224         * command character is found.
225         * 
226         * @param string
227         *            the string with the command chararcter candidates
228         * @param start
229         *            the start index in string, inclusive
230         * @param end
231         *            the end index in string, exclusive
232         * @return the matching command or null if not found
233         */
234        private static Command fromCommandChars(String string, int start, int end) {
235                final int s = Math.max(0, start);
236                final int e = Math.min(string.length(), end);
237                if (s < e) {
238                        for (final Command command : Command.values()) {
239                                for (int i = s; i < e; i++) {
240                                        if (string.charAt(i) == command.commandChar) {
241                                                return command;
242                                        }
243                                }
244                        }
245                }
246                return null;
247        }
248
249        /**
250         * Returns the command constant taken from the command option set in args
251         * using the {@link #matches(SedArguments)} method Returns null if no
252         * command option is set.
253         * 
254         * @param args
255         *            the sed command arguments
256         * @return the matching command or null if not found
257         */
258        public static Command fromArgs(SedArguments args) {
259                for (final Command command : values()) {
260                        if (command.matches(args)) {
261                                return command;
262                        }
263                }
264                return null;
265        }
266}