Add a sort command

git-svn-id: https://svn.apache.org/repos/asf/geronimo/gshell/trunk@758311 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/gshell-commands/gshell-text/src/main/java/org/apache/geronimo/gshell/commands/text/SortAction.java b/gshell-commands/gshell-text/src/main/java/org/apache/geronimo/gshell/commands/text/SortAction.java
new file mode 100644
index 0000000..c3c925a
--- /dev/null
+++ b/gshell-commands/gshell-text/src/main/java/org/apache/geronimo/gshell/commands/text/SortAction.java
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.geronimo.gshell.commands.text;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+
+import org.apache.geronimo.gshell.vfs.support.VfsActionSupport;
+import org.apache.geronimo.gshell.vfs.FileObjects;
+import org.apache.geronimo.gshell.clp.Option;
+import org.apache.geronimo.gshell.clp.Argument;
+import org.apache.geronimo.gshell.command.CommandContext;
+import org.apache.geronimo.gshell.io.Closer;
+import org.apache.commons.vfs.FileObject;
+
+/**
+ * Sort lines of text
+ *
+ * @version $Rev: 722776 $ $Date: 2008-12-03 05:59:59 +0100 (Wed, 03 Dec 2008) $
+ */
+public class SortAction extends VfsActionSupport {
+
+    @Option(name = "-f")
+    private boolean caseInsensitive;
+
+    @Option(name = "-r")
+    private boolean reverse;
+
+    @Option(name = "-u")
+    private boolean unique;
+
+    @Option(name = "-t")
+    private String separator;
+
+    @Option(name = "-b")
+    private boolean ignoreBlanks;
+
+    @Option(name = "-k", argumentRequired = true, multiValued = true)
+    private List<String> sortFields;
+
+    @Option(name = "-n")
+    private boolean numeric;
+
+    @Argument(index = 0, required=false)
+    private String path;
+
+
+    public Object execute(CommandContext context) throws Exception {
+        assert context != null;
+
+        if (path != null) {
+            FileObject file = resolveFile(context, path);
+
+            try {
+                sort(context, file);
+            }
+            finally {
+                FileObjects.close(file);
+            }
+        }
+        else {
+            sort(context.getIo().inputStream, context.getIo().out);
+        }
+        return Result.SUCCESS;
+    }
+
+    protected void sort(final CommandContext context, final FileObject file) throws Exception {
+        assert context != null;
+        assert file != null;
+
+        ensureFileExists(file);
+        ensureFileHasContent(file);
+        ensureFileIsReadable(file);
+
+        BufferedInputStream input = new BufferedInputStream(file.getContent().getInputStream());
+        try {
+            sort(input, context.getIo().out);
+        }
+        finally {
+            Closer.close(input);
+        }
+    }
+
+    protected void sort(InputStream input, PrintWriter out) throws Exception {
+        BufferedReader r = new BufferedReader(new InputStreamReader(input));
+        List<String> strings = new ArrayList<String>();
+        for (String s = r.readLine(); s != null; s = r.readLine()) {
+            strings.add(s);
+        }
+        char sep = (separator == null || separator.length() == 0) ? '\0' : separator.charAt(0);
+        Collections.sort(strings, new SortComparator(caseInsensitive, reverse, ignoreBlanks, numeric, sep, sortFields));
+        String last = null;
+        for (String s : strings) {
+            if (last == null) {
+                last = s;
+            } else if (!unique || !s.equals(last)) {
+                out.println(s);
+            }
+        }
+    }
+
+    public static class SortComparator implements Comparator<String> {
+
+        private boolean caseInsensitive;
+        private boolean reverse;
+        private boolean ignoreBlanks;
+        private boolean numeric;
+        private char separator;
+        private List<Key> sortKeys;
+
+        private static Pattern fpPattern;
+        static {
+            final String Digits     = "(\\p{Digit}+)";
+            final String HexDigits  = "(\\p{XDigit}+)";
+            final String Exp        = "[eE][+-]?" + Digits;
+            final String fpRegex    = "([\\x00-\\x20]*[+-]?(NaN|Infinity|(((" + Digits + "(\\.)?(" + Digits + "?)(" + Exp + ")?)|(\\.(" + Digits + ")(" + Exp + ")?)|(((0[xX]" + HexDigits + "(\\.)?)|(0[xX]" + HexDigits + "?(\\.)" + HexDigits + "))[pP][+-]?" + Digits + "))" + "[fFdD]?))[\\x00-\\x20]*)(.*)";
+            fpPattern = Pattern.compile(fpRegex);
+        }
+
+        public SortComparator(boolean caseInsensitive,
+                              boolean reverse,
+                              boolean ignoreBlanks,
+                              boolean numeric,
+                              char separator,
+                              List<String> sortFields) {
+            this.caseInsensitive = caseInsensitive;
+            this.reverse = reverse;
+            this.separator = separator;
+            this.ignoreBlanks = ignoreBlanks;
+            this.numeric = numeric;
+            if (sortFields == null || sortFields.size() == 0) {
+                sortFields = new ArrayList<String>();
+                sortFields.add("1");
+            }
+            sortKeys = new ArrayList<Key>();
+            for (String f : sortFields) {
+                sortKeys.add(new Key(f));
+            }
+        }
+
+        public int compare(String o1, String o2) {
+            int res = 0;
+
+            List<Integer> fi1 = getFieldIndexes(o1);
+            List<Integer> fi2 = getFieldIndexes(o2);
+            for (Key key : sortKeys) {
+                int[] k1 = getSortKey(o1, fi1, key);
+                int[] k2 = getSortKey(o2, fi2, key);
+                if (key.numeric) {
+                    Double d1 = getDouble(o1, k1[0], k1[1]);
+                    Double d2 = getDouble(o2, k2[0], k2[1]);
+                    res = d1.compareTo(d2);
+                } else {
+                    res = compareRegion(o1, k1[0], k1[1], o2, k2[0], k2[1], key.caseInsensitive);
+                }
+                if (res != 0) {
+                    if (key.reverse) {
+                        res = - res;
+                    }
+                    break;
+                }
+            }
+            return res;
+        }
+
+        protected Double getDouble(String s, int start, int end) {
+            Matcher m = fpPattern.matcher(s.substring(start, end));
+            m.find();
+            return new Double(s.substring(0, m.end(1)));
+        }
+
+        protected int compareRegion(String s1, int start1, int end1, String s2, int start2, int end2, boolean caseInsensitive) {
+            int n1 = end1, n2 = end2;
+            for (int i1 = start1, i2 = start2; i1 < end1 && i2 < n2; i1++, i2++) {
+                char c1 = s1.charAt(i1);
+                char c2 = s2.charAt(i2);
+                if (c1 != c2) {
+                    if (caseInsensitive) {
+                        c1 = Character.toUpperCase(c1);
+                        c2 = Character.toUpperCase(c2);
+                        if (c1 != c2) {
+                            c1 = Character.toLowerCase(c1);
+                            c2 = Character.toLowerCase(c2);
+                            if (c1 != c2) {
+                                return c1 - c2;
+                            }
+                        }
+                    } else {
+                        return c1 - c2;
+                    }
+                }
+            }
+            return n1 - n2;
+        }
+
+        protected int[] getSortKey(String str, List<Integer> fields, Key key) {
+            int start;
+            int end;
+            if (key.startField * 2 < fields.size()) {
+                start = fields.get((key.startField - 1) * 2);
+                if (key.ignoreBlanksStart) {
+                    while (start < fields.get((key.startField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(start))) {
+                        start++;
+                    }
+                }
+                if (key.startChar > 0) {
+                    start = Math.min(start + key.startChar - 1, fields.get((key.startField - 1) * 2 + 1));
+                }
+            } else {
+                start = 0;
+            }
+            if (key.endField > 0 && key.endField * 2 < fields.size()) {
+                end =  fields.get((key.endField - 1) * 2);
+                if (key.ignoreBlanksEnd) {
+                    while (end < fields.get((key.endField - 1) * 2 + 1) && Character.isWhitespace(str.charAt(end))) {
+                        end++;
+                    }
+                }
+                if (key.endChar > 0) {
+                    end = Math.min(end + key.endChar - 1, fields.get((key.endField - 1) * 2 + 1));
+                }
+            } else {
+                end = str.length();
+            }
+            return new int[] { start, end };
+        }
+
+        protected List<Integer> getFieldIndexes(String o) {
+            List<Integer> fields = new ArrayList<Integer>();
+            if (o.length() > 0) {
+                if (separator == '\0') {
+                    int i = 0;
+                    fields.add(0);
+                    for (int idx = 1; idx < o.length(); idx++) {
+                        if (Character.isWhitespace(o.charAt(idx)) && !Character.isWhitespace(o.charAt(idx - 1))) {
+                            fields.add(idx - 1);
+                            fields.add(idx);
+                        }
+                    }
+                    fields.add(o.length() - 1);
+                } else {
+                    int last = -1;
+                    for (int idx = o.indexOf(separator); idx >= 0; idx = o.indexOf(separator, idx + 1)) {
+                        if (last >= 0) {
+                            fields.add(last);
+                            fields.add(idx - 1);
+                        } else if (idx > 0) {
+                            fields.add(0);
+                            fields.add(idx - 1);
+                        }
+                        last = idx + 1;
+                    }
+                    if (last < o.length()) {
+                        fields.add(last < 0 ? 0 : last);
+                        fields.add(o.length() - 1);
+                    }
+                }
+            }
+            return fields;
+        }
+
+        public class Key {
+            int startField;
+            int startChar;
+            int endField;
+            int endChar;
+            boolean ignoreBlanksStart;
+            boolean ignoreBlanksEnd;
+            boolean caseInsensitive;
+            boolean reverse;
+            boolean numeric;
+
+            public Key(String str) {
+                boolean modifiers = false;
+                boolean startPart = true;
+                boolean inField = true;
+                boolean inChar = false;
+                for (char c : str.toCharArray()) {
+                    switch (c) {
+                        case '0':
+                        case '1':
+                        case '2':
+                        case '3':
+                        case '4':
+                        case '5':
+                        case '6':
+                        case '7':
+                        case '8':
+                        case '9':
+                            if (!inField && !inChar) {
+                                throw new IllegalArgumentException("Bad field syntax: " + str);
+                            }
+                            if (startPart) {
+                                if (inChar) {
+                                    startChar = startChar * 10 + (c - '0');
+                                } else {
+                                    startField = startField * 10 + (c - '0');
+                                }
+                            } else {
+                                if (inChar) {
+                                    endChar = endChar * 10 + (c - '0');
+                                } else {
+                                    endField = endField * 10 + (c - '0');
+                                }
+                            }
+                            break;
+                        case '.':
+                            if (!inField) {
+                                throw new IllegalArgumentException("Bad field syntax: " + str);
+                            }
+                            inField = false;
+                            inChar = true;
+                            break;
+                        case 'n':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            numeric = true;
+                            break;
+                        case 'f':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            caseInsensitive = true;
+                            break;
+                        case 'r':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            reverse = true;
+                            break;
+                        case 'b':
+                            inField = false;
+                            inChar = false;
+                            modifiers = true;
+                            if (startPart) {
+                                ignoreBlanksStart = true;
+                            } else {
+                                ignoreBlanksEnd = true;
+                            }
+                            break;
+                        case ',':
+                            inField = true;
+                            inChar = false;
+                            startPart = false;
+                            break;
+                        default:
+                            throw new IllegalArgumentException("Bad field syntax: " + str);
+                    }
+                }
+                if (!modifiers) {
+                    ignoreBlanksStart = ignoreBlanksEnd = SortComparator.this.ignoreBlanks;
+                    reverse = SortComparator.this.reverse;
+                    caseInsensitive = SortComparator.this.caseInsensitive;
+                    numeric = SortComparator.this.numeric;
+                }
+                if (startField < 1) {
+                    throw new IllegalArgumentException("Bad field syntax: " + str);
+                }
+            }
+        }
+    }
+
+}
diff --git a/gshell-commands/gshell-text/src/main/resources/META-INF/gshell/components.xml b/gshell-commands/gshell-text/src/main/resources/META-INF/gshell/components.xml
index 9fc3518..68cf200 100644
--- a/gshell-commands/gshell-text/src/main/resources/META-INF/gshell/components.xml
+++ b/gshell-commands/gshell-text/src/main/resources/META-INF/gshell/components.xml
@@ -52,6 +52,10 @@
             <gshell:command name="grep">
                 <gshell:action class="org.apache.geronimo.gshell.commands.text.GrepAction" parent="vfsCommandActionTemplate"/>
             </gshell:command>
+
+            <gshell:command name="sort">
+                <gshell:action class="org.apache.geronimo.gshell.commands.text.SortAction" parent="vfsCommandActionTemplate"/>
+            </gshell:command>
         </gshell:command-bundle>
     </gshell:plugin>
 
diff --git a/gshell-commands/gshell-text/src/main/resources/org/apache/geronimo/gshell/commands/text/SortAction.properties b/gshell-commands/gshell-text/src/main/resources/org/apache/geronimo/gshell/commands/text/SortAction.properties
new file mode 100644
index 0000000..826d238
--- /dev/null
+++ b/gshell-commands/gshell-text/src/main/resources/org/apache/geronimo/gshell/commands/text/SortAction.properties
@@ -0,0 +1,27 @@
+##
+## Licensed to the Apache Software Foundation (ASF) under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  The ASF licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##  http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing,
+## software distributed under the License is distributed on an
+## "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+## KIND, either express or implied.  See the License for the
+## specific language governing permissions and limitations
+## under the License.
+##
+
+##
+## $Rev$ $Date$
+##
+
+command.description=Sort lines of text.
+
+command.manual=\
+  TODO: sort manual
\ No newline at end of file
diff --git a/gshell-commands/gshell-text/src/test/java/org/apache/geronimo/gshell/commands/text/SortTest.java b/gshell-commands/gshell-text/src/test/java/org/apache/geronimo/gshell/commands/text/SortTest.java
new file mode 100644
index 0000000..d1a7dc6
--- /dev/null
+++ b/gshell-commands/gshell-text/src/test/java/org/apache/geronimo/gshell/commands/text/SortTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.geronimo.gshell.commands.text;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Collections;
+
+import junit.framework.TestCase;
+import org.apache.geronimo.gshell.commands.text.SortAction;
+
+public class SortTest extends TestCase {
+
+    public void testFieldIndexesDefaultSep() {
+        SortAction.SortComparator comparator = new SortAction.SortComparator(false, false, false, false, '\0', null);
+        List<Integer> indexes = comparator.getFieldIndexes(" ad  re  t ");
+        assertTrue(Arrays.asList(0, 2, 3, 6, 7, 9, 10, 10).equals(indexes));
+    }
+
+    public void testFieldIndexesWithSep() {
+        SortAction.SortComparator comparator = new SortAction.SortComparator(false, false, false, false, '[', null);
+        List<Integer> indexes = comparator.getFieldIndexes("[  10] [Active     ] [       ] [    8] OPS4J Pax Logging - Service (1.3.0)");
+        assertTrue(Arrays.asList(1, 6, 8, 20, 22, 30, 32, 73 ).equals(indexes));
+
+        indexes = comparator.getFieldIndexes(" ad  re  t ");
+        assertTrue(Arrays.asList(0, 10).equals(indexes));
+    }
+
+    public void testSort() {
+        String s0 = "0321   abcd  ddcba   a";
+        String s1 = " 57t   bcad  ddacb   b";
+        String s2 = "  128  cab   ddbac   c";
+        List<String> strings = Arrays.asList(s0, s1, s2);
+
+        Collections.sort(strings, new SortAction.SortComparator(false, false, false, false, '\0', Arrays.asList("2")));
+        assertTrue(Arrays.asList(s0, s1, s2).equals(strings));
+
+        Collections.sort(strings, new SortAction.SortComparator(false, false, false, false, '\0', Arrays.asList("2.2b")));
+        assertTrue(Arrays.asList(s2, s0, s1).equals(strings));
+
+        Collections.sort(strings, new SortAction.SortComparator(false, false, false, true, '\0', null));
+        assertTrue(Arrays.asList(s1, s2, s0).equals(strings));
+    }
+
+}