Thursday, March 24, 2016

Bash long options

UPDATED: Long options with arguments in the "name=value" style. The original post neglected this important case.

For years I've never know quite the right way to handle long options in Bash without significant, ugly coding. The usual sources (Advanced Bash-Scripting Guide, The Bash Hackers Wiki, others) are not much help. An occasional glimpse appears on StackOverflow, but not well explained or voted.

Solution

Working with a colleague yesterday, we found this:

name=Bob
while getopts :hn:-: opt
do
    [[ - == $opt ]] && opt=${OPTARG%%=*} OPTARG=${OPTARG#*=}
    case $opt in
    h | help ) print_help ; exit 0 ;;
    n | name ) name=$OPTARG ;;
    * ) print_usage >&2 ; exit 2 ;;
    esac
done
shift $((OPTIND - 1))
echo "$0: $name"
$ ./try-me -h
Usage: ./try-me [-h|--help][-n|--name <name>]
$ ./try-me --help
Usage: ./try-me [-h|--help][-n|--name <name>]
$ ./try-me -n Fred
./try-me: Fred
$ ./try-me --name=Fred
./try-me: Fred

Magic!

I checked with bash 3.2 and 4.3. At least for these, the '-' option argument has a bit of magic when it takes an argument. When the argument to '-' starts with a dash, as in --help (here "-help" is the argument to the '-' option), getopts drops the argument's leading '-', and OPTARG is just the text ("help" in this example). Only '-' has this magic.

Add a quick check for '-' at the top of the while-loop, and the case-block is simple and clear.

Bob's your uncle.

UPDATE: Followup on Bash long options.

Tuesday, March 01, 2016

Hand-rolling builders in Java

I showed off a hand-rolled example Java builder pattern today. It has some benefits over existing builder solutions, but is more work than I like:

  1. All parts of the builder are immutable; you can pass a partially built object for another object to complete (I'm looking at you, Lombok)
  2. It's syntactically complete; that is, code won't compile without providing all arguments for the thing to build
  3. It's easy to generalize; in fact, I'm thinking about an annotation processor to generate it for you (but not there quite yet)
@EqualsAndHashCode
@ToString
public final class CartesianPoint {
    public final int x;
    public final int y;

    public static Builder builder() {
        return new Builder();
    }

    private CartesianPoint(final int x, final int y) {
        this.x = x;
        this.y = y;
    }

    public static final class Builder {
        public WithX x(final int x) {
            return new WithX(x);
        }

        @RequiredArgsConstructor
        public static final class WithX {
            private final int x;

            public WithY y(final int y) {
                return new WithY(y);
            }

            @RequiredArgsConstructor
            public final class WithY {
                private final int y;

                public CartesianPoint build() {
                    return new CartesianPoint(x, y);
                }
            }
        }
    }
}

That was a lot to say! Which is why most times you don't hand-roll builders. Usage is obvious:

@Test
public void shouldBuild() {
    final CartesianPoint point = CartesianPoint.builder().
            x(1).
            y(2).
            build();
    assertThat(point.x).isEqualTo(1)
    assertThat(point.y).isEqualTo(2);
}

Adding caching for equivalent values is not hard:

public static final class Builder {
    private static final ConcurrentMap
            cache = new ConcurrentHashMap<>();

    public WithX x(final int x) {
        return new WithX(x);
    }

    @RequiredArgsConstructor
    public static final class WithX {
        private final int x;

        public WithY y(final int y) {
            return new WithY(y);
        }

        @RequiredArgsConstructor
        public final class WithY {
            private final int y;

            public CartesianPoint build() {
                final CartesianPoint point = new CartesianPoint(x, y);
                final CartesianPoint cached = cache.
                        putIfAbsent(point, point);
                return null == cached ? point : cached;
            }
        }
    }
}