HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavaMinor

Converting Shell input into Windows DOS compliant input

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
compliantintoshellinputdoswindowsconverting

Problem

I have a shell and batch script to make specific calls to a MBean on a JMX server. The shell input args are not working in windows because windows handles " differently than linux.

Assuming this is a bean name : bean = "hostName=\"localhost\",name=\"foobar\"\""; and the whole input args on linux is something like this:

String[] params = new String[]{"-srv", "localhost:1234", "-usr", "foo", "-pwd", "bar", "-b", bean};

This works perfectly fine on a shell but not for windows.

I wrote one method to convert the bean name to be windows dos compliant. This is essentially accomplished by adding 2 double quotes to a single double quote and adding only 2 double quotes to two double quotes:

" -> """ and "" -> """"

public static void main(String[] args) throws Exception
  {   

    String bean = "hostName=\"localhost\",name=\"foobar\"\"";
    String[] params = new String[]{"-srv", "localhost:1234", "-usr", "foo", "-pwd", "bar", "-b", bean};

    for ( int i = 0; i  """ -> """"
                dosCompliantName.append(c);
                dosCompliantName.append(c);
              }
            }
            catch (StringIndexOutOfBoundsException e)
            {
              // we have reached the end and the last char was a double quote so we add 2
              dosCompliantName.append(c);
              dosCompliantName.append(c);
            }
          }
          else
          {
            dosCompliantName.append(c);
          }
        }

        System.out.println(dosCompliantName);
        params[i + 1] = dosCompliantName.toString();
      }
    }
  }


The input/output for Linux and Windows on the console should look like this:

Linux : hostName="localhost",name="foobar""

Windows:hostName="""localhost""",name="""foobar""""

This looks like to work but to me it looks terribly inefficient, ugly and the readability is also not really great. How can I improve this?

Note: of course this whole stuff is extracted into its own method but for the s

Solution

First some comments:

-
using Exception to control flow of the program is considered to be
a bad programming style - exceptions represent an exceptional state
and checking the condition of being at the end of the string is not
an exception

-
command line arguments' names are actually constant as long as the

interface they represent remains unchanged, so they should be

represented as constants and not string literals.

-
there is also only a limited set of them, so they should be

implemented as an Enum.

I assume you get the arguments as a String array - given the above you should first transform it into a Map - the most efficient would be an EnumMap, but I would suggest using a LinkedHashMap which will preserve the order of the arguments.

This will also validate the input and only accept valid arguments, thus preventing the program to crash because of an invalid input.

Now what you actually do to the beanName is basically enclosing a single or double apostrophe within a pair of apostrophes. There is a way to do it without iterating over the string variable character by character - use a regular expression, all you need is:

beanName = beanName.replaceAll("([\"]{1,2})", "\"$1\"")


Now this means "find all groups of single or double apostrophes and replace each of them a with starting apostrophe followed by the found group and an ending apostrophe". This works only for single or double apostrophes - so does probably the right thing for your use case.

To enclose any number of input apostrophes in a pair of the same, you'd use:

beanName = beanName.replaceAll("([\"]+)", "\"$1\"")


meaning "find all groups consisting of one or more apostrophes and replace each of them a with starting apostrophe followed by the found group and an ending apostrophe". Now try to do this without a regular expression - it would require much more complicated code.

All this will reduce the very act of processing the beanName to a mere one liner if you exclude checking for null:

public static enum Param {
  SRV("-srv"), USR("-usr"), PWD("-pwd"), BEAN("-b");

  private final String paramString;

  private Param(String paramString) {
    this.paramString = paramString;
  }

  public String getParamString() {
    return paramString;
  }

  public static Param paramOfValue(String value) {
    for (Param p : values()) {
      if (p.paramString.equals(value)) {
        return p;
      }
    }
    return null;
  }
}

public static void main(String[] args) throws Exception {

  // the bean name to test
  String bean = "hostName=\"localhost\",name=\"foobar\"\"";

  // input is always a String array
  String[] params = new String[] { "-srv", "localhost:1234", "-usr", "foo", "-pwd", "bar", "-b", bean };

  // convert the String array to a Map - only elements which have a predecessor 
  // being a valid argument are being put into the map
  Map paramsMap = new LinkedHashMap<>();
  Arrays.stream(params).reduce((first, second) -> {
    Param param = Param.paramOfValue(first);
    if (param != null) {
      paramsMap.put(param, second);
    }
    return second;
  });
  // process the bean's name if present
  String beanName = paramsMap.get(Param.BEAN);
  if (beanName != null) {
    paramsMap.put(Param.BEAN, beanName.replaceAll("([\"]{1,2})", "\"$1\""));
  }
  System.out.println(paramsMap);

  // create a String array out of the map
  String[] processedParams = paramsMap
      .entrySet()
      .stream()
      .map(e -> Stream.of(e.getKey().getParamString(), e.getValue()))
      .flatMap(e -> e)
      .collect(Collectors.toList())
      .toArray(new String[0]);
  System.out.println(Arrays.toString(processedParams));
}


The output is:

{SRV=localhost:1234, USR=foo, PWD=bar, BEAN=hostName="""localhost""",name="""foobar""""}
[-srv, localhost:1234, -usr, foo, -pwd, bar, -b, hostName="""localhost""",name="""foobar""""]


The code above is less efficient performance-wise since it does more - yet it is safer and much more readable (and IMO elegant :-)). Simplicity and readability reduces errors and thus testing and maintenance costs.

If you refactor processing of the input parameters (validation, conversion into a map) and creating the String array out of the method it gets very short, very straightforward and more efficient. Since the input validation should be done anyway its cost shouldn't actually be taken into account.

Now the command line processing is pretty ad-hoc, there are better ways to do it - for example using org.apache.commons.cli.

Code Snippets

beanName = beanName.replaceAll("([\"]{1,2})", "\"$1\"")
beanName = beanName.replaceAll("([\"]+)", "\"$1\"")
public static enum Param {
  SRV("-srv"), USR("-usr"), PWD("-pwd"), BEAN("-b");

  private final String paramString;

  private Param(String paramString) {
    this.paramString = paramString;
  }

  public String getParamString() {
    return paramString;
  }

  public static Param paramOfValue(String value) {
    for (Param p : values()) {
      if (p.paramString.equals(value)) {
        return p;
      }
    }
    return null;
  }
}

public static void main(String[] args) throws Exception {

  // the bean name to test
  String bean = "hostName=\"localhost\",name=\"foobar\"\"";

  // input is always a String array
  String[] params = new String[] { "-srv", "localhost:1234", "-usr", "foo", "-pwd", "bar", "-b", bean };

  // convert the String array to a Map - only elements which have a predecessor 
  // being a valid argument are being put into the map
  Map<Param, String> paramsMap = new LinkedHashMap<>();
  Arrays.stream(params).reduce((first, second) -> {
    Param param = Param.paramOfValue(first);
    if (param != null) {
      paramsMap.put(param, second);
    }
    return second;
  });
  // process the bean's name if present
  String beanName = paramsMap.get(Param.BEAN);
  if (beanName != null) {
    paramsMap.put(Param.BEAN, beanName.replaceAll("([\"]{1,2})", "\"$1\""));
  }
  System.out.println(paramsMap);

  // create a String array out of the map
  String[] processedParams = paramsMap
      .entrySet()
      .stream()
      .map(e -> Stream.of(e.getKey().getParamString(), e.getValue()))
      .flatMap(e -> e)
      .collect(Collectors.toList())
      .toArray(new String[0]);
  System.out.println(Arrays.toString(processedParams));
}
{SRV=localhost:1234, USR=foo, PWD=bar, BEAN=hostName="""localhost""",name="""foobar""""}
[-srv, localhost:1234, -usr, foo, -pwd, bar, -b, hostName="""localhost""",name="""foobar""""]

Context

StackExchange Code Review Q#139639, answer score: 2

Revisions (0)

No revisions yet.