aboutsummaryrefslogtreecommitdiff
path: root/bash/bashrc.d/sd.bash
blob: 70903246999a50ba124475ca9acd49b91450e994 (plain) (blame)
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#
# sd -- sibling/switch directory -- 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. Completion included.
#
sd() {

    # For completeness' sake, we'll pass any options to cd
    local arg
    local -a opts
    for arg ; do
        case $arg in
            --)
                shift
                break
                ;;
            -*)
                shift
                opts=("${opts[@]}" "$arg")
                ;;
            *)
                break
                ;;
        esac
    done

    # Set up local variable for the sibling to which we'll attempt to move,
    # assuming we find one
    local dirname

    # If we have one argument, it's easy, we just try to move to that one
    if (($# == 1)) ; then
        dirname=$1

    # If no argument, the user is lazy; if there's only one sibling, we'll do
    # what they mean and switch to it
    elif (($# == 0)) ; then

        # This subshell switches on globbing functions to try to find all the
        # current directory's siblings; it exits non-zero if it found anything
        # other than one
        if ! dirname=$(
            shopt -s dotglob extglob nullglob
            local -a siblings

            # Generate relative paths of all siblings
            siblings=(../!("${PWD##*/}")/)

            # Strip the trailing slash
            siblings=("${siblings[@]%/}")

            # Strip everything up to the basename
            siblings=("${siblings[@]##*/}")

            # If some number of siblings besides one, exit non-zero
            if ((${#siblings[@]} != 1)) ; then
                exit 1
            fi

            # Otherwise, just print it
            printf %s "${siblings[0]}"

        # This block is run if the subshell fails due to there not being a
        # single sibling
        ) ; then
            printf 'bash: %s: No single sibling directory\n' \
                "$FUNCNAME" >&2
            return 1
        fi

    # Any other number of arguments is a usage error; say so
    else
        printf 'bash: %s: usage: %s [DIR]\n' \
            "$FUNCNAME" "$FUNCNAME" >&2
        return 2
    fi

    # Try to change into the determined directory
    builtin cd "${opts[@]}" ../"$dirname"
}

# Completion function for sd; any sibling directories, excluding the self
_sd() {

    # Only makes sense for the first argument
    ((COMP_CWORD == 1)) || return 1

    # Current directory can't be root directory
    [[ $PWD != / ]] || return 1

    # Build list of matching sibiling directories
    while IFS= read -d '' -r dirname ; do
        COMPREPLY=("${COMPREPLY[@]}" "$dirname")
    done < <(

        # Set options to glob correctly
        shopt -s dotglob extglob nullglob

        # Collect directory names, exclude current directory, strip leading ../
        # and trailing /
        local -a dirnames
        dirnames=(../"${COMP_WORDS[COMP_CWORD]}"*/)
        dirnames=("${dirnames[@]#../}")
        dirnames=("${dirnames[@]%/}")

        # Iterate again, but exclude the current directory this time
        local -a sibs
        local dirname
        for dirname in "${dirnames[@]}" ; do
            [[ $dirname != "${PWD##*/}" ]] || continue
            sibs=("${sibs[@]}" "$dirname")
        done

        # Bail if no results to prevent empty output
        ((${#sibs[@]})) || exit 1

        # Print results, null-delimited
        printf '%q\0' "${sibs[@]}"
    )
}
complete -F _sd sd