001package org.unix4j.unix.sed;
002
003import java.util.Arrays;
004import java.util.regex.Matcher;
005
006import org.unix4j.line.Line;
007import org.unix4j.line.SimpleLine;
008import org.unix4j.processor.LineProcessor;
009import org.unix4j.util.StringUtil;
010
011final class SubstituteProcessor extends AbstractRegexpProcessor {
012        
013        private static final int[] EMPTY_OCCURRENCE = new int[0];
014
015        private final String replacement;
016        private final int[] occurrences;
017
018        public SubstituteProcessor(Command command, SedArguments args, LineProcessor output) {
019                super(command, args, output);
020                this.replacement = getReplacement(args);
021                this.occurrences = args.isOccurrenceSet() ? args.getOccurrence() : EMPTY_OCCURRENCE;
022                for (int i = 0; i < occurrences.length; i++) {
023                        if (occurrences[i] <= 0) {
024                                throw new IllegalArgumentException("invalid occurrence index " + occurrences[i] + " in sed " + command + " command");
025                        }
026                }
027                Arrays.sort(occurrences);
028        }
029        public SubstituteProcessor(Command command, String script, SedArguments args, LineProcessor output) {
030                this(command, deriveArgs(command, script, args), output);
031        }
032
033        private static SedArguments deriveArgs(Command command, String script, SedArguments args) {
034                final int start = StringUtil.findStartTrimWhitespace(script) + 1;
035                final int mid = indexOfNextDelimiter(script, start);
036                final int end = indexOfNextDelimiter(script, mid);
037                if (mid < 0 || end < 0) {
038                        throw new IllegalArgumentException("invalid script for sed " + command + " command: " + script);
039                }
040                args = parseSubstituteFlags(command, args, script, end + 1);
041                args.setRegexp(script.substring(start + 1, mid));
042                args.setReplacement(script.substring(mid + 1, end));
043                return args;
044        }
045
046        private static SedArguments parseSubstituteFlags(Command command, SedArguments args, String script, int start) {
047                final int end = StringUtil.findWhitespace(script, start);
048                if (end < StringUtil.findEndTrimWhitespace(script)) {
049                        throw new IllegalArgumentException("extra non-whitespace characters found after substitute command in sed script: " + script);
050                }
051                if (start < end) {
052                        final SedOptions.Default options = new SedOptions.Default(args.getOptions());
053                        //g, p, I flags
054                        int index;
055                        for (index = end - 1; index >= start; index--) {
056                                final char flag = script.charAt(index);
057                                if (flag == 'g') {
058                                        options.set(SedOption.global);
059                                } else if (flag == 'p') {
060                                        options.set(SedOption.print);
061                                } else if (flag == 'I') {
062                                        options.set(SedOption.ignoreCase);
063                                } else {
064                                        break;
065                                }
066                        }
067                        args = new SedArguments(options);
068                        //occurrence index
069                        if (index >= start) {
070                                final String occurrenceStr = script.substring(start, index + 1);
071                                final int occurrence;
072                                try {
073                                        occurrence = Integer.parseInt(occurrenceStr);
074                                } catch (NumberFormatException e) {
075                                        throw new IllegalArgumentException("invalid substitute flags in sed script: " + script, e);
076                                }
077                                if (occurrence <= 0) {
078                                        throw new IllegalArgumentException("invalid occurrence index " + occurrence + " in sed script: " + script);
079                                }
080                                args.setOccurrence(occurrence);
081                        }
082                }
083                return args;
084        }
085        @Override
086        public boolean processLine(Line line) {
087                final Matcher matcher = regexp.matcher(line.getContent());
088                if (matcher.find()) {
089                        boolean matches = true;
090                        final StringBuffer changed = new StringBuffer();//cannot use StringBuilder here since matcher does not support it
091                        if (occurrences.length > 0) {
092                                int current = 1;
093                                for (int i = 0; i < occurrences.length; i++) {
094                                        final int occurrence = occurrences[i];
095                                        while (matches && current < occurrence) {
096                                                 matches = matcher.find();
097                                                 current++;//one too much after unsuccessful find, but does not matter anymore
098                                        }
099                                        if (matches) {
100                                        matcher.appendReplacement(changed, replacement);
101                                        } else {
102                                                break;
103                                        }
104                                }
105                                if (matches && occurrences.length == 1 && args.isGlobal()) {
106                                        //replace all subsequent occurrences in this case
107                                        matches = matcher.find();
108                                        while (matches) {
109                                        matcher.appendReplacement(changed, replacement);
110                                        matches = matcher.find();
111                                        }
112                                }
113                        } else {
114                                while (matches) {
115                                matcher.appendReplacement(changed, replacement);
116                                matches = args.isGlobal() && matcher.find();
117                                }
118                        }
119                matcher.appendTail(changed);
120                        return output.processLine(new SimpleLine(changed, line.getLineEnding()));
121                } else {
122                        if (args.isQuiet()) {
123                                return true;
124                        }
125                        return output.processLine(line);
126                }
127        }
128}