aboutsummaryrefslogtreecommitdiff
path: root/sh/shrc.d/sd.sh
blob: 105978323f7cc8bdeef8f1cb26fcd6eeccea1ca0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# Shortcut to switch to another directory with the same parent, i.e. a sibling
# of the current directory.
#
#     $ pwd
#     /home/you
#     $ sd friend
#     $ pwd
#     /home/friend
#     $ sd you
#     $ pwd
#     /home/you
#
# If no arguments are given and there's only one other sibling, switch to that;
# nice way to quickly toggle between two siblings.
#
#     $ cd -- "$(mktemp -d)"
#     $ pwd
#     /tmp/tmp.ZSunna5Eup
#     $ mkdir a b
#     $ ls
#     a b
#     $ cd a
#     pwd
#     /tmp/tmp.ZSunna5Eup/a
#     $ sd
#     $ pwd
#     /tmp/tmp.ZSunna5Eup/b
#     $ sd
#     $ pwd
#     /tmp/tmp.ZSunna5Eup/a
#
# Seems to work for symbolic links.
sd() {

    # Check argument count
    if [ "$#" -gt 1 ] ; then
        printf >&2 'sd(): Too many arguments\n'
        return 2
    fi

    # Strip trailing slashes
    while : ; do
        case $1 in
            *?/) set -- "${1%/}" ;;
            *) break ;;
        esac
    done

    # Read sole optional argument
    case $1 in

        # Root has no siblings
        /)
            printf >&2 'sd(): Radical misunderstanding\n'
            return 2
            ;;

        # Slashes aren't allowed
        */*)
            printf >&2 'sd(): Illegal slash\n'
            return 2
            ;;

        # If blank, we try to find if there's just one sibling, and change to
        # that if so
        '')
            # First a special case: root dir
            case $PWD in
                *[!/]*) ;;
                *)
                    printf >&2 'sd(): No siblings\n'
                    return 1
                    ;;
            esac

            # Get a full list of directories at this level; include dotfiles,
            # but not the . and .. entries, using glob tricks to avoid Bash
            # ruining things with `dotglob`
            set -- ../[!.]*/
            [ -e "$1" ] || shift
            set -- ../.[!.]*/ "$@"
            [ -e "$1" ] || shift
            set -- ../..?*/ "$@"
            [ -e "$1" ] || shift

            # Check the number of matches
            case $# in

                # One match?  Must be $PWD, so no siblings--throw in 0 just in
                # case, but that Shouldn't Happen (TM)
                0|1)
                    printf >&2 'sd(): No siblings\n'
                    return 1
                    ;;

                # Two matches; hopefully just one sibling, but which is it?
                2)

                    # Push PWD onto the stack, strip trailing slashes
                    set -- "$1" "$2" "$PWD"
                    while : ; do
                        case $3 in
                            */) set -- "$1" "$2" "${3%/}" ;;
                            *) break ;;
                        esac
                    done

                    # Pick whichever of our two parameters doesn't look like
                    # PWD as our sole parameter
                    case $1 in
                        ../"${3##*/}"/) set -- "$2" ;;
                        *) set -- "$1" ;;
                    esac
                    ;;

                # Anything else?  Multiple siblings--user will need to specify
                *)
                    printf >&2 'sd(): Multiple siblings\n'
                    return 1
                    ;;
            esac
            ;;

        # If not, simply set our target to that directory, and let `cd` do the
        # complaining if it doesn't exist
        *) set -- ../"$1" ;;
    esac

    # Try and change into the first parameter
    # shellcheck disable=SC2164
    cd -- "$1"
}